1 package edgecliutils
2
3 import (
4 "fmt"
5 "net"
6 "strconv"
7 "strings"
8
9 "edge-infra.dev/pkg/edge/api/graph/model"
10 "edge-infra.dev/pkg/edge/api/utils"
11 clustertype "edge-infra.dev/pkg/edge/constants/api/cluster"
12 "edge-infra.dev/pkg/edge/constants/api/fleet"
13 "edge-infra.dev/pkg/edge/edgecli/flagutil"
14 "edge-infra.dev/pkg/edge/registration"
15 "edge-infra.dev/pkg/lib/networkvalidator"
16
17 "github.com/thoas/go-funk"
18 "github.com/urfave/cli/v2"
19 )
20
21 var (
22
23 ExcludedForToken = []string{flagutil.UsernameFlag, flagutil.PasswordFlag, flagutil.OrganizationFlag}
24 )
25
26 func GetConnectionFlags() []cli.Flag {
27 return []cli.Flag{
28 &cli.StringFlag{
29 Name: flagutil.UsernameFlag,
30 Aliases: []string{"user", "bff-user", "bsl-user"},
31 Usage: "BSL username with edge credentials",
32 Required: false,
33 },
34 &cli.StringFlag{
35 Name: flagutil.PasswordFlag,
36 Aliases: []string{"pass", "bff-password", "bsl-password"},
37 Usage: "BSL password for username",
38 Required: false,
39 },
40 &cli.StringFlag{
41 Name: flagutil.Endpoint,
42 Aliases: []string{"api-endpoint"},
43 Usage: "URL pointing to edge bff",
44 Required: false,
45 Value: `https://dev1.edge-preprod.dev/api/v2`,
46 DefaultText: `https://dev1.edge-preprod.dev/api/v2`,
47 },
48 &cli.StringFlag{
49 Name: BearerTokenFlag,
50 Aliases: []string{"bearer-token"},
51 Usage: "Bearer token from UI",
52 Required: false,
53 },
54 &cli.StringFlag{
55 Name: flagutil.OrganizationFlag,
56 Aliases: []string{"bsl-organization"},
57 Usage: "BSL organization for username",
58 Required: false,
59 },
60 }
61 }
62
63 func GetCommonFlags() []cli.Flag {
64 return append(GetConnectionFlags(),
65 &cli.BoolFlag{
66 Name: NonInteractiveModeFlag,
67 Aliases: []string{"y"},
68 Usage: "return errors of other flags",
69 Required: false,
70
71 },
72 &cli.StringFlag{
73 Name: KubeConfigFlag,
74 Usage: "path to a kubeconfig file, for the store cluster",
75 Required: false,
76 },
77 &cli.StringFlag{
78 Name: KubeConfigContextFlag,
79 Usage: "kubeconfig context, for store cluster",
80 Required: false,
81 },
82 &cli.StringFlag{
83 Name: LumperImageFlag,
84 Usage: "override default lumper image",
85 Required: false,
86 Value: registration.DefaultLumperImage,
87 DefaultText: registration.DefaultLumperImage,
88 },
89 )
90 }
91
92 type ValidateFlagsFunc = func(sf cli.StringFlag) string
93
94
95 func ValidateRequiredFlags(c *cli.Context, validate ValidateFlagsFunc) error {
96 fmt.Println("Validating flags...")
97 interactiveMode := true
98
99
100 if c.Bool(NonInteractiveModeFlag) {
101 interactiveMode = false
102 }
103
104 hasToken := funk.Find(c.Command.Flags, func(f cli.Flag) bool {
105 sf, ok := f.(*cli.StringFlag)
106 return ok && sf.Name == BearerTokenFlag && c.String(sf.Name) != ""
107 }) != nil
108
109
110 for _, f := range c.Command.Flags {
111 sf, ok := f.(*cli.StringFlag)
112
113 if ok && len(strings.TrimSpace(c.String(sf.Name))) == 0 {
114 if hasToken && utils.Contains(ExcludedForToken, sf.Name) {
115 continue
116 }
117 if interactiveMode {
118 sf.Value = validate(*sf)
119 if err := c.Set(sf.Name, sf.Value); err != nil {
120 return err
121 }
122 } else {
123 return fmt.Errorf("flag %v value cannot be empty. Please use command -y for more assistance regarding flag declaration", sf.Name)
124 }
125 }
126 }
127
128
129 endpoint := c.String(flagutil.Endpoint)
130 if len(endpoint) == 0 {
131 return fmt.Errorf("flag %v value cannot be empty", flagutil.Endpoint)
132 }
133
134 return nil
135 }
136
137
138
139 func RequireFlag(flag cli.StringFlag) string {
140 switch flag.Name {
141 case flagutil.UsernameFlag:
142 var input string
143 fmt.Println("Please enter BSL username.")
144 _, _ = fmt.Scanln(&input)
145 for input == "" {
146 fmt.Println("Please enter a valid username.")
147 _, _ = fmt.Scanln(&input)
148 }
149 flag.Value = input
150 case flagutil.PasswordFlag:
151 var input string
152 fmt.Println("Please enter BSL password.")
153 _, _ = fmt.Scanln(&input)
154 for input == "" {
155 fmt.Println("Please enter a valid password.")
156 _, _ = fmt.Scanln(&input)
157 }
158 flag.Value = input
159 case flagutil.OrganizationFlag:
160 var input string
161 fmt.Println("Please enter BSL organization.")
162 _, _ = fmt.Scanln(&input)
163 for input == "" {
164 fmt.Println("Please enter a valid BSL organization.")
165 _, _ = fmt.Scanln(&input)
166 }
167 flag.Value = input
168 case StoreFlag:
169 var input string
170 fmt.Println("Please enter store.")
171 _, _ = fmt.Scanln(&input)
172 for input == "" {
173 fmt.Println("Please enter a valid store.")
174 _, _ = fmt.Scanln(&input)
175 }
176 flag.Value = input
177 case BannerFlag:
178 var input string
179 fmt.Println("Please enter banner.")
180 _, _ = fmt.Scanln(&input)
181 for input == "" {
182 fmt.Println("Please enter a valid banner.")
183 _, _ = fmt.Scanln(&input)
184 }
185 flag.Value = input
186 }
187 return flag.Value
188 }
189
190
191
192 func ValidateTerminalFlags(
193 role model.TerminalRoleType, class *model.TerminalClassType, _ string, mac string, ipv4addr *string, ipv6addr *string, prefixLen4 string, prefixLen6 string, hostname *string, dhcp6 bool) (int, int, error) {
194 var err error
195
196 if !(role == model.TerminalRoleTypeControlplane || role == model.TerminalRoleTypeWorker) {
197 return 0, 0, fmt.Errorf("role must be one of '%v' or '%v'", model.TerminalRoleTypeControlplane, model.TerminalRoleTypeWorker)
198 }
199
200 if !isValidTerminalClass(class) {
201 return 0, 0, fmt.Errorf("class must be one of '%v' or '%v'", model.TerminalClassTypeServer, model.TerminalClassTypeTouchpoint)
202 }
203
204 _, err = net.ParseMAC(mac)
205 if err != nil {
206 return 0, 0, fmt.Errorf("invalid MAC address '%v'", mac)
207 }
208
209 if ipv4addr != nil && !networkvalidator.ValidateIP(*ipv4addr) {
210 return 0, 0, fmt.Errorf("invalid IPv4 address %s", *ipv4addr)
211 }
212
213 if ipv6addr != nil && !networkvalidator.ValidateIP(*ipv6addr) {
214 return 0, 0, fmt.Errorf("invalid IPv6 address %s", *ipv6addr)
215 }
216
217 if ipv6addr != nil && dhcp6 {
218 return 0, 0, fmt.Errorf("must not provide IPv6 address if DHCP6 is enabled")
219 }
220
221 prefLen4, err := strconv.Atoi(prefixLen4)
222 if err != nil || prefLen4 < 0 || prefLen4 > 32 {
223 return 0, 0, fmt.Errorf("invalid ipv4 network prefix length '%v'", prefixLen4)
224 }
225
226 prefLen6, err := strconv.Atoi(prefixLen6)
227 if err != nil || prefLen6 < 0 || prefLen6 > 128 {
228 return 0, 0, fmt.Errorf("invalid ipv6 network prefix length '%v'", prefixLen4)
229 }
230
231 if hostname != nil {
232 if err = networkvalidator.IsValidHostname(*hostname); err != nil {
233 return 0, 0, err
234 }
235 }
236
237 return prefLen4, prefLen6, nil
238 }
239
240 func isValidTerminalClass(class *model.TerminalClassType) bool {
241 if class == nil {
242 return false
243 }
244 return *class == model.TerminalClassTypeServer || *class == model.TerminalClassTypeTouchpoint
245 }
246
247
248
249 func FlagWasPassed(ctx *cli.Context, flagName string) bool {
250 value := ctx.Value(flagName)
251 var result bool
252
253
254
255 switch val := value.(type) {
256 case bool:
257 {
258 result = val
259 }
260 case int:
261 {
262 result = val != 0
263 }
264 default:
265 {
266 result = val != ""
267 }
268 }
269
270 return result
271 }
272
273 func GetOptionalFlagValue[T any](ctx *cli.Context, flagName string) *T {
274 var flagValue *T
275
276 if FlagWasPassed(ctx, flagName) {
277 value := ctx.Value(flagName).(T)
278 flagValue = &value
279 }
280
281 return flagValue
282 }
283
284
285
286
287
288 func FindFirst[U any, E *U](pred func(E) bool, elts []E) (int, E) {
289 for idx, elt := range elts {
290 if pred(elt) {
291 return idx, elt
292 }
293 }
294 return 0, nil
295 }
296
297
298
299
300
301 func SafeInsert[E any](elts []E, idx int, newElt E) []E {
302 if idx == 0 {
303 elts = append([]E{newElt}, elts...)
304 } else {
305 elts[idx] = newElt
306 }
307 return elts
308 }
309
310 func MakeTermInputFromContext(ctx *cli.Context, currentTerminal *model.Terminal) (*model.TerminalIDInput, error) {
311 lane := GetOptionalFlagValue[string](ctx, LaneFlag)
312 role := GetOptionalFlagValue[string](ctx, RoleFlag)
313 class := GetOptionalFlagValue[string](ctx, ClassFlag)
314 discoverDisks := GetOptionalFlagValue[string](ctx, DiscoverDisksFlag)
315
316 terminalDiskID := GetOptionalFlagValue[string](ctx, DiskID)
317 devicePath := GetOptionalFlagValue[string](ctx, DiskDevicePath)
318
319 mac := GetOptionalFlagValue[string](ctx, MacAddressFlag)
320
321 ipv4addr := GetOptionalFlagValue[string](ctx, IPv4Flag)
322 ipv6addr := GetOptionalFlagValue[string](ctx, IPv6Flag)
323 gateway4addr := GetOptionalFlagValue[string](ctx, Gateway4Flag)
324 gateway6addr := GetOptionalFlagValue[string](ctx, Gateway6Flag)
325 prefixLen4 := GetOptionalFlagValue[int](ctx, PrefixLen4)
326 prefixLen6 := GetOptionalFlagValue[int](ctx, PrefixLen6)
327 dhcp4 := GetOptionalFlagValue[bool](ctx, Dhcp4Flag)
328 dhcp6 := GetOptionalFlagValue[bool](ctx, Dhcp6Flag)
329
330 modifyDisk := false
331 if devicePath != nil {
332 modifyDisk = true
333 }
334 var includeDisk *bool
335 if set := ctx.IsSet(DiskInclude); set {
336 modifyDisk = modifyDisk || set
337 includeDisk = func(b bool) *bool { return &b }(ctx.Value(DiskInclude).(bool))
338 }
339 var expectEmpty *bool
340 if set := ctx.IsSet(DiskExpectEmpty); set {
341 modifyDisk = modifyDisk || set
342 expectEmpty = func(b bool) *bool { return &b }(ctx.Value(DiskExpectEmpty).(bool))
343 }
344
345 if terminalDiskID == nil && modifyDisk {
346 return nil, fmt.Errorf("cannot modify a terminal disk without providing the disk ID")
347 }
348
349 modifyAddr4 := FlagWasPassed(ctx, IPv4Flag) || FlagWasPassed(ctx, PrefixLen4)
350 modifyAddr6 := FlagWasPassed(ctx, IPv6Flag) || FlagWasPassed(ctx, PrefixLen6)
351
352 if modifyAddr4 && modifyAddr6 {
353 return nil, fmt.Errorf("cannot modify both an IPv4 and IPv6 address of an interface at the same time")
354 }
355
356 if mac == nil && (modifyAddr4 || modifyAddr6 || FlagWasPassed(ctx, Gateway4Flag) || FlagWasPassed(ctx, Gateway6Flag) || FlagWasPassed(ctx, Dhcp4Flag) || FlagWasPassed(ctx, Dhcp6Flag)) {
357 return nil, fmt.Errorf("if modifying an interface, must pass its MAC address by which to identify it")
358 }
359
360 ipaddr := ipv4addr
361 prefixLen := prefixLen4
362 family := model.InetTypeInet
363
364 if modifyAddr6 {
365 ipaddr = ipv6addr
366 prefixLen = prefixLen6
367 family = model.InetTypeInet6
368 }
369
370 return makeTermInput(currentTerminal, lane, role, class, mac, ipaddr, gateway4addr, gateway6addr, discoverDisks, terminalDiskID, devicePath, prefixLen, dhcp4, dhcp6, includeDisk, expectEmpty, family)
371 }
372
373 func ValidatingClusterTypeAndFleetType(c *cli.Context) error {
374 err := ValidatingFleetType(c)
375 if err != nil {
376 return err
377 }
378
379 err = ValidatingClusterType(c)
380 if err != nil {
381 return err
382 }
383
384 return nil
385 }
386
387 func ValidatingFleetType(c *cli.Context) error {
388 fleetType := c.String(FleetFlag)
389 if len(fleetType) > 0 {
390 fleetTypeError := fleet.IsValid(fleetType)
391 if fleetTypeError != nil {
392 return fleetTypeError
393 }
394 }
395 return nil
396 }
397
398 func ValidatingClusterType(c *cli.Context) error {
399 clusterType := c.String(ClusterTypeFlag)
400 if len(clusterType) > 0 {
401 clusterTypeError := clustertype.Type(clusterType).IsValid()
402 if clusterTypeError != nil {
403 return clusterTypeError
404 }
405 }
406 return nil
407 }
408
View as plain text