diff --git a/config.go b/config.go new file mode 100644 index 0000000..0c0d91e --- /dev/null +++ b/config.go @@ -0,0 +1,288 @@ +// +// Copyright (c) 2017 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Red Hat trademarks are not licensed under Apache License, Version 2. +// No permission is granted to use or replicate Red Hat trademarks that +// are incorporated in this software or its documentation. +// + +package config + +import ( + "fmt" + "io/ioutil" + "strings" + "sync" + + logging "github.com/op/go-logging" + yaml "gopkg.in/yaml.v1" +) + +var ( + log = logging.MustGetLogger("config") +) + +// Config - The base config for the pieces of the applcation +type Config struct { + config map[string]interface{} + mutex sync.RWMutex +} + +// CreateConfig - Read config file and create the Config struct +func CreateConfig(configFile string) (*Config, error) { + var err error + // Load struct + var dat []byte + if dat, err = ioutil.ReadFile(configFile); err != nil { + return &Config{config: map[string]interface{}{}, mutex: sync.RWMutex{}}, err + } + c := map[string]interface{}{} + + if err = yaml.Unmarshal(dat, &c); err != nil { + return &Config{config: map[string]interface{}{}, mutex: sync.RWMutex{}}, err + } + return &Config{config: c, mutex: sync.RWMutex{}}, nil +} + +// Empty - Determines if the configuration is empty +func (c *Config) Empty() bool { + return len(c.config) == 0 +} + +// GetString - Retrieve the configuration value as a string. +func (c *Config) GetString(key string) string { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + keys := strings.Split(key, ".") + val := retrieveValueFromKeys(keys, subMap) + if v, ok := val.(string); ok { + return v + } + log.Debugf("Unable to get %v from config", key) + return "" +} + +// GetSliceOfStrings - Retrieve the configuration value that is a slice of strings. +func (c *Config) GetSliceOfStrings(key string) []string { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + keys := strings.Split(key, ".") + val := retrieveValueFromKeys(keys, subMap) + var s []string + if v, ok := val.([]interface{}); ok { + for _, str := range v { + if st, ok := str.(string); ok { + s = append(s, st) + } else { + return nil + } + } + return s + } + log.Debugf("Unable to get %v from config", key) + return nil +} + +// GetInt - Retrieve the configuration value as a int. +func (c *Config) GetInt(key string) int { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + keys := strings.Split(key, ".") + val := retrieveValueFromKeys(keys, subMap) + if v, ok := val.(int); ok { + return v + } + log.Debugf("Unable to get %v from config", key) + return 0 +} + +// GetBool - Retrieve the configuration value as a bool. +func (c *Config) GetBool(key string) bool { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + keys := strings.Split(key, ".") + val := retrieveValueFromKeys(keys, subMap) + if v, ok := val.(bool); ok { + return v + } + log.Debugf("Unable to get %v from config", key) + return false +} + +// GetFloat64 - Retrieve the configuration value as a float64 +func (c *Config) GetFloat64(key string) float64 { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + keys := strings.Split(key, ".") + val := retrieveValueFromKeys(keys, subMap) + if v, ok := val.(float64); ok { + return v + } + log.Debugf("Unable to get %v from config", key) + return float64(0) +} + +// GetFloat32 - Retrieve the configuration value as a float32 +func (c *Config) GetFloat32(key string) float32 { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + keys := strings.Split(key, ".") + val := retrieveValueFromKeys(keys, subMap) + if v, ok := val.(float64); ok { + return float32(v) + } + log.Debugf("Unable to get %v from config", key) + return float32(0) +} + +// GetSubConfig - Retrieve the sub map +func (c *Config) GetSubConfig(key string) *Config { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + keys := strings.Split(key, ".") + val := retrieveValueFromKeys(keys, subMap) + switch val.(type) { + case *Config: + return val.(*Config) + default: + log.Debugf("Unable to get %v from config", key) + return &Config{config: map[string]interface{}{}, mutex: sync.RWMutex{}} + } +} + +// ToMap - Retrieve a copy of the undlying map for the config. +func (c *Config) ToMap() map[string]interface{} { + c.mutex.RLock() + subMap := createNewMap(c.config) + //Can unlock the map for reading after we copy the map. + c.mutex.RUnlock() + return subMap +} + +//createNewMap - Need to create a new map to work with. +func createNewMap(config map[string]interface{}) map[string]interface{} { + newMap := map[string]interface{}{} + for k, v := range config { + newMap[k] = v + } + return newMap +} + +func retrieveValueFromKeys(keys []string, subMap map[string]interface{}) interface{} { + var val interface{} + for i, key := range keys { + val = subMap[key] + switch val.(type) { + case map[string]interface{}: + //If the final value then we should return the a new + // configuration with the submap as the config + subMap = val.(map[string]interface{}) + if i == len(keys)-1 { + return &Config{mutex: sync.RWMutex{}, config: val.(map[string]interface{})} + } + case map[interface{}]interface{}: + var err error + subMap, err = createStringMap(val.(map[interface{}]interface{})) + if err != nil { + return &Config{mutex: sync.RWMutex{}, config: map[string]interface{}{}} + } + // We do not know what to do if the key is not a string. Error here. + if i == len(keys)-1 { + return &Config{mutex: sync.RWMutex{}, config: subMap} + } + case []interface{}: + //If array of values we will attempt to make a map using the a + // name field on the underlying object. + //TODO: we should eventually add this back to the original map, + // then we could check for existence of this sub map. + subMap = createSubMapFromArray(val.([]interface{})) + if len(subMap) > 0 && i == len(keys)-1 { + return &Config{config: subMap, mutex: sync.RWMutex{}} + } + if i == len(keys)-1 { + return val + } + default: + if i == len(keys)-1 { + return val + } + //If invalid key with no value, return an empty configuration. + return &Config{config: map[string]interface{}{}, mutex: sync.RWMutex{}} + } + } + return val +} + +// createSubMapFromArray - create the submap from the array +// checks if the name field is in the underlying map and will use as the key +func createSubMapFromArray(val []interface{}) map[string]interface{} { + subMap := map[string]interface{}{} + for _, value := range val { + if v, ok := value.(map[string]interface{}); ok { + // If no name field don't fill up the map + if name, ok := v["name"]; ok { + if n, ok := name.(string); ok { + subMap[n] = value + } + } + } else if v, ok := value.(map[interface{}]interface{}); ok { + // If no name field don't fill up the map + s, err := createStringMap(v) + if err != nil { + return subMap + } + if name, ok := s["name"]; ok { + if n, ok := name.(string); ok { + subMap[n] = value + } + } + if t, ok := s["type"]; ok { + if tp, ok := t.(string); ok { + subMap[tp] = value + } + } + } else { + //We don't know what to do if they are not key value pairs. + return subMap + } + } + return subMap +} + +func createStringMap(val map[interface{}]interface{}) (map[string]interface{}, error) { + subMap := map[string]interface{}{} + for key, v := range val { + if k, ok := key.(string); ok { + subMap[k] = v + } else { + return subMap, fmt.Errorf("Unable to understand non string key") + } + } + return subMap, nil +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..39b5482 --- /dev/null +++ b/config_test.go @@ -0,0 +1,194 @@ +// +// Copyright (c) 2017 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Red Hat trademarks are not licensed under Apache License, Version 2. +// No permission is granted to use or replicate Red Hat trademarks that +// are incorporated in this software or its documentation. +// + +package config + +import ( + "fmt" + "os" + "testing" + + ft "github.com/openshift/ansible-service-broker/pkg/fusortest" +) + +/*func TestCreateConfig(t *testing.T) { + config, err := CreateConfig("testdata/test-config.yaml") + if err != nil { + t.Fatal(err.Error()) + } + + ft.AssertEqual(t, config.Registry[0].Type, "dockerhub", "mismatch registry type") + ft.AssertEqual(t, config.Registry[0].Name, "docker", "mismatch registry name") + ft.AssertEqual(t, config.Registry[0].URL, "https://registry.hub.docker.com", + "mismatch registry url") + ft.AssertEqual(t, config.Registry[0].User, "DOCKERHUB_USER", "mismatch registry user") + ft.AssertEqual(t, config.Registry[0].Pass, "DOCKERHUB_PASS", "mismatch registry pass") + ft.AssertEqual(t, config.Registry[0].Org, "DOCKERHUB_ORG", "mismatch registry org") + ft.AssertFalse(t, config.Registry[0].Fail, "mismatch registry fail") + ft.AssertEqual(t, config.Registry[1].WhiteList[0], "^legitimate.*-apb$", + "mismatch whitelist entry") + ft.AssertEqual(t, config.Registry[1].BlackList[1], "^specific-blacklist-apb$", + "mismatch blacklist entry") + + ft.AssertEqual(t, config.Dao.EtcdHost, "localhost", "") + ft.AssertEqual(t, config.Dao.EtcdPort, "2379", "") + + ft.AssertEqual(t, config.Log.LogFile, "/var/log/ansible-service-broker/asb.log", "") + ft.AssertTrue(t, config.Log.Stdout, "") + ft.AssertTrue(t, config.Log.Color, "") + ft.AssertEqual(t, config.Log.Level, "debug", "") + + ft.AssertEqual(t, config.Openshift.Host, "", "") + ft.AssertEqual(t, config.Openshift.CAFile, "", "") + ft.AssertEqual(t, config.Openshift.BearerTokenFile, "", "") + ft.AssertEqual(t, config.Openshift.PullPolicy, "IfNotPresent", "") + ft.AssertEqual(t, config.Openshift.SandboxRole, "edit", "") + + ft.AssertTrue(t, config.Broker.BootstrapOnStartup, "mismatch bootstrap") + ft.AssertTrue(t, config.Broker.DevBroker, "mismatch dev") + ft.AssertTrue(t, config.Broker.Recovery, "mismatch recovery") + ft.AssertTrue(t, config.Broker.OutputRequest, "mismatch output") + ft.AssertFalse(t, config.Broker.LaunchApbOnBind, "mismatch launch") + ft.AssertEqual(t, config.Broker.SSLCert, "/path/to/cert", "mismatch cert") + ft.AssertEqual(t, config.Broker.SSLCertKey, "/path/to/key", "mismatch key") + + ft.AssertEqual(t, config.Broker.Auth[0].Type, "basic", "mismatch basic") + ft.AssertTrue(t, config.Broker.Auth[0].Enabled, "mismatch enable") + ft.AssertEqual(t, config.Broker.Auth[1].Type, "oauth", "mismatch basic") + ft.AssertFalse(t, config.Broker.Auth[1].Enabled, "mismatch enable") +} +*/ + +var config *Config + +func TestMain(m *testing.M) { + /* + config = Config{config: map[string]interface{}{ + "registry": []interface{}{ + map[string]interface{}{ + "type": "dockerhub", + "name": "dh", + "url": "https://registry.hub.docker.com", + "user": "shurley", + "pass": "testingboom", + "org": "shurley", + }, map[string]interface{}{ + "pass": "testingboom", + "org": "ansibleplaybookbundle", + "type": "dockerhub", + "name": "play", + "url": "https://registry.hub.docker.com", + "user": "shurley", + }, + }, + "broker": map[string]interface{}{ + "launch_apb_on_bind": "false", + "bootstrap_on_startup": true, + "recovery": true, + "output_request": true, + "ssl_cert_key": "/var/run/secrets/kubernetes.io/serviceaccount/tls.key", + "ssl_cert": "/var/run/secrets/kubernetes.io/serviceaccount/tls.crt", + "refresh_interval": "600s", + "dev_broker": true, + "testInt": 100, + "testFloat32": 32.87, + "testFloat64": 45677.0799485958595, + }, + }, + mutex: sync.RWMutex{}, + } + */ + c, err := CreateConfig("testdata/generated_local_development.yaml") + if err != nil { + fmt.Printf("Unable to create config - %v", err) + } + config = c + retCode := m.Run() + os.Exit(retCode) +} + +func TestConfigGetInt(t *testing.T) { + testInt := config.GetInt("broker.testInt") + testInvalidInt := config.GetInt("makes.no.sense") + ft.AssertEqual(t, 100, testInt) + ft.AssertEqual(t, 0, testInvalidInt) +} + +func TestConfigGetString(t *testing.T) { + testString := config.GetString("registry.dh.user") + testInvalidString := config.GetString("makes.no.sense") + ft.AssertEqual(t, "shurley", testString) + ft.AssertEqual(t, "", testInvalidString) +} + +func TestConfigGetSliceString(t *testing.T) { + testString := config.GetSliceOfStrings("registry.dh.black_list") + whiteList := config.GetSliceOfStrings("registry.dh.white_list") + value := []string{"malicious.*-apb$", "^specific-blacklist-apb$"} + whiteListValue := []string{"*-apb$"} + if len(testString) == 0 || len(whiteList) == 0 { + t.Fail() + } + for i, str := range testString { + ft.AssertEqual(t, value[i], str) + } + for i, str := range whiteList { + ft.AssertEqual(t, whiteListValue[i], str) + } +} + +func TestConfigGetFloat32(t *testing.T) { + testFloat32 := config.GetFloat32("broker.testFloat32") + testInvalidFloat32 := config.GetFloat32("makes.no.sense") + var defaultFloat32 float32 + ft.AssertEqual(t, float32(32.87), testFloat32) + ft.AssertEqual(t, defaultFloat32, testInvalidFloat32) +} + +func TestConfigGetFloat64(t *testing.T) { + testFloat64 := config.GetFloat64("broker.testFloat64") + testInvalidFloat64 := config.GetFloat64("makes.no.sense") + var defaultFloat64 float64 + ft.AssertEqual(t, float64(45677.0799485958595), testFloat64) + ft.AssertEqual(t, defaultFloat64, testInvalidFloat64) +} + +func TestConfigGetBool(t *testing.T) { + testBoolTrue := config.GetBool("broker.recovery") + testInvalidBool := config.GetBool("makes.no.sense") + ft.AssertTrue(t, testBoolTrue) + ft.AssertFalse(t, testInvalidBool) +} + +func TestConfigGetSubMap(t *testing.T) { + testInvalidSubMap := config.GetSubConfig("makes.no.sense") + testNoNameArray := config.GetSubConfig("dh_no_names") + testSubMap := config.GetSubConfig("registry") + ft.AssertEqual(t, testSubMap.GetString("dh.name"), "dh") + ft.AssertEqual(t, testSubMap.GetString("play.user"), "shurley") + ft.AssertEqual(t, testSubMap.GetString("dh.url"), "https://registry.hub.docker.com") + ft.AssertEqual(t, testSubMap.GetString("dh.pass"), "testingboom") + ft.AssertEqual(t, testSubMap.GetString("dh.org"), "shurley") + ft.AssertEqual(t, testSubMap.GetString("play.org"), "ansibleplaybookbundle") + ft.AssertEqual(t, testSubMap.GetString("dh.type"), "dockerhub") + ft.AssertEqual(t, testSubMap.GetString("play.type"), "dockerhub") + ft.AssertTrue(t, testInvalidSubMap.Empty()) + ft.AssertTrue(t, testNoNameArray.Empty()) +} diff --git a/testdata/generated_local_development.yaml b/testdata/generated_local_development.yaml new file mode 100644 index 0000000..08c1fe6 --- /dev/null +++ b/testdata/generated_local_development.yaml @@ -0,0 +1,44 @@ +--- +registry: + - type: dockerhub + name: dh + url: https://registry.hub.docker.com + user: shurley + pass: testingboom + org: shurley + white_list: + - "*-apb$" + black_list: + - "malicious.*-apb$" + - "^specific-blacklist-apb$" + - type: dockerhub + name: play + user: shurley + pass: testingboom + org: ansibleplaybookbundle + url: https://registry.hub.docker.com +dao: + etcd_host: 'localhost' + etcd_port: 2379 +log: + logfile: /tmp/ansible-service-broker-asb.log + stdout: true + level: debug + color: true +openshift: + host: 192.168.37.1 + bearer_token_file: + ca_file: + image_pull_policy: Always +broker: + dev_broker: true + launch_apb_on_bind: false + bootstrap_on_startup: true + recovery: true + output_request: true + ssl_cert_key: /var/run/secrets/kubernetes.io/serviceaccount/tls.key + ssl_cert: /var/run/secrets/kubernetes.io/serviceaccount/tls.crt + refresh_interval: 600s + testInt: 100 + testFloat32: 32.87 + testFloat64: 45677.0799485958595