config.go 8.9 KB

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