forked from TrueCloudLab/distribution
d5b2f94c7c
Without this, the log message for the user indicates a problem with the yaml file, so identifying the actual error is hard. This change fixes the output so that the incorrect environment variable is easy to spot. Fixes #3653 Signed-off-by: James Hewitt <james.hewitt@uk.ibm.com>
283 lines
8 KiB
Go
283 lines
8 KiB
Go
package configuration
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// Version is a major/minor version pair of the form Major.Minor
|
|
// Major version upgrades indicate structure or type changes
|
|
// Minor version upgrades should be strictly additive
|
|
type Version string
|
|
|
|
// MajorMinorVersion constructs a Version from its Major and Minor components
|
|
func MajorMinorVersion(major, minor uint) Version {
|
|
return Version(fmt.Sprintf("%d.%d", major, minor))
|
|
}
|
|
|
|
func (version Version) major() (uint, error) {
|
|
majorPart := strings.Split(string(version), ".")[0]
|
|
major, err := strconv.ParseUint(majorPart, 10, 0)
|
|
return uint(major), err
|
|
}
|
|
|
|
// Major returns the major version portion of a Version
|
|
func (version Version) Major() uint {
|
|
major, _ := version.major()
|
|
return major
|
|
}
|
|
|
|
func (version Version) minor() (uint, error) {
|
|
minorPart := strings.Split(string(version), ".")[1]
|
|
minor, err := strconv.ParseUint(minorPart, 10, 0)
|
|
return uint(minor), err
|
|
}
|
|
|
|
// Minor returns the minor version portion of a Version
|
|
func (version Version) Minor() uint {
|
|
minor, _ := version.minor()
|
|
return minor
|
|
}
|
|
|
|
// VersionedParseInfo defines how a specific version of a configuration should
|
|
// be parsed into the current version
|
|
type VersionedParseInfo struct {
|
|
// Version is the version which this parsing information relates to
|
|
Version Version
|
|
// ParseAs defines the type which a configuration file of this version
|
|
// should be parsed into
|
|
ParseAs reflect.Type
|
|
// ConversionFunc defines a method for converting the parsed configuration
|
|
// (of type ParseAs) into the current configuration version
|
|
// Note: this method signature is very unclear with the absence of generics
|
|
ConversionFunc func(interface{}) (interface{}, error)
|
|
}
|
|
|
|
type envVar struct {
|
|
name string
|
|
value string
|
|
}
|
|
|
|
type envVars []envVar
|
|
|
|
func (a envVars) Len() int { return len(a) }
|
|
func (a envVars) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
func (a envVars) Less(i, j int) bool { return a[i].name < a[j].name }
|
|
|
|
// Parser can be used to parse a configuration file and environment of a defined
|
|
// version into a unified output structure
|
|
type Parser struct {
|
|
prefix string
|
|
mapping map[Version]VersionedParseInfo
|
|
env envVars
|
|
}
|
|
|
|
// NewParser returns a *Parser with the given environment prefix which handles
|
|
// versioned configurations which match the given parseInfos
|
|
func NewParser(prefix string, parseInfos []VersionedParseInfo) *Parser {
|
|
p := Parser{prefix: prefix, mapping: make(map[Version]VersionedParseInfo)}
|
|
|
|
for _, parseInfo := range parseInfos {
|
|
p.mapping[parseInfo.Version] = parseInfo
|
|
}
|
|
|
|
for _, env := range os.Environ() {
|
|
envParts := strings.SplitN(env, "=", 2)
|
|
p.env = append(p.env, envVar{envParts[0], envParts[1]})
|
|
}
|
|
|
|
// We must sort the environment variables lexically by name so that
|
|
// more specific variables are applied before less specific ones
|
|
// (i.e. REGISTRY_STORAGE before
|
|
// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY). This sucks, but it's a
|
|
// lot simpler and easier to get right than unmarshalling map entries
|
|
// into temporaries and merging with the existing entry.
|
|
sort.Sort(p.env)
|
|
|
|
return &p
|
|
}
|
|
|
|
// Parse reads in the given []byte and environment and writes the resulting
|
|
// configuration into the input v
|
|
//
|
|
// Environment variables may be used to override configuration parameters other
|
|
// than version, following the scheme below:
|
|
// v.Abc may be replaced by the value of PREFIX_ABC,
|
|
// v.Abc.Xyz may be replaced by the value of PREFIX_ABC_XYZ, and so forth
|
|
func (p *Parser) Parse(in []byte, v interface{}) error {
|
|
var versionedStruct struct {
|
|
Version Version
|
|
}
|
|
|
|
if err := yaml.Unmarshal(in, &versionedStruct); err != nil {
|
|
return err
|
|
}
|
|
|
|
parseInfo, ok := p.mapping[versionedStruct.Version]
|
|
if !ok {
|
|
return fmt.Errorf("unsupported version: %q", versionedStruct.Version)
|
|
}
|
|
|
|
parseAs := reflect.New(parseInfo.ParseAs)
|
|
err := yaml.Unmarshal(in, parseAs.Interface())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, envVar := range p.env {
|
|
pathStr := envVar.name
|
|
if strings.HasPrefix(pathStr, strings.ToUpper(p.prefix)+"_") {
|
|
path := strings.Split(pathStr, "_")
|
|
|
|
err = p.overwriteFields(parseAs, pathStr, path[1:], envVar.value)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing environment variable %s: %v", pathStr, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
c, err := parseInfo.ConversionFunc(parseAs.Interface())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reflect.ValueOf(v).Elem().Set(reflect.Indirect(reflect.ValueOf(c)))
|
|
return nil
|
|
}
|
|
|
|
// overwriteFields replaces configuration values with alternate values specified
|
|
// through the environment. Precondition: an empty path slice must never be
|
|
// passed in.
|
|
func (p *Parser) overwriteFields(v reflect.Value, fullpath string, path []string, payload string) error {
|
|
for v.Kind() == reflect.Ptr {
|
|
if v.IsNil() {
|
|
panic("encountered nil pointer while handling environment variable " + fullpath)
|
|
}
|
|
v = reflect.Indirect(v)
|
|
}
|
|
switch v.Kind() {
|
|
case reflect.Struct:
|
|
return p.overwriteStruct(v, fullpath, path, payload)
|
|
case reflect.Map:
|
|
return p.overwriteMap(v, fullpath, path, payload)
|
|
case reflect.Interface:
|
|
if v.NumMethod() == 0 {
|
|
if !v.IsNil() {
|
|
return p.overwriteFields(v.Elem(), fullpath, path, payload)
|
|
}
|
|
// Interface was empty; create an implicit map
|
|
var template map[string]interface{}
|
|
wrappedV := reflect.MakeMap(reflect.TypeOf(template))
|
|
v.Set(wrappedV)
|
|
return p.overwriteMap(wrappedV, fullpath, path, payload)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) overwriteStruct(v reflect.Value, fullpath string, path []string, payload string) error {
|
|
// Generate case-insensitive map of struct fields
|
|
byUpperCase := make(map[string]int)
|
|
for i := 0; i < v.NumField(); i++ {
|
|
sf := v.Type().Field(i)
|
|
upper := strings.ToUpper(sf.Name)
|
|
if _, present := byUpperCase[upper]; present {
|
|
panic(fmt.Sprintf("field name collision in configuration object: %s", sf.Name))
|
|
}
|
|
byUpperCase[upper] = i
|
|
}
|
|
|
|
fieldIndex, present := byUpperCase[path[0]]
|
|
if !present {
|
|
logrus.Warnf("Ignoring unrecognized environment variable %s", fullpath)
|
|
return nil
|
|
}
|
|
field := v.Field(fieldIndex)
|
|
sf := v.Type().Field(fieldIndex)
|
|
|
|
if len(path) == 1 {
|
|
// Env var specifies this field directly
|
|
fieldVal := reflect.New(sf.Type)
|
|
err := yaml.Unmarshal([]byte(payload), fieldVal.Interface())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
field.Set(reflect.Indirect(fieldVal))
|
|
return nil
|
|
}
|
|
|
|
// If the field is nil, must create an object
|
|
switch sf.Type.Kind() {
|
|
case reflect.Map:
|
|
if field.IsNil() {
|
|
field.Set(reflect.MakeMap(sf.Type))
|
|
}
|
|
case reflect.Ptr:
|
|
if field.IsNil() {
|
|
field.Set(reflect.New(field.Type().Elem()))
|
|
}
|
|
}
|
|
|
|
err := p.overwriteFields(field, fullpath, path[1:], payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) overwriteMap(m reflect.Value, fullpath string, path []string, payload string) error {
|
|
if m.Type().Key().Kind() != reflect.String {
|
|
// non-string keys unsupported
|
|
logrus.Warnf("Ignoring environment variable %s involving map with non-string keys", fullpath)
|
|
return nil
|
|
}
|
|
|
|
if len(path) > 1 {
|
|
// If a matching key exists, get its value and continue the
|
|
// overwriting process.
|
|
for _, k := range m.MapKeys() {
|
|
if strings.ToUpper(k.String()) == path[0] {
|
|
mapValue := m.MapIndex(k)
|
|
// If the existing value is nil, we want to
|
|
// recreate it instead of using this value.
|
|
if (mapValue.Kind() == reflect.Ptr ||
|
|
mapValue.Kind() == reflect.Interface ||
|
|
mapValue.Kind() == reflect.Map) &&
|
|
mapValue.IsNil() {
|
|
break
|
|
}
|
|
return p.overwriteFields(mapValue, fullpath, path[1:], payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
// (Re)create this key
|
|
var mapValue reflect.Value
|
|
if m.Type().Elem().Kind() == reflect.Map {
|
|
mapValue = reflect.MakeMap(m.Type().Elem())
|
|
} else {
|
|
mapValue = reflect.New(m.Type().Elem())
|
|
}
|
|
if len(path) > 1 {
|
|
err := p.overwriteFields(mapValue, fullpath, path[1:], payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
err := yaml.Unmarshal([]byte(payload), mapValue.Interface())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
m.SetMapIndex(reflect.ValueOf(strings.ToLower(path[0])), reflect.Indirect(mapValue))
|
|
|
|
return nil
|
|
}
|