-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
parser.go
302 lines (263 loc) · 8.55 KB
/
parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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
}