src/jetstream/repository/interfaces/config/config.go
// Package config allows a struct-loading approach to configuration.
// This is a modified version of the ucpconfig package
package config
import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/govau/cf-common/env"
log "github.com/sirupsen/logrus"
)
const secretsDir = "/etc/secrets"
// APIKeysConfigValue - special type for configuring whether API keys feature is enabled
type APIKeysConfigValue string
// APIKeysConfigEnum - defines possible configuration values for Stratos API keys feature
var APIKeysConfigEnum = struct {
Disabled APIKeysConfigValue
AdminOnly APIKeysConfigValue
AllUsers APIKeysConfigValue
}{
Disabled: "disabled",
AdminOnly: "admin_only",
AllUsers: "all_users",
}
// verifies that given string is a valid config value (i.e., present in APIKeysConfigEnum)
func parseAPIKeysConfigValue(input string) (APIKeysConfigValue, error) {
t := reflect.TypeOf(APIKeysConfigEnum)
v := reflect.ValueOf(APIKeysConfigEnum)
var allowedValues []string
for i := 0; i < t.NumField(); i++ {
allowedValue := string(v.Field(i).Interface().(APIKeysConfigValue))
if allowedValue == input {
return APIKeysConfigValue(input), nil
}
allowedValues = append(allowedValues, allowedValue)
}
return "", fmt.Errorf("Invalid value %q, allowed values: %q", input, allowedValues)
}
// UserEndpointsConfigValue - special type for configuring whether user endpoints feature is enabled
type UserEndpointsConfigValue string
// UserEndpointsConfigEnum - defines possible configuration values for Stratos user endpoints feature
var UserEndpointsConfigEnum = struct {
Disabled UserEndpointsConfigValue
AdminOnly UserEndpointsConfigValue
Enabled UserEndpointsConfigValue
}{
Disabled: "disabled",
AdminOnly: "admin_only",
Enabled: "enabled",
}
// verifies that given string is a valid config value
func parseUserEndpointsConfigValue(input string) (UserEndpointsConfigValue, error) {
t := reflect.TypeOf(UserEndpointsConfigEnum)
v := reflect.ValueOf(UserEndpointsConfigEnum)
var allowedValues []string
for i := 0; i < t.NumField(); i++ {
allowedValue := string(v.Field(i).Interface().(UserEndpointsConfigValue))
if allowedValue == input {
return UserEndpointsConfigValue(input), nil
}
allowedValues = append(allowedValues, allowedValue)
}
return "", fmt.Errorf("Invalid value %q, allowed values: %q", input, allowedValues)
}
var urlType *url.URL
// Load the given pointer to struct with values from the environment and the
// /etc/secrets/ directory.
//
// In order to make the struct load correctly, use struct tags to define the
// configuration name, if the configName struct tag is ommitted it will
// not attempt to look anything up. This is contrary to most serialization
// libraries like JSON which require a "-" struct tag to bypass deserialization.
//
// type A struct {
// Port uint `configName:"PORT"`
// Name string `configName:"SERVICE_NAME"`
// Struct *myType
// }
//
// The name will be given as defined to Getenv, and if that fails a lookup
// it's name is then munged to conform to the /etc/secrets filename structure
// and the file is attempted to be read.
func Load(intf interface{}, envLookup env.Lookup) error {
value := reflect.ValueOf(intf)
if value.Kind() != reflect.Ptr {
return errors.New("config: must provide pointer to struct value")
}
value = value.Elem()
if value.Kind() != reflect.Struct {
return errors.New("config: must provide pointer to struct value")
}
nFields := value.NumField()
typ := value.Type()
for i := 0; i < nFields; i++ {
field := value.Field(i)
strField := typ.Field(i)
tag := strField.Tag.Get("configName")
if tag == "" {
continue
}
if err := setFieldValue(value, field, tag, envLookup); err != nil {
return err
}
}
return nil
}
func setFieldValue(value reflect.Value, field reflect.Value, tags string, envLookup env.Lookup) error {
// Allow multiple values, separated by ',' to allow fallbacks
tagList := strings.Split(tags, ",")
for _, tag := range tagList {
val, ok := envLookup(strings.TrimSpace(tag))
if ok {
return SetStructFieldValue(value, field, val)
}
}
return nil
}
func SetStructFieldValue(value reflect.Value, field reflect.Value, val string) error {
var newVal interface{}
var err error
typ := field.Type()
kind := typ.Kind()
switch kind {
case reflect.Int:
var i int
i, err = strconv.Atoi(val)
newVal = i
case reflect.Int64:
var i int64
i, err = strconv.ParseInt(val, 10, 64)
newVal = i
case reflect.Uint:
var i uint64
i, err = strconv.ParseUint(val, 10, int(typ.Size())*32)
newVal = uint(i)
case reflect.Uint64:
var i uint64
i, err = strconv.ParseUint(val, 10, 64)
newVal = i
case reflect.Float64:
var i float64
i, err = strconv.ParseFloat(val, 64)
newVal = i
case reflect.Slice:
sliceTyp := typ.Elem()
if sliceTyp.Kind() != reflect.String {
return fmt.Errorf("failed to decode value: unsupported slice type %q, only []string supported", kind.String())
}
newVal = strings.Split(val, ",")
case reflect.Bool:
var b bool
b, err = strconv.ParseBool(val)
newVal = b
case reflect.String:
apiKeysConfigType := reflect.TypeOf((*APIKeysConfigValue)(nil)).Elem()
userEndpointsConfigType := reflect.TypeOf((*UserEndpointsConfigValue)(nil)).Elem()
if typ == apiKeysConfigType {
newVal, err = parseAPIKeysConfigValue(val)
} else if typ == userEndpointsConfigType {
newVal, err = parseUserEndpointsConfigValue(val)
} else {
newVal = val
}
default:
if typ == reflect.TypeOf(urlType) {
newVal, err = url.Parse(val)
} else {
return fmt.Errorf("failed to decode value: unsupported type %q", kind.String())
}
}
if err != nil {
return fmt.Errorf("failed to decode value %q to %q: %v", val, kind.String(), err)
}
field.Set(reflect.ValueOf(newVal))
return nil
}
// NewSecretsDirLookup - create a secret dir lookup
// reads a variable in the form HELLO_THERE from a file
// in /etc/secrets/hello-there
func NewSecretsDirLookup(secretsDir string) env.Lookup {
return func(name string) (string, bool) {
name = strings.ToLower(strings.Replace(name, "_", "-", -1))
filename := filepath.Join(secretsDir, name)
if _, err := os.Stat(filename); err == nil {
contents, err := ioutil.ReadFile(filename)
if err != nil {
log.Warnf("Error reading secrets file: %s, %s", filename, err)
return "", false
}
return strings.TrimSpace(string(contents)), true
}
// File does not exist
return "", false
}
}
type notFoundErr string
func isNotFoundErr(err error) bool {
if err == nil {
return false
}
_, ok := err.(notFoundErr)
return ok
}
func (n notFoundErr) Error() string {
return fmt.Sprintf("could not find secret file: %s", string(n))
}
// NewConfigFileLookup - Load the configuration values in the specified config file if it exists
func NewConfigFileLookup(path string) env.Lookup {
// Check if the config file exists
if _, err := os.Stat(path); err != nil {
return env.NoopLookup
}
file, err := os.Open(path)
if err != nil {
log.Warn("Error reading configuration file, ignoring this file: ", err)
return env.NoopLookup
}
defer file.Close()
loadedConfig := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
if strings.Index(line, "#") != 0 {
// Not a comment
keyValue := strings.SplitN(line, "=", 2)
if len(keyValue) == 2 {
loadedConfig[keyValue[0]] = keyValue[1]
}
}
}
if scanner.Err() != nil {
// Error reading configuration file, ignoring this file
return env.NoopLookup
}
log.Debugf("Loaded configuration from file: %s", path)
return func(k string) (string, bool) {
v, ok := loadedConfig[k]
return v, ok
}
}