...

Source file src/edge-infra.dev/test/framework/config/config.go

Documentation: edge-infra.dev/test/framework/config

     1  // Package config simplifies the declaration of configuration options.
     2  //
     3  // Forked from https://github.com/kubernetes/kubernetes/blob/master/test/edge/e2e/framework/config/config.go
     4  // because the K8s repo is intentionally set up in a way that allows you to
     5  // import it.
     6  //
     7  // Right now the implementation maps them directly to command line
     8  // flags. When combined with test/edge/e2e/framework/viperconfig in a test
     9  // suite, those flags then can also be read from a config file.
    10  //
    11  // The command line flags all get stored in a private flag set. The
    12  // developer of the E2E test suite decides how they are exposed. Options
    13  // include:
    14  //   - exposing as normal flags in the actual command line:
    15  //     CopyFlags(Flags, flag.CommandLine)
    16  //   - populate via test/edge/e2e/framework/viperconfig:
    17  //     viperconfig.ViperizeFlags("my-config.yaml", "", Flags)
    18  //   - a combination of both:
    19  //     CopyFlags(Flags, flag.CommandLine)
    20  //     viperconfig.ViperizeFlags("my-config.yaml", "", flag.CommandLine)
    21  //
    22  // Instead of defining flags one-by-one, test developers annotate a
    23  // structure with tags and then call a single function. This is the
    24  // same approach as in https://godoc.org/github.com/jessevdk/go-flags,
    25  // but implemented so that a test suite can continue to use the normal
    26  // "flag" package.
    27  //
    28  // For example, a file storage/csi.go might define:
    29  //
    30  //	var scaling struct {
    31  //	        NumNodes int  `default:"1" description:"number of nodes to run on"`
    32  //	        Master string
    33  //	}
    34  //	_ = config.AddOptions(&scaling, "storage.csi.scaling")
    35  //
    36  // This defines the following command line flags:
    37  //
    38  //	-storage.csi.scaling.numNodes=<int>  - number of nodes to run on (default: 1)
    39  //	-storage.csi.scaling.master=<string>
    40  //
    41  // All fields in the structure must be exported and have one of the following
    42  // types (same as in the `flag` package):
    43  // - bool
    44  // - time.Duration
    45  // - float64
    46  // - string
    47  // - int
    48  // - int64
    49  // - uint
    50  // - uint64
    51  // - and/or nested or embedded structures containing those basic types.
    52  //
    53  // Each basic entry may have a tag with these optional keys:
    54  //
    55  //	    usage:   additional explanation of the option
    56  //	    default: the default value, in the same format as it would
    57  //	             be given on the command line and true/false for
    58  //	             a boolean
    59  //	    name:    explicit flag name, defaults to the struct field name, lower
    60  //							  case
    61  //
    62  // The names of the final configuration options are a combination of an
    63  // optional common prefix for all options in the structure and the
    64  // name of the fields, concatenated with a dot. To get names that are
    65  // consistent with the command line flags defined by `ginkgo`, the
    66  // initial character of each field name is converted to lower case.
    67  //
    68  // There is currently no support for aliases, so renaming the fields
    69  // or the common prefix will be visible to users of the test suite and
    70  // may breaks scripts which use the old names.
    71  //
    72  // The variable will be filled with the actual values by the test
    73  // suite before running tests. Beware that the code which registers
    74  // Ginkgo tests cannot use those config options, because registering
    75  // tests and options both run before the E2E test suite handles
    76  // parameters.
    77  package config
    78  
    79  import (
    80  	"flag"
    81  	"fmt"
    82  	"reflect"
    83  	"strconv"
    84  	"strings"
    85  	"time"
    86  )
    87  
    88  // Flags is the flag set that AddOptions adds to. Test authors should
    89  // also use it instead of directly adding to the global command line.
    90  var Flags = flag.NewFlagSet("", flag.ContinueOnError)
    91  
    92  // CopyFlags ensures that all flags that are defined in the source flag
    93  // set appear in the target flag set as if they had been defined there
    94  // directly. From the flag package it inherits the behavior that there
    95  // is a panic if the target already contains a flag from the source.
    96  func CopyFlags(source *flag.FlagSet, target *flag.FlagSet) {
    97  	source.VisitAll(func(flag *flag.Flag) {
    98  		// We don't need to copy flag.DefValue. The original
    99  		// default (from, say, flag.String) was stored in
   100  		// the value and gets extracted by Var for the help
   101  		// message.
   102  		target.Var(flag.Value, flag.Name, flag.Usage)
   103  	})
   104  }
   105  
   106  // AddOptions analyzes the options value and creates the necessary
   107  // flags to populate it.
   108  //
   109  // The prefix can be used to root the options deeper in the overall
   110  // set of options, with a dot separating different levels.
   111  //
   112  // The function always returns true, to enable this simplified
   113  // registration of options:
   114  // _ = AddOptions(...)
   115  //
   116  // It panics when it encounters an error, like unsupported types
   117  // or option name conflicts.
   118  func AddOptions(options interface{}, prefix string) bool {
   119  	return AddOptionsToSet(Flags, options, prefix)
   120  }
   121  
   122  // AddOptionsToSet is the same as AddOption, except that it allows choosing the flag set.
   123  func AddOptionsToSet(flags *flag.FlagSet, options interface{}, prefix string) bool {
   124  	optionsType := reflect.TypeOf(options)
   125  	if optionsType == nil {
   126  		panic("options parameter without a type - nil?!")
   127  	}
   128  	if optionsType.Kind() != reflect.Ptr || optionsType.Elem().Kind() != reflect.Struct {
   129  		panic(fmt.Sprintf("need a pointer to a struct, got instead: %T", options))
   130  	}
   131  	addStructFields(flags, optionsType.Elem(), reflect.Indirect(reflect.ValueOf(options)), prefix)
   132  	return true
   133  }
   134  
   135  func addStructFields(flags *flag.FlagSet, structType reflect.Type, structValue reflect.Value, prefix string) {
   136  	for i := 0; i < structValue.NumField(); i++ {
   137  		entry := structValue.Field(i)
   138  		addr := entry.Addr()
   139  		structField := structType.Field(i)
   140  		usage := structField.Tag.Get("usage")
   141  		def := structField.Tag.Get("default")
   142  		name := structField.Tag.Get("name")
   143  		if name == "" {
   144  			name = strings.ToLower(structField.Name)
   145  		}
   146  		if prefix != "" {
   147  			name = prefix + "." + name
   148  		}
   149  		if structField.PkgPath != "" {
   150  			panic(fmt.Sprintf("struct entry %q not exported", name))
   151  		}
   152  		ptr := addr.Interface()
   153  		if structField.Anonymous {
   154  			// Entries in embedded fields are treated like
   155  			// entries, in the struct itself, i.e. we add
   156  			// them with the same prefix.
   157  			addStructFields(flags, structField.Type, entry, prefix)
   158  			continue
   159  		}
   160  		if structField.Type.Kind() == reflect.Struct {
   161  			// Add nested options.
   162  			addStructFields(flags, structField.Type, entry, name)
   163  			continue
   164  		}
   165  		// We could switch based on structField.Type. Doing a
   166  		// switch after getting an interface holding the
   167  		// pointer to the entry has the advantage that we
   168  		// immediately have something that we can add as flag
   169  		// variable.
   170  		//
   171  		// Perhaps generics will make this entire switch redundant someday...
   172  		switch ptr := ptr.(type) {
   173  		case *bool:
   174  			var defValue bool
   175  			parseDefault(&defValue, name, def)
   176  			flags.BoolVar(ptr, name, defValue, usage)
   177  		case *time.Duration:
   178  			var defValue time.Duration
   179  			parseDefault(&defValue, name, def)
   180  			flags.DurationVar(ptr, name, defValue, usage)
   181  		case *float64:
   182  			var defValue float64
   183  			parseDefault(&defValue, name, def)
   184  			flags.Float64Var(ptr, name, defValue, usage)
   185  		case *string:
   186  			flags.StringVar(ptr, name, def, usage)
   187  		case *int:
   188  			var defValue int
   189  			parseDefault(&defValue, name, def)
   190  			flags.IntVar(ptr, name, defValue, usage)
   191  		case *int64:
   192  			var defValue int64
   193  			parseDefault(&defValue, name, def)
   194  			flags.Int64Var(ptr, name, defValue, usage)
   195  		case *uint:
   196  			var defValue uint
   197  			parseDefault(&defValue, name, def)
   198  			flags.UintVar(ptr, name, defValue, usage)
   199  		case *uint64:
   200  			var defValue uint64
   201  			parseDefault(&defValue, name, def)
   202  			flags.Uint64Var(ptr, name, defValue, usage)
   203  		default:
   204  			panic(fmt.Sprintf("unsupported struct entry type %q: %T", name, entry.Interface()))
   205  		}
   206  	}
   207  }
   208  
   209  // parseDefault is necessary because "flag" wants the default in the
   210  // actual type and cannot take a string. It would be nice to reuse the
   211  // existing code for parsing from the "flag" package, but it isn't
   212  // exported.
   213  func parseDefault(value interface{}, name, def string) {
   214  	if def == "" {
   215  		return
   216  	}
   217  	checkErr := func(err error, value interface{}) {
   218  		if err != nil {
   219  			panic(fmt.Sprintf("invalid default %q for %T entry %s: %s", def, value, name, err))
   220  		}
   221  	}
   222  	switch value := value.(type) {
   223  	case *bool:
   224  		v, err := strconv.ParseBool(def)
   225  		checkErr(err, *value)
   226  		*value = v
   227  	case *time.Duration:
   228  		v, err := time.ParseDuration(def)
   229  		checkErr(err, *value)
   230  		*value = v
   231  	case *float64:
   232  		v, err := strconv.ParseFloat(def, 64)
   233  		checkErr(err, *value)
   234  		*value = v
   235  	case *int:
   236  		v, err := strconv.Atoi(def)
   237  		checkErr(err, *value)
   238  		*value = v
   239  	case *int64:
   240  		v, err := strconv.ParseInt(def, 0, 64)
   241  		checkErr(err, *value)
   242  		*value = v
   243  	case *uint:
   244  		v, err := strconv.ParseUint(def, 0, strconv.IntSize)
   245  		checkErr(err, *value)
   246  		*value = uint(v)
   247  	case *uint64:
   248  		v, err := strconv.ParseUint(def, 0, 64)
   249  		checkErr(err, *value)
   250  		*value = v
   251  	default:
   252  		panic(fmt.Sprintf("%q: setting defaults not supported for type %T", name, value))
   253  	}
   254  }
   255  

View as plain text