Adds documentation for the configuration parser and tests
This commit is contained in:
parent
0ad4bba103
commit
2013ef5a5b
2 changed files with 83 additions and 7 deletions
|
@ -10,13 +10,19 @@ import (
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CurrentVersion is the most recent Version that can be parsed
|
||||||
var CurrentVersion = Version{Major: 0, Minor: 1}
|
var CurrentVersion = Version{Major: 0, Minor: 1}
|
||||||
|
|
||||||
|
// Configuration is a versioned system configuration
|
||||||
|
// When marshaled into yaml, this produces a document matching the current version's format
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Version Version `yaml:"version"`
|
Version Version `yaml:"version"`
|
||||||
Registry Registry `yaml:"registry"`
|
Registry Registry `yaml:"registry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version is a major/minor version pair
|
||||||
|
// Minor version upgrades should be strictly additive
|
||||||
|
// Major version upgrades indicate structure or type changes
|
||||||
type Version struct {
|
type Version struct {
|
||||||
Major uint
|
Major uint
|
||||||
Minor uint
|
Minor uint
|
||||||
|
@ -26,17 +32,26 @@ func (version Version) String() string {
|
||||||
return fmt.Sprintf("%d.%d", version.Major, version.Minor)
|
return fmt.Sprintf("%d.%d", version.Major, version.Minor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalYAML is implemented to serialize the Version into a string format
|
||||||
func (version Version) MarshalYAML() (interface{}, error) {
|
func (version Version) MarshalYAML() (interface{}, error) {
|
||||||
return version.String(), nil
|
return version.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Registry defines the configuration for a registry
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
|
// LogLevel specifies the level at which the registry will be logged
|
||||||
LogLevel string
|
LogLevel string
|
||||||
Storage Storage
|
|
||||||
|
// Storage specifies the configuration of the registry's object storage
|
||||||
|
Storage Storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Storage defines the configuration for registry object storage
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
Type string
|
// Type specifies the storage driver type (examples: inmemory, filesystem, s3, ...)
|
||||||
|
Type string
|
||||||
|
|
||||||
|
// Parameters specifies the key/value parameters map passed to the storage driver constructor
|
||||||
Parameters map[string]string
|
Parameters map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,16 +59,44 @@ func (storage Storage) MarshalYAML() (interface{}, error) {
|
||||||
return yaml.MapSlice{yaml.MapItem{storage.Type, storage.Parameters}}, nil
|
return yaml.MapSlice{yaml.MapItem{storage.Type, storage.Parameters}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// untypedConfiguration is the unmarshalable configuration struct that only assumes the existence of
|
||||||
|
// a version string parameter
|
||||||
|
// This is done to parse the configuration version, then parse the remainder with a version-specific
|
||||||
|
// parser
|
||||||
type untypedConfiguration struct {
|
type untypedConfiguration struct {
|
||||||
Version string `yaml:"version"`
|
// Version is the version string defined in a configuration yaml
|
||||||
|
// This can safely parse versions defined as float types in yaml
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
|
||||||
|
// Registry is an untyped placeholder for the Registry configuration, which can later be parsed
|
||||||
|
// into a current Registry struct
|
||||||
Registry interface{} `yaml:"registry"`
|
Registry interface{} `yaml:"registry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type v_0_1_RegistryConfiguration struct {
|
// V_0_1_RegistryConfiguration is the unmarshalable Registry configuration struct specific to
|
||||||
LogLevel string `yaml:"loglevel"`
|
// Version{0, 1}
|
||||||
Storage interface{} `yaml:"storage"`
|
type V_0_1_RegistryConfiguration struct {
|
||||||
|
// LogLevel is the level at which the registry will log
|
||||||
|
// The loglevel can be overridden with the environment variable REGISTRY_LOGLEVEL, for example:
|
||||||
|
// REGISTRY_LOGLEVEL=info
|
||||||
|
LogLevel string `yaml:"loglevel"`
|
||||||
|
|
||||||
|
// Storage is an untyped placeholder for the Storage configuration, which can later be parsed as
|
||||||
|
// a Storage struct
|
||||||
|
// The storage type can be overridden with the environment variable REGISTRY_STORAGE, for
|
||||||
|
// example: REGISTRY_STORAGE=s3
|
||||||
|
// Note: If REGISTRY_STORAGE changes the storage type, all included parameters will be ignored
|
||||||
|
// The storage parameters can be overridden with any environment variable of the format:
|
||||||
|
// REGISTRY_STORAGE_<storage driver type>_<parameter name>, for example:
|
||||||
|
// REGISTRY_STORAGE_S3_BUCKET=my-bucket
|
||||||
|
Storage interface{} `yaml:"storage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse parses an input configuration yaml document into a Configuration struct
|
||||||
|
// This should be capable of handling old configuration format versions
|
||||||
|
//
|
||||||
|
// Environment variables may be used to override configuration parameters other than version, which
|
||||||
|
// may be defined on a per-version basis. See V_0_1_RegistryConfiguration for more details
|
||||||
func Parse(in []byte) (*Configuration, error) {
|
func Parse(in []byte) (*Configuration, error) {
|
||||||
var untypedConfig untypedConfiguration
|
var untypedConfig untypedConfiguration
|
||||||
var config Configuration
|
var config Configuration
|
||||||
|
@ -65,6 +108,8 @@ func Parse(in []byte) (*Configuration, error) {
|
||||||
if untypedConfig.Version == "" {
|
if untypedConfig.Version == "" {
|
||||||
return nil, fmt.Errorf("Please specify a configuration version. Current version is %s", CurrentVersion)
|
return nil, fmt.Errorf("Please specify a configuration version. Current version is %s", CurrentVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert the version string from X.Y to Version{X, Y}
|
||||||
versionParts := strings.Split(untypedConfig.Version, ".")
|
versionParts := strings.Split(untypedConfig.Version, ".")
|
||||||
if len(versionParts) != 2 {
|
if len(versionParts) != 2 {
|
||||||
return nil, fmt.Errorf("Invalid version: %s Expected format: X.Y", untypedConfig.Version)
|
return nil, fmt.Errorf("Invalid version: %s Expected format: X.Y", untypedConfig.Version)
|
||||||
|
@ -79,6 +124,7 @@ func Parse(in []byte) (*Configuration, error) {
|
||||||
}
|
}
|
||||||
config.Version = Version{Major: uint(majorVersion), Minor: uint(minorVersion)}
|
config.Version = Version{Major: uint(majorVersion), Minor: uint(minorVersion)}
|
||||||
|
|
||||||
|
// Parse the remainder of the configuration depending on the provided version
|
||||||
switch config.Version {
|
switch config.Version {
|
||||||
case Version{0, 1}:
|
case Version{0, 1}:
|
||||||
registry, err := parseV_0_1_Registry(untypedConfig.Registry)
|
registry, err := parseV_0_1_Registry(untypedConfig.Registry)
|
||||||
|
@ -100,6 +146,7 @@ func Parse(in []byte) (*Configuration, error) {
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseV_0_1_Registry parses a Registry configuration for Version{0, 1}
|
||||||
func parseV_0_1_Registry(registry interface{}) (*Registry, error) {
|
func parseV_0_1_Registry(registry interface{}) (*Registry, error) {
|
||||||
envMap := getEnvMap()
|
envMap := getEnvMap()
|
||||||
|
|
||||||
|
@ -107,7 +154,7 @@ func parseV_0_1_Registry(registry interface{}) (*Registry, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var v_0_1 v_0_1_RegistryConfiguration
|
var v_0_1 V_0_1_RegistryConfiguration
|
||||||
err = yaml.Unmarshal(registryBytes, &v_0_1)
|
err = yaml.Unmarshal(registryBytes, &v_0_1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -123,8 +170,10 @@ func parseV_0_1_Registry(registry interface{}) (*Registry, error) {
|
||||||
|
|
||||||
switch v_0_1.Storage.(type) {
|
switch v_0_1.Storage.(type) {
|
||||||
case string:
|
case string:
|
||||||
|
// Storage is provided only by type
|
||||||
storage.Type = v_0_1.Storage.(string)
|
storage.Type = v_0_1.Storage.(string)
|
||||||
case map[interface{}]interface{}:
|
case map[interface{}]interface{}:
|
||||||
|
// Storage is provided as a {type: parameters} map
|
||||||
storageMap := v_0_1.Storage.(map[interface{}]interface{})
|
storageMap := v_0_1.Storage.(map[interface{}]interface{})
|
||||||
if len(storageMap) > 1 {
|
if len(storageMap) > 1 {
|
||||||
keys := make([]string, 0, len(storageMap))
|
keys := make([]string, 0, len(storageMap))
|
||||||
|
@ -136,6 +185,8 @@ func parseV_0_1_Registry(registry interface{}) (*Registry, error) {
|
||||||
var params map[interface{}]interface{}
|
var params map[interface{}]interface{}
|
||||||
// There will only be one key-value pair at this point
|
// There will only be one key-value pair at this point
|
||||||
for k, v := range storageMap {
|
for k, v := range storageMap {
|
||||||
|
// Parameters may be parsed as numerical or boolean values, so just convert these to
|
||||||
|
// strings
|
||||||
storage.Type = toString(k)
|
storage.Type = toString(k)
|
||||||
paramsMap, ok := v.(map[interface{}]interface{})
|
paramsMap, ok := v.(map[interface{}]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -164,6 +215,8 @@ func parseV_0_1_Registry(registry interface{}) (*Registry, error) {
|
||||||
return nil, fmt.Errorf("Must provide exactly one storage type, optionally with parameters. Provided: %v", v_0_1.Storage)
|
return nil, fmt.Errorf("Must provide exactly one storage type, optionally with parameters. Provided: %v", v_0_1.Storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find all environment variables of the format:
|
||||||
|
// REGISTRY_STORAGE_<storage driver type>_<parameter name>
|
||||||
storageParamsRegexp, err := regexp.Compile(fmt.Sprintf("^REGISTRY_STORAGE_%s_([A-Z0-9]+)$", strings.ToUpper(storage.Type)))
|
storageParamsRegexp, err := regexp.Compile(fmt.Sprintf("^REGISTRY_STORAGE_%s_([A-Z0-9]+)$", strings.ToUpper(storage.Type)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -177,6 +230,7 @@ func parseV_0_1_Registry(registry interface{}) (*Registry, error) {
|
||||||
return &Registry{LogLevel: v_0_1.LogLevel, Storage: storage}, nil
|
return &Registry{LogLevel: v_0_1.LogLevel, Storage: storage}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEnvMap reads the current environment variables and converts these into a key/value map
|
||||||
func getEnvMap() map[string]string {
|
func getEnvMap() map[string]string {
|
||||||
envMap := make(map[string]string)
|
envMap := make(map[string]string)
|
||||||
for _, env := range os.Environ() {
|
for _, env := range os.Environ() {
|
||||||
|
@ -186,6 +240,7 @@ func getEnvMap() map[string]string {
|
||||||
return envMap
|
return envMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toString converts reasonable objects into strings that may be used for configuration parameters
|
||||||
func toString(v interface{}) string {
|
func toString(v interface{}) string {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
// Hook up gocheck into the "go test" runner
|
// Hook up gocheck into the "go test" runner
|
||||||
func Test(t *testing.T) { TestingT(t) }
|
func Test(t *testing.T) { TestingT(t) }
|
||||||
|
|
||||||
|
// configStruct is a canonical example configuration, which should map to configYamlV_0_1
|
||||||
var configStruct = Configuration{
|
var configStruct = Configuration{
|
||||||
Version: Version{
|
Version: Version{
|
||||||
Major: 0,
|
Major: 0,
|
||||||
|
@ -36,6 +37,7 @@ var configStruct = Configuration{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configYamlV_0_1 is a Version{0, 1} yaml document representing configStruct
|
||||||
var configYamlV_0_1 = `
|
var configYamlV_0_1 = `
|
||||||
version: 0.1
|
version: 0.1
|
||||||
|
|
||||||
|
@ -65,6 +67,8 @@ func (suite *ConfigSuite) SetUpTest(c *C) {
|
||||||
suite.expectedConfig = copyConfig(configStruct)
|
suite.expectedConfig = copyConfig(configStruct)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMarshalRoundtrip validates that configStruct can be marshaled and unmarshaled without
|
||||||
|
// changing any parameters
|
||||||
func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) {
|
func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) {
|
||||||
configBytes, err := yaml.Marshal(suite.expectedConfig)
|
configBytes, err := yaml.Marshal(suite.expectedConfig)
|
||||||
c.Assert(err, IsNil)
|
c.Assert(err, IsNil)
|
||||||
|
@ -73,12 +77,15 @@ func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) {
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
c.Assert(config, DeepEquals, suite.expectedConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseSimple validates that configYamlV_0_1 can be parsed into a struct matching configStruct
|
||||||
func (suite *ConfigSuite) TestParseSimple(c *C) {
|
func (suite *ConfigSuite) TestParseSimple(c *C) {
|
||||||
config, err := Parse([]byte(configYamlV_0_1))
|
config, err := Parse([]byte(configYamlV_0_1))
|
||||||
c.Assert(err, IsNil)
|
c.Assert(err, IsNil)
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
c.Assert(config, DeepEquals, suite.expectedConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseWithSameEnvStorage validates that providing environment variables that match the given
|
||||||
|
// storage type and parameters will not alter the parsed Configuration struct
|
||||||
func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) {
|
func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) {
|
||||||
os.Setenv("REGISTRY_STORAGE", "s3")
|
os.Setenv("REGISTRY_STORAGE", "s3")
|
||||||
os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1")
|
os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1")
|
||||||
|
@ -88,6 +95,9 @@ func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) {
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
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) {
|
func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) {
|
||||||
suite.expectedConfig.Registry.Storage.Parameters["region"] = "us-west-1"
|
suite.expectedConfig.Registry.Storage.Parameters["region"] = "us-west-1"
|
||||||
suite.expectedConfig.Registry.Storage.Parameters["secure"] = "true"
|
suite.expectedConfig.Registry.Storage.Parameters["secure"] = "true"
|
||||||
|
@ -102,6 +112,8 @@ func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) {
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
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) {
|
func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) {
|
||||||
suite.expectedConfig.Registry.Storage = Storage{Type: "inmemory", Parameters: map[string]string{}}
|
suite.expectedConfig.Registry.Storage = Storage{Type: "inmemory", Parameters: map[string]string{}}
|
||||||
|
|
||||||
|
@ -112,6 +124,9 @@ func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) {
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
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) {
|
func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) {
|
||||||
suite.expectedConfig.Registry.Storage = Storage{Type: "filesystem", Parameters: map[string]string{}}
|
suite.expectedConfig.Registry.Storage = Storage{Type: "filesystem", Parameters: map[string]string{}}
|
||||||
suite.expectedConfig.Registry.Storage.Parameters["rootdirectory"] = "/tmp/testroot"
|
suite.expectedConfig.Registry.Storage.Parameters["rootdirectory"] = "/tmp/testroot"
|
||||||
|
@ -124,6 +139,8 @@ func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) {
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
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) {
|
func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) {
|
||||||
os.Setenv("REGISTRY_LOGLEVEL", "info")
|
os.Setenv("REGISTRY_LOGLEVEL", "info")
|
||||||
|
|
||||||
|
@ -132,6 +149,8 @@ func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) {
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
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) {
|
func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) {
|
||||||
suite.expectedConfig.Registry.LogLevel = "error"
|
suite.expectedConfig.Registry.LogLevel = "error"
|
||||||
|
|
||||||
|
@ -142,6 +161,8 @@ func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) {
|
||||||
c.Assert(config, DeepEquals, suite.expectedConfig)
|
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) {
|
func (suite *ConfigSuite) TestParseInvalidVersion(c *C) {
|
||||||
suite.expectedConfig.Version = Version{Major: CurrentVersion.Major, Minor: CurrentVersion.Minor + 1}
|
suite.expectedConfig.Version = Version{Major: CurrentVersion.Major, Minor: CurrentVersion.Minor + 1}
|
||||||
configBytes, err := yaml.Marshal(suite.expectedConfig)
|
configBytes, err := yaml.Marshal(suite.expectedConfig)
|
||||||
|
|
Loading…
Reference in a new issue