...

Source file src/edge-infra.dev/pkg/edge/edgeadmin/edgecliutils/flags.go

Documentation: edge-infra.dev/pkg/edge/edgeadmin/edgecliutils

     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  	// ExcludedForToken these flags are excluded when token is available
    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  			//HasBeenSet: true,
    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  // ValidateRequiredFlags required field value, value cannot be empty or space
    95  func ValidateRequiredFlags(c *cli.Context, validate ValidateFlagsFunc) error {
    96  	fmt.Println("Validating flags...")
    97  	interactiveMode := true
    98  
    99  	//Verifies the value of the non-interactive mode flag
   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  	//Queries through flags
   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 { // nolint nestif
   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  	// we set bff-endpoint as a not-required field in the cli flag, there is a default value for the flag.
   128  	// However, if the end user choose to use the flag, it cannot be blank or empty spaces
   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  // These flags, if not already provided on command line, must be entered interactively,
   138  // since they represent credentials
   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  // Check that all strings successfully parse as the things they're supposed to represent. Return only the parsed value(s)
   191  // that we currently need, or an error on any validation failure.
   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  // This avoids an apparent bug whereby cli.Context.IsSet(myflag) always returns true for a
   248  // flag that is defined, even if you didn't pass it when you ran the CLI command.
   249  func FlagWasPassed(ctx *cli.Context, flagName string) bool {
   250  	value := ctx.Value(flagName)
   251  	var result bool
   252  
   253  	// Boolean flags are different from flags of any other type, since their "value" is
   254  	// indicated by their presence/absence, not by a value that follows the flag.
   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  // Return the position and value of the first element in the slice that
   285  // satisfies the predicate. The slice's elements must be of pointer type,
   286  // because in Go we use the nil pointer to represent the absence of a value.
   287  // (If no element satisfies the predicate, we return the nil pointer.)
   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  // Add a new element to a slice at the given index. Return the new slice.
   298  // The index passed must be obtained with FindFirst, and so will always be
   299  // 0 if len(elts) == 0 (or elt not found), or 0 <= idx < len(elts) if found.
   300  // So this function will never panic if used as intended.
   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