Skip to content

Commit f31a02b

Browse files
committed
feat: support boolean and integer expansion
This PR implements support for boolean and integer values in secret sources. Previously, only string values were read from secret sources, and non-string values were skipped, so this should not be a breaking change. Fixes helmfile#190 Related to helmfile#492 Signed-off-by: German Lashevich <german.lashevich@gmail.com>
1 parent 6914dae commit f31a02b

4 files changed

Lines changed: 126 additions & 52 deletions

File tree

pkg/expansion/expand_match.go

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@ package expansion
33
import (
44
"fmt"
55
"regexp"
6+
"slices"
67
"strings"
78
)
89

910
type ExpandRegexMatch struct {
1011
Target *regexp.Regexp
11-
Lookup func(string) (string, error)
12+
Lookup func(string) (interface{}, error)
1213
Only []string
1314
}
1415

1516
var DefaultRefRegexp = regexp.MustCompile(`((secret)?ref)\+([^\+:]*:\/\/[^\+\n ]+[^\+\n ",])\+?`)
1617

18+
func (e *ExpandRegexMatch) shouldExpand(kind string) bool {
19+
return len(e.Only) == 0 || slices.Contains(e.Only, kind)
20+
}
21+
1722
func (e *ExpandRegexMatch) InString(s string) (string, error) {
1823
var sb strings.Builder
1924
for {
@@ -22,40 +27,60 @@ func (e *ExpandRegexMatch) InString(s string) (string, error) {
2227
sb.WriteString(s)
2328
return sb.String(), nil
2429
}
30+
2531
kind := s[ixs[2]:ixs[3]]
26-
if len(e.Only) > 0 {
27-
var shouldExpand bool
28-
for _, k := range e.Only {
29-
if k == kind {
30-
shouldExpand = true
31-
break
32-
}
33-
}
34-
if !shouldExpand {
35-
sb.WriteString(s)
36-
return sb.String(), nil
37-
}
32+
if !e.shouldExpand(kind) {
33+
sb.WriteString(s)
34+
// FIXME: this skips the rest of the string, is this intended?
35+
return sb.String(), nil
3836
}
37+
3938
ref := s[ixs[6]:ixs[7]]
4039
val, err := e.Lookup(ref)
4140
if err != nil {
4241
return "", fmt.Errorf("expand %s: %v", ref, err)
4342
}
4443
sb.WriteString(s[:ixs[0]])
45-
sb.WriteString(val)
44+
fmt.Fprintf(&sb, "%v", val)
4645
s = s[ixs[1]:]
4746
}
4847
}
4948

49+
// InValue expands matches in the given string value.
50+
// If the entire string matches the regex, it expands and preserves the type.
51+
// If only part of the string matches, it expands as a string.
52+
func (e *ExpandRegexMatch) InValue(s string) (interface{}, error) {
53+
ixs := e.Target.FindStringSubmatchIndex(s)
54+
switch {
55+
// No match, return as is
56+
case ixs == nil:
57+
return s, nil
58+
// Full match, expand preserving type
59+
case ixs[0] == 0 && ixs[1] == len(s):
60+
kind := s[ixs[2]:ixs[3]]
61+
ref := s[ixs[6]:ixs[7]]
62+
if !e.shouldExpand(kind) {
63+
return s, nil
64+
}
65+
val, err := e.Lookup(ref)
66+
if err != nil {
67+
return nil, fmt.Errorf("expand %s: %v", ref, err)
68+
}
69+
return val, nil
70+
// Partial match, expand as string
71+
default:
72+
return e.InString(s)
73+
}
74+
}
75+
5076
func (e *ExpandRegexMatch) InMap(target map[string]interface{}) (map[string]interface{}, error) {
5177
ret, err := ModifyStringValues(target, func(p string) (interface{}, error) {
52-
ret, err := e.InString(p)
78+
ret, err := e.InValue(p)
5379
if err != nil {
5480
return nil, err
5581
}
5682
return ret, nil
5783
})
58-
5984
if err != nil {
6085
return nil, err
6186
}

pkg/expansion/expand_match_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ func TestExpandRegexpMatchInString(t *testing.T) {
144144
tc := testcases[i]
145145

146146
t.Run(tc.name, func(t *testing.T) {
147-
lookup := func(m string) (string, error) {
147+
lookup := func(m string) (interface{}, error) {
148148
parsed, err := url.Parse(m)
149149
if err != nil {
150150
return "", err
@@ -160,7 +160,6 @@ func TestExpandRegexpMatchInString(t *testing.T) {
160160
}
161161

162162
actual, err := expand.InString(tc.input)
163-
164163
if err != nil {
165164
t.Fatalf("unexpected error: %v", err)
166165
}
@@ -227,7 +226,7 @@ func TestExpandRegexpMatchInMap(t *testing.T) {
227226
tc := testcases[i]
228227

229228
t.Run(tc.name, func(t *testing.T) {
230-
lookup := func(m string) (string, error) {
229+
lookup := func(m string) (interface{}, error) {
231230
parsed, err := url.Parse(m)
232231
if err != nil {
233232
return "", err
@@ -242,7 +241,6 @@ func TestExpandRegexpMatchInMap(t *testing.T) {
242241
}
243242

244243
actual, err := expand.InMap(tc.input)
245-
246244
if err != nil {
247245
t.Fatalf("unexpected error: %v", err)
248246
}

vals.go

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,7 @@ const (
119119
ProviderServercore = "servercore"
120120
)
121121

122-
var (
123-
EnvFallbackPrefix = "VALS_"
124-
)
122+
var EnvFallbackPrefix = "VALS_"
125123

126124
type Evaluator interface {
127125
Eval(map[string]interface{}) (map[string]interface{}, error)
@@ -344,26 +342,24 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
344342
expand := expansion.ExpandRegexMatch{
345343
Only: only,
346344
Target: expansion.DefaultRefRegexp,
347-
Lookup: func(key string) (string, error) {
345+
Lookup: func(key string) (interface{}, error) {
348346
if val, ok := r.docCache.Get(key); ok {
349-
valStr, ok := val.(string)
350-
if !ok {
351-
return "", fmt.Errorf("error reading string from cache: unsupported value type %T", val)
347+
if isTerminalValue(val) {
348+
return val, nil
352349
}
353-
return valStr, nil
350+
return nil, fmt.Errorf("error reading string from cache: unsupported value type %T", val)
354351
}
355352

356353
uri, err := url.Parse(key)
357354
if err != nil {
358-
return "", err
355+
return nil, err
359356
}
360357

361358
hash := uriToProviderHash(uri)
362359

363360
p, err := updateProviders(uri, hash)
364-
365361
if err != nil {
366-
return "", err
362+
return nil, err
367363
}
368364

369365
var frag string
@@ -402,12 +398,12 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
402398
if cachedStr, ok := r.strCache.Get(cacheKey); ok {
403399
str, ok = cachedStr.(string)
404400
if !ok {
405-
return "", fmt.Errorf("error reading str from cache: unsupported value type %T", cachedStr)
401+
return nil, fmt.Errorf("error reading str from cache: unsupported value type %T", cachedStr)
406402
}
407403
} else {
408404
str, err = p.GetString(path)
409405
if err != nil {
410-
return "", err
406+
return nil, err
411407
}
412408
r.strCache.Add(cacheKey, str)
413409
}
@@ -419,7 +415,7 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
419415
if cachedMap, ok := r.docCache.Get(mapRequestURI); ok {
420416
obj, ok = cachedMap.(map[string]interface{})
421417
if !ok {
422-
return "", fmt.Errorf("error reading map from cache: unsupported value type %T", cachedMap)
418+
return nil, fmt.Errorf("error reading map from cache: unsupported value type %T", cachedMap)
423419
}
424420
} else if uri.Scheme == "httpjson" {
425421
// Due to the unpredictability in the structure of the JSON object,
@@ -431,13 +427,13 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
431427
// object, accommodating different configurations and variations.
432428
value, err := p.GetString(key)
433429
if err != nil {
434-
return "", err
430+
return nil, err
435431
}
436432
return value, nil
437433
} else {
438434
obj, err = p.GetStringMap(path)
439435
if err != nil {
440-
return "", err
436+
return nil, err
441437
}
442438
r.docCache.Add(mapRequestURI, obj)
443439
}
@@ -446,9 +442,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
446442
for i, k := range keys {
447443
newobj := map[string]interface{}{}
448444
switch t := obj[k].(type) {
449-
case string:
445+
case bool, int, string:
450446
if i != len(keys)-1 {
451-
return "", fmt.Errorf("unexpected type of value for key at %d=%s in %v: expected map[string]interface{}, got %v(%T)", i, k, keys, t, t)
447+
return nil, fmt.Errorf("unexpected type of value for key at %d=%s in %v: expected map[string]interface{}, got %v(%T)", i, k, keys, t, t)
452448
}
453449
r.docCache.Add(key, t)
454450
return t, nil
@@ -458,22 +454,32 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) {
458454
for k, v := range t {
459455
newobj[fmt.Sprintf("%v", k)] = v
460456
}
457+
default:
461458
}
462459
obj = newobj
463460
}
464461

465462
if r.Options.FailOnMissingKeyInMap {
466-
return "", fmt.Errorf("no value found for key %s", frag)
463+
return nil, fmt.Errorf("no value found for key %s", frag)
467464
}
468465

469-
return "", nil
466+
return nil, nil
470467
}
471468
},
472469
}
473470

474471
return &expand, nil
475472
}
476473

474+
func isTerminalValue(v any) bool {
475+
switch v.(type) {
476+
case bool, int, string:
477+
return true
478+
default:
479+
return false
480+
}
481+
}
482+
477483
// Eval replaces 'ref+<provider>://xxxxx' entries by their actual values
478484
func (r *Runtime) Eval(template map[string]interface{}) (map[string]interface{}, error) {
479485
expand, err := r.prepare()
@@ -598,8 +604,10 @@ func applyEnvWithQuote(quote bool) func(map[string]interface{}, ...Options) ([]s
598604
}
599605
}
600606

601-
var Env = applyEnvWithQuote(false)
602-
var QuotedEnv = applyEnvWithQuote(true)
607+
var (
608+
Env = applyEnvWithQuote(false)
609+
QuotedEnv = applyEnvWithQuote(true)
610+
)
603611

604612
type ExecConfig struct {
605613
Stdout io.Writer

0 commit comments

Comments
 (0)