github.com/m3db/m3@v1.5.0/src/x/config/config_test.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
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"os"
    28  	"testing"
    29  
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  	"go.uber.org/config"
    33  )
    34  
    35  const (
    36  	goodConfig = `
    37  listen_address: localhost:4385
    38  buffer_space: 1024
    39  servers:
    40      - server1:8090
    41      - server2:8010
    42  `
    43  	badConfigInvalidKey = `
    44  unknown_key: unknown_key_value
    45  listen_address: localhost:4385
    46  buffer_space: 1024
    47  servers:
    48      - server1:8090
    49      - server2:8010
    50  `
    51  	badConfigInvalidValue = `
    52  listen_address: localhost:4385
    53  buffer_space: 254
    54  servers:
    55      - server1:8090
    56      - server2:8010
    57  `
    58  )
    59  
    60  type configuration struct {
    61  	ListenAddress string   `yaml:"listen_address" validate:"nonzero"`
    62  	BufferSpace   int      `yaml:"buffer_space" validate:"min=255"`
    63  	Servers       []string `validate:"nonzero"`
    64  }
    65  
    66  type commitlogPolicyConfiguration struct {
    67  	FlushMaxBytes       int    `yaml:"flushMaxBytes" validate:"nonzero"`
    68  	FlushEvery          string `yaml:"flushEvery" validate:"nonzero"`
    69  	DeprecatedBlockSize int    `yaml:"blockSize"`
    70  }
    71  
    72  type configurationDeprecated struct {
    73  	ListenAddress string   `yaml:"listen_address" validate:"nonzero"`
    74  	BufferSpace   int      `yaml:"buffer_space" validate:"min=255"`
    75  	Servers       []string `validate:"nonzero"`
    76  	DeprecatedFoo string   `yaml:"foo"`
    77  	DeprecatedBar int      `yaml:"bar"`
    78  	DeprecatedBaz *int     `yaml:"baz"`
    79  }
    80  
    81  type nestedConfigurationDeprecated struct {
    82  	ListenAddress string                       `yaml:"listen_address" validate:"nonzero"`
    83  	BufferSpace   int                          `yaml:"buffer_space" validate:"min=255"`
    84  	Servers       []string                     `validate:"nonzero"`
    85  	CommitLog     commitlogPolicyConfiguration `yaml:"commitlog"`
    86  }
    87  
    88  type nestedConfigurationMultipleDeprecated struct {
    89  	ListenAddress      string                       `yaml:"listen_address" validate:"nonzero"`
    90  	BufferSpace        int                          `yaml:"buffer_space" validate:"min=255"`
    91  	Servers            []string                     `validate:"nonzero"`
    92  	CommitLog          commitlogPolicyConfiguration `yaml:"commitlog"`
    93  	DeprecatedMultiple configurationDeprecated      `yaml:"multiple"`
    94  }
    95  
    96  func TestLoadFile(t *testing.T) {
    97  	var cfg configuration
    98  
    99  	err := LoadFile(&cfg, "./no-config.yaml", Options{})
   100  	require.Error(t, err)
   101  
   102  	// invalid yaml file
   103  	err = LoadFile(&cfg, "./config.go", Options{})
   104  	require.Error(t, err)
   105  
   106  	fname := writeFile(t, goodConfig)
   107  	defer func() {
   108  		require.NoError(t, os.Remove(fname))
   109  	}()
   110  
   111  	err = LoadFile(&cfg, fname, Options{})
   112  	require.NoError(t, err)
   113  	require.Equal(t, "localhost:4385", cfg.ListenAddress)
   114  	require.Equal(t, 1024, cfg.BufferSpace)
   115  	require.Equal(t, []string{"server1:8090", "server2:8010"}, cfg.Servers)
   116  }
   117  
   118  func TestLoadWithInvalidFile(t *testing.T) {
   119  	var cfg configuration
   120  
   121  	// no file provided
   122  	err := LoadFiles(&cfg, nil, Options{})
   123  	require.Error(t, err)
   124  	require.Equal(t, errNoFilesToLoad, err)
   125  
   126  	// non-exist file provided
   127  	err = LoadFiles(&cfg, []string{"./no-config.yaml"}, Options{})
   128  	require.Error(t, err)
   129  
   130  	// invalid yaml file
   131  	err = LoadFiles(&cfg, []string{"./config.go"}, Options{})
   132  	require.Error(t, err)
   133  
   134  	fname := writeFile(t, goodConfig)
   135  	defer func() {
   136  		require.NoError(t, os.Remove(fname))
   137  	}()
   138  
   139  	// non-exist file in the file list
   140  	err = LoadFiles(&cfg, []string{fname, "./no-config.yaml"}, Options{})
   141  	require.Error(t, err)
   142  
   143  	// invalid file in the file list
   144  	err = LoadFiles(&cfg, []string{fname, "./config.go"}, Options{})
   145  	require.Error(t, err)
   146  }
   147  
   148  func TestLoadFileInvalidKey(t *testing.T) {
   149  	var cfg configuration
   150  
   151  	fname := writeFile(t, badConfigInvalidKey)
   152  	defer func() {
   153  		require.NoError(t, os.Remove(fname))
   154  	}()
   155  
   156  	err := LoadFile(&cfg, fname, Options{})
   157  	require.Error(t, err)
   158  }
   159  
   160  func TestLoadFileInvalidKeyDisableMarshalStrict(t *testing.T) {
   161  	var cfg configuration
   162  
   163  	fname := writeFile(t, badConfigInvalidKey)
   164  	defer func() {
   165  		require.NoError(t, os.Remove(fname))
   166  	}()
   167  
   168  	err := LoadFile(&cfg, fname, Options{DisableUnmarshalStrict: true})
   169  	require.NoError(t, err)
   170  	require.Equal(t, "localhost:4385", cfg.ListenAddress)
   171  	require.Equal(t, 1024, cfg.BufferSpace)
   172  	require.Equal(t, []string{"server1:8090", "server2:8010"}, cfg.Servers)
   173  }
   174  
   175  func TestLoadFileInvalidValue(t *testing.T) {
   176  	var cfg configuration
   177  
   178  	fname := writeFile(t, badConfigInvalidValue)
   179  	defer func() {
   180  		require.NoError(t, os.Remove(fname))
   181  	}()
   182  
   183  	err := LoadFile(&cfg, fname, Options{})
   184  	require.Error(t, err)
   185  }
   186  
   187  func TestLoadFileInvalidValueDisableValidate(t *testing.T) {
   188  	var cfg configuration
   189  
   190  	fname := writeFile(t, badConfigInvalidValue)
   191  	defer func() {
   192  		require.NoError(t, os.Remove(fname))
   193  	}()
   194  
   195  	err := LoadFile(&cfg, fname, Options{DisableValidate: true})
   196  	require.NoError(t, err)
   197  	require.Equal(t, "localhost:4385", cfg.ListenAddress)
   198  	require.Equal(t, 254, cfg.BufferSpace)
   199  	require.Equal(t, []string{"server1:8090", "server2:8010"}, cfg.Servers)
   200  }
   201  
   202  func TestLoadFilesExtends(t *testing.T) {
   203  	fname := writeFile(t, goodConfig)
   204  	defer func() {
   205  		require.NoError(t, os.Remove(fname))
   206  	}()
   207  
   208  	partialConfig := `
   209  buffer_space: 8080
   210  servers:
   211      - server3:8080
   212      - server4:8080
   213  `
   214  	partial := writeFile(t, partialConfig)
   215  	defer func() {
   216  		require.NoError(t, os.Remove(partial))
   217  	}()
   218  
   219  	var cfg configuration
   220  	err := LoadFiles(&cfg, []string{fname, partial}, Options{})
   221  	require.NoError(t, err)
   222  
   223  	require.Equal(t, "localhost:4385", cfg.ListenAddress)
   224  	require.Equal(t, 8080, cfg.BufferSpace)
   225  	require.Equal(t, []string{"server3:8080", "server4:8080"}, cfg.Servers)
   226  }
   227  
   228  func TestLoadFilesDeepExtends(t *testing.T) {
   229  	type innerConfig struct {
   230  		K1 string `yaml:"k1"`
   231  		K2 string `yaml:"k2"`
   232  	}
   233  
   234  	type nestedConfig struct {
   235  		Foo innerConfig `yaml:"foo"`
   236  	}
   237  
   238  	const (
   239  		base = `
   240  foo:
   241    k1: v1_base
   242    k2: v2_base
   243  `
   244  		override = `
   245  foo:
   246    k1: v1_override
   247  `
   248  	)
   249  
   250  	baseFile, overrideFile := writeFile(t, base), writeFile(t, override)
   251  
   252  	var cfg nestedConfig
   253  	require.NoError(t, LoadFiles(&cfg, []string{baseFile, overrideFile}, Options{}))
   254  
   255  	assert.Equal(t, nestedConfig{
   256  		Foo: innerConfig{
   257  			K1: "v1_override",
   258  			K2: "v2_base",
   259  		},
   260  	}, cfg)
   261  
   262  }
   263  
   264  func TestLoadFilesValidateOnce(t *testing.T) {
   265  	const invalidConfig1 = `
   266      listen_address:
   267      buffer_space: 256
   268      servers:
   269      `
   270  
   271  	const invalidConfig2 = `
   272      listen_address: "localhost:8080"
   273      servers:
   274        - server2:8010
   275      `
   276  
   277  	fname1 := writeFile(t, invalidConfig1)
   278  	defer func() {
   279  		require.NoError(t, os.Remove(fname1))
   280  	}()
   281  
   282  	fname2 := writeFile(t, invalidConfig2)
   283  	defer func() {
   284  		require.NoError(t, os.Remove(fname2))
   285  	}()
   286  
   287  	// Either config by itself will not pass validation.
   288  	var cfg1 configuration
   289  	err := LoadFiles(&cfg1, []string{fname1}, Options{})
   290  	require.Error(t, err)
   291  
   292  	var cfg2 configuration
   293  	err = LoadFiles(&cfg2, []string{fname2}, Options{})
   294  	require.Error(t, err)
   295  
   296  	// But merging load has no error.
   297  	var mergedCfg configuration
   298  	err = LoadFiles(&mergedCfg, []string{fname1, fname2}, Options{})
   299  	require.NoError(t, err)
   300  
   301  	require.Equal(t, "localhost:8080", mergedCfg.ListenAddress)
   302  	require.Equal(t, 256, mergedCfg.BufferSpace)
   303  	require.Equal(t, []string{"server2:8010"}, mergedCfg.Servers)
   304  }
   305  
   306  func TestLoadFilesEnvExpansion(t *testing.T) {
   307  	const withEnv = `
   308      listen_address: localhost:${PORT:8080}
   309      buffer_space: ${BUFFER_SPACE}
   310      servers:
   311        - server2:8010
   312      `
   313  
   314  	mapLookup := func(m map[string]string) config.LookupFunc {
   315  		return func(s string) (string, bool) {
   316  			r, ok := m[s]
   317  			return r, ok
   318  		}
   319  	}
   320  
   321  	newMapLookupOptions := func(m map[string]string) Options {
   322  		return Options{
   323  			Expand: mapLookup(m),
   324  		}
   325  	}
   326  
   327  	type testCase struct {
   328  		Name        string
   329  		Options     Options
   330  		Expected    configuration
   331  		ExpectedErr error
   332  	}
   333  
   334  	cases := []testCase{{
   335  		Name: "all_provided",
   336  		Options: newMapLookupOptions(map[string]string{
   337  			"PORT":         "9090",
   338  			"BUFFER_SPACE": "256",
   339  		}),
   340  		Expected: configuration{
   341  			ListenAddress: "localhost:9090",
   342  			BufferSpace:   256,
   343  			Servers:       []string{"server2:8010"},
   344  		},
   345  	}, {
   346  		Name: "missing_no_default",
   347  		Options: newMapLookupOptions(map[string]string{
   348  			"PORT": "9090",
   349  			// missing BUFFER_SPACE,
   350  		}),
   351  		ExpectedErr: errors.New("couldn't expand environment: default is empty for \"BUFFER_SPACE\" (use \"\" for empty string)"),
   352  	}, {
   353  		Name: "missing_with_default",
   354  		Options: newMapLookupOptions(map[string]string{
   355  			"BUFFER_SPACE": "256",
   356  		}),
   357  		Expected: configuration{
   358  			ListenAddress: "localhost:8080",
   359  			BufferSpace:   256,
   360  			Servers:       []string{"server2:8010"},
   361  		},
   362  	}}
   363  
   364  	doTest := func(t *testing.T, tc testCase) {
   365  		fname1 := writeFile(t, withEnv)
   366  		defer func() {
   367  			require.NoError(t, os.Remove(fname1))
   368  		}()
   369  		var cfg configuration
   370  
   371  		err := LoadFile(&cfg, fname1, tc.Options)
   372  		if tc.ExpectedErr != nil {
   373  			require.EqualError(t, err, tc.ExpectedErr.Error())
   374  			return
   375  		}
   376  
   377  		require.NoError(t, err)
   378  		assert.Equal(t, tc.Expected, cfg)
   379  	}
   380  
   381  	for _, tc := range cases {
   382  		t.Run(tc.Name, func(t *testing.T) {
   383  			doTest(t, tc)
   384  		})
   385  	}
   386  
   387  	t.Run("uses os.LookupEnv by default", func(t *testing.T) {
   388  		curOSLookupEnv := osLookupEnv
   389  		osLookupEnv = mapLookup(map[string]string{
   390  			"PORT":         "9090",
   391  			"BUFFER_SPACE": "256",
   392  		})
   393  		defer func() {
   394  			osLookupEnv = curOSLookupEnv
   395  		}()
   396  
   397  		doTest(t, testCase{
   398  			// use defaults
   399  			Options: Options{},
   400  			Expected: configuration{
   401  				ListenAddress: "localhost:9090",
   402  				BufferSpace:   256,
   403  				Servers:       []string{"server2:8010"},
   404  			},
   405  		})
   406  	})
   407  }
   408  
   409  func TestDeprecationCheck(t *testing.T) {
   410  	t.Run("StandardConfig", func(t *testing.T) {
   411  		// OK
   412  		var cfg configuration
   413  		fname := writeFile(t, goodConfig)
   414  		defer func() {
   415  			require.NoError(t, os.Remove(fname))
   416  		}()
   417  
   418  		err := LoadFile(&cfg, fname, Options{})
   419  		require.NoError(t, err)
   420  
   421  		df := []string{}
   422  		ss := deprecationCheck(cfg, df)
   423  		require.Len(t, ss, 0)
   424  
   425  		// Deprecated
   426  		badConfig := `
   427  listen_address: localhost:4385
   428  buffer_space: 1024
   429  servers:
   430    - server1:8090
   431    - server2:8010
   432  foo: ok
   433  bar: 42
   434  `
   435  		var cfg2 configurationDeprecated
   436  		fname2 := writeFile(t, badConfig)
   437  		defer func() {
   438  			require.NoError(t, os.Remove(fname2))
   439  		}()
   440  
   441  		err = LoadFile(&cfg2, fname2, Options{})
   442  		require.NoError(t, err)
   443  
   444  		actual := deprecationCheck(cfg2, df)
   445  		expect := []string{"DeprecatedFoo", "DeprecatedBar"}
   446  		require.Equal(t, len(expect), len(actual),
   447  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   448  		require.Equal(t, expect, actual,
   449  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   450  	})
   451  
   452  	t.Run("DeprecatedZeroValue", func(t *testing.T) {
   453  		// OK
   454  		var cfg configuration
   455  		fname := writeFile(t, goodConfig)
   456  		defer func() {
   457  			require.NoError(t, os.Remove(fname))
   458  		}()
   459  
   460  		err := LoadFile(&cfg, fname, Options{})
   461  		require.NoError(t, err)
   462  
   463  		df := []string{}
   464  		ss := deprecationCheck(cfg, df)
   465  		require.Equal(t, 0, len(ss))
   466  
   467  		// Deprecated zero value should be ok and not printed
   468  		badConfig := `
   469  listen_address: localhost:4385
   470  buffer_space: 1024
   471  servers:
   472    - server1:8090
   473    - server2:8010
   474  foo: ok
   475  bar: 42
   476  baz: null
   477  `
   478  		var cfg2 configurationDeprecated
   479  		fname2 := writeFile(t, badConfig)
   480  		defer func() {
   481  			require.NoError(t, os.Remove(fname2))
   482  		}()
   483  
   484  		err = LoadFile(&cfg2, fname2, Options{})
   485  		require.NoError(t, err)
   486  
   487  		actual := deprecationCheck(cfg2, df)
   488  		expect := []string{"DeprecatedFoo", "DeprecatedBar"}
   489  		require.Equal(t, len(expect), len(actual),
   490  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   491  		require.Equal(t, expect, actual,
   492  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   493  	})
   494  
   495  	t.Run("DeprecatedNilValue", func(t *testing.T) {
   496  		// OK
   497  		var cfg configuration
   498  		fname := writeFile(t, goodConfig)
   499  		defer func() {
   500  			require.NoError(t, os.Remove(fname))
   501  		}()
   502  
   503  		err := LoadFile(&cfg, fname, Options{})
   504  		require.NoError(t, err)
   505  
   506  		df := []string{}
   507  		ss := deprecationCheck(cfg, df)
   508  		require.Equal(t, 0, len(ss))
   509  
   510  		// Deprecated nil/unset value should be ok and not printed
   511  		validConfig := `
   512  listen_address: localhost:4385
   513  buffer_space: 1024
   514  servers:
   515    - server1:8090
   516    - server2:8010
   517  `
   518  		var cfg2 configurationDeprecated
   519  		fname2 := writeFile(t, validConfig)
   520  		defer func() {
   521  			require.NoError(t, os.Remove(fname2))
   522  		}()
   523  
   524  		err = LoadFile(&cfg2, fname2, Options{})
   525  		require.NoError(t, err)
   526  
   527  		actual := deprecationCheck(cfg2, df)
   528  		require.Equal(t, 0, len(actual),
   529  			fmt.Sprintf("expect %#v should be equal actual %#v", 0, actual))
   530  	})
   531  
   532  	t.Run("NestedConfig", func(t *testing.T) {
   533  		// Single Deprecation
   534  		var cfg nestedConfigurationDeprecated
   535  		nc := `
   536  listen_address: localhost:4385
   537  buffer_space: 1024
   538  servers:
   539    - server1:8090
   540    - server2:8010
   541  commitlog:
   542    flushMaxBytes: 42
   543    flushEvery: second
   544    blockSize: 23
   545  `
   546  		fname := writeFile(t, nc)
   547  		defer func() {
   548  			require.NoError(t, os.Remove(fname))
   549  		}()
   550  
   551  		err := LoadFile(&cfg, fname, Options{})
   552  		require.NoError(t, err)
   553  
   554  		df := []string{}
   555  		actual := deprecationCheck(cfg, df)
   556  		expect := []string{"DeprecatedBlockSize"}
   557  		require.Equal(t, len(expect), len(actual),
   558  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   559  		require.Equal(t, expect, actual,
   560  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   561  
   562  		// Multiple deprecation
   563  		var cfg2 nestedConfigurationMultipleDeprecated
   564  		nc = `
   565  listen_address: localhost:4385
   566  buffer_space: 1024
   567  servers:
   568    - server1:8090
   569    - server2:8010
   570  commitlog:
   571    flushMaxBytes: 42
   572    flushEvery: second
   573  multiple:
   574    listen_address: localhost:4385
   575    buffer_space: 1024
   576    servers:
   577      - server1:8090
   578      - server2:8010
   579    foo: ok
   580    bar: 42
   581  `
   582  
   583  		fname2 := writeFile(t, nc)
   584  		defer func() {
   585  			require.NoError(t, os.Remove(fname2))
   586  		}()
   587  
   588  		err = LoadFile(&cfg2, fname2, Options{})
   589  		require.NoError(t, err)
   590  
   591  		df = []string{}
   592  		actual = deprecationCheck(cfg2, df)
   593  		expect = []string{
   594  			"DeprecatedMultiple",
   595  			"DeprecatedFoo",
   596  			"DeprecatedBar",
   597  		}
   598  		require.Equal(t, len(expect), len(actual),
   599  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   600  		require.True(t, slicesContainSameStrings(expect, actual),
   601  			fmt.Sprintf("expect %#v should be equal actual %#v", expect, actual))
   602  	})
   603  }
   604  
   605  func slicesContainSameStrings(s1, s2 []string) bool {
   606  	if len(s1) != len(s2) {
   607  		return false
   608  	}
   609  
   610  	m := make(map[string]bool, len(s1))
   611  	for _, v := range s1 {
   612  		m[v] = true
   613  	}
   614  	for _, v := range s2 {
   615  		if _, ok := m[v]; !ok {
   616  			return false
   617  		}
   618  	}
   619  	return true
   620  }
   621  
   622  func writeFile(t *testing.T, contents string) string {
   623  	f, err := ioutil.TempFile("", "configtest")
   624  	require.NoError(t, err)
   625  
   626  	defer f.Close()
   627  
   628  	_, err = f.Write([]byte(contents))
   629  	require.NoError(t, err)
   630  
   631  	return f.Name()
   632  }