1 package configuration
2
3 import (
4 "bytes"
5 "net/http"
6 "os"
7 "reflect"
8 "strings"
9 "testing"
10 "time"
11
12 . "gopkg.in/check.v1"
13 "gopkg.in/yaml.v2"
14 )
15
16
17 func Test(t *testing.T) { TestingT(t) }
18
19
20 var configStruct = Configuration{
21 Version: "0.1",
22 Log: struct {
23 AccessLog struct {
24 Disabled bool `yaml:"disabled,omitempty"`
25 } `yaml:"accesslog,omitempty"`
26 Level Loglevel `yaml:"level,omitempty"`
27 Formatter string `yaml:"formatter,omitempty"`
28 Fields map[string]interface{} `yaml:"fields,omitempty"`
29 Hooks []LogHook `yaml:"hooks,omitempty"`
30 }{
31 Level: "info",
32 Fields: map[string]interface{}{"environment": "test"},
33 },
34 Storage: Storage{
35 "s3": Parameters{
36 "region": "us-east-1",
37 "bucket": "my-bucket",
38 "rootdirectory": "/registry",
39 "encrypt": true,
40 "secure": false,
41 "accesskey": "SAMPLEACCESSKEY",
42 "secretkey": "SUPERSECRET",
43 "host": nil,
44 "port": 42,
45 },
46 },
47 Auth: Auth{
48 "silly": Parameters{
49 "realm": "silly",
50 "service": "silly",
51 },
52 },
53 Reporting: Reporting{
54 Bugsnag: BugsnagReporting{
55 APIKey: "BugsnagApiKey",
56 },
57 },
58 Notifications: Notifications{
59 Endpoints: []Endpoint{
60 {
61 Name: "endpoint-1",
62 URL: "http://example.com",
63 Headers: http.Header{
64 "Authorization": []string{"Bearer <example>"},
65 },
66 IgnoredMediaTypes: []string{"application/octet-stream"},
67 Ignore: Ignore{
68 MediaTypes: []string{"application/octet-stream"},
69 Actions: []string{"pull"},
70 },
71 },
72 },
73 },
74 Catalog: Catalog{
75 MaxEntries: 1000,
76 },
77 HTTP: struct {
78 Addr string `yaml:"addr,omitempty"`
79 Net string `yaml:"net,omitempty"`
80 Host string `yaml:"host,omitempty"`
81 Prefix string `yaml:"prefix,omitempty"`
82 Secret string `yaml:"secret,omitempty"`
83 RelativeURLs bool `yaml:"relativeurls,omitempty"`
84 DrainTimeout time.Duration `yaml:"draintimeout,omitempty"`
85 TLS struct {
86 Certificate string `yaml:"certificate,omitempty"`
87 Key string `yaml:"key,omitempty"`
88 ClientCAs []string `yaml:"clientcas,omitempty"`
89 MinimumTLS string `yaml:"minimumtls,omitempty"`
90 CipherSuites []string `yaml:"ciphersuites,omitempty"`
91 LetsEncrypt struct {
92 CacheFile string `yaml:"cachefile,omitempty"`
93 Email string `yaml:"email,omitempty"`
94 Hosts []string `yaml:"hosts,omitempty"`
95 } `yaml:"letsencrypt,omitempty"`
96 } `yaml:"tls,omitempty"`
97 Headers http.Header `yaml:"headers,omitempty"`
98 Debug struct {
99 Addr string `yaml:"addr,omitempty"`
100 Prometheus struct {
101 Enabled bool `yaml:"enabled,omitempty"`
102 Path string `yaml:"path,omitempty"`
103 } `yaml:"prometheus,omitempty"`
104 } `yaml:"debug,omitempty"`
105 HTTP2 struct {
106 Disabled bool `yaml:"disabled,omitempty"`
107 } `yaml:"http2,omitempty"`
108 }{
109 TLS: struct {
110 Certificate string `yaml:"certificate,omitempty"`
111 Key string `yaml:"key,omitempty"`
112 ClientCAs []string `yaml:"clientcas,omitempty"`
113 MinimumTLS string `yaml:"minimumtls,omitempty"`
114 CipherSuites []string `yaml:"ciphersuites,omitempty"`
115 LetsEncrypt struct {
116 CacheFile string `yaml:"cachefile,omitempty"`
117 Email string `yaml:"email,omitempty"`
118 Hosts []string `yaml:"hosts,omitempty"`
119 } `yaml:"letsencrypt,omitempty"`
120 }{
121 ClientCAs: []string{"/path/to/ca.pem"},
122 },
123 Headers: http.Header{
124 "X-Content-Type-Options": []string{"nosniff"},
125 },
126 HTTP2: struct {
127 Disabled bool `yaml:"disabled,omitempty"`
128 }{
129 Disabled: false,
130 },
131 },
132 }
133
134
135 var configYamlV0_1 = `
136 version: 0.1
137 log:
138 level: info
139 fields:
140 environment: test
141 storage:
142 s3:
143 region: us-east-1
144 bucket: my-bucket
145 rootdirectory: /registry
146 encrypt: true
147 secure: false
148 accesskey: SAMPLEACCESSKEY
149 secretkey: SUPERSECRET
150 host: ~
151 port: 42
152 auth:
153 silly:
154 realm: silly
155 service: silly
156 notifications:
157 endpoints:
158 - name: endpoint-1
159 url: http://example.com
160 headers:
161 Authorization: [Bearer <example>]
162 ignoredmediatypes:
163 - application/octet-stream
164 ignore:
165 mediatypes:
166 - application/octet-stream
167 actions:
168 - pull
169 reporting:
170 bugsnag:
171 apikey: BugsnagApiKey
172 http:
173 clientcas:
174 - /path/to/ca.pem
175 headers:
176 X-Content-Type-Options: [nosniff]
177 `
178
179
180
181 var inmemoryConfigYamlV0_1 = `
182 version: 0.1
183 log:
184 level: info
185 storage: inmemory
186 auth:
187 silly:
188 realm: silly
189 service: silly
190 notifications:
191 endpoints:
192 - name: endpoint-1
193 url: http://example.com
194 headers:
195 Authorization: [Bearer <example>]
196 ignoredmediatypes:
197 - application/octet-stream
198 ignore:
199 mediatypes:
200 - application/octet-stream
201 actions:
202 - pull
203 http:
204 headers:
205 X-Content-Type-Options: [nosniff]
206 `
207
208 type ConfigSuite struct {
209 expectedConfig *Configuration
210 }
211
212 var _ = Suite(new(ConfigSuite))
213
214 func (suite *ConfigSuite) SetUpTest(c *C) {
215 os.Clearenv()
216 suite.expectedConfig = copyConfig(configStruct)
217 }
218
219
220
221 func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) {
222 configBytes, err := yaml.Marshal(suite.expectedConfig)
223 c.Assert(err, IsNil)
224 config, err := Parse(bytes.NewReader(configBytes))
225 c.Log(string(configBytes))
226 c.Assert(err, IsNil)
227 c.Assert(config, DeepEquals, suite.expectedConfig)
228 }
229
230
231
232 func (suite *ConfigSuite) TestParseSimple(c *C) {
233 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
234 c.Assert(err, IsNil)
235 c.Assert(config, DeepEquals, suite.expectedConfig)
236 }
237
238
239
240 func (suite *ConfigSuite) TestParseInmemory(c *C) {
241 suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
242 suite.expectedConfig.Reporting = Reporting{}
243 suite.expectedConfig.Log.Fields = nil
244
245 config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
246 c.Assert(err, IsNil)
247 c.Assert(config, DeepEquals, suite.expectedConfig)
248 }
249
250
251
252
253 func (suite *ConfigSuite) TestParseIncomplete(c *C) {
254 incompleteConfigYaml := "version: 0.1"
255 _, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
256 c.Assert(err, NotNil)
257
258 suite.expectedConfig.Log.Fields = nil
259 suite.expectedConfig.Storage = Storage{"filesystem": Parameters{"rootdirectory": "/tmp/testroot"}}
260 suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
261 suite.expectedConfig.Reporting = Reporting{}
262 suite.expectedConfig.Notifications = Notifications{}
263 suite.expectedConfig.HTTP.Headers = nil
264
265
266
267 os.Setenv("REGISTRY_STORAGE", "filesystem")
268 os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
269 os.Setenv("REGISTRY_AUTH", "silly")
270 os.Setenv("REGISTRY_AUTH_SILLY_REALM", "silly")
271
272 config, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
273 c.Assert(err, IsNil)
274 c.Assert(config, DeepEquals, suite.expectedConfig)
275 }
276
277
278
279
280 func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) {
281 suite.expectedConfig.Storage = Storage{"s3": Parameters{"region": "us-east-1"}}
282
283 os.Setenv("REGISTRY_STORAGE", "s3")
284 os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1")
285
286 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
287 c.Assert(err, IsNil)
288 c.Assert(config, DeepEquals, suite.expectedConfig)
289 }
290
291
292
293
294 func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) {
295 suite.expectedConfig.Storage.setParameter("region", "us-west-1")
296 suite.expectedConfig.Storage.setParameter("secure", true)
297 suite.expectedConfig.Storage.setParameter("newparam", "some Value")
298
299 os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-west-1")
300 os.Setenv("REGISTRY_STORAGE_S3_SECURE", "true")
301 os.Setenv("REGISTRY_STORAGE_S3_NEWPARAM", "some Value")
302
303 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
304 c.Assert(err, IsNil)
305 c.Assert(config, DeepEquals, suite.expectedConfig)
306 }
307
308
309
310 func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) {
311 suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
312
313 os.Setenv("REGISTRY_STORAGE", "inmemory")
314
315 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
316 c.Assert(err, IsNil)
317 c.Assert(config, DeepEquals, suite.expectedConfig)
318 }
319
320
321
322
323 func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) {
324 suite.expectedConfig.Storage = Storage{"filesystem": Parameters{}}
325 suite.expectedConfig.Storage.setParameter("rootdirectory", "/tmp/testroot")
326
327 os.Setenv("REGISTRY_STORAGE", "filesystem")
328 os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
329
330 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
331 c.Assert(err, IsNil)
332 c.Assert(config, DeepEquals, suite.expectedConfig)
333 }
334
335
336
337 func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) {
338 os.Setenv("REGISTRY_LOGLEVEL", "info")
339
340 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
341 c.Assert(err, IsNil)
342 c.Assert(config, DeepEquals, suite.expectedConfig)
343 }
344
345
346
347 func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) {
348 suite.expectedConfig.Log.Level = "error"
349
350 os.Setenv("REGISTRY_LOG_LEVEL", "error")
351
352 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
353 c.Assert(err, IsNil)
354 c.Assert(config, DeepEquals, suite.expectedConfig)
355 }
356
357
358
359 func (suite *ConfigSuite) TestParseInvalidLoglevel(c *C) {
360 invalidConfigYaml := "version: 0.1\nloglevel: derp\nstorage: inmemory"
361 _, err := Parse(bytes.NewReader([]byte(invalidConfigYaml)))
362 c.Assert(err, NotNil)
363
364 os.Setenv("REGISTRY_LOGLEVEL", "derp")
365
366 _, err = Parse(bytes.NewReader([]byte(configYamlV0_1)))
367 c.Assert(err, NotNil)
368
369 }
370
371
372
373 func (suite *ConfigSuite) TestParseWithDifferentEnvReporting(c *C) {
374 suite.expectedConfig.Reporting.Bugsnag.APIKey = "anotherBugsnagApiKey"
375 suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
376 suite.expectedConfig.Reporting.NewRelic.LicenseKey = "NewRelicLicenseKey"
377 suite.expectedConfig.Reporting.NewRelic.Name = "some NewRelic NAME"
378
379 os.Setenv("REGISTRY_REPORTING_BUGSNAG_APIKEY", "anotherBugsnagApiKey")
380 os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
381 os.Setenv("REGISTRY_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
382 os.Setenv("REGISTRY_REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
383
384 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
385 c.Assert(err, IsNil)
386 c.Assert(config, DeepEquals, suite.expectedConfig)
387 }
388
389
390
391 func (suite *ConfigSuite) TestParseInvalidVersion(c *C) {
392 suite.expectedConfig.Version = MajorMinorVersion(CurrentVersion.Major(), CurrentVersion.Minor()+1)
393 configBytes, err := yaml.Marshal(suite.expectedConfig)
394 c.Assert(err, IsNil)
395 _, err = Parse(bytes.NewReader(configBytes))
396 c.Assert(err, NotNil)
397 }
398
399
400
401 func (suite *ConfigSuite) TestParseExtraneousVars(c *C) {
402 suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
403
404
405 os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
406
407
408 os.Setenv("registry_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
409 os.Setenv("REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
410 os.Setenv("REGISTRY_DUCKS", "quack")
411 os.Setenv("REGISTRY_REPORTING_ASDF", "ghjk")
412
413 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
414 c.Assert(err, IsNil)
415 c.Assert(config, DeepEquals, suite.expectedConfig)
416 }
417
418
419
420 func (suite *ConfigSuite) TestParseEnvVarImplicitMaps(c *C) {
421 readonly := make(map[string]interface{})
422 readonly["enabled"] = true
423
424 maintenance := make(map[string]interface{})
425 maintenance["readonly"] = readonly
426
427 suite.expectedConfig.Storage["maintenance"] = maintenance
428
429 os.Setenv("REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED", "true")
430
431 config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
432 c.Assert(err, IsNil)
433 c.Assert(config, DeepEquals, suite.expectedConfig)
434 }
435
436
437
438 func (suite *ConfigSuite) TestParseEnvWrongTypeMap(c *C) {
439 os.Setenv("REGISTRY_STORAGE_S3", "somestring")
440
441 _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
442 c.Assert(err, NotNil)
443 }
444
445
446
447 func (suite *ConfigSuite) TestParseEnvWrongTypeStruct(c *C) {
448 os.Setenv("REGISTRY_STORAGE_LOG", "somestring")
449
450 _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
451 c.Assert(err, NotNil)
452 }
453
454
455
456 func (suite *ConfigSuite) TestParseEnvWrongTypeSlice(c *C) {
457 os.Setenv("REGISTRY_LOG_HOOKS", "somestring")
458
459 _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
460 c.Assert(err, NotNil)
461 }
462
463
464
465
466 func (suite *ConfigSuite) TestParseEnvMany(c *C) {
467 os.Setenv("REGISTRY_VERSION", "0.1")
468 os.Setenv("REGISTRY_LOG_LEVEL", "debug")
469 os.Setenv("REGISTRY_LOG_FORMATTER", "json")
470 os.Setenv("REGISTRY_LOG_HOOKS", "json")
471 os.Setenv("REGISTRY_LOG_FIELDS", "abc: xyz")
472 os.Setenv("REGISTRY_LOG_HOOKS", "- type: asdf")
473 os.Setenv("REGISTRY_LOGLEVEL", "debug")
474 os.Setenv("REGISTRY_STORAGE", "s3")
475 os.Setenv("REGISTRY_AUTH_PARAMS", "param1: value1")
476 os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
477 os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
478
479 _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
480 c.Assert(err, IsNil)
481 }
482
483 func checkStructs(c *C, t reflect.Type, structsChecked map[string]struct{}) {
484 for t.Kind() == reflect.Ptr || t.Kind() == reflect.Map || t.Kind() == reflect.Slice {
485 t = t.Elem()
486 }
487
488 if t.Kind() != reflect.Struct {
489 return
490 }
491 if _, present := structsChecked[t.String()]; present {
492
493 return
494 }
495
496 structsChecked[t.String()] = struct{}{}
497
498 byUpperCase := make(map[string]int)
499 for i := 0; i < t.NumField(); i++ {
500 sf := t.Field(i)
501
502
503 yamlTag := sf.Tag.Get("yaml")
504 if strings.Contains(yamlTag, "_") {
505 c.Fatalf("yaml field name includes _ character: %s", yamlTag)
506 }
507 upper := strings.ToUpper(sf.Name)
508 if _, present := byUpperCase[upper]; present {
509 c.Fatalf("field name collision in configuration object: %s", sf.Name)
510 }
511 byUpperCase[upper] = i
512
513 checkStructs(c, sf.Type, structsChecked)
514 }
515 }
516
517
518
519 func (suite *ConfigSuite) TestValidateConfigStruct(c *C) {
520 structsChecked := make(map[string]struct{})
521 checkStructs(c, reflect.TypeOf(Configuration{}), structsChecked)
522 }
523
524 func copyConfig(config Configuration) *Configuration {
525 configCopy := new(Configuration)
526
527 configCopy.Version = MajorMinorVersion(config.Version.Major(), config.Version.Minor())
528 configCopy.Loglevel = config.Loglevel
529 configCopy.Log = config.Log
530 configCopy.Catalog = config.Catalog
531 configCopy.Log.Fields = make(map[string]interface{}, len(config.Log.Fields))
532 for k, v := range config.Log.Fields {
533 configCopy.Log.Fields[k] = v
534 }
535
536 configCopy.Storage = Storage{config.Storage.Type(): Parameters{}}
537 for k, v := range config.Storage.Parameters() {
538 configCopy.Storage.setParameter(k, v)
539 }
540 configCopy.Reporting = Reporting{
541 Bugsnag: BugsnagReporting{config.Reporting.Bugsnag.APIKey, config.Reporting.Bugsnag.ReleaseStage, config.Reporting.Bugsnag.Endpoint},
542 NewRelic: NewRelicReporting{config.Reporting.NewRelic.LicenseKey, config.Reporting.NewRelic.Name, config.Reporting.NewRelic.Verbose},
543 }
544
545 configCopy.Auth = Auth{config.Auth.Type(): Parameters{}}
546 for k, v := range config.Auth.Parameters() {
547 configCopy.Auth.setParameter(k, v)
548 }
549
550 configCopy.Notifications = Notifications{Endpoints: []Endpoint{}}
551 configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, config.Notifications.Endpoints...)
552
553 configCopy.HTTP.Headers = make(http.Header)
554 for k, v := range config.HTTP.Headers {
555 configCopy.HTTP.Headers[k] = v
556 }
557
558 return configCopy
559 }
560
View as plain text