github.com/npaton/distribution@v2.3.1-rc.0+incompatible/configuration/configuration_test.go (about)

     1  package configuration
     2  
     3  import (
     4  	"bytes"
     5  	"net/http"
     6  	"os"
     7  	"reflect"
     8  	"strings"
     9  	"testing"
    10  
    11  	. "gopkg.in/check.v1"
    12  	"gopkg.in/yaml.v2"
    13  )
    14  
    15  // Hook up gocheck into the "go test" runner
    16  func Test(t *testing.T) { TestingT(t) }
    17  
    18  // configStruct is a canonical example configuration, which should map to configYamlV0_1
    19  var configStruct = Configuration{
    20  	Version: "0.1",
    21  	Log: struct {
    22  		Level     Loglevel               `yaml:"level"`
    23  		Formatter string                 `yaml:"formatter,omitempty"`
    24  		Fields    map[string]interface{} `yaml:"fields,omitempty"`
    25  		Hooks     []LogHook              `yaml:"hooks,omitempty"`
    26  	}{
    27  		Fields: map[string]interface{}{"environment": "test"},
    28  	},
    29  	Loglevel: "info",
    30  	Storage: Storage{
    31  		"s3": Parameters{
    32  			"region":        "us-east-1",
    33  			"bucket":        "my-bucket",
    34  			"rootdirectory": "/registry",
    35  			"encrypt":       true,
    36  			"secure":        false,
    37  			"accesskey":     "SAMPLEACCESSKEY",
    38  			"secretkey":     "SUPERSECRET",
    39  			"host":          nil,
    40  			"port":          42,
    41  		},
    42  	},
    43  	Auth: Auth{
    44  		"silly": Parameters{
    45  			"realm":   "silly",
    46  			"service": "silly",
    47  		},
    48  	},
    49  	Reporting: Reporting{
    50  		Bugsnag: BugsnagReporting{
    51  			APIKey: "BugsnagApiKey",
    52  		},
    53  	},
    54  	Notifications: Notifications{
    55  		Endpoints: []Endpoint{
    56  			{
    57  				Name: "endpoint-1",
    58  				URL:  "http://example.com",
    59  				Headers: http.Header{
    60  					"Authorization": []string{"Bearer <example>"},
    61  				},
    62  			},
    63  		},
    64  	},
    65  	HTTP: struct {
    66  		Addr   string `yaml:"addr,omitempty"`
    67  		Net    string `yaml:"net,omitempty"`
    68  		Host   string `yaml:"host,omitempty"`
    69  		Prefix string `yaml:"prefix,omitempty"`
    70  		Secret string `yaml:"secret,omitempty"`
    71  		TLS    struct {
    72  			Certificate string   `yaml:"certificate,omitempty"`
    73  			Key         string   `yaml:"key,omitempty"`
    74  			ClientCAs   []string `yaml:"clientcas,omitempty"`
    75  		} `yaml:"tls,omitempty"`
    76  		Headers http.Header `yaml:"headers,omitempty"`
    77  		Debug   struct {
    78  			Addr string `yaml:"addr,omitempty"`
    79  		} `yaml:"debug,omitempty"`
    80  	}{
    81  		TLS: struct {
    82  			Certificate string   `yaml:"certificate,omitempty"`
    83  			Key         string   `yaml:"key,omitempty"`
    84  			ClientCAs   []string `yaml:"clientcas,omitempty"`
    85  		}{
    86  			ClientCAs: []string{"/path/to/ca.pem"},
    87  		},
    88  		Headers: http.Header{
    89  			"X-Content-Type-Options": []string{"nosniff"},
    90  		},
    91  	},
    92  }
    93  
    94  // configYamlV0_1 is a Version 0.1 yaml document representing configStruct
    95  var configYamlV0_1 = `
    96  version: 0.1
    97  log:
    98    fields:
    99      environment: test
   100  loglevel: info
   101  storage:
   102    s3:
   103      region: us-east-1
   104      bucket: my-bucket
   105      rootdirectory: /registry
   106      encrypt: true
   107      secure: false
   108      accesskey: SAMPLEACCESSKEY
   109      secretkey: SUPERSECRET
   110      host: ~
   111      port: 42
   112  auth:
   113    silly:
   114      realm: silly
   115      service: silly
   116  notifications:
   117    endpoints:
   118      - name: endpoint-1
   119        url:  http://example.com
   120        headers:
   121          Authorization: [Bearer <example>]
   122  reporting:
   123    bugsnag:
   124      apikey: BugsnagApiKey
   125  http:
   126    clientcas:
   127      - /path/to/ca.pem
   128    headers:
   129      X-Content-Type-Options: [nosniff]
   130  `
   131  
   132  // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
   133  // storage driver with no parameters
   134  var inmemoryConfigYamlV0_1 = `
   135  version: 0.1
   136  loglevel: info
   137  storage: inmemory
   138  auth:
   139    silly:
   140      realm: silly
   141      service: silly
   142  notifications:
   143    endpoints:
   144      - name: endpoint-1
   145        url:  http://example.com
   146        headers:
   147          Authorization: [Bearer <example>]
   148  http:
   149    headers:
   150      X-Content-Type-Options: [nosniff]
   151  `
   152  
   153  type ConfigSuite struct {
   154  	expectedConfig *Configuration
   155  }
   156  
   157  var _ = Suite(new(ConfigSuite))
   158  
   159  func (suite *ConfigSuite) SetUpTest(c *C) {
   160  	os.Clearenv()
   161  	suite.expectedConfig = copyConfig(configStruct)
   162  }
   163  
   164  // TestMarshalRoundtrip validates that configStruct can be marshaled and
   165  // unmarshaled without changing any parameters
   166  func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) {
   167  	configBytes, err := yaml.Marshal(suite.expectedConfig)
   168  	c.Assert(err, IsNil)
   169  	config, err := Parse(bytes.NewReader(configBytes))
   170  	c.Assert(err, IsNil)
   171  	c.Assert(config, DeepEquals, suite.expectedConfig)
   172  }
   173  
   174  // TestParseSimple validates that configYamlV0_1 can be parsed into a struct
   175  // matching configStruct
   176  func (suite *ConfigSuite) TestParseSimple(c *C) {
   177  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   178  	c.Assert(err, IsNil)
   179  	c.Assert(config, DeepEquals, suite.expectedConfig)
   180  }
   181  
   182  // TestParseInmemory validates that configuration yaml with storage provided as
   183  // a string can be parsed into a Configuration struct with no storage parameters
   184  func (suite *ConfigSuite) TestParseInmemory(c *C) {
   185  	suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
   186  	suite.expectedConfig.Reporting = Reporting{}
   187  	suite.expectedConfig.Log.Fields = nil
   188  
   189  	config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
   190  	c.Assert(err, IsNil)
   191  	c.Assert(config, DeepEquals, suite.expectedConfig)
   192  }
   193  
   194  // TestParseIncomplete validates that an incomplete yaml configuration cannot
   195  // be parsed without providing environment variables to fill in the missing
   196  // components.
   197  func (suite *ConfigSuite) TestParseIncomplete(c *C) {
   198  	incompleteConfigYaml := "version: 0.1"
   199  	_, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
   200  	c.Assert(err, NotNil)
   201  
   202  	suite.expectedConfig.Log.Fields = nil
   203  	suite.expectedConfig.Storage = Storage{"filesystem": Parameters{"rootdirectory": "/tmp/testroot"}}
   204  	suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
   205  	suite.expectedConfig.Reporting = Reporting{}
   206  	suite.expectedConfig.Notifications = Notifications{}
   207  	suite.expectedConfig.HTTP.Headers = nil
   208  
   209  	// Note: this also tests that REGISTRY_STORAGE and
   210  	// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together
   211  	os.Setenv("REGISTRY_STORAGE", "filesystem")
   212  	os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
   213  	os.Setenv("REGISTRY_AUTH", "silly")
   214  	os.Setenv("REGISTRY_AUTH_SILLY_REALM", "silly")
   215  
   216  	config, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
   217  	c.Assert(err, IsNil)
   218  	c.Assert(config, DeepEquals, suite.expectedConfig)
   219  }
   220  
   221  // TestParseWithSameEnvStorage validates that providing environment variables
   222  // that match the given storage type will only include environment-defined
   223  // parameters and remove yaml-defined parameters
   224  func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) {
   225  	suite.expectedConfig.Storage = Storage{"s3": Parameters{"region": "us-east-1"}}
   226  
   227  	os.Setenv("REGISTRY_STORAGE", "s3")
   228  	os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1")
   229  
   230  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   231  	c.Assert(err, IsNil)
   232  	c.Assert(config, DeepEquals, suite.expectedConfig)
   233  }
   234  
   235  // TestParseWithDifferentEnvStorageParams validates that providing environment variables that change
   236  // and add to the given storage parameters will change and add parameters to the parsed
   237  // Configuration struct
   238  func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) {
   239  	suite.expectedConfig.Storage.setParameter("region", "us-west-1")
   240  	suite.expectedConfig.Storage.setParameter("secure", true)
   241  	suite.expectedConfig.Storage.setParameter("newparam", "some Value")
   242  
   243  	os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-west-1")
   244  	os.Setenv("REGISTRY_STORAGE_S3_SECURE", "true")
   245  	os.Setenv("REGISTRY_STORAGE_S3_NEWPARAM", "some Value")
   246  
   247  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   248  	c.Assert(err, IsNil)
   249  	c.Assert(config, DeepEquals, suite.expectedConfig)
   250  }
   251  
   252  // TestParseWithDifferentEnvStorageType validates that providing an environment variable that
   253  // changes the storage type will be reflected in the parsed Configuration struct
   254  func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) {
   255  	suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
   256  
   257  	os.Setenv("REGISTRY_STORAGE", "inmemory")
   258  
   259  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   260  	c.Assert(err, IsNil)
   261  	c.Assert(config, DeepEquals, suite.expectedConfig)
   262  }
   263  
   264  // TestParseWithDifferentEnvStorageTypeAndParams validates that providing an environment variable
   265  // that changes the storage type will be reflected in the parsed Configuration struct and that
   266  // environment storage parameters will also be included
   267  func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) {
   268  	suite.expectedConfig.Storage = Storage{"filesystem": Parameters{}}
   269  	suite.expectedConfig.Storage.setParameter("rootdirectory", "/tmp/testroot")
   270  
   271  	os.Setenv("REGISTRY_STORAGE", "filesystem")
   272  	os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
   273  
   274  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   275  	c.Assert(err, IsNil)
   276  	c.Assert(config, DeepEquals, suite.expectedConfig)
   277  }
   278  
   279  // TestParseWithSameEnvLoglevel validates that providing an environment variable defining the log
   280  // level to the same as the one provided in the yaml will not change the parsed Configuration struct
   281  func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) {
   282  	os.Setenv("REGISTRY_LOGLEVEL", "info")
   283  
   284  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   285  	c.Assert(err, IsNil)
   286  	c.Assert(config, DeepEquals, suite.expectedConfig)
   287  }
   288  
   289  // TestParseWithDifferentEnvLoglevel validates that providing an environment variable defining the
   290  // log level will override the value provided in the yaml document
   291  func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) {
   292  	suite.expectedConfig.Loglevel = "error"
   293  
   294  	os.Setenv("REGISTRY_LOGLEVEL", "error")
   295  
   296  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   297  	c.Assert(err, IsNil)
   298  	c.Assert(config, DeepEquals, suite.expectedConfig)
   299  }
   300  
   301  // TestParseInvalidLoglevel validates that the parser will fail to parse a
   302  // configuration if the loglevel is malformed
   303  func (suite *ConfigSuite) TestParseInvalidLoglevel(c *C) {
   304  	invalidConfigYaml := "version: 0.1\nloglevel: derp\nstorage: inmemory"
   305  	_, err := Parse(bytes.NewReader([]byte(invalidConfigYaml)))
   306  	c.Assert(err, NotNil)
   307  
   308  	os.Setenv("REGISTRY_LOGLEVEL", "derp")
   309  
   310  	_, err = Parse(bytes.NewReader([]byte(configYamlV0_1)))
   311  	c.Assert(err, NotNil)
   312  
   313  }
   314  
   315  // TestParseWithDifferentEnvReporting validates that environment variables
   316  // properly override reporting parameters
   317  func (suite *ConfigSuite) TestParseWithDifferentEnvReporting(c *C) {
   318  	suite.expectedConfig.Reporting.Bugsnag.APIKey = "anotherBugsnagApiKey"
   319  	suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
   320  	suite.expectedConfig.Reporting.NewRelic.LicenseKey = "NewRelicLicenseKey"
   321  	suite.expectedConfig.Reporting.NewRelic.Name = "some NewRelic NAME"
   322  
   323  	os.Setenv("REGISTRY_REPORTING_BUGSNAG_APIKEY", "anotherBugsnagApiKey")
   324  	os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
   325  	os.Setenv("REGISTRY_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
   326  	os.Setenv("REGISTRY_REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
   327  
   328  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   329  	c.Assert(err, IsNil)
   330  	c.Assert(config, DeepEquals, suite.expectedConfig)
   331  }
   332  
   333  // TestParseInvalidVersion validates that the parser will fail to parse a newer configuration
   334  // version than the CurrentVersion
   335  func (suite *ConfigSuite) TestParseInvalidVersion(c *C) {
   336  	suite.expectedConfig.Version = MajorMinorVersion(CurrentVersion.Major(), CurrentVersion.Minor()+1)
   337  	configBytes, err := yaml.Marshal(suite.expectedConfig)
   338  	c.Assert(err, IsNil)
   339  	_, err = Parse(bytes.NewReader(configBytes))
   340  	c.Assert(err, NotNil)
   341  }
   342  
   343  // TestParseExtraneousVars validates that environment variables referring to
   344  // nonexistent variables don't cause side effects.
   345  func (suite *ConfigSuite) TestParseExtraneousVars(c *C) {
   346  	suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
   347  
   348  	// A valid environment variable
   349  	os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
   350  
   351  	// Environment variables which shouldn't set config items
   352  	os.Setenv("registry_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
   353  	os.Setenv("REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
   354  	os.Setenv("REGISTRY_DUCKS", "quack")
   355  	os.Setenv("REGISTRY_REPORTING_ASDF", "ghjk")
   356  
   357  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   358  	c.Assert(err, IsNil)
   359  	c.Assert(config, DeepEquals, suite.expectedConfig)
   360  }
   361  
   362  // TestParseEnvVarImplicitMaps validates that environment variables can set
   363  // values in maps that don't already exist.
   364  func (suite *ConfigSuite) TestParseEnvVarImplicitMaps(c *C) {
   365  	readonly := make(map[string]interface{})
   366  	readonly["enabled"] = true
   367  
   368  	maintenance := make(map[string]interface{})
   369  	maintenance["readonly"] = readonly
   370  
   371  	suite.expectedConfig.Storage["maintenance"] = maintenance
   372  
   373  	os.Setenv("REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED", "true")
   374  
   375  	config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   376  	c.Assert(err, IsNil)
   377  	c.Assert(config, DeepEquals, suite.expectedConfig)
   378  }
   379  
   380  // TestParseEnvWrongTypeMap validates that incorrectly attempting to unmarshal a
   381  // string over existing map fails.
   382  func (suite *ConfigSuite) TestParseEnvWrongTypeMap(c *C) {
   383  	os.Setenv("REGISTRY_STORAGE_S3", "somestring")
   384  
   385  	_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   386  	c.Assert(err, NotNil)
   387  }
   388  
   389  // TestParseEnvWrongTypeStruct validates that incorrectly attempting to
   390  // unmarshal a string into a struct fails.
   391  func (suite *ConfigSuite) TestParseEnvWrongTypeStruct(c *C) {
   392  	os.Setenv("REGISTRY_STORAGE_LOG", "somestring")
   393  
   394  	_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   395  	c.Assert(err, NotNil)
   396  }
   397  
   398  // TestParseEnvWrongTypeSlice validates that incorrectly attempting to
   399  // unmarshal a string into a slice fails.
   400  func (suite *ConfigSuite) TestParseEnvWrongTypeSlice(c *C) {
   401  	os.Setenv("REGISTRY_LOG_HOOKS", "somestring")
   402  
   403  	_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   404  	c.Assert(err, NotNil)
   405  }
   406  
   407  // TestParseEnvMany tests several environment variable overrides.
   408  // The result is not checked - the goal of this test is to detect panics
   409  // from misuse of reflection.
   410  func (suite *ConfigSuite) TestParseEnvMany(c *C) {
   411  	os.Setenv("REGISTRY_VERSION", "0.1")
   412  	os.Setenv("REGISTRY_LOG_LEVEL", "debug")
   413  	os.Setenv("REGISTRY_LOG_FORMATTER", "json")
   414  	os.Setenv("REGISTRY_LOG_HOOKS", "json")
   415  	os.Setenv("REGISTRY_LOG_FIELDS", "abc: xyz")
   416  	os.Setenv("REGISTRY_LOG_HOOKS", "- type: asdf")
   417  	os.Setenv("REGISTRY_LOGLEVEL", "debug")
   418  	os.Setenv("REGISTRY_STORAGE", "s3")
   419  	os.Setenv("REGISTRY_AUTH_PARAMS", "param1: value1")
   420  	os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
   421  	os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
   422  
   423  	_, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
   424  	c.Assert(err, IsNil)
   425  }
   426  
   427  func checkStructs(c *C, t reflect.Type, structsChecked map[string]struct{}) {
   428  	for t.Kind() == reflect.Ptr || t.Kind() == reflect.Map || t.Kind() == reflect.Slice {
   429  		t = t.Elem()
   430  	}
   431  
   432  	if t.Kind() != reflect.Struct {
   433  		return
   434  	}
   435  	if _, present := structsChecked[t.String()]; present {
   436  		// Already checked this type
   437  		return
   438  	}
   439  
   440  	structsChecked[t.String()] = struct{}{}
   441  
   442  	byUpperCase := make(map[string]int)
   443  	for i := 0; i < t.NumField(); i++ {
   444  		sf := t.Field(i)
   445  
   446  		// Check that the yaml tag does not contain an _.
   447  		yamlTag := sf.Tag.Get("yaml")
   448  		if strings.Contains(yamlTag, "_") {
   449  			c.Fatalf("yaml field name includes _ character: %s", yamlTag)
   450  		}
   451  		upper := strings.ToUpper(sf.Name)
   452  		if _, present := byUpperCase[upper]; present {
   453  			c.Fatalf("field name collision in configuration object: %s", sf.Name)
   454  		}
   455  		byUpperCase[upper] = i
   456  
   457  		checkStructs(c, sf.Type, structsChecked)
   458  	}
   459  }
   460  
   461  // TestValidateConfigStruct makes sure that the config struct has no members
   462  // with yaml tags that would be ambiguous to the environment variable parser.
   463  func (suite *ConfigSuite) TestValidateConfigStruct(c *C) {
   464  	structsChecked := make(map[string]struct{})
   465  	checkStructs(c, reflect.TypeOf(Configuration{}), structsChecked)
   466  }
   467  
   468  func copyConfig(config Configuration) *Configuration {
   469  	configCopy := new(Configuration)
   470  
   471  	configCopy.Version = MajorMinorVersion(config.Version.Major(), config.Version.Minor())
   472  	configCopy.Loglevel = config.Loglevel
   473  	configCopy.Log = config.Log
   474  	configCopy.Log.Fields = make(map[string]interface{}, len(config.Log.Fields))
   475  	for k, v := range config.Log.Fields {
   476  		configCopy.Log.Fields[k] = v
   477  	}
   478  
   479  	configCopy.Storage = Storage{config.Storage.Type(): Parameters{}}
   480  	for k, v := range config.Storage.Parameters() {
   481  		configCopy.Storage.setParameter(k, v)
   482  	}
   483  	configCopy.Reporting = Reporting{
   484  		Bugsnag:  BugsnagReporting{config.Reporting.Bugsnag.APIKey, config.Reporting.Bugsnag.ReleaseStage, config.Reporting.Bugsnag.Endpoint},
   485  		NewRelic: NewRelicReporting{config.Reporting.NewRelic.LicenseKey, config.Reporting.NewRelic.Name, config.Reporting.NewRelic.Verbose},
   486  	}
   487  
   488  	configCopy.Auth = Auth{config.Auth.Type(): Parameters{}}
   489  	for k, v := range config.Auth.Parameters() {
   490  		configCopy.Auth.setParameter(k, v)
   491  	}
   492  
   493  	configCopy.Notifications = Notifications{Endpoints: []Endpoint{}}
   494  	for _, v := range config.Notifications.Endpoints {
   495  		configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v)
   496  	}
   497  
   498  	configCopy.HTTP.Headers = make(http.Header)
   499  	for k, v := range config.HTTP.Headers {
   500  		configCopy.HTTP.Headers[k] = v
   501  	}
   502  
   503  	return configCopy
   504  }