Initial configuration parser
This commit is contained in:
parent
df7eed3a2c
commit
0ad4bba103
2 changed files with 365 additions and 0 deletions
194
configuration/configuration.go
Normal file
194
configuration/configuration.go
Normal file
|
@ -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)
|
||||||
|
}
|
171
configuration/configuration_test.go
Normal file
171
configuration/configuration_test.go
Normal file
|
@ -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 a new issue