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  }