blob: a575f0cecc0457d1cb1de1c526c221386200fb91 [file] [log] [blame]
package valueutils
import (
"bytes"
"fmt"
"io"
"strconv"
"github.com/lrills/helm-unittest/unittest/common"
)
// GetValueOfSetPath get the value of the `--set` format path from a manifest
func GetValueOfSetPath(manifest common.K8sManifest, path string) (interface{}, error) {
if path == "" {
return manifest, nil
}
tr := fetchTraverser{manifest}
reader := bytes.NewBufferString(path)
if e := traverseSetPath(reader, &tr, expectKey); e != nil {
return nil, e
}
return tr.data, nil
}
// BuildValueOfSetPath build the complete form the `--set` format path and its value
func BuildValueOfSetPath(val interface{}, path string) (map[interface{}]interface{}, error) {
if path == "" {
return nil, fmt.Errorf("set path is empty")
}
tr := buildTraverser{val, nil}
reader := bytes.NewBufferString(path)
if err := traverseSetPath(reader, &tr, expectKey); err != nil {
return nil, err
}
return tr.getBuildedData(), nil
}
// MergeValues deeply merge values, copied from helm
func MergeValues(dest map[interface{}]interface{}, src map[interface{}]interface{}) map[interface{}]interface{} {
for k, v := range src {
// If the key doesn't exist already, then just set the key to that value
if _, exists := dest[k]; !exists {
dest[k] = v
continue
}
nextMap, ok := v.(map[interface{}]interface{})
// If it isn't another map, overwrite the value
if !ok {
dest[k] = v
continue
}
// If the key doesn't exist already, then just set the key to that value
if _, exists := dest[k]; !exists {
dest[k] = nextMap
continue
}
// Edge case: If the key exists in the destination, but isn't a map
destMap, isMap := dest[k].(map[interface{}]interface{})
// If the source map has a map for this key, prefer it
if !isMap {
dest[k] = v
continue
}
// If we got to this point, it is a map in both, so merge them
dest[k] = MergeValues(destMap, nextMap)
}
return dest
}
type parseTraverser interface {
traverseMapKey(string) error
traverseListIdx(int) error
}
type fetchTraverser struct {
data interface{}
}
func (tr *fetchTraverser) traverseMapKey(key string) error {
if dmap, ok := tr.data.(map[interface{}]interface{}); ok {
tr.data = dmap[key]
return nil
} else if dman, ok := tr.data.(common.K8sManifest); ok {
tr.data = dman[key]
return nil
}
return fmt.Errorf(
"can't get [\"%s\"] from a non map type:\n%s",
key, common.TrustedMarshalYAML(tr.data),
)
}
func (tr *fetchTraverser) traverseListIdx(idx int) error {
if d, ok := tr.data.([]interface{}); ok {
if idx < 0 || idx >= len(d) {
return fmt.Errorf("[%d] :\n%s", idx, common.TrustedMarshalYAML(d))
}
tr.data = d[idx]
return nil
}
return fmt.Errorf(
"can't get [%d] from a non array type:\n%s",
idx, common.TrustedMarshalYAML(tr.data),
)
}
type buildTraverser struct {
data interface{}
cursors []interface{}
}
func (tr *buildTraverser) traverseMapKey(key string) error {
tr.cursors = append(tr.cursors, key)
return nil
}
func (tr *buildTraverser) traverseListIdx(idx int) error {
tr.cursors = append(tr.cursors, idx)
return nil
}
func (tr buildTraverser) getBuildedData() map[interface{}]interface{} {
builded := make(map[interface{}]interface{})
var current interface{} = builded
for depth, cursor := range tr.cursors {
var next interface{}
if depth == len(tr.cursors)-1 {
next = tr.data
} else {
if idx, ok := tr.cursors[depth+1].(int); ok {
next = make([]interface{}, idx+1)
} else {
next = make(map[interface{}]interface{})
}
}
if key, isString := cursor.(string); isString {
current.(map[interface{}]interface{})[key] = next
} else {
current.([]interface{})[cursor.(int)] = next
}
current = next
}
return builded
}
const (
expectKey = iota
expectIndex = iota
expectDenotation = iota
expectEscaping = iota
)
// create a buffer for escapedMapKey
var bufferedMapKey string
func traverseSetPath(in io.RuneReader, traverser parseTraverser, state int) error {
illegal := runeSet([]rune{',', '{', '}', '='})
stop := runeSet([]rune{'.', '[', ']', ',', '{', '}', '='})
k, last, err := runesUntil(in, stop)
if _, ok := illegal[last]; ok {
return fmt.Errorf("Invalid token found %s", string(last))
}
if err != nil {
if err == io.EOF {
switch {
case len(k) != 0 && state == expectKey:
return traverser.traverseMapKey(string(k))
case len(k) == 0 && state == expectDenotation:
return nil
default:
return fmt.Errorf("Unexpected end of")
}
}
return err
}
var nextState int
switch state {
case expectIndex:
nextState, err = handleExpectIndex(k, last, traverser)
case expectDenotation:
nextState, err = handleExpectDenotation(k, last)
case expectKey:
nextState, err = handleExpectKey(k, last, traverser)
case expectEscaping:
nextState, err = handleExpectEscaping(k, last, traverser)
}
if err != nil {
return err
}
if e := traverseSetPath(in, traverser, nextState); e != nil {
return e
}
return nil
}
func handleExpectIndex(k []rune, last rune, traverser parseTraverser) (int, error) {
if last != ']' {
return -1, fmt.Errorf("Missing index value")
}
idx, idxErr := strconv.Atoi(string(k))
if idxErr != nil {
return -1, idxErr
}
if e := traverser.traverseListIdx(idx); e != nil {
return -1, e
}
return expectDenotation, nil
}
func handleExpectDenotation(k []rune, last rune) (int, error) {
switch last {
case '.':
return expectKey, nil
case '[':
return expectIndex, nil
default:
return -1, fmt.Errorf("Invalid denotation token %s", string(last))
}
}
func handleExpectKey(k []rune, last rune, traverser parseTraverser) (int, error) {
switch last {
case '.':
if e := traverser.traverseMapKey(string(k)); e != nil {
return -1, e
}
return expectKey, nil
case '[':
if len(k) == 0 {
bufferedMapKey = ""
return expectEscaping, nil
}
if e := traverser.traverseMapKey(string(k)); e != nil {
return -1, e
}
return expectIndex, nil
default:
return -1, fmt.Errorf("Invalid key %s", string(last))
}
}
func handleExpectEscaping(k []rune, last rune, traverser parseTraverser) (int, error) {
switch last {
case '.':
bufferedMapKey += string(k) + "."
return expectEscaping, nil
case ']':
bufferedMapKey += string(k)
if e := traverser.traverseMapKey(bufferedMapKey); e != nil {
return -1, e
}
return expectDenotation, nil
default:
return -1, fmt.Errorf("Invalid escaping token %s", string(last))
}
}
// copy from helm
func runesUntil(in io.RuneReader, stop map[rune]bool) ([]rune, rune, error) {
v := []rune{}
for {
switch r, _, e := in.ReadRune(); {
case e != nil:
return v, r, e
case inMap(r, stop):
return v, r, nil
case r == '\\':
next, _, e := in.ReadRune()
if e != nil {
return v, next, e
}
v = append(v, next)
default:
v = append(v, r)
}
}
}
// copy from helm
func inMap(k rune, m map[rune]bool) bool {
_, ok := m[k]
return ok
}
// copy from helm
func runeSet(r []rune) map[rune]bool {
s := make(map[rune]bool, len(r))
for _, rr := range r {
s[rr] = true
}
return s
}