cloudfoundry/stratos

View on GitHub
src/jetstream/repository/interfaces/config/config.go

Summary

Maintainability
A
25 mins
Test Coverage
// 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
    }
}