utils/configutil/config.go (88 lines of code) (raw):

// Copyright (c) 2016-2019 Uber Technologies, Inc. // // 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 configutil provides an interface for loading and validating configuration // data from YAML files. // // Other YAML files could be included via the following directive: // // production.yaml: // extends: base.yaml // // There is no multiple inheritance supported. Dependency tree suppossed to // form a linked list. // // // Values from multiple configurations within the same hierarchy are deep merged // // Note regarding configuration merging: // Array defined in YAML will be overriden based on load sequence. // e.g. in the base.yaml: // sports: // - football // in the development.yaml: // extends: base.yaml // sports: // - basketball // after the merge: // sports: // - basketball // only keep the latest one // // Map defined in YAML will be merged together based on load sequence. // e.g. in the base.yaml: // sports: // football: true // in the development.yaml: // extends: base.yaml // sports: // basketball: true // after the merge: // sports: // combine all the map fields // football: true // basketball: true // package configutil import ( "bytes" "errors" "fmt" "io/ioutil" "path" "path/filepath" "github.com/uber/kraken/utils/stringset" "gopkg.in/validator.v2" "gopkg.in/yaml.v2" ) // ErrCycleRef is returned when there are circular dependencies detected in // configuraiton files extending each other. var ErrCycleRef = errors.New("cyclic reference in configuration extends detected") // Extends define a keywoword in config for extending a base configuration file. type Extends struct { Extends string `yaml:"extends"` } // ValidationError is the returned when a configuration fails to pass // validation. type ValidationError struct { errorMap validator.ErrorMap } // ErrForField returns the validation error for the given field. func (e ValidationError) ErrForField(name string) error { return e.errorMap[name] } // Error implements the `error` interface. func (e ValidationError) Error() string { var w bytes.Buffer fmt.Fprintf(&w, "validation failed") for f, err := range e.errorMap { fmt.Fprintf(&w, " %s: %v\n", f, err) } return w.String() } // Load loads configuration based on config file name. It will // follow extends directives and do a deep merge of those config // files. func Load(filename string, config interface{}) error { filenames, err := resolveExtends(filename, readExtend) if err != nil { return err } return loadFiles(config, filenames) } type getExtend func(filename string) (extends string, err error) // resolveExtends returns the list of config paths that the original config `filename` // points to. func resolveExtends(filename string, extendReader getExtend) ([]string, error) { filenames := []string{filename} seen := make(stringset.Set) for { extends, err := extendReader(filename) if err != nil { return nil, err } else if extends == "" { break } // If the file path of the extends field in the config is not absolute // we assume that it is in the same directory as the current config // file. if !filepath.IsAbs(extends) { extends = path.Join(filepath.Dir(filename), extends) } // Prevent circular references. if seen.Has(extends) { return nil, ErrCycleRef } filenames = append([]string{extends}, filenames...) seen.Add(extends) filename = extends } return filenames, nil } func readExtend(configFile string) (string, error) { data, err := ioutil.ReadFile(configFile) if err != nil { return "", err } var cfg Extends if err := yaml.Unmarshal(data, &cfg); err != nil { return "", fmt.Errorf("unmarshal %s: %s", configFile, err) } return cfg.Extends, nil } // loadFiles loads a list of files, deep-merging values. func loadFiles(config interface{}, fnames []string) error { for _, fname := range fnames { data, err := ioutil.ReadFile(fname) if err != nil { return err } if err := yaml.Unmarshal(data, config); err != nil { return fmt.Errorf("unmarshal %s: %s", fname, err) } } // Validate on the merged config at the end. if err := validator.Validate(config); err != nil { return ValidationError{ errorMap: err.(validator.ErrorMap), } } return nil }