github.com/uber/kraken@v0.1.4/utils/configutil/config.go (about) 1 // Copyright (c) 2016-2019 Uber Technologies, Inc. 2 // 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 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // Package configutil provides an interface for loading and validating configuration 15 // data from YAML files. 16 // 17 // Other YAML files could be included via the following directive: 18 // 19 // production.yaml: 20 // extends: base.yaml 21 // 22 // There is no multiple inheritance supported. Dependency tree suppossed to 23 // form a linked list. 24 // 25 // 26 // Values from multiple configurations within the same hierarchy are deep merged 27 // 28 // Note regarding configuration merging: 29 // Array defined in YAML will be overriden based on load sequence. 30 // e.g. in the base.yaml: 31 // sports: 32 // - football 33 // in the development.yaml: 34 // extends: base.yaml 35 // sports: 36 // - basketball 37 // after the merge: 38 // sports: 39 // - basketball // only keep the latest one 40 // 41 // Map defined in YAML will be merged together based on load sequence. 42 // e.g. in the base.yaml: 43 // sports: 44 // football: true 45 // in the development.yaml: 46 // extends: base.yaml 47 // sports: 48 // basketball: true 49 // after the merge: 50 // sports: // combine all the map fields 51 // football: true 52 // basketball: true 53 // 54 package configutil 55 56 import ( 57 "bytes" 58 "errors" 59 "fmt" 60 "io/ioutil" 61 "path" 62 "path/filepath" 63 64 "github.com/uber/kraken/utils/stringset" 65 66 "gopkg.in/validator.v2" 67 "gopkg.in/yaml.v2" 68 ) 69 70 // ErrCycleRef is returned when there are circular dependencies detected in 71 // configuraiton files extending each other. 72 var ErrCycleRef = errors.New("cyclic reference in configuration extends detected") 73 74 // Extends define a keywoword in config for extending a base configuration file. 75 type Extends struct { 76 Extends string `yaml:"extends"` 77 } 78 79 // ValidationError is the returned when a configuration fails to pass 80 // validation. 81 type ValidationError struct { 82 errorMap validator.ErrorMap 83 } 84 85 // ErrForField returns the validation error for the given field. 86 func (e ValidationError) ErrForField(name string) error { 87 return e.errorMap[name] 88 } 89 90 // Error implements the `error` interface. 91 func (e ValidationError) Error() string { 92 var w bytes.Buffer 93 94 fmt.Fprintf(&w, "validation failed") 95 for f, err := range e.errorMap { 96 fmt.Fprintf(&w, " %s: %v\n", f, err) 97 } 98 99 return w.String() 100 } 101 102 // Load loads configuration based on config file name. It will 103 // follow extends directives and do a deep merge of those config 104 // files. 105 func Load(filename string, config interface{}) error { 106 filenames, err := resolveExtends(filename, readExtend) 107 if err != nil { 108 return err 109 } 110 return loadFiles(config, filenames) 111 } 112 113 type getExtend func(filename string) (extends string, err error) 114 115 // resolveExtends returns the list of config paths that the original config `filename` 116 // points to. 117 func resolveExtends(filename string, extendReader getExtend) ([]string, error) { 118 filenames := []string{filename} 119 seen := make(stringset.Set) 120 for { 121 extends, err := extendReader(filename) 122 if err != nil { 123 return nil, err 124 } else if extends == "" { 125 break 126 } 127 128 // If the file path of the extends field in the config is not absolute 129 // we assume that it is in the same directory as the current config 130 // file. 131 if !filepath.IsAbs(extends) { 132 extends = path.Join(filepath.Dir(filename), extends) 133 } 134 135 // Prevent circular references. 136 if seen.Has(extends) { 137 return nil, ErrCycleRef 138 } 139 140 filenames = append([]string{extends}, filenames...) 141 seen.Add(extends) 142 filename = extends 143 } 144 return filenames, nil 145 } 146 147 func readExtend(configFile string) (string, error) { 148 data, err := ioutil.ReadFile(configFile) 149 if err != nil { 150 return "", err 151 } 152 153 var cfg Extends 154 if err := yaml.Unmarshal(data, &cfg); err != nil { 155 return "", fmt.Errorf("unmarshal %s: %s", configFile, err) 156 } 157 return cfg.Extends, nil 158 } 159 160 // loadFiles loads a list of files, deep-merging values. 161 func loadFiles(config interface{}, fnames []string) error { 162 for _, fname := range fnames { 163 data, err := ioutil.ReadFile(fname) 164 if err != nil { 165 return err 166 } 167 168 if err := yaml.Unmarshal(data, config); err != nil { 169 return fmt.Errorf("unmarshal %s: %s", fname, err) 170 } 171 } 172 173 // Validate on the merged config at the end. 174 if err := validator.Validate(config); err != nil { 175 return ValidationError{ 176 errorMap: err.(validator.ErrorMap), 177 } 178 } 179 return nil 180 }