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 }