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.Slice: idx, err := strconv.Atoi(path[0]) if err != nil { panic("non-numeric index: " + path[0]) } if idx > v.Len() { panic("undefined index: " + path[0]) } // if there is no element or the current slice length // is the same as the indexed variable create a new element, // append it and then set it to the passed in env var value. if v.Len() == 0 || idx == v.Len() { typ := v.Type().Elem() elem := reflect.New(typ).Elem() v.Set(reflect.Append(v, elem)) } return p.overwriteFields(v.Index(idx), fullpath, path[1:], 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 }