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:
eddycharly 2019-12-16 23:31:42 +01:00 committed by KUOKA Yusuke
parent 03898b7a98
commit 4b2d6946be
3 changed files with 196 additions and 26 deletions

View File

@ -2,10 +2,11 @@ package main
import (
"fmt"
"github.com/roboll/helmfile/pkg/app/version"
"os"
"strings"
"github.com/roboll/helmfile/pkg/app/version"
"github.com/roboll/helmfile/pkg/app"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/maputil"
@ -483,10 +484,10 @@ func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
ops := strings.Split(optsSet[i], ",")
for j := range ops {
op := strings.SplitN(ops[j], "=", 2)
k := strings.Split(op[0], ".")
k := maputil.ParseKey(op[0])
v := op[1]
set = maputil.Set(set, k, v)
maputil.Set(set, k, v)
}
}
conf.set = set

View File

@ -1,6 +1,9 @@
package maputil
import "fmt"
import (
"fmt"
"strings"
)
func CastKeysToStrings(s interface{}) (map[string]interface{}, error) {
new := map[string]interface{}{}
@ -60,32 +63,129 @@ func recursivelyStringifyMapKey(v interface{}) (interface{}, error) {
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 {
panic(fmt.Errorf("bug: unexpected length of key: %d", len(key)))
}
k := key[0]
if len(key) == 1 {
m[k] = value
return m
for len(key) > 1 {
m, key = getCursor(key[0]).getMap(m), key[1:]
}
remain := key[1:]
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
getCursor(key[0]).set(m, value)
}

View File

@ -59,3 +59,72 @@ func TestMapUtil_IFKeys(t *testing.T) {
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])
}
}
}
}