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