123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- /*
- Copyright 2018 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- // Package config simplifies the declaration of configuration options.
- // Right now the implementation maps them directly command line
- // flags. When combined with test/e2e/framework/viper in a test suite,
- // those flags then can also be read from a config file.
- //
- // Instead of defining flags one-by-one, developers annotate a
- // structure with tags and then call a single function. This is the
- // same approach as in https://godoc.org/github.com/jessevdk/go-flags,
- // but implemented so that a test suite can continue to use the normal
- // "flag" package.
- //
- // For example, a file storage/csi.go might define:
- //
- // var scaling struct {
- // NumNodes int `default:"1" description:"number of nodes to run on"`
- // Master string
- // }
- // _ = config.AddOptions(&scaling, "storage.csi.scaling")
- //
- // This defines the following command line flags:
- //
- // -storage.csi.scaling.numNodes=<int> - number of nodes to run on (default: 1)
- // -storage.csi.scaling.master=<string>
- //
- // All fields in the structure must be exported and have one of the following
- // types (same as in the `flag` package):
- // - bool
- // - time.Duration
- // - float64
- // - string
- // - int
- // - int64
- // - uint
- // - uint64
- // - and/or nested or embedded structures containing those basic types.
- //
- // Each basic entry may have a tag with these optional keys:
- //
- // usage: additional explanation of the option
- // default: the default value, in the same format as it would
- // be given on the command line and true/false for
- // a boolean
- //
- // The names of the final configuration options are a combination of an
- // optional common prefix for all options in the structure and the
- // name of the fields, concatenated with a dot. To get names that are
- // consistent with the command line flags defined by `ginkgo`, the
- // initial character of each field name is converted to lower case.
- //
- // There is currently no support for aliases, so renaming the fields
- // or the common prefix will be visible to users of the test suite and
- // may breaks scripts which use the old names.
- //
- // The variable will be filled with the actual values by the test
- // suite before running tests. Beware that the code which registers
- // Ginkgo tests cannot use those config options, because registering
- // tests and options both run before the E2E test suite handles
- // parameters.
- package config
- import (
- "flag"
- "fmt"
- "reflect"
- "strconv"
- "time"
- "unicode"
- "unicode/utf8"
- )
- // CommandLine is the flag set that AddOptions adds to. Usually this
- // is the same as the default in the flag package, but can also be
- // something else (for example during testing).
- var CommandLine = flag.CommandLine
- // AddOptions analyzes the options value and creates the necessary
- // flags to populate it.
- //
- // The prefix can be used to root the options deeper in the overall
- // set of options, with a dot separating different levels.
- //
- // The function always returns true, to enable this simplified
- // registration of options:
- // _ = AddOptions(...)
- //
- // It panics when it encounters an error, like unsupported types
- // or option name conflicts.
- func AddOptions(options interface{}, prefix string) bool {
- optionsType := reflect.TypeOf(options)
- if optionsType == nil {
- panic("options parameter without a type - nil?!")
- }
- if optionsType.Kind() != reflect.Ptr || optionsType.Elem().Kind() != reflect.Struct {
- panic(fmt.Sprintf("need a pointer to a struct, got instead: %T", options))
- }
- addStructFields(optionsType.Elem(), reflect.Indirect(reflect.ValueOf(options)), prefix)
- return true
- }
- func addStructFields(structType reflect.Type, structValue reflect.Value, prefix string) {
- for i := 0; i < structValue.NumField(); i++ {
- entry := structValue.Field(i)
- addr := entry.Addr()
- structField := structType.Field(i)
- name := structField.Name
- r, n := utf8.DecodeRuneInString(name)
- name = string(unicode.ToLower(r)) + name[n:]
- usage := structField.Tag.Get("usage")
- def := structField.Tag.Get("default")
- if prefix != "" {
- name = prefix + "." + name
- }
- if structField.PkgPath != "" {
- panic(fmt.Sprintf("struct entry %q not exported", name))
- }
- ptr := addr.Interface()
- if structField.Anonymous {
- // Entries in embedded fields are treated like
- // entries, in the struct itself, i.e. we add
- // them with the same prefix.
- addStructFields(structField.Type, entry, prefix)
- continue
- }
- if structField.Type.Kind() == reflect.Struct {
- // Add nested options.
- addStructFields(structField.Type, entry, name)
- continue
- }
- // We could switch based on structField.Type. Doing a
- // switch after getting an interface holding the
- // pointer to the entry has the advantage that we
- // immediately have something that we can add as flag
- // variable.
- //
- // Perhaps generics will make this entire switch redundant someday...
- switch ptr := ptr.(type) {
- case *bool:
- var defValue bool
- parseDefault(&defValue, name, def)
- CommandLine.BoolVar(ptr, name, defValue, usage)
- case *time.Duration:
- var defValue time.Duration
- parseDefault(&defValue, name, def)
- CommandLine.DurationVar(ptr, name, defValue, usage)
- case *float64:
- var defValue float64
- parseDefault(&defValue, name, def)
- CommandLine.Float64Var(ptr, name, defValue, usage)
- case *string:
- CommandLine.StringVar(ptr, name, def, usage)
- case *int:
- var defValue int
- parseDefault(&defValue, name, def)
- CommandLine.IntVar(ptr, name, defValue, usage)
- case *int64:
- var defValue int64
- parseDefault(&defValue, name, def)
- CommandLine.Int64Var(ptr, name, defValue, usage)
- case *uint:
- var defValue uint
- parseDefault(&defValue, name, def)
- CommandLine.UintVar(ptr, name, defValue, usage)
- case *uint64:
- var defValue uint64
- parseDefault(&defValue, name, def)
- CommandLine.Uint64Var(ptr, name, defValue, usage)
- default:
- panic(fmt.Sprintf("unsupported struct entry type %q: %T", name, entry.Interface()))
- }
- }
- }
- // parseDefault is necessary because "flag" wants the default in the
- // actual type and cannot take a string. It would be nice to reuse the
- // existing code for parsing from the "flag" package, but it isn't
- // exported.
- func parseDefault(value interface{}, name, def string) {
- if def == "" {
- return
- }
- checkErr := func(err error, value interface{}) {
- if err != nil {
- panic(fmt.Sprintf("invalid default %q for %T entry %s: %s", def, value, name, err))
- }
- }
- switch value := value.(type) {
- case *bool:
- v, err := strconv.ParseBool(def)
- checkErr(err, *value)
- *value = v
- case *time.Duration:
- v, err := time.ParseDuration(def)
- checkErr(err, *value)
- *value = v
- case *float64:
- v, err := strconv.ParseFloat(def, 64)
- checkErr(err, *value)
- *value = v
- case *int:
- v, err := strconv.Atoi(def)
- checkErr(err, *value)
- *value = v
- case *int64:
- v, err := strconv.ParseInt(def, 0, 64)
- checkErr(err, *value)
- *value = v
- case *uint:
- v, err := strconv.ParseUint(def, 0, strconv.IntSize)
- checkErr(err, *value)
- *value = uint(v)
- case *uint64:
- v, err := strconv.ParseUint(def, 0, 64)
- checkErr(err, *value)
- *value = v
- default:
- panic(fmt.Sprintf("%q: setting defaults not supported for type %T", name, value))
- }
- }
|