configuration_test.go 17 KB

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