config.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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 command line
  15. // flags. When combined with test/e2e/framework/viper in a test suite,
  16. // those flags then can also be read from a config file.
  17. //
  18. // Instead of defining flags one-by-one, developers annotate a
  19. // structure with tags and then call a single function. This is the
  20. // same approach as in https://godoc.org/github.com/jessevdk/go-flags,
  21. // but implemented so that a test suite can continue to use the normal
  22. // "flag" package.
  23. //
  24. // For example, a file storage/csi.go might define:
  25. //
  26. // var scaling struct {
  27. // NumNodes int `default:"1" description:"number of nodes to run on"`
  28. // Master string
  29. // }
  30. // _ = config.AddOptions(&scaling, "storage.csi.scaling")
  31. //
  32. // This defines the following command line flags:
  33. //
  34. // -storage.csi.scaling.numNodes=<int> - number of nodes to run on (default: 1)
  35. // -storage.csi.scaling.master=<string>
  36. //
  37. // All fields in the structure must be exported and have one of the following
  38. // types (same as in the `flag` package):
  39. // - bool
  40. // - time.Duration
  41. // - float64
  42. // - string
  43. // - int
  44. // - int64
  45. // - uint
  46. // - uint64
  47. // - and/or nested or embedded structures containing those basic types.
  48. //
  49. // Each basic entry may have a tag with these optional keys:
  50. //
  51. // usage: additional explanation of the option
  52. // default: the default value, in the same format as it would
  53. // be given on the command line and true/false for
  54. // a boolean
  55. //
  56. // The names of the final configuration options are a combination of an
  57. // optional common prefix for all options in the structure and the
  58. // name of the fields, concatenated with a dot. To get names that are
  59. // consistent with the command line flags defined by `ginkgo`, the
  60. // initial character of each field name is converted to lower case.
  61. //
  62. // There is currently no support for aliases, so renaming the fields
  63. // or the common prefix will be visible to users of the test suite and
  64. // may breaks scripts which use the old names.
  65. //
  66. // The variable will be filled with the actual values by the test
  67. // suite before running tests. Beware that the code which registers
  68. // Ginkgo tests cannot use those config options, because registering
  69. // tests and options both run before the E2E test suite handles
  70. // parameters.
  71. package config
  72. import (
  73. "flag"
  74. "fmt"
  75. "reflect"
  76. "strconv"
  77. "time"
  78. "unicode"
  79. "unicode/utf8"
  80. )
  81. // CommandLine is the flag set that AddOptions adds to. Usually this
  82. // is the same as the default in the flag package, but can also be
  83. // something else (for example during testing).
  84. var CommandLine = flag.CommandLine
  85. // AddOptions analyzes the options value and creates the necessary
  86. // flags to populate it.
  87. //
  88. // The prefix can be used to root the options deeper in the overall
  89. // set of options, with a dot separating different levels.
  90. //
  91. // The function always returns true, to enable this simplified
  92. // registration of options:
  93. // _ = AddOptions(...)
  94. //
  95. // It panics when it encounters an error, like unsupported types
  96. // or option name conflicts.
  97. func AddOptions(options interface{}, prefix string) bool {
  98. optionsType := reflect.TypeOf(options)
  99. if optionsType == nil {
  100. panic("options parameter without a type - nil?!")
  101. }
  102. if optionsType.Kind() != reflect.Ptr || optionsType.Elem().Kind() != reflect.Struct {
  103. panic(fmt.Sprintf("need a pointer to a struct, got instead: %T", options))
  104. }
  105. addStructFields(optionsType.Elem(), reflect.Indirect(reflect.ValueOf(options)), prefix)
  106. return true
  107. }
  108. func addStructFields(structType reflect.Type, structValue reflect.Value, prefix string) {
  109. for i := 0; i < structValue.NumField(); i++ {
  110. entry := structValue.Field(i)
  111. addr := entry.Addr()
  112. structField := structType.Field(i)
  113. name := structField.Name
  114. r, n := utf8.DecodeRuneInString(name)
  115. name = string(unicode.ToLower(r)) + name[n:]
  116. usage := structField.Tag.Get("usage")
  117. def := structField.Tag.Get("default")
  118. if prefix != "" {
  119. name = prefix + "." + name
  120. }
  121. if structField.PkgPath != "" {
  122. panic(fmt.Sprintf("struct entry %q not exported", name))
  123. }
  124. ptr := addr.Interface()
  125. if structField.Anonymous {
  126. // Entries in embedded fields are treated like
  127. // entries, in the struct itself, i.e. we add
  128. // them with the same prefix.
  129. addStructFields(structField.Type, entry, prefix)
  130. continue
  131. }
  132. if structField.Type.Kind() == reflect.Struct {
  133. // Add nested options.
  134. addStructFields(structField.Type, entry, name)
  135. continue
  136. }
  137. // We could switch based on structField.Type. Doing a
  138. // switch after getting an interface holding the
  139. // pointer to the entry has the advantage that we
  140. // immediately have something that we can add as flag
  141. // variable.
  142. //
  143. // Perhaps generics will make this entire switch redundant someday...
  144. switch ptr := ptr.(type) {
  145. case *bool:
  146. var defValue bool
  147. parseDefault(&defValue, name, def)
  148. CommandLine.BoolVar(ptr, name, defValue, usage)
  149. case *time.Duration:
  150. var defValue time.Duration
  151. parseDefault(&defValue, name, def)
  152. CommandLine.DurationVar(ptr, name, defValue, usage)
  153. case *float64:
  154. var defValue float64
  155. parseDefault(&defValue, name, def)
  156. CommandLine.Float64Var(ptr, name, defValue, usage)
  157. case *string:
  158. CommandLine.StringVar(ptr, name, def, usage)
  159. case *int:
  160. var defValue int
  161. parseDefault(&defValue, name, def)
  162. CommandLine.IntVar(ptr, name, defValue, usage)
  163. case *int64:
  164. var defValue int64
  165. parseDefault(&defValue, name, def)
  166. CommandLine.Int64Var(ptr, name, defValue, usage)
  167. case *uint:
  168. var defValue uint
  169. parseDefault(&defValue, name, def)
  170. CommandLine.UintVar(ptr, name, defValue, usage)
  171. case *uint64:
  172. var defValue uint64
  173. parseDefault(&defValue, name, def)
  174. CommandLine.Uint64Var(ptr, name, defValue, usage)
  175. default:
  176. panic(fmt.Sprintf("unsupported struct entry type %q: %T", name, entry.Interface()))
  177. }
  178. }
  179. }
  180. // parseDefault is necessary because "flag" wants the default in the
  181. // actual type and cannot take a string. It would be nice to reuse the
  182. // existing code for parsing from the "flag" package, but it isn't
  183. // exported.
  184. func parseDefault(value interface{}, name, def string) {
  185. if def == "" {
  186. return
  187. }
  188. checkErr := func(err error, value interface{}) {
  189. if err != nil {
  190. panic(fmt.Sprintf("invalid default %q for %T entry %s: %s", def, value, name, err))
  191. }
  192. }
  193. switch value := value.(type) {
  194. case *bool:
  195. v, err := strconv.ParseBool(def)
  196. checkErr(err, *value)
  197. *value = v
  198. case *time.Duration:
  199. v, err := time.ParseDuration(def)
  200. checkErr(err, *value)
  201. *value = v
  202. case *float64:
  203. v, err := strconv.ParseFloat(def, 64)
  204. checkErr(err, *value)
  205. *value = v
  206. case *int:
  207. v, err := strconv.Atoi(def)
  208. checkErr(err, *value)
  209. *value = v
  210. case *int64:
  211. v, err := strconv.ParseInt(def, 0, 64)
  212. checkErr(err, *value)
  213. *value = v
  214. case *uint:
  215. v, err := strconv.ParseUint(def, 0, strconv.IntSize)
  216. checkErr(err, *value)
  217. *value = uint(v)
  218. case *uint64:
  219. v, err := strconv.ParseUint(def, 0, 64)
  220. checkErr(err, *value)
  221. *value = v
  222. default:
  223. panic(fmt.Sprintf("%q: setting defaults not supported for type %T", name, value))
  224. }
  225. }