mirror of https://github.com/k3s-io/k3s
235 lines
7.6 KiB
Go
235 lines
7.6 KiB
Go
/*
|
|
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))
|
|
}
|
|
}
|