Skip to content

Commit b167ded

Browse files
nhtzrAdan Urban Reyes
and
Adan Urban Reyes
authored
perf(yaml): Improve large config files loading times (#48)
* TODO * TODO * set resolved, update tests * revert io.readall * test value from flat key. Consider non string cases * tests for the code coverage god * fix(ghw): fix github workflow --------- Co-authored-by: Adan Urban Reyes <[email protected]>
1 parent 8000827 commit b167ded

File tree

3 files changed

+617
-124
lines changed

3 files changed

+617
-124
lines changed

.github/workflows/build.yaml

+10
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,13 @@ jobs:
3636
uses: shogo82148/actions-goveralls@v1
3737
with:
3838
path-to-profile: profile.cov
39+
flag-name: Go-${{ matrix.go }}
40+
parallel: true
41+
42+
finish:
43+
needs: test
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: shogo82148/actions-goveralls@v1
47+
with:
48+
parallel-finished: true

pkg/yaml/yaml.go

+204-96
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package yaml
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"regexp"
8+
"strconv"
79
"strings"
810

911
"github.com/armory/go-yaml-tools/pkg/secrets"
@@ -12,141 +14,224 @@ import (
1214
log "github.com/sirupsen/logrus"
1315
)
1416

17+
type ObjectMap = map[interface{}]interface{}
18+
type StringMap = map[string]string
19+
type OutputMap = map[string]interface{}
20+
1521
// Resolve takes an array of yaml maps and returns a single map of a merged
1622
// properties. The order of `ymlTemplates` matters, it should go from lowest
1723
// to highest precendence.
18-
func Resolve(ymlTemplates []map[interface{}]interface{}, envKeyPairs map[string]string) (map[string]interface{}, error) {
24+
func Resolve(ymlTemplates []ObjectMap, envKeyPairs StringMap) (OutputMap, error) {
1925
log.Debugf("Using environ %+v\n", envKeyPairs)
2026

21-
mergedMap := map[interface{}]interface{}{}
27+
mergedMap := ObjectMap{}
2228
for _, yml := range ymlTemplates {
2329
if err := mergo.Merge(&mergedMap, yml, mergo.WithOverride); err != nil {
2430
log.Error(err)
2531
}
2632
}
2733

2834
// unlike other secret engines, the vault config needs to be registered before it can decrypt anything
29-
vaultCfg := extractVaultConfig(mergedMap)
30-
if vaultCfg != nil && (secrets.VaultConfig{}) != *vaultCfg {
35+
vaultCfg, err := extractVaultConfig(mergedMap)
36+
if err == nil {
3137
if err := secrets.RegisterVaultConfig(*vaultCfg); err != nil {
3238
log.Errorf("Error registering vault config: %v", err)
3339
}
3440
}
3541

3642
stringMap := convertToStringMap(mergedMap)
3743

38-
err := subValues(stringMap, stringMap, envKeyPairs)
39-
40-
if err != nil {
44+
if err := subValues(stringMap, stringMap, envKeyPairs); err != nil {
4145
return nil, err
4246
}
4347

4448
return stringMap, nil
4549
}
4650

47-
func extractVaultConfig(m map[interface{}]interface{}) *secrets.VaultConfig {
48-
if secretsMap, ok := m["secrets"].(map[interface{}]interface{}); ok {
49-
if vaultmap, ok := secretsMap["vault"].(map[interface{}]interface{}); ok {
50-
cfg, err := secrets.DecodeVaultConfig(vaultmap)
51-
if err != nil {
52-
log.Errorf("Error decoding vault config: %v", err)
53-
return nil
54-
}
55-
return cfg
56-
}
51+
var EVCErrorMissingKey = errors.New("missing secrets.vault key")
52+
var EVCErrorDecoding = errors.New("error decoding vault config")
53+
var EVCErrorEmpty = errors.New("empty decoded vault config")
54+
55+
func extractVaultConfig(m ObjectMap) (*secrets.VaultConfig, error) {
56+
secretsMap, ok := m["secrets"].(ObjectMap)
57+
if !ok {
58+
return nil, EVCErrorMissingKey
5759
}
58-
return nil
60+
vaultmap, ok := secretsMap["vault"].(ObjectMap)
61+
if !ok {
62+
return nil, EVCErrorMissingKey
63+
}
64+
cfg, err := secrets.DecodeVaultConfig(vaultmap)
65+
if err != nil {
66+
return nil, fmt.Errorf("%w: %v", EVCErrorDecoding, err)
67+
}
68+
if cfg == nil {
69+
return nil, EVCErrorEmpty
70+
}
71+
if *cfg == (secrets.VaultConfig{}) {
72+
return nil, EVCErrorEmpty
73+
}
74+
return cfg, nil
5975
}
6076

61-
func convertToStringMap(m map[interface{}]interface{}) map[string]interface{} {
62-
newMap := map[string]interface{}{}
77+
func convertToStringMap(m ObjectMap) OutputMap {
78+
newMap := OutputMap{}
79+
var kstring string
6380
for k, v := range m {
64-
switch v.(type) {
65-
case map[interface{}]interface{}:
66-
stringMap := convertToStringMap(v.(map[interface{}]interface{}))
67-
newMap[k.(string)] = stringMap
68-
case []interface{}:
69-
var collection []interface{}
70-
for _, vv := range v.([]interface{}) {
71-
switch vv.(type) {
72-
case map[interface{}]interface{}:
73-
collection = append(collection, convertToStringMap(vv.(map[interface{}]interface{})))
74-
case string, int, bool, float64:
75-
collection = append(collection, fmt.Sprintf("%v", vv))
76-
}
77-
}
78-
newMap[k.(string)] = collection
81+
kstring = k.(string)
82+
convertOneValueToStringMap(v, newMap, kstring)
83+
}
84+
return newMap
85+
}
7986

80-
default:
81-
newMap[k.(string)] = fmt.Sprintf("%v", v)
87+
func convertOneValueToStringMap(v interface{}, newMap OutputMap, kstring string) {
88+
switch v := v.(type) {
89+
case ObjectMap:
90+
newMap[kstring] = convertToStringMap(v)
91+
case []interface{}:
92+
for i := range v {
93+
converOneArrayToStringMap(v[:], i)
8294
}
95+
newMap[kstring] = v
96+
case string:
97+
newMap[kstring] = v
98+
case int:
99+
newMap[kstring] = strconv.Itoa(v)
100+
case bool:
101+
newMap[kstring] = strconv.FormatBool(v)
102+
case float64:
103+
newMap[kstring] = strconv.FormatFloat(v, 'g', -1, 64)
104+
case float32:
105+
newMap[kstring] = strconv.FormatFloat(float64(v), 'g', -1, 64)
106+
case fmt.Stringer:
107+
newMap[kstring] = v.String()
108+
default:
109+
newMap[kstring] = fmt.Sprintf("%v", v)
110+
}
111+
}
112+
113+
func converOneArrayToStringMap(v []interface{}, i int) {
114+
switch vv := v[i].(type) {
115+
case ObjectMap:
116+
v[i] = convertToStringMap(vv)
117+
case []interface{}:
118+
for j := range v {
119+
converOneArrayToStringMap(vv[:], j)
120+
}
121+
v[i] = vv
122+
case string:
123+
v[i] = vv
124+
case int:
125+
v[i] = strconv.Itoa(vv)
126+
case bool:
127+
v[i] = strconv.FormatBool(vv)
128+
case float64:
129+
v[i] = strconv.FormatFloat(vv, 'g', -1, 64)
130+
case float32:
131+
v[i] = strconv.FormatFloat(float64(vv), 'g', -1, 64)
132+
case fmt.Stringer:
133+
v[i] = vv.String()
134+
default:
135+
v[i] = fmt.Sprintf("%v", vv)
83136
}
84-
return newMap
85137
}
86138

87-
func subValues(fullMap map[string]interface{}, subMap map[string]interface{}, env map[string]string) error {
139+
var re = regexp.MustCompile("\\$\\{(.*?)}")
140+
141+
func subValues(fullMap OutputMap, subMap OutputMap, env StringMap) error {
88142
//responsible for finding all variables that need to be substituted
89-
keepResolving := true
90143
loops := 0
91-
re := regexp.MustCompile("\\$\\{(.*?)\\}")
92-
for keepResolving && loops < len(subMap) {
144+
for loops < len(subMap) {
93145
loops++
94146
for k, value := range subMap {
95-
switch value.(type) {
96-
case map[string]interface{}:
97-
err := subValues(fullMap, value.(map[string]interface{}), env)
98-
if err != nil {
99-
return err
100-
}
101-
case []interface{}:
102-
sliceMap := make(map[string]interface{})
103-
for i := 0; i < len(value.([]interface{})); i++ {
104-
sliceMap[fmt.Sprint(i)] = value.([]interface{})[i]
105-
}
106-
err := subValues(fullMap, sliceMap, env)
107-
if err != nil {
108-
return err
109-
}
110-
case string:
111-
valStr := value.(string)
112-
113-
secret, wasSecret, err := resolveSecret(valStr)
114-
if err != nil {
115-
return err
116-
}
117-
118-
if wasSecret {
119-
subMap[k] = secret
120-
continue
121-
}
122-
123-
keys := re.FindAllStringSubmatch(valStr, -1)
124-
for _, keyToSub := range keys {
125-
resolvedValue := resolveSubs(fullMap, keyToSub[1], env)
126-
subMap[k] = strings.Replace(valStr, "${"+keyToSub[1]+"}", resolvedValue, -1)
127-
}
147+
if err := processOneSubvalue(fullMap, subMap, env, value, k); err != nil {
148+
return err
128149
}
129150
}
130151
}
131152
return nil
132153
}
133154

134-
func resolveSecret(valStr string) (string, bool, error) {
135-
// if the value is a secret resolve it
136-
if secrets.IsEncryptedSecret(valStr) {
137-
d, err := secrets.NewDecrypter(context.TODO(), valStr)
155+
func processOneSubvalue(fullMap OutputMap, subMap OutputMap, env StringMap, value interface{}, k string) error {
156+
var secret string
157+
var decrypter secrets.Decrypter
158+
var valueBytes []byte
159+
switch value := value.(type) {
160+
case map[string]interface{}:
161+
err := subValues(fullMap, value, env)
138162
if err != nil {
139-
return "", true, err
163+
return err
164+
}
165+
case []interface{}:
166+
for i := 0; i < len(value); i++ {
167+
err := processOneSubvalueFromArray(fullMap, value[:], env, value[i], i)
168+
if err != nil {
169+
return err
170+
}
140171
}
172+
case string:
173+
if secrets.IsEncryptedSecret(value) {
174+
var err error
175+
if decrypter, err = secrets.NewDecrypter(context.TODO(), value); err != nil {
176+
return err
177+
} else if secret, err = decrypter.Decrypt(); err != nil {
178+
return err
179+
}
180+
subMap[k] = secret
181+
return nil
182+
}
183+
184+
valueBytes = []byte(value)
185+
valueBytes = re.ReplaceAllFunc(valueBytes, func(key []byte) []byte {
186+
i := len(key) - 1
187+
myKey := string(key[2:i])
188+
return []byte(resolveSubs(fullMap, myKey, env))
189+
})
190+
value = string(valueBytes)
191+
subMap[k] = value
192+
}
193+
return nil
194+
}
141195

142-
secret, err := d.Decrypt()
196+
func processOneSubvalueFromArray(fullMap OutputMap, subslice []interface{}, env StringMap, value interface{}, k int) error {
197+
var secret string
198+
var decrypter secrets.Decrypter
199+
var valueBytes []byte
200+
switch value := value.(type) {
201+
case map[string]interface{}:
202+
err := subValues(fullMap, value, env)
143203
if err != nil {
144-
return "", true, err
204+
return err
205+
}
206+
case []interface{}:
207+
for i := 0; i < len(value); i++ {
208+
err := processOneSubvalueFromArray(fullMap, value[:], env, value[i], i)
209+
if err != nil {
210+
return err
211+
}
212+
}
213+
case string:
214+
if secrets.IsEncryptedSecret(value) {
215+
var err error
216+
if decrypter, err = secrets.NewDecrypter(context.TODO(), value); err != nil {
217+
return err
218+
} else if secret, err = decrypter.Decrypt(); err != nil {
219+
return err
220+
}
221+
subslice[k] = secret
222+
return nil
145223
}
146224

147-
return secret, true, nil
225+
valueBytes = []byte(value)
226+
valueBytes = re.ReplaceAllFunc(valueBytes, func(key []byte) []byte {
227+
i := len(key) - 1
228+
myKey := string(key[2:i])
229+
return []byte(resolveSubs(fullMap, myKey, env))
230+
})
231+
value = string(valueBytes)
232+
subslice[k] = value
148233
}
149-
return valStr, false, nil
234+
return nil
150235
}
151236

152237
func resolveSubs(m map[string]interface{}, keyToSub string, env map[string]string) string {
@@ -159,7 +244,7 @@ func resolveSubs(m map[string]interface{}, keyToSub string, env map[string]strin
159244
defaultKey = keyDefaultSplit[1]
160245
}
161246

162-
if v := valueFromFlatKey(subKey, m); v != "" {
247+
if v, err := valueFromFlatKey(subKey, m); err == nil {
163248
defaultKey = v
164249
} else if v, ok := env[subKey]; ok {
165250
defaultKey = v
@@ -168,17 +253,40 @@ func resolveSubs(m map[string]interface{}, keyToSub string, env map[string]strin
168253
return defaultKey
169254
}
170255

171-
func valueFromFlatKey(flatKey string, m map[string]interface{}) string {
172-
keys := strings.Split(flatKey, ".")
173-
for _, key := range keys {
174-
switch m[key].(type) {
175-
case map[string]interface{}:
176-
m = m[key].(map[string]interface{})
177-
case string:
178-
return m[key].(string)
179-
default:
180-
continue
256+
var VFFKErrorNotFound = errors.New("not found")
257+
var VFFKErrorInvalidIntermediaryType = errors.New("expected map[string]interface{}")
258+
var VFFKErrorInvalidLeafType = errors.New("expected string or stringer()")
259+
260+
func valueFromFlatKey(flatKey string, root map[string]interface{}) (string, error) {
261+
fields := strings.Split(flatKey, ".")
262+
var currVal interface{} = root
263+
var currMap OutputMap // pre-alloc OutputMap ref. Actually assigned & used in loop below
264+
var ok bool
265+
for i := range fields {
266+
if currVal == nil {
267+
return "", fmt.Errorf("path %q was %w", flatKey, VFFKErrorNotFound)
268+
}
269+
if currMap, ok = currVal.(OutputMap); !ok {
270+
return "", fmt.Errorf("path %q was of type %T, %w", strings.Join(fields[:i], "."), currVal, VFFKErrorInvalidIntermediaryType)
181271
}
272+
if currVal, ok = currMap[fields[i]]; !ok {
273+
return "", fmt.Errorf("path %q was %w", flatKey, VFFKErrorNotFound)
274+
}
275+
}
276+
switch v := currVal.(type) {
277+
case string:
278+
return v, nil
279+
case fmt.Stringer:
280+
return v.String(), nil
281+
case float32:
282+
return strconv.FormatFloat(float64(v), 'g', -1, 64), nil
283+
case float64:
284+
return strconv.FormatFloat(v, 'g', -1, 64), nil
285+
case int:
286+
return strconv.Itoa(v), nil
287+
case bool:
288+
return strconv.FormatBool(v), nil
289+
default:
290+
return "", fmt.Errorf("path %q is type %T, %w", flatKey, v, VFFKErrorInvalidLeafType)
182291
}
183-
return ""
184292
}

0 commit comments

Comments
 (0)