package edgecliutils import ( "fmt" "net" "strconv" "strings" "edge-infra.dev/pkg/edge/api/graph/model" "edge-infra.dev/pkg/edge/api/utils" clustertype "edge-infra.dev/pkg/edge/constants/api/cluster" "edge-infra.dev/pkg/edge/constants/api/fleet" "edge-infra.dev/pkg/edge/edgecli/flagutil" "edge-infra.dev/pkg/edge/registration" "edge-infra.dev/pkg/lib/networkvalidator" "github.com/thoas/go-funk" "github.com/urfave/cli/v2" ) var ( // ExcludedForToken these flags are excluded when token is available ExcludedForToken = []string{flagutil.UsernameFlag, flagutil.PasswordFlag, flagutil.OrganizationFlag} ) func GetConnectionFlags() []cli.Flag { return []cli.Flag{ &cli.StringFlag{ Name: flagutil.UsernameFlag, Aliases: []string{"user", "bff-user", "bsl-user"}, Usage: "BSL username with edge credentials", Required: false, }, &cli.StringFlag{ Name: flagutil.PasswordFlag, Aliases: []string{"pass", "bff-password", "bsl-password"}, Usage: "BSL password for username", Required: false, }, &cli.StringFlag{ Name: flagutil.Endpoint, Aliases: []string{"api-endpoint"}, Usage: "URL pointing to edge bff", Required: false, Value: `https://dev1.edge-preprod.dev/api/v2`, DefaultText: `https://dev1.edge-preprod.dev/api/v2`, }, &cli.StringFlag{ Name: BearerTokenFlag, Aliases: []string{"bearer-token"}, Usage: "Bearer token from UI", Required: false, }, &cli.StringFlag{ Name: flagutil.OrganizationFlag, Aliases: []string{"bsl-organization"}, Usage: "BSL organization for username", Required: false, }, } } func GetCommonFlags() []cli.Flag { return append(GetConnectionFlags(), &cli.BoolFlag{ Name: NonInteractiveModeFlag, Aliases: []string{"y"}, Usage: "return errors of other flags", Required: false, //HasBeenSet: true, }, &cli.StringFlag{ Name: KubeConfigFlag, Usage: "path to a kubeconfig file, for the store cluster", Required: false, }, &cli.StringFlag{ Name: KubeConfigContextFlag, Usage: "kubeconfig context, for store cluster", Required: false, }, &cli.StringFlag{ Name: LumperImageFlag, Usage: "override default lumper image", Required: false, Value: registration.DefaultLumperImage, DefaultText: registration.DefaultLumperImage, }, ) } type ValidateFlagsFunc = func(sf cli.StringFlag) string // ValidateRequiredFlags required field value, value cannot be empty or space func ValidateRequiredFlags(c *cli.Context, validate ValidateFlagsFunc) error { fmt.Println("Validating flags...") interactiveMode := true //Verifies the value of the non-interactive mode flag if c.Bool(NonInteractiveModeFlag) { interactiveMode = false } hasToken := funk.Find(c.Command.Flags, func(f cli.Flag) bool { sf, ok := f.(*cli.StringFlag) return ok && sf.Name == BearerTokenFlag && c.String(sf.Name) != "" }) != nil //Queries through flags for _, f := range c.Command.Flags { sf, ok := f.(*cli.StringFlag) if ok && len(strings.TrimSpace(c.String(sf.Name))) == 0 { // nolint nestif if hasToken && utils.Contains(ExcludedForToken, sf.Name) { continue } if interactiveMode { sf.Value = validate(*sf) if err := c.Set(sf.Name, sf.Value); err != nil { return err } } else { return fmt.Errorf("flag %v value cannot be empty. Please use command -y for more assistance regarding flag declaration", sf.Name) } } } // we set bff-endpoint as a not-required field in the cli flag, there is a default value for the flag. // However, if the end user choose to use the flag, it cannot be blank or empty spaces endpoint := c.String(flagutil.Endpoint) if len(endpoint) == 0 { return fmt.Errorf("flag %v value cannot be empty", flagutil.Endpoint) } return nil } // These flags, if not already provided on command line, must be entered interactively, // since they represent credentials func RequireFlag(flag cli.StringFlag) string { switch flag.Name { case flagutil.UsernameFlag: var input string fmt.Println("Please enter BSL username.") _, _ = fmt.Scanln(&input) for input == "" { fmt.Println("Please enter a valid username.") _, _ = fmt.Scanln(&input) } flag.Value = input case flagutil.PasswordFlag: var input string fmt.Println("Please enter BSL password.") _, _ = fmt.Scanln(&input) for input == "" { fmt.Println("Please enter a valid password.") _, _ = fmt.Scanln(&input) } flag.Value = input case flagutil.OrganizationFlag: var input string fmt.Println("Please enter BSL organization.") _, _ = fmt.Scanln(&input) for input == "" { fmt.Println("Please enter a valid BSL organization.") _, _ = fmt.Scanln(&input) } flag.Value = input case StoreFlag: var input string fmt.Println("Please enter store.") _, _ = fmt.Scanln(&input) for input == "" { fmt.Println("Please enter a valid store.") _, _ = fmt.Scanln(&input) } flag.Value = input case BannerFlag: var input string fmt.Println("Please enter banner.") _, _ = fmt.Scanln(&input) for input == "" { fmt.Println("Please enter a valid banner.") _, _ = fmt.Scanln(&input) } flag.Value = input } return flag.Value } // Check that all strings successfully parse as the things they're supposed to represent. Return only the parsed value(s) // that we currently need, or an error on any validation failure. func ValidateTerminalFlags( role model.TerminalRoleType, class *model.TerminalClassType, _ string, mac string, ipv4addr *string, ipv6addr *string, prefixLen4 string, prefixLen6 string, hostname *string, dhcp6 bool) (int, int, error) { var err error if !(role == model.TerminalRoleTypeControlplane || role == model.TerminalRoleTypeWorker) { return 0, 0, fmt.Errorf("role must be one of '%v' or '%v'", model.TerminalRoleTypeControlplane, model.TerminalRoleTypeWorker) } if !isValidTerminalClass(class) { return 0, 0, fmt.Errorf("class must be one of '%v' or '%v'", model.TerminalClassTypeServer, model.TerminalClassTypeTouchpoint) } _, err = net.ParseMAC(mac) if err != nil { return 0, 0, fmt.Errorf("invalid MAC address '%v'", mac) } if ipv4addr != nil && !networkvalidator.ValidateIP(*ipv4addr) { return 0, 0, fmt.Errorf("invalid IPv4 address %s", *ipv4addr) } if ipv6addr != nil && !networkvalidator.ValidateIP(*ipv6addr) { return 0, 0, fmt.Errorf("invalid IPv6 address %s", *ipv6addr) } if ipv6addr != nil && dhcp6 { return 0, 0, fmt.Errorf("must not provide IPv6 address if DHCP6 is enabled") } prefLen4, err := strconv.Atoi(prefixLen4) if err != nil || prefLen4 < 0 || prefLen4 > 32 { return 0, 0, fmt.Errorf("invalid ipv4 network prefix length '%v'", prefixLen4) } prefLen6, err := strconv.Atoi(prefixLen6) if err != nil || prefLen6 < 0 || prefLen6 > 128 { return 0, 0, fmt.Errorf("invalid ipv6 network prefix length '%v'", prefixLen4) } if hostname != nil { if err = networkvalidator.IsValidHostname(*hostname); err != nil { return 0, 0, err } } return prefLen4, prefLen6, nil } func isValidTerminalClass(class *model.TerminalClassType) bool { if class == nil { return false } return *class == model.TerminalClassTypeServer || *class == model.TerminalClassTypeTouchpoint } // This avoids an apparent bug whereby cli.Context.IsSet(myflag) always returns true for a // flag that is defined, even if you didn't pass it when you ran the CLI command. func FlagWasPassed(ctx *cli.Context, flagName string) bool { value := ctx.Value(flagName) var result bool // Boolean flags are different from flags of any other type, since their "value" is // indicated by their presence/absence, not by a value that follows the flag. switch val := value.(type) { case bool: { result = val } case int: { result = val != 0 } default: { result = val != "" } } return result } func GetOptionalFlagValue[T any](ctx *cli.Context, flagName string) *T { var flagValue *T if FlagWasPassed(ctx, flagName) { value := ctx.Value(flagName).(T) flagValue = &value } return flagValue } // Return the position and value of the first element in the slice that // satisfies the predicate. The slice's elements must be of pointer type, // because in Go we use the nil pointer to represent the absence of a value. // (If no element satisfies the predicate, we return the nil pointer.) func FindFirst[U any, E *U](pred func(E) bool, elts []E) (int, E) { for idx, elt := range elts { if pred(elt) { return idx, elt } } return 0, nil } // Add a new element to a slice at the given index. Return the new slice. // The index passed must be obtained with FindFirst, and so will always be // 0 if len(elts) == 0 (or elt not found), or 0 <= idx < len(elts) if found. // So this function will never panic if used as intended. func SafeInsert[E any](elts []E, idx int, newElt E) []E { if idx == 0 { elts = append([]E{newElt}, elts...) } else { elts[idx] = newElt } return elts } func MakeTermInputFromContext(ctx *cli.Context, currentTerminal *model.Terminal) (*model.TerminalIDInput, error) { lane := GetOptionalFlagValue[string](ctx, LaneFlag) role := GetOptionalFlagValue[string](ctx, RoleFlag) class := GetOptionalFlagValue[string](ctx, ClassFlag) discoverDisks := GetOptionalFlagValue[string](ctx, DiscoverDisksFlag) terminalDiskID := GetOptionalFlagValue[string](ctx, DiskID) devicePath := GetOptionalFlagValue[string](ctx, DiskDevicePath) mac := GetOptionalFlagValue[string](ctx, MacAddressFlag) ipv4addr := GetOptionalFlagValue[string](ctx, IPv4Flag) ipv6addr := GetOptionalFlagValue[string](ctx, IPv6Flag) gateway4addr := GetOptionalFlagValue[string](ctx, Gateway4Flag) gateway6addr := GetOptionalFlagValue[string](ctx, Gateway6Flag) prefixLen4 := GetOptionalFlagValue[int](ctx, PrefixLen4) prefixLen6 := GetOptionalFlagValue[int](ctx, PrefixLen6) dhcp4 := GetOptionalFlagValue[bool](ctx, Dhcp4Flag) dhcp6 := GetOptionalFlagValue[bool](ctx, Dhcp6Flag) modifyDisk := false if devicePath != nil { modifyDisk = true } var includeDisk *bool if set := ctx.IsSet(DiskInclude); set { modifyDisk = modifyDisk || set includeDisk = func(b bool) *bool { return &b }(ctx.Value(DiskInclude).(bool)) } var expectEmpty *bool if set := ctx.IsSet(DiskExpectEmpty); set { modifyDisk = modifyDisk || set expectEmpty = func(b bool) *bool { return &b }(ctx.Value(DiskExpectEmpty).(bool)) } if terminalDiskID == nil && modifyDisk { return nil, fmt.Errorf("cannot modify a terminal disk without providing the disk ID") } modifyAddr4 := FlagWasPassed(ctx, IPv4Flag) || FlagWasPassed(ctx, PrefixLen4) modifyAddr6 := FlagWasPassed(ctx, IPv6Flag) || FlagWasPassed(ctx, PrefixLen6) if modifyAddr4 && modifyAddr6 { return nil, fmt.Errorf("cannot modify both an IPv4 and IPv6 address of an interface at the same time") } if mac == nil && (modifyAddr4 || modifyAddr6 || FlagWasPassed(ctx, Gateway4Flag) || FlagWasPassed(ctx, Gateway6Flag) || FlagWasPassed(ctx, Dhcp4Flag) || FlagWasPassed(ctx, Dhcp6Flag)) { return nil, fmt.Errorf("if modifying an interface, must pass its MAC address by which to identify it") } ipaddr := ipv4addr prefixLen := prefixLen4 family := model.InetTypeInet if modifyAddr6 { ipaddr = ipv6addr prefixLen = prefixLen6 family = model.InetTypeInet6 } return makeTermInput(currentTerminal, lane, role, class, mac, ipaddr, gateway4addr, gateway6addr, discoverDisks, terminalDiskID, devicePath, prefixLen, dhcp4, dhcp6, includeDisk, expectEmpty, family) } func ValidatingClusterTypeAndFleetType(c *cli.Context) error { err := ValidatingFleetType(c) if err != nil { return err } err = ValidatingClusterType(c) if err != nil { return err } return nil } func ValidatingFleetType(c *cli.Context) error { fleetType := c.String(FleetFlag) if len(fleetType) > 0 { fleetTypeError := fleet.IsValid(fleetType) if fleetTypeError != nil { return fleetTypeError } } return nil } func ValidatingClusterType(c *cli.Context) error { clusterType := c.String(ClusterTypeFlag) if len(clusterType) > 0 { clusterTypeError := clustertype.Type(clusterType).IsValid() if clusterTypeError != nil { return clusterTypeError } } return nil }