123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- package configuration
- import (
- "bytes"
- "net/http"
- "os"
- "reflect"
- "strings"
- "testing"
- . "gopkg.in/check.v1"
- "gopkg.in/yaml.v2"
- )
- // Hook up gocheck into the "go test" runner
- func Test(t *testing.T) { TestingT(t) }
- // configStruct is a canonical example configuration, which should map to configYamlV0_1
- var configStruct = Configuration{
- Version: "0.1",
- Log: struct {
- Level Loglevel `yaml:"level"`
- Formatter string `yaml:"formatter,omitempty"`
- Fields map[string]interface{} `yaml:"fields,omitempty"`
- Hooks []LogHook `yaml:"hooks,omitempty"`
- }{
- Fields: map[string]interface{}{"environment": "test"},
- },
- Loglevel: "info",
- Storage: Storage{
- "s3": Parameters{
- "region": "us-east-1",
- "bucket": "my-bucket",
- "rootdirectory": "/registry",
- "encrypt": true,
- "secure": false,
- "accesskey": "SAMPLEACCESSKEY",
- "secretkey": "SUPERSECRET",
- "host": nil,
- "port": 42,
- },
- },
- Auth: Auth{
- "silly": Parameters{
- "realm": "silly",
- "service": "silly",
- },
- },
- Reporting: Reporting{
- Bugsnag: BugsnagReporting{
- APIKey: "BugsnagApiKey",
- },
- },
- Notifications: Notifications{
- Endpoints: []Endpoint{
- {
- Name: "endpoint-1",
- URL: "http://example.com",
- Headers: http.Header{
- "Authorization": []string{"Bearer <example>"},
- },
- },
- },
- },
- HTTP: struct {
- Addr string `yaml:"addr,omitempty"`
- Net string `yaml:"net,omitempty"`
- Host string `yaml:"host,omitempty"`
- Prefix string `yaml:"prefix,omitempty"`
- Secret string `yaml:"secret,omitempty"`
- RelativeURLs bool `yaml:"relativeurls,omitempty"`
- TLS struct {
- Certificate string `yaml:"certificate,omitempty"`
- Key string `yaml:"key,omitempty"`
- ClientCAs []string `yaml:"clientcas,omitempty"`
- LetsEncrypt struct {
- CacheFile string `yaml:"cachefile,omitempty"`
- Email string `yaml:"email,omitempty"`
- } `yaml:"letsencrypt,omitempty"`
- } `yaml:"tls,omitempty"`
- Headers http.Header `yaml:"headers,omitempty"`
- Debug struct {
- Addr string `yaml:"addr,omitempty"`
- } `yaml:"debug,omitempty"`
- HTTP2 struct {
- Disabled bool `yaml:"disabled,omitempty"`
- } `yaml:"http2,omitempty"`
- }{
- TLS: struct {
- Certificate string `yaml:"certificate,omitempty"`
- Key string `yaml:"key,omitempty"`
- ClientCAs []string `yaml:"clientcas,omitempty"`
- LetsEncrypt struct {
- CacheFile string `yaml:"cachefile,omitempty"`
- Email string `yaml:"email,omitempty"`
- } `yaml:"letsencrypt,omitempty"`
- }{
- ClientCAs: []string{"/path/to/ca.pem"},
- },
- Headers: http.Header{
- "X-Content-Type-Options": []string{"nosniff"},
- },
- HTTP2: struct {
- Disabled bool `yaml:"disabled,omitempty"`
- }{
- Disabled: false,
- },
- },
- }
- // configYamlV0_1 is a Version 0.1 yaml document representing configStruct
- var configYamlV0_1 = `
- version: 0.1
- log:
- fields:
- environment: test
- loglevel: info
- storage:
- s3:
- region: us-east-1
- bucket: my-bucket
- rootdirectory: /registry
- encrypt: true
- secure: false
- accesskey: SAMPLEACCESSKEY
- secretkey: SUPERSECRET
- host: ~
- port: 42
- auth:
- silly:
- realm: silly
- service: silly
- notifications:
- endpoints:
- - name: endpoint-1
- url: http://example.com
- headers:
- Authorization: [Bearer <example>]
- reporting:
- bugsnag:
- apikey: BugsnagApiKey
- http:
- clientcas:
- - /path/to/ca.pem
- headers:
- X-Content-Type-Options: [nosniff]
- `
- // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
- // storage driver with no parameters
- var inmemoryConfigYamlV0_1 = `
- version: 0.1
- loglevel: info
- storage: inmemory
- auth:
- silly:
- realm: silly
- service: silly
- notifications:
- endpoints:
- - name: endpoint-1
- url: http://example.com
- headers:
- Authorization: [Bearer <example>]
- http:
- headers:
- X-Content-Type-Options: [nosniff]
- `
- type ConfigSuite struct {
- expectedConfig *Configuration
- }
- var _ = Suite(new(ConfigSuite))
- func (suite *ConfigSuite) SetUpTest(c *C) {
- os.Clearenv()
- suite.expectedConfig = copyConfig(configStruct)
- }
- // TestMarshalRoundtrip validates that configStruct can be marshaled and
- // unmarshaled without changing any parameters
- func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) {
- configBytes, err := yaml.Marshal(suite.expectedConfig)
- c.Assert(err, IsNil)
- config, err := Parse(bytes.NewReader(configBytes))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseSimple validates that configYamlV0_1 can be parsed into a struct
- // matching configStruct
- func (suite *ConfigSuite) TestParseSimple(c *C) {
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseInmemory validates that configuration yaml with storage provided as
- // a string can be parsed into a Configuration struct with no storage parameters
- func (suite *ConfigSuite) TestParseInmemory(c *C) {
- suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
- suite.expectedConfig.Reporting = Reporting{}
- suite.expectedConfig.Log.Fields = nil
- config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseIncomplete validates that an incomplete yaml configuration cannot
- // be parsed without providing environment variables to fill in the missing
- // components.
- func (suite *ConfigSuite) TestParseIncomplete(c *C) {
- incompleteConfigYaml := "version: 0.1"
- _, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
- c.Assert(err, NotNil)
- suite.expectedConfig.Log.Fields = nil
- suite.expectedConfig.Storage = Storage{"filesystem": Parameters{"rootdirectory": "/tmp/testroot"}}
- suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
- suite.expectedConfig.Reporting = Reporting{}
- suite.expectedConfig.Notifications = Notifications{}
- suite.expectedConfig.HTTP.Headers = nil
- // Note: this also tests that REGISTRY_STORAGE and
- // REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together
- os.Setenv("REGISTRY_STORAGE", "filesystem")
- os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
- os.Setenv("REGISTRY_AUTH", "silly")
- os.Setenv("REGISTRY_AUTH_SILLY_REALM", "silly")
- config, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseWithSameEnvStorage validates that providing environment variables
- // that match the given storage type will only include environment-defined
- // parameters and remove yaml-defined parameters
- func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) {
- suite.expectedConfig.Storage = Storage{"s3": Parameters{"region": "us-east-1"}}
- os.Setenv("REGISTRY_STORAGE", "s3")
- os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseWithDifferentEnvStorageParams validates that providing environment variables that change
- // and add to the given storage parameters will change and add parameters to the parsed
- // Configuration struct
- func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) {
- suite.expectedConfig.Storage.setParameter("region", "us-west-1")
- suite.expectedConfig.Storage.setParameter("secure", true)
- suite.expectedConfig.Storage.setParameter("newparam", "some Value")
- os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-west-1")
- os.Setenv("REGISTRY_STORAGE_S3_SECURE", "true")
- os.Setenv("REGISTRY_STORAGE_S3_NEWPARAM", "some Value")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseWithDifferentEnvStorageType validates that providing an environment variable that
- // changes the storage type will be reflected in the parsed Configuration struct
- func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) {
- suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
- os.Setenv("REGISTRY_STORAGE", "inmemory")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseWithDifferentEnvStorageTypeAndParams validates that providing an environment variable
- // that changes the storage type will be reflected in the parsed Configuration struct and that
- // environment storage parameters will also be included
- func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) {
- suite.expectedConfig.Storage = Storage{"filesystem": Parameters{}}
- suite.expectedConfig.Storage.setParameter("rootdirectory", "/tmp/testroot")
- os.Setenv("REGISTRY_STORAGE", "filesystem")
- os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseWithSameEnvLoglevel validates that providing an environment variable defining the log
- // level to the same as the one provided in the yaml will not change the parsed Configuration struct
- func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) {
- os.Setenv("REGISTRY_LOGLEVEL", "info")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseWithDifferentEnvLoglevel validates that providing an environment variable defining the
- // log level will override the value provided in the yaml document
- func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) {
- suite.expectedConfig.Loglevel = "error"
- os.Setenv("REGISTRY_LOGLEVEL", "error")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseInvalidLoglevel validates that the parser will fail to parse a
- // configuration if the loglevel is malformed
- func (suite *ConfigSuite) TestParseInvalidLoglevel(c *C) {
- invalidConfigYaml := "version: 0.1\nloglevel: derp\nstorage: inmemory"
- _, err := Parse(bytes.NewReader([]byte(invalidConfigYaml)))
- c.Assert(err, NotNil)
- os.Setenv("REGISTRY_LOGLEVEL", "derp")
- _, err = Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, NotNil)
- }
- // TestParseWithDifferentEnvReporting validates that environment variables
- // properly override reporting parameters
- func (suite *ConfigSuite) TestParseWithDifferentEnvReporting(c *C) {
- suite.expectedConfig.Reporting.Bugsnag.APIKey = "anotherBugsnagApiKey"
- suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
- suite.expectedConfig.Reporting.NewRelic.LicenseKey = "NewRelicLicenseKey"
- suite.expectedConfig.Reporting.NewRelic.Name = "some NewRelic NAME"
- os.Setenv("REGISTRY_REPORTING_BUGSNAG_APIKEY", "anotherBugsnagApiKey")
- os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
- os.Setenv("REGISTRY_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
- os.Setenv("REGISTRY_REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseInvalidVersion validates that the parser will fail to parse a newer configuration
- // version than the CurrentVersion
- func (suite *ConfigSuite) TestParseInvalidVersion(c *C) {
- suite.expectedConfig.Version = MajorMinorVersion(CurrentVersion.Major(), CurrentVersion.Minor()+1)
- configBytes, err := yaml.Marshal(suite.expectedConfig)
- c.Assert(err, IsNil)
- _, err = Parse(bytes.NewReader(configBytes))
- c.Assert(err, NotNil)
- }
- // TestParseExtraneousVars validates that environment variables referring to
- // nonexistent variables don't cause side effects.
- func (suite *ConfigSuite) TestParseExtraneousVars(c *C) {
- suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080"
- // A valid environment variable
- os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080")
- // Environment variables which shouldn't set config items
- os.Setenv("registry_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey")
- os.Setenv("REPORTING_NEWRELIC_NAME", "some NewRelic NAME")
- os.Setenv("REGISTRY_DUCKS", "quack")
- os.Setenv("REGISTRY_REPORTING_ASDF", "ghjk")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseEnvVarImplicitMaps validates that environment variables can set
- // values in maps that don't already exist.
- func (suite *ConfigSuite) TestParseEnvVarImplicitMaps(c *C) {
- readonly := make(map[string]interface{})
- readonly["enabled"] = true
- maintenance := make(map[string]interface{})
- maintenance["readonly"] = readonly
- suite.expectedConfig.Storage["maintenance"] = maintenance
- os.Setenv("REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED", "true")
- config, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- c.Assert(config, DeepEquals, suite.expectedConfig)
- }
- // TestParseEnvWrongTypeMap validates that incorrectly attempting to unmarshal a
- // string over existing map fails.
- func (suite *ConfigSuite) TestParseEnvWrongTypeMap(c *C) {
- os.Setenv("REGISTRY_STORAGE_S3", "somestring")
- _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, NotNil)
- }
- // TestParseEnvWrongTypeStruct validates that incorrectly attempting to
- // unmarshal a string into a struct fails.
- func (suite *ConfigSuite) TestParseEnvWrongTypeStruct(c *C) {
- os.Setenv("REGISTRY_STORAGE_LOG", "somestring")
- _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, NotNil)
- }
- // TestParseEnvWrongTypeSlice validates that incorrectly attempting to
- // unmarshal a string into a slice fails.
- func (suite *ConfigSuite) TestParseEnvWrongTypeSlice(c *C) {
- os.Setenv("REGISTRY_LOG_HOOKS", "somestring")
- _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, NotNil)
- }
- // TestParseEnvMany tests several environment variable overrides.
- // The result is not checked - the goal of this test is to detect panics
- // from misuse of reflection.
- func (suite *ConfigSuite) TestParseEnvMany(c *C) {
- os.Setenv("REGISTRY_VERSION", "0.1")
- os.Setenv("REGISTRY_LOG_LEVEL", "debug")
- os.Setenv("REGISTRY_LOG_FORMATTER", "json")
- os.Setenv("REGISTRY_LOG_HOOKS", "json")
- os.Setenv("REGISTRY_LOG_FIELDS", "abc: xyz")
- os.Setenv("REGISTRY_LOG_HOOKS", "- type: asdf")
- os.Setenv("REGISTRY_LOGLEVEL", "debug")
- os.Setenv("REGISTRY_STORAGE", "s3")
- os.Setenv("REGISTRY_AUTH_PARAMS", "param1: value1")
- os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
- os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2")
- _, err := Parse(bytes.NewReader([]byte(configYamlV0_1)))
- c.Assert(err, IsNil)
- }
- func checkStructs(c *C, t reflect.Type, structsChecked map[string]struct{}) {
- for t.Kind() == reflect.Ptr || t.Kind() == reflect.Map || t.Kind() == reflect.Slice {
- t = t.Elem()
- }
- if t.Kind() != reflect.Struct {
- return
- }
- if _, present := structsChecked[t.String()]; present {
- // Already checked this type
- return
- }
- structsChecked[t.String()] = struct{}{}
- byUpperCase := make(map[string]int)
- for i := 0; i < t.NumField(); i++ {
- sf := t.Field(i)
- // Check that the yaml tag does not contain an _.
- yamlTag := sf.Tag.Get("yaml")
- if strings.Contains(yamlTag, "_") {
- c.Fatalf("yaml field name includes _ character: %s", yamlTag)
- }
- upper := strings.ToUpper(sf.Name)
- if _, present := byUpperCase[upper]; present {
- c.Fatalf("field name collision in configuration object: %s", sf.Name)
- }
- byUpperCase[upper] = i
- checkStructs(c, sf.Type, structsChecked)
- }
- }
- // TestValidateConfigStruct makes sure that the config struct has no members
- // with yaml tags that would be ambiguous to the environment variable parser.
- func (suite *ConfigSuite) TestValidateConfigStruct(c *C) {
- structsChecked := make(map[string]struct{})
- checkStructs(c, reflect.TypeOf(Configuration{}), structsChecked)
- }
- func copyConfig(config Configuration) *Configuration {
- configCopy := new(Configuration)
- configCopy.Version = MajorMinorVersion(config.Version.Major(), config.Version.Minor())
- configCopy.Loglevel = config.Loglevel
- configCopy.Log = config.Log
- configCopy.Log.Fields = make(map[string]interface{}, len(config.Log.Fields))
- for k, v := range config.Log.Fields {
- configCopy.Log.Fields[k] = v
- }
- configCopy.Storage = Storage{config.Storage.Type(): Parameters{}}
- for k, v := range config.Storage.Parameters() {
- configCopy.Storage.setParameter(k, v)
- }
- configCopy.Reporting = Reporting{
- Bugsnag: BugsnagReporting{config.Reporting.Bugsnag.APIKey, config.Reporting.Bugsnag.ReleaseStage, config.Reporting.Bugsnag.Endpoint},
- NewRelic: NewRelicReporting{config.Reporting.NewRelic.LicenseKey, config.Reporting.NewRelic.Name, config.Reporting.NewRelic.Verbose},
- }
- configCopy.Auth = Auth{config.Auth.Type(): Parameters{}}
- for k, v := range config.Auth.Parameters() {
- configCopy.Auth.setParameter(k, v)
- }
- configCopy.Notifications = Notifications{Endpoints: []Endpoint{}}
- for _, v := range config.Notifications.Endpoints {
- configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v)
- }
- configCopy.HTTP.Headers = make(http.Header)
- for k, v := range config.HTTP.Headers {
- configCopy.HTTP.Headers[k] = v
- }
- return configCopy
- }
|