github.com/abayer/test-infra@v0.0.5/mungegithub/options/options.go (about) 1 /* 2 Copyright 2017 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 options 18 19 import ( 20 "bytes" 21 "flag" 22 "fmt" 23 "io/ioutil" 24 "reflect" 25 "sort" 26 "strconv" 27 "strings" 28 "sync" 29 "time" 30 31 "k8s.io/apimachinery/pkg/util/sets" 32 33 "github.com/ghodss/yaml" 34 "github.com/golang/glog" 35 ) 36 37 // Options represents the configuration options for mungegithub. 38 // 39 // Options are loaded from a yaml string->string configmap and are updated whenever Load is called. 40 // Options must be registered at least once before they can be retrieved, but registration and 41 // loading may happen in any order (this makes Options compatible with a plugin architecture). 42 // Option keys must be unique across all options of all types. 43 // Options may be registered multiple times safely so long as the option is always bound to the same 44 // pointer. (registration is idempotent) 45 // The defaultVal is used if the options does not have a value specified. 46 // The description explains the option as an entry in the text returned by Descriptions(). 47 type Options struct { 48 rawConfig map[string]string 49 options map[string]*option 50 51 callbacks []UpdateCallback 52 53 sync.Mutex 54 } 55 56 func New() *Options { 57 return &Options{options: map[string]*option{}} 58 } 59 60 type UpdateCallbackError struct { 61 err error 62 } 63 64 func (e *UpdateCallbackError) Error() string { 65 return fmt.Sprintf("error from option update callback: %v", e.err) 66 } 67 68 type UpdateCallback func(changed sets.String) error 69 70 func (o *Options) RegisterUpdateCallback(callback UpdateCallback) { 71 o.callbacks = append(o.callbacks, callback) 72 } 73 74 type optionType string 75 76 const ( 77 typeString optionType = "string" 78 typeStringSlice optionType = "[]string" 79 typeInt optionType = "int" 80 typeUint64 optionType = "uint64" 81 typeBool optionType = "bool" 82 typeDuration optionType = "time.Duration" 83 // typeSecret is the same as typeString except that values are not printed. 84 typeSecret optionType = "SECRET" 85 // typeUnknown is assigned to options that appear in the configmap, but are not registered. 86 // Options of this type are represented as strings. 87 typeUnknown optionType = "UNKNOWN" 88 ) 89 90 type option struct { 91 description string 92 optType optionType 93 // val and defaultVal include a level of pointer indirection. 94 // (e.g. If optType=="string", val and defaultVal are of type *string not string.) 95 val interface{} 96 defaultVal interface{} 97 98 raw string 99 } 100 101 // ToFlags registers all options as string flags with the flag.CommandLine flagset. 102 // All options should be registered before ToFlags is called. 103 func (o *Options) ToFlags() { 104 for key, opt := range o.options { 105 flag.String(key, strings.Trim(toString(opt.optType, opt.defaultVal), "\""), opt.description) 106 } 107 } 108 109 // Load updates options based on the contents of a config file and returns the set of changed options. 110 func (o *Options) Load(file string) (sets.String, error) { 111 firstLoad := o.rawConfig == nil 112 113 b, err := ioutil.ReadFile(file) 114 if err != nil || b == nil { 115 return nil, fmt.Errorf("could not read config file %q: %v", file, err) 116 } 117 changed, err := o.populateFromYaml(b) 118 if err != nil { 119 return changed, err 120 } 121 122 if len(changed) > 0 && !firstLoad { 123 for _, callback := range o.callbacks { 124 if err = callback(changed); err != nil { 125 return changed, &UpdateCallbackError{err: err} 126 } 127 } 128 } 129 return changed, nil 130 } 131 132 // PopulateFromString loads values from the provided yaml string and returns the set of changed options. 133 // This function should only be used in tests where the config is not loaded from a file. 134 func (o *Options) PopulateFromString(yaml string) sets.String { 135 changed, err := o.populateFromYaml([]byte(yaml)) 136 if err != nil { 137 glog.Fatalf("Failed to populate Options with values from %q. Err: %v.", yaml, err) 138 } 139 return changed 140 } 141 142 // PopulateFromFlags loads values into options from command line flags. 143 // This function must be proceeded by a call to ToFlags and the flags must have been parsed since 144 // then. 145 func (o *Options) PopulateFromFlags() { 146 if !flag.Parsed() { 147 flag.Parse() 148 } 149 150 flags := map[string]string{} 151 flag.Visit(func(f *flag.Flag) { 152 flags[f.Name] = f.Value.String() 153 }) 154 155 o.populateFromMap(flags) 156 } 157 158 // FlagsSpecified returns the names of the flags that were specified that correspond to options. 159 // This function must have been proceeded by a call to ToFlags and the flags must have been parsed 160 // since then. 161 func (o *Options) FlagsSpecified() sets.String { 162 if !flag.Parsed() { 163 flag.Parse() 164 } 165 166 specified := sets.String{} 167 flag.Visit(func(f *flag.Flag) { 168 if _, ok := o.options[f.Name]; ok { 169 specified.Insert(f.Name) 170 } 171 }) 172 return specified 173 } 174 175 func (o *Options) populateFromYaml(rawCM []byte) (sets.String, error) { 176 var configmap map[string]string 177 if err := yaml.Unmarshal(rawCM, &configmap); err != nil { 178 return nil, fmt.Errorf("failed to unmarshal configmap from yaml: %v", err) 179 } 180 181 return o.populateFromMap(configmap), nil 182 } 183 184 func (o *Options) populateFromMap(configmap map[string]string) sets.String { 185 o.Lock() 186 defer o.Unlock() 187 188 changed := sets.NewString() 189 for key, opt := range o.options { 190 if opt.optType == typeUnknown { 191 delete(o.options, key) 192 continue 193 } 194 if raw, ok := configmap[key]; ok { 195 opt.raw = raw 196 if opt.fromString() { 197 // The value changed. 198 changed.Insert(key) 199 } 200 delete(configmap, key) 201 } else { 202 if opt.moveToVal(opt.defaultVal) { 203 // The value changed. 204 changed.Insert(key) 205 } 206 } 207 } 208 for key, raw := range configmap { 209 o.options[key] = &option{ 210 optType: typeUnknown, 211 raw: raw, 212 } 213 } 214 o.rawConfig = configmap 215 return changed 216 } 217 218 // fromString converts opt.raw to opt.optType and moves the resulting value into opt.val. 219 // iff the value changed 'true' is returned. 220 func (opt *option) fromString() bool { 221 var err error 222 var newVal interface{} 223 switch opt.optType { 224 case typeString, typeSecret: 225 newVal = &opt.raw 226 case typeStringSlice: 227 slice := []string{} 228 for _, raw := range strings.Split(opt.raw, ",") { 229 if raw = strings.TrimSpace(raw); len(raw) > 0 { 230 slice = append(slice, raw) 231 } 232 } 233 newVal = &slice 234 case typeInt: 235 var i int 236 if i, err = strconv.Atoi(opt.raw); err != nil { 237 glog.Fatalf("Cannot convert %q to type 'int'.", opt.raw) 238 } 239 newVal = &i 240 case typeUint64: 241 var ui uint64 242 if ui, err = strconv.ParseUint(opt.raw, 10, 64); err != nil { 243 glog.Fatalf("Cannot convert %q to type 'uint64'.", opt.raw) 244 } 245 newVal = &ui 246 case typeBool: 247 var b bool 248 if b, err = strconv.ParseBool(opt.raw); err != nil { 249 glog.Fatalf("Cannot convert %q to type 'bool'.", opt.raw) 250 } 251 newVal = &b 252 case typeDuration: 253 var dur time.Duration 254 if dur, err = time.ParseDuration(opt.raw); err != nil { 255 glog.Fatalf("Cannot convert %q to type 'time.Duration'.", opt.raw) 256 } 257 newVal = &dur 258 default: 259 glog.Fatalf("Unrecognized type '%s'.", opt.optType) 260 } 261 return opt.moveToVal(newVal) 262 } 263 264 // moveToVal moves the specified value to 'val', maintaining the original 'val' ptr. 265 // iff the value changed 'true' is returned. 266 func (opt *option) moveToVal(newVal interface{}) bool { 267 changed := !reflect.DeepEqual(opt.val, newVal) 268 switch opt.optType { 269 case typeString, typeSecret: 270 *opt.val.(*string) = *newVal.(*string) 271 case typeStringSlice: 272 *opt.val.(*[]string) = *newVal.(*[]string) 273 case typeInt: 274 *opt.val.(*int) = *newVal.(*int) 275 case typeUint64: 276 *opt.val.(*uint64) = *newVal.(*uint64) 277 case typeBool: 278 *opt.val.(*bool) = *newVal.(*bool) 279 case typeDuration: 280 *opt.val.(*time.Duration) = *newVal.(*time.Duration) 281 default: 282 glog.Fatalf("Unrecognized type '%s'.", opt.optType) 283 } 284 return changed 285 } 286 287 // register tries to register an option of any optionType (with the exception of typeUnknown). 288 // register may be called before or after the configmap is loaded, but options cannot be retrieved 289 // until they are registered. 290 func (o *Options) register(optType optionType, key, description string, val, defaultVal interface{}) interface{} { 291 if optType == typeUnknown { 292 glog.Fatalf("Key '%s' cannot be registered as type 'typeUnknown'.", key) 293 } 294 opt, ok := o.options[key] 295 if ok { 296 if opt.optType == typeUnknown { 297 // Convert opt.raw to optType. 298 opt.val = val 299 opt.optType = optType 300 opt.defaultVal = defaultVal 301 opt.description = description 302 opt.fromString() 303 } else if opt.optType != optType { 304 glog.Fatalf( 305 "Cannot register key: '%s' as a '%s'. It is already registered as a '%s'.", 306 key, 307 optType, 308 opt.optType, 309 ) 310 } else if opt.val != val { 311 glog.Fatalf( 312 "Cannot register key: '%s' to pointer %p. It is already bound to %p.", 313 key, 314 val, 315 opt.val, 316 ) 317 } else if description != opt.description { 318 glog.Fatalf( 319 "Cannot register key: '%s' with description %q. It already has description %q.", 320 key, 321 description, 322 opt.description, 323 ) 324 } else if !reflect.DeepEqual(defaultVal, opt.defaultVal) { 325 glog.Fatalf( 326 "Cannot register key: '%s' with default value %s. It already has default value %s.", 327 key, 328 toString(optType, defaultVal), 329 toString(optType, opt.defaultVal), 330 ) 331 } 332 } else { 333 opt = &option{ 334 description: description, 335 optType: optType, 336 val: val, 337 defaultVal: defaultVal, 338 } 339 o.options[key] = opt 340 opt.moveToVal(defaultVal) 341 } 342 return opt.val 343 } 344 345 // RegisterString registers a `string` option under the specified key. 346 func (o *Options) RegisterString(ptr *string, key string, defaultVal string, description string) { 347 o.register(typeString, key, description, ptr, &defaultVal) 348 } 349 350 // RegisterStringSlice registers a `[]string` option under the specified key. 351 func (o *Options) RegisterStringSlice(ptr *[]string, key string, defaultVal []string, description string) { 352 *ptr = defaultVal 353 o.register(typeStringSlice, key, description, ptr, &defaultVal) 354 } 355 356 // RegisterInt registers an `int` option under the specified key. 357 func (o *Options) RegisterInt(ptr *int, key string, defaultVal int, description string) { 358 o.register(typeInt, key, description, ptr, &defaultVal) 359 } 360 361 // RegisterUint64 registers a `uint64` option under the specified key. 362 func (o *Options) RegisterUint64(ptr *uint64, key string, defaultVal uint64, description string) { 363 o.register(typeUint64, key, description, ptr, &defaultVal) 364 } 365 366 // RegisterBool registers a `bool` option under the specified key. 367 func (o *Options) RegisterBool(ptr *bool, key string, defaultVal bool, description string) { 368 o.register(typeBool, key, description, ptr, &defaultVal) 369 } 370 371 // RegisterDuration registers a `time.Duration` option under the specified key. 372 func (o *Options) RegisterDuration(ptr *time.Duration, key string, defaultVal time.Duration, description string) { 373 o.register(typeDuration, key, description, ptr, &defaultVal) 374 } 375 376 // GetString gets the `string` option under the specified key. 377 func (o *Options) GetString(key string) *string { 378 opt, ok := o.options[key] 379 if !ok { 380 glog.Fatalf("Programmer Error: option key '%s' is not registered!", key) 381 } 382 if opt.optType != typeString { 383 glog.Fatalf("The option with key '%s' has type '%s' not '%s'.", key, opt.optType, typeString) 384 } 385 return o.options[key].val.(*string) 386 } 387 388 // GetStringSlice gets the `[]string` option under the specified key. 389 func (o *Options) GetStringSlice(key string) *[]string { 390 opt, ok := o.options[key] 391 if !ok { 392 glog.Fatalf("Programmer Error: option key '%s' is not registered!", key) 393 } 394 if opt.optType != typeStringSlice { 395 glog.Fatalf("The option with key '%s' has type '%s' not '%s'.", 396 key, 397 opt.optType, 398 typeStringSlice, 399 ) 400 } 401 return o.options[key].val.(*[]string) 402 } 403 404 // GetInt gets then `int` option under the specified key. 405 func (o *Options) GetInt(key string) *int { 406 opt, ok := o.options[key] 407 if !ok { 408 glog.Fatalf("Programmer Error: option key '%s' is not registered!", key) 409 } 410 if opt.optType != typeInt { 411 glog.Fatalf("The option with key '%s' has type '%s' not '%s'.", key, opt.optType, typeInt) 412 } 413 return o.options[key].val.(*int) 414 } 415 416 // GetUint64 gets the `uint64` option under the specified key. 417 func (o *Options) GetUint64(key string) *uint64 { 418 opt, ok := o.options[key] 419 if !ok { 420 glog.Fatalf("Programmer Error: option key '%s' is not registered!", key) 421 } 422 if opt.optType != typeUint64 { 423 glog.Fatalf("The option with key '%s' has type '%s' not '%s'.", key, opt.optType, typeUint64) 424 } 425 return o.options[key].val.(*uint64) 426 } 427 428 // GetBool gets the `bool` option under the specified key. 429 func (o *Options) GetBool(key string) *bool { 430 opt, ok := o.options[key] 431 if !ok { 432 glog.Fatalf("Programmer Error: option key '%s' is not registered!", key) 433 } 434 if opt.optType != typeBool { 435 glog.Fatalf("The option with key '%s' has type '%s' not '%s'.", key, opt.optType, typeBool) 436 } 437 return o.options[key].val.(*bool) 438 } 439 440 // GetDuration gets the `time.Duration` option under the specified key. 441 func (o *Options) GetDuration(key string) *time.Duration { 442 opt, ok := o.options[key] 443 if !ok { 444 glog.Fatalf("Programmer Error: option key '%s' is not registered!", key) 445 } 446 if opt.optType != typeDuration { 447 glog.Fatalf("The option with key '%s' has type '%s' not '%s'.", key, opt.optType, typeDuration) 448 } 449 return o.options[key].val.(*time.Duration) 450 } 451 452 func toString(optType optionType, val interface{}) string { 453 switch optType { 454 case typeString: 455 return fmt.Sprintf("%q", *val.(*string)) 456 case typeStringSlice: 457 if len(*val.(*[]string)) == 0 { 458 return "[]" 459 } 460 return fmt.Sprintf("[\"%s\"]", strings.Join(*val.(*[]string), "\", \"")) 461 case typeInt: 462 return fmt.Sprintf("%d", *val.(*int)) 463 case typeUint64: 464 return fmt.Sprintf("%d", *val.(*uint64)) 465 case typeBool: 466 return fmt.Sprintf("%v", *val.(*bool)) 467 case typeDuration: 468 return fmt.Sprintf("%v", *val.(*time.Duration)) 469 case typeSecret: 470 return "<REDACTED>" 471 case typeUnknown: 472 return fmt.Sprintf("<UNREGISTERED> %q", val) 473 default: 474 glog.Fatalf("Unrecognized type '%s'.", optType) 475 return "" 476 } 477 } 478 479 func (o *Options) keysSortedAndWidth() ([]string, int) { 480 keys := make([]string, 0, len(o.options)) 481 width := 0 482 for key := range o.options { 483 keys = append(keys, key) 484 if len(key) > width { 485 width = len(key) 486 } 487 } 488 sort.Strings(keys) 489 return keys, width 490 } 491 492 func (o *Options) Descriptions() string { 493 var buf bytes.Buffer 494 fmt.Fprint(&buf, "The below options are available. They are listed in the format 'option: (default value) \"Description\"'.\n") 495 sorted, width := o.keysSortedAndWidth() 496 width++ 497 for _, key := range sorted { 498 if opt := o.options[key]; opt.optType != typeUnknown { 499 fmt.Fprintf(&buf, "%-*s (%s) %q\n", width, key+":", toString(opt.optType, opt.defaultVal), opt.description) 500 } 501 } 502 return buf.String() 503 } 504 505 func (o *Options) CurrentValues() string { 506 var buf bytes.Buffer 507 fmt.Fprint(&buf, "Currently configured option values:\n") 508 sorted, width := o.keysSortedAndWidth() 509 width++ 510 for _, key := range sorted { 511 if opt := o.options[key]; opt.optType != typeUnknown { 512 fmt.Fprintf(&buf, "%-*s %s\n", width, key+":", toString(opt.optType, opt.val)) 513 } 514 } 515 return buf.String() 516 }