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