configuration_test.go 17 KB

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