github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/config/config.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  // Package config provides utilities for loading configuration files.
    22  package config
    23  
    24  import (
    25  	"errors"
    26  	"io"
    27  	"os"
    28  	"reflect"
    29  	"strings"
    30  
    31  	"go.uber.org/config"
    32  	"go.uber.org/zap"
    33  	validator "gopkg.in/validator.v2"
    34  	"gopkg.in/yaml.v2"
    35  )
    36  
    37  const (
    38  	deprecatedPrefix = "Deprecated"
    39  )
    40  
    41  var (
    42  	errNoFilesToLoad = errors.New("attempt to load config with no files")
    43  
    44  	// osLookupEnv allows simple mocking of os.LookupEnv for the test of that
    45  	// code path. Use Expand option
    46  	// for most test cases instead.
    47  	osLookupEnv = os.LookupEnv
    48  )
    49  
    50  // Options is an options set used when parsing config.
    51  type Options struct {
    52  	DisableUnmarshalStrict bool
    53  	DisableValidate        bool
    54  
    55  	// Expand provides values for templated strings of the form ${KEY}.
    56  	// By default, we extract these values from the environment.
    57  	Expand config.LookupFunc
    58  }
    59  
    60  // LoadFile loads a config from a file.
    61  func LoadFile(dst interface{}, file string, opts Options) error {
    62  	return LoadFiles(dst, []string{file}, opts)
    63  }
    64  
    65  // LoadFiles loads a config from list of files. If value for a property is
    66  // present in multiple files, the value from the last file will be applied.
    67  // Validation is done after merging all values.
    68  func LoadFiles(dst interface{}, files []string, opts Options) error {
    69  	if len(files) == 0 {
    70  		return errNoFilesToLoad
    71  	}
    72  
    73  	yamlOpts := make([]config.YAMLOption, 0, len(files))
    74  	for _, name := range files {
    75  		yamlOpts = append(yamlOpts, config.File(name))
    76  	}
    77  
    78  	if opts.DisableUnmarshalStrict {
    79  		yamlOpts = append(yamlOpts, config.Permissive())
    80  	}
    81  
    82  	expand := opts.Expand
    83  	if expand == nil {
    84  		expand = osLookupEnv
    85  	}
    86  
    87  	yamlOpts = append(yamlOpts, config.Expand(expand))
    88  
    89  	provider, err := config.NewYAML(yamlOpts...)
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	if err := provider.Get(config.Root).Populate(dst); err != nil {
    95  		return err
    96  	}
    97  
    98  	if opts.DisableValidate {
    99  		return nil
   100  	}
   101  
   102  	return validator.Validate(dst)
   103  }
   104  
   105  // Dump writes the given configuration to stream dst as YAML.
   106  func Dump(cfg interface{}, dst io.Writer) error {
   107  	return yaml.NewEncoder(dst).Encode(cfg)
   108  }
   109  
   110  // deprecationCheck checks the config for deprecated fields and returns any in
   111  // slice of strings.
   112  func deprecationCheck(cfg interface{}, df []string) []string {
   113  	n := reflect.TypeOf(cfg).NumField()
   114  	for i := 0; i < n; i++ {
   115  		v := reflect.ValueOf(cfg).Field(i)
   116  		if v.Kind() == reflect.Struct {
   117  			df = deprecationCheck(v.Interface(), df)
   118  		}
   119  		name := reflect.TypeOf(cfg).Field(i).Name
   120  		if strings.HasPrefix(name, deprecatedPrefix) && !v.IsZero() {
   121  			// Exclude unset deprecated config values from
   122  			// raising warnings via checking IsZero.
   123  			df = append(df, name)
   124  		}
   125  	}
   126  	return df
   127  }
   128  
   129  // WarnOnDeprecation emits a warning for every deprecated field
   130  func WarnOnDeprecation(cfg interface{}, logger *zap.Logger) {
   131  	deprecatedFields := []string{}
   132  	for _, v := range deprecationCheck(cfg, deprecatedFields) {
   133  		logger.Warn("using deprecated configuration field", zap.String("field", v))
   134  	}
   135  }