284 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			284 lines
		
	
	
		
			8.0 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.Cut(string(version), ".")
 | |
| 	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.Cut(string(version), ".")
 | |
| 	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() {
 | |
| 		k, v, _ := strings.Cut(env, "=")
 | |
| 		p.env = append(p.env, envVar{k, v})
 | |
| 	}
 | |
| 
 | |
| 	// 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
 | |
| }
 |