Add indexed key support to --state-values-set (#1042)
Adds support for indexed key arguments and escaped dot arguments to `--state-values-set`. Things like `--state-values-set config\.yml.something=abc` or `--state-values-set config.something[0]=abc` can be passed in the command line. Resolves #899
This commit is contained in:
		
							parent
							
								
									03898b7a98
								
							
						
					
					
						commit
						4b2d6946be
					
				
							
								
								
									
										7
									
								
								main.go
								
								
								
								
							
							
						
						
									
										7
									
								
								main.go
								
								
								
								
							|  | @ -2,10 +2,11 @@ package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/roboll/helmfile/pkg/app/version" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/roboll/helmfile/pkg/app/version" | ||||||
|  | 
 | ||||||
| 	"github.com/roboll/helmfile/pkg/app" | 	"github.com/roboll/helmfile/pkg/app" | ||||||
| 	"github.com/roboll/helmfile/pkg/helmexec" | 	"github.com/roboll/helmfile/pkg/helmexec" | ||||||
| 	"github.com/roboll/helmfile/pkg/maputil" | 	"github.com/roboll/helmfile/pkg/maputil" | ||||||
|  | @ -483,10 +484,10 @@ func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) { | ||||||
| 			ops := strings.Split(optsSet[i], ",") | 			ops := strings.Split(optsSet[i], ",") | ||||||
| 			for j := range ops { | 			for j := range ops { | ||||||
| 				op := strings.SplitN(ops[j], "=", 2) | 				op := strings.SplitN(ops[j], "=", 2) | ||||||
| 				k := strings.Split(op[0], ".") | 				k := maputil.ParseKey(op[0]) | ||||||
| 				v := op[1] | 				v := op[1] | ||||||
| 
 | 
 | ||||||
| 				set = maputil.Set(set, k, v) | 				maputil.Set(set, k, v) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		conf.set = set | 		conf.set = set | ||||||
|  |  | ||||||
|  | @ -1,6 +1,9 @@ | ||||||
| package maputil | package maputil | ||||||
| 
 | 
 | ||||||
| import "fmt" | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| func CastKeysToStrings(s interface{}) (map[string]interface{}, error) { | func CastKeysToStrings(s interface{}) (map[string]interface{}, error) { | ||||||
| 	new := map[string]interface{}{} | 	new := map[string]interface{}{} | ||||||
|  | @ -60,32 +63,129 @@ func recursivelyStringifyMapKey(v interface{}) (interface{}, error) { | ||||||
| 	return casted_v, nil | 	return casted_v, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func Set(m map[string]interface{}, key []string, value string) map[string]interface{} { | type arg interface { | ||||||
|  | 	getMap(map[string]interface{}) map[string]interface{} | ||||||
|  | 	set(map[string]interface{}, string) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type keyArg struct { | ||||||
|  | 	key string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a keyArg) getMap(m map[string]interface{}) map[string]interface{} { | ||||||
|  | 	_, ok := m[a.key] | ||||||
|  | 	if !ok { | ||||||
|  | 		m[a.key] = map[string]interface{}{} | ||||||
|  | 	} | ||||||
|  | 	switch t := m[a.key].(type) { | ||||||
|  | 	case map[string]interface{}: | ||||||
|  | 		return t | ||||||
|  | 	default: | ||||||
|  | 		panic(fmt.Errorf("unexpected type: %v(%T)", t, t)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a keyArg) set(m map[string]interface{}, value string) { | ||||||
|  | 	m[a.key] = value | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type indexedKeyArg struct { | ||||||
|  | 	key   string | ||||||
|  | 	index int | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a indexedKeyArg) getArray(m map[string]interface{}) []interface{} { | ||||||
|  | 	_, ok := m[a.key] | ||||||
|  | 	if !ok { | ||||||
|  | 		m[a.key] = make([]interface{}, a.index+1) | ||||||
|  | 	} | ||||||
|  | 	switch t := m[a.key].(type) { | ||||||
|  | 	case []interface{}: | ||||||
|  | 		if len(t) <= a.index { | ||||||
|  | 			t2 := make([]interface{}, a.index+1) | ||||||
|  | 			copy(t, t2) | ||||||
|  | 			t = t2 | ||||||
|  | 		} | ||||||
|  | 		return t | ||||||
|  | 	default: | ||||||
|  | 		panic(fmt.Errorf("unexpected type: %v(%T)", t, t)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a indexedKeyArg) getMap(m map[string]interface{}) map[string]interface{} { | ||||||
|  | 	t := a.getArray(m) | ||||||
|  | 	if t[a.index] == nil { | ||||||
|  | 		t[a.index] = map[string]interface{}{} | ||||||
|  | 	} | ||||||
|  | 	switch t := t[a.index].(type) { | ||||||
|  | 	case map[string]interface{}: | ||||||
|  | 		return t | ||||||
|  | 	default: | ||||||
|  | 		panic(fmt.Errorf("unexpected type: %v(%T)", t, t)) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (a indexedKeyArg) set(m map[string]interface{}, value string) { | ||||||
|  | 	t := a.getArray(m) | ||||||
|  | 	t[a.index] = value | ||||||
|  | 	m[a.key] = t | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getCursor(key string) arg { | ||||||
|  | 	key = strings.ReplaceAll(key, "[", " ") | ||||||
|  | 	key = strings.ReplaceAll(key, "]", " ") | ||||||
|  | 	k := key | ||||||
|  | 	idx := 0 | ||||||
|  | 
 | ||||||
|  | 	n, err := fmt.Sscanf(key, "%s %d", &k, &idx) | ||||||
|  | 
 | ||||||
|  | 	if n == 2 && err == nil { | ||||||
|  | 		return indexedKeyArg{ | ||||||
|  | 			key:   k, | ||||||
|  | 			index: idx, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return keyArg{ | ||||||
|  | 		key: key, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func ParseKey(key string) []string { | ||||||
|  | 	r := []string{} | ||||||
|  | 	part := "" | ||||||
|  | 	escaped := false | ||||||
|  | 	for _, rune := range key { | ||||||
|  | 		split := false | ||||||
|  | 		switch { | ||||||
|  | 		case escaped == false && rune == '\\': | ||||||
|  | 			escaped = true | ||||||
|  | 			continue | ||||||
|  | 		case rune == '.': | ||||||
|  | 			split = escaped == false | ||||||
|  | 		} | ||||||
|  | 		escaped = false | ||||||
|  | 		if split { | ||||||
|  | 			r = append(r, part) | ||||||
|  | 			part = "" | ||||||
|  | 		} else { | ||||||
|  | 			part = part + string(rune) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if len(part) > 0 { | ||||||
|  | 		r = append(r, part) | ||||||
|  | 	} | ||||||
|  | 	return r | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func Set(m map[string]interface{}, key []string, value string) { | ||||||
| 	if len(key) == 0 { | 	if len(key) == 0 { | ||||||
| 		panic(fmt.Errorf("bug: unexpected length of key: %d", len(key))) | 		panic(fmt.Errorf("bug: unexpected length of key: %d", len(key))) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	k := key[0] | 	for len(key) > 1 { | ||||||
| 
 | 		m, key = getCursor(key[0]).getMap(m), key[1:] | ||||||
| 	if len(key) == 1 { |  | ||||||
| 		m[k] = value |  | ||||||
| 		return m |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	remain := key[1:] | 	getCursor(key[0]).set(m, value) | ||||||
| 
 |  | ||||||
| 	nested, ok := m[k] |  | ||||||
| 	if !ok { |  | ||||||
| 		nested = map[string]interface{}{} |  | ||||||
| 	} |  | ||||||
| 	switch t := nested.(type) { |  | ||||||
| 	case map[string]interface{}: |  | ||||||
| 		nested = Set(t, remain, value) |  | ||||||
| 	default: |  | ||||||
| 		panic(fmt.Errorf("unexpected type: %v(%T)", t, t)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	m[k] = nested |  | ||||||
| 
 |  | ||||||
| 	return m |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -59,3 +59,72 @@ func TestMapUtil_IFKeys(t *testing.T) { | ||||||
| 		t.Errorf("unexpected c: expected=C, got=%s", c) | 		t.Errorf("unexpected c: expected=C, got=%s", c) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestMapUtil_KeyArg(t *testing.T) { | ||||||
|  | 	m := map[string]interface{}{} | ||||||
|  | 
 | ||||||
|  | 	key := []string{"a", "b", "c"} | ||||||
|  | 
 | ||||||
|  | 	Set(m, key, "C") | ||||||
|  | 
 | ||||||
|  | 	c := (((m["a"].(map[string]interface{}))["b"]).(map[string]interface{}))["c"] | ||||||
|  | 
 | ||||||
|  | 	if c != "C" { | ||||||
|  | 		t.Errorf("unexpected c: expected=C, got=%s", c) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestMapUtil_IndexedKeyArg(t *testing.T) { | ||||||
|  | 	m := map[string]interface{}{} | ||||||
|  | 
 | ||||||
|  | 	key := []string{"a", "b[0]", "c"} | ||||||
|  | 
 | ||||||
|  | 	Set(m, key, "C") | ||||||
|  | 
 | ||||||
|  | 	c := (((m["a"].(map[string]interface{}))["b"].([]interface{}))[0].(map[string]interface{}))["c"] | ||||||
|  | 
 | ||||||
|  | 	if c != "C" { | ||||||
|  | 		t.Errorf("unexpected c: expected=C, got=%s", c) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type parseKeyTc struct { | ||||||
|  | 	key    string | ||||||
|  | 	result map[int]string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestMapUtil_ParseKey(t *testing.T) { | ||||||
|  | 	tcs := []parseKeyTc{ | ||||||
|  | 		{ | ||||||
|  | 			key: `a.b.c`, | ||||||
|  | 			result: map[int]string{ | ||||||
|  | 				0: "a", | ||||||
|  | 				1: "b", | ||||||
|  | 				2: "c", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			key: `a\.b.c`, | ||||||
|  | 			result: map[int]string{ | ||||||
|  | 				0: "a.b", | ||||||
|  | 				1: "c", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			key: `a\.b\.c`, | ||||||
|  | 			result: map[int]string{ | ||||||
|  | 				0: "a.b.c", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range tcs { | ||||||
|  | 		parts := ParseKey(tc.key) | ||||||
|  | 
 | ||||||
|  | 		for index, value := range tc.result { | ||||||
|  | 			if parts[index] != value { | ||||||
|  | 				t.Errorf("unexpected key part[%d]: expected=%s, got=%s", index, value, parts[index]) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue