Initial configuration parser
							parent
							
								
									df7eed3a2c
								
							
						
					
					
						commit
						0ad4bba103
					
				|  | @ -0,0 +1,194 @@ | ||||||
|  | package configuration | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"gopkg.in/yaml.v2" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var CurrentVersion = Version{Major: 0, Minor: 1} | ||||||
|  | 
 | ||||||
|  | type Configuration struct { | ||||||
|  | 	Version  Version  `yaml:"version"` | ||||||
|  | 	Registry Registry `yaml:"registry"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Version struct { | ||||||
|  | 	Major uint | ||||||
|  | 	Minor uint | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (version Version) String() string { | ||||||
|  | 	return fmt.Sprintf("%d.%d", version.Major, version.Minor) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (version Version) MarshalYAML() (interface{}, error) { | ||||||
|  | 	return version.String(), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Registry struct { | ||||||
|  | 	LogLevel string | ||||||
|  | 	Storage  Storage | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Storage struct { | ||||||
|  | 	Type       string | ||||||
|  | 	Parameters map[string]string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (storage Storage) MarshalYAML() (interface{}, error) { | ||||||
|  | 	return yaml.MapSlice{yaml.MapItem{storage.Type, storage.Parameters}}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type untypedConfiguration struct { | ||||||
|  | 	Version  string      `yaml:"version"` | ||||||
|  | 	Registry interface{} `yaml:"registry"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type v_0_1_RegistryConfiguration struct { | ||||||
|  | 	LogLevel string      `yaml:"loglevel"` | ||||||
|  | 	Storage  interface{} `yaml:"storage"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func Parse(in []byte) (*Configuration, error) { | ||||||
|  | 	var untypedConfig untypedConfiguration | ||||||
|  | 	var config Configuration | ||||||
|  | 
 | ||||||
|  | 	err := yaml.Unmarshal(in, &untypedConfig) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if untypedConfig.Version == "" { | ||||||
|  | 		return nil, fmt.Errorf("Please specify a configuration version. Current version is %s", CurrentVersion) | ||||||
|  | 	} | ||||||
|  | 	versionParts := strings.Split(untypedConfig.Version, ".") | ||||||
|  | 	if len(versionParts) != 2 { | ||||||
|  | 		return nil, fmt.Errorf("Invalid version: %s Expected format: X.Y", untypedConfig.Version) | ||||||
|  | 	} | ||||||
|  | 	majorVersion, err := strconv.ParseUint(versionParts[0], 10, 0) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("Major version must be of type uint, received %v", versionParts[0]) | ||||||
|  | 	} | ||||||
|  | 	minorVersion, err := strconv.ParseUint(versionParts[1], 10, 0) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("Minor version must be of type uint, received %v", versionParts[1]) | ||||||
|  | 	} | ||||||
|  | 	config.Version = Version{Major: uint(majorVersion), Minor: uint(minorVersion)} | ||||||
|  | 
 | ||||||
|  | 	switch config.Version { | ||||||
|  | 	case Version{0, 1}: | ||||||
|  | 		registry, err := parseV_0_1_Registry(untypedConfig.Registry) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		config.Registry = *registry | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("Unsupported configuration version %s Current version is %s", config.Version, CurrentVersion) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	switch config.Registry.LogLevel { | ||||||
|  | 	case "error", "warn", "info", "debug": | ||||||
|  | 	default: | ||||||
|  | 		return nil, fmt.Errorf("Invalid loglevel %s Must be one of [error, warn, info, debug]", config.Registry.LogLevel) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &config, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func parseV_0_1_Registry(registry interface{}) (*Registry, error) { | ||||||
|  | 	envMap := getEnvMap() | ||||||
|  | 
 | ||||||
|  | 	registryBytes, err := yaml.Marshal(registry) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	var v_0_1 v_0_1_RegistryConfiguration | ||||||
|  | 	err = yaml.Unmarshal(registryBytes, &v_0_1) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if logLevel, ok := envMap["REGISTRY_LOGLEVEL"]; ok { | ||||||
|  | 		v_0_1.LogLevel = logLevel | ||||||
|  | 	} | ||||||
|  | 	v_0_1.LogLevel = strings.ToLower(v_0_1.LogLevel) | ||||||
|  | 
 | ||||||
|  | 	var storage Storage | ||||||
|  | 	storage.Parameters = make(map[string]string) | ||||||
|  | 
 | ||||||
|  | 	switch v_0_1.Storage.(type) { | ||||||
|  | 	case string: | ||||||
|  | 		storage.Type = v_0_1.Storage.(string) | ||||||
|  | 	case map[interface{}]interface{}: | ||||||
|  | 		storageMap := v_0_1.Storage.(map[interface{}]interface{}) | ||||||
|  | 		if len(storageMap) > 1 { | ||||||
|  | 			keys := make([]string, 0, len(storageMap)) | ||||||
|  | 			for key := range storageMap { | ||||||
|  | 				keys = append(keys, toString(key)) | ||||||
|  | 			} | ||||||
|  | 			return nil, fmt.Errorf("Must provide exactly one storage type. Provided: %v", keys) | ||||||
|  | 		} | ||||||
|  | 		var params map[interface{}]interface{} | ||||||
|  | 		// There will only be one key-value pair at this point
 | ||||||
|  | 		for k, v := range storageMap { | ||||||
|  | 			storage.Type = toString(k) | ||||||
|  | 			paramsMap, ok := v.(map[interface{}]interface{}) | ||||||
|  | 			if !ok { | ||||||
|  | 				return nil, fmt.Errorf("Must provide parameters as a map[string]string. Provided: %#v", v) | ||||||
|  | 			} | ||||||
|  | 			params = paramsMap | ||||||
|  | 		} | ||||||
|  | 		for k, v := range params { | ||||||
|  | 			storage.Parameters[toString(k)] = toString(v) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	case interface{}: | ||||||
|  | 		// Bad type for storage
 | ||||||
|  | 		return nil, fmt.Errorf("Registry storage must be provided by name, optionally with parameters. Provided: %v", v_0_1.Storage) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if storageType, ok := envMap["REGISTRY_STORAGE"]; ok { | ||||||
|  | 		if storageType != storage.Type { | ||||||
|  | 			storage.Type = storageType | ||||||
|  | 			// Reset the storage parameters because we're using a different storage type
 | ||||||
|  | 			storage.Parameters = make(map[string]string) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if storage.Type == "" { | ||||||
|  | 		return nil, fmt.Errorf("Must provide exactly one storage type, optionally with parameters. Provided: %v", v_0_1.Storage) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	storageParamsRegexp, err := regexp.Compile(fmt.Sprintf("^REGISTRY_STORAGE_%s_([A-Z0-9]+)$", strings.ToUpper(storage.Type))) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	for k, v := range envMap { | ||||||
|  | 		if submatches := storageParamsRegexp.FindStringSubmatch(k); submatches != nil { | ||||||
|  | 			storage.Parameters[strings.ToLower(submatches[1])] = v | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &Registry{LogLevel: v_0_1.LogLevel, Storage: storage}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getEnvMap() map[string]string { | ||||||
|  | 	envMap := make(map[string]string) | ||||||
|  | 	for _, env := range os.Environ() { | ||||||
|  | 		envParts := strings.SplitN(env, "=", 2) | ||||||
|  | 		envMap[envParts[0]] = envParts[1] | ||||||
|  | 	} | ||||||
|  | 	return envMap | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func toString(v interface{}) string { | ||||||
|  | 	if v == nil { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprint(v) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,171 @@ | ||||||
|  | package configuration | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"os" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"gopkg.in/yaml.v2" | ||||||
|  | 
 | ||||||
|  | 	. "gopkg.in/check.v1" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Hook up gocheck into the "go test" runner
 | ||||||
|  | func Test(t *testing.T) { TestingT(t) } | ||||||
|  | 
 | ||||||
|  | var configStruct = Configuration{ | ||||||
|  | 	Version: Version{ | ||||||
|  | 		Major: 0, | ||||||
|  | 		Minor: 1, | ||||||
|  | 	}, | ||||||
|  | 	Registry: Registry{ | ||||||
|  | 		LogLevel: "info", | ||||||
|  | 		Storage: Storage{ | ||||||
|  | 			Type: "s3", | ||||||
|  | 			Parameters: map[string]string{ | ||||||
|  | 				"region":    "us-east-1", | ||||||
|  | 				"bucket":    "my-bucket", | ||||||
|  | 				"rootpath":  "/registry", | ||||||
|  | 				"encrypt":   "true", | ||||||
|  | 				"secure":    "false", | ||||||
|  | 				"accesskey": "SAMPLEACCESSKEY", | ||||||
|  | 				"secretkey": "SUPERSECRET", | ||||||
|  | 				"host":      "", | ||||||
|  | 				"port":      "", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var configYamlV_0_1 = ` | ||||||
|  | version: 0.1 | ||||||
|  | 
 | ||||||
|  | registry: | ||||||
|  |   loglevel: info | ||||||
|  |   storage: | ||||||
|  |     s3: | ||||||
|  |       region: us-east-1 | ||||||
|  |       bucket: my-bucket | ||||||
|  |       rootpath: /registry | ||||||
|  |       encrypt: true | ||||||
|  |       secure: false | ||||||
|  |       accesskey: SAMPLEACCESSKEY | ||||||
|  |       secretkey: SUPERSECRET | ||||||
|  |       host: ~ | ||||||
|  |       port: ~ | ||||||
|  | ` | ||||||
|  | 
 | ||||||
|  | type ConfigSuite struct { | ||||||
|  | 	expectedConfig *Configuration | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var _ = Suite(new(ConfigSuite)) | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) SetUpTest(c *C) { | ||||||
|  | 	os.Clearenv() | ||||||
|  | 	suite.expectedConfig = copyConfig(configStruct) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) { | ||||||
|  | 	configBytes, err := yaml.Marshal(suite.expectedConfig) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	config, err := Parse(configBytes) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseSimple(c *C) { | ||||||
|  | 	config, err := Parse([]byte(configYamlV_0_1)) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) { | ||||||
|  | 	os.Setenv("REGISTRY_STORAGE", "s3") | ||||||
|  | 	os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1") | ||||||
|  | 
 | ||||||
|  | 	config, err := Parse([]byte(configYamlV_0_1)) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) { | ||||||
|  | 	suite.expectedConfig.Registry.Storage.Parameters["region"] = "us-west-1" | ||||||
|  | 	suite.expectedConfig.Registry.Storage.Parameters["secure"] = "true" | ||||||
|  | 	suite.expectedConfig.Registry.Storage.Parameters["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([]byte(configYamlV_0_1)) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) { | ||||||
|  | 	suite.expectedConfig.Registry.Storage = Storage{Type: "inmemory", Parameters: map[string]string{}} | ||||||
|  | 
 | ||||||
|  | 	os.Setenv("REGISTRY_STORAGE", "inmemory") | ||||||
|  | 
 | ||||||
|  | 	config, err := Parse([]byte(configYamlV_0_1)) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) { | ||||||
|  | 	suite.expectedConfig.Registry.Storage = Storage{Type: "filesystem", Parameters: map[string]string{}} | ||||||
|  | 	suite.expectedConfig.Registry.Storage.Parameters["rootdirectory"] = "/tmp/testroot" | ||||||
|  | 
 | ||||||
|  | 	os.Setenv("REGISTRY_STORAGE", "filesystem") | ||||||
|  | 	os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot") | ||||||
|  | 
 | ||||||
|  | 	config, err := Parse([]byte(configYamlV_0_1)) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) { | ||||||
|  | 	os.Setenv("REGISTRY_LOGLEVEL", "info") | ||||||
|  | 
 | ||||||
|  | 	config, err := Parse([]byte(configYamlV_0_1)) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) { | ||||||
|  | 	suite.expectedConfig.Registry.LogLevel = "error" | ||||||
|  | 
 | ||||||
|  | 	os.Setenv("REGISTRY_LOGLEVEL", "error") | ||||||
|  | 
 | ||||||
|  | 	config, err := Parse([]byte(configYamlV_0_1)) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	c.Assert(config, DeepEquals, suite.expectedConfig) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *ConfigSuite) TestParseInvalidVersion(c *C) { | ||||||
|  | 	suite.expectedConfig.Version = Version{Major: CurrentVersion.Major, Minor: CurrentVersion.Minor + 1} | ||||||
|  | 	configBytes, err := yaml.Marshal(suite.expectedConfig) | ||||||
|  | 	c.Assert(err, IsNil) | ||||||
|  | 	_, err = Parse(configBytes) | ||||||
|  | 	c.Assert(err, NotNil) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func copyConfig(config Configuration) *Configuration { | ||||||
|  | 	configCopy := new(Configuration) | ||||||
|  | 
 | ||||||
|  | 	configCopy.Version = *new(Version) | ||||||
|  | 	configCopy.Version.Major = config.Version.Major | ||||||
|  | 	configCopy.Version.Minor = config.Version.Minor | ||||||
|  | 
 | ||||||
|  | 	configCopy.Registry = *new(Registry) | ||||||
|  | 	configCopy.Registry.LogLevel = config.Registry.LogLevel | ||||||
|  | 
 | ||||||
|  | 	configCopy.Registry.Storage = *new(Storage) | ||||||
|  | 	configCopy.Registry.Storage.Type = config.Registry.Storage.Type | ||||||
|  | 	configCopy.Registry.Storage.Parameters = make(map[string]string) | ||||||
|  | 	for k, v := range config.Registry.Storage.Parameters { | ||||||
|  | 		configCopy.Registry.Storage.Parameters[k] = v | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return configCopy | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue