mirror of https://github.com/helm/helm
Merge pull request #1576 from technosophos/feat/set-parser
fix(helm): improve --set parserpull/1484/head
commit
a081e27598
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*Package strvals provides tools for working with strval lines.
|
||||||
|
|
||||||
|
Helm supports a compressed format for YAML settings which we call strvals.
|
||||||
|
The format is roughly like this:
|
||||||
|
|
||||||
|
name=value,topname.subname=value
|
||||||
|
|
||||||
|
The above is equivalent to the YAML document
|
||||||
|
|
||||||
|
name: value
|
||||||
|
topname:
|
||||||
|
subname: value
|
||||||
|
|
||||||
|
This package provides a parser and utilities for converting the strvals format
|
||||||
|
to other formats.
|
||||||
|
*/
|
||||||
|
package strvals
|
@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package strvals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToYAML takes a string of arguments and converts to a YAML document.
|
||||||
|
func ToYAML(s string) (string, error) {
|
||||||
|
m, err := Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
d, err := yaml.Marshal(m)
|
||||||
|
return string(d), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a set line.
|
||||||
|
//
|
||||||
|
// A set line is of the form name1=value1,name2=value2
|
||||||
|
func Parse(s string) (map[string]interface{}, error) {
|
||||||
|
vals := map[string]interface{}{}
|
||||||
|
scanner := bytes.NewBufferString(s)
|
||||||
|
t := newParser(scanner, vals)
|
||||||
|
err := t.parse()
|
||||||
|
return vals, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//ParseInto parses a strvals line and merges the result into dest.
|
||||||
|
//
|
||||||
|
// If the strval string has a key that exists in dest, it overwrites the
|
||||||
|
// dest version.
|
||||||
|
func ParseInto(s string, dest map[string]interface{}) error {
|
||||||
|
scanner := bytes.NewBufferString(s)
|
||||||
|
t := newParser(scanner, dest)
|
||||||
|
return t.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parser is a simple parser that takes a strvals line and parses it into a
|
||||||
|
// map representation.
|
||||||
|
type parser struct {
|
||||||
|
sc *bytes.Buffer
|
||||||
|
data map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParser(sc *bytes.Buffer, data map[string]interface{}) *parser {
|
||||||
|
return &parser{sc: sc, data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *parser) parse() error {
|
||||||
|
for {
|
||||||
|
err := t.key(t.data)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runeSet(r []rune) map[rune]bool {
|
||||||
|
s := make(map[rune]bool, len(r))
|
||||||
|
for _, rr := range r {
|
||||||
|
s[rr] = true
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *parser) key(data map[string]interface{}) error {
|
||||||
|
stop := runeSet([]rune{'=', ',', '.'})
|
||||||
|
for {
|
||||||
|
switch k, last, err := runesUntil(t.sc, stop); {
|
||||||
|
case err != nil:
|
||||||
|
if len(k) == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("key %q has no value", string(k))
|
||||||
|
//set(data, string(k), "")
|
||||||
|
//return err
|
||||||
|
case last == '=':
|
||||||
|
//End of key. Consume =, Get value.
|
||||||
|
// FIXME: Get value list first
|
||||||
|
vl, e := t.valList()
|
||||||
|
switch e {
|
||||||
|
case nil:
|
||||||
|
set(data, string(k), vl)
|
||||||
|
return nil
|
||||||
|
case io.EOF:
|
||||||
|
set(data, string(k), "")
|
||||||
|
return e
|
||||||
|
case ErrNotList:
|
||||||
|
v, e := t.val()
|
||||||
|
set(data, string(k), typedVal(v))
|
||||||
|
return e
|
||||||
|
default:
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
case last == ',':
|
||||||
|
// No value given. Set the value to empty string. Return error.
|
||||||
|
set(data, string(k), "")
|
||||||
|
return fmt.Errorf("key %q has no value (cannot end with ,)", string(k))
|
||||||
|
case last == '.':
|
||||||
|
// First, create or find the target map.
|
||||||
|
inner := map[string]interface{}{}
|
||||||
|
if _, ok := data[string(k)]; ok {
|
||||||
|
inner = data[string(k)].(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse
|
||||||
|
e := t.key(inner)
|
||||||
|
if len(inner) == 0 {
|
||||||
|
return fmt.Errorf("key map %q has no value", string(k))
|
||||||
|
}
|
||||||
|
set(data, string(k), inner)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(data map[string]interface{}, key string, val interface{}) {
|
||||||
|
// If key is empty, don't set it.
|
||||||
|
if len(key) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *parser) val() ([]rune, error) {
|
||||||
|
v := []rune{}
|
||||||
|
stop := runeSet([]rune{','})
|
||||||
|
v, _, err := runesUntil(t.sc, stop)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNotList = errors.New("not a list")
|
||||||
|
|
||||||
|
func (t *parser) valList() ([]interface{}, error) {
|
||||||
|
|
||||||
|
r, _, e := t.sc.ReadRune()
|
||||||
|
if e != nil {
|
||||||
|
return []interface{}{}, e
|
||||||
|
}
|
||||||
|
|
||||||
|
if r != '{' {
|
||||||
|
t.sc.UnreadRune()
|
||||||
|
return []interface{}{}, ErrNotList
|
||||||
|
}
|
||||||
|
|
||||||
|
list := []interface{}{}
|
||||||
|
stop := runeSet([]rune{',', '}'})
|
||||||
|
for {
|
||||||
|
switch v, last, err := runesUntil(t.sc, stop); {
|
||||||
|
case err != nil:
|
||||||
|
if err == io.EOF {
|
||||||
|
err = errors.New("list must terminate with '}'")
|
||||||
|
}
|
||||||
|
return list, err
|
||||||
|
case last == '}':
|
||||||
|
// If this is followed by ',', consume it.
|
||||||
|
if r, _, e := t.sc.ReadRune(); e == nil && r != ',' {
|
||||||
|
t.sc.UnreadRune()
|
||||||
|
}
|
||||||
|
list = append(list, typedVal(v))
|
||||||
|
return list, nil
|
||||||
|
case last == ',':
|
||||||
|
list = append(list, typedVal(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runesUntil(in *bytes.Buffer, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inMap(k rune, m map[rune]bool) bool {
|
||||||
|
_, ok := m[k]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *parser) listVal() []rune {
|
||||||
|
v := []rune{}
|
||||||
|
for {
|
||||||
|
switch r, _, e := t.sc.ReadRune(); {
|
||||||
|
case e != nil:
|
||||||
|
// End of input or error with reader stops value parsing.
|
||||||
|
return v
|
||||||
|
case r == '\\':
|
||||||
|
//Escape char. Consume next and append.
|
||||||
|
next, _, e := t.sc.ReadRune()
|
||||||
|
if e != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
v = append(v, next)
|
||||||
|
case r == ',':
|
||||||
|
//End of key. Consume ',' and return.
|
||||||
|
return v
|
||||||
|
default:
|
||||||
|
v = append(v, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func typedVal(v []rune) interface{} {
|
||||||
|
val := string(v)
|
||||||
|
if strings.EqualFold(val, "true") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(val, "false") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if iv, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||||
|
return iv
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package strvals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseSet(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
str string
|
||||||
|
expect map[string]interface{}
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"name1=value1",
|
||||||
|
map[string]interface{}{"name1": "value1"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1=value1,name2=value2",
|
||||||
|
map[string]interface{}{"name1": "value1", "name2": "value2"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1=value1,name2=value2,",
|
||||||
|
map[string]interface{}{"name1": "value1", "name2": "value2"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1=value1,,,,name2=value2,",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1=,name2=value2",
|
||||||
|
expect: map[string]interface{}{"name1": "", "name2": "value2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1,name2=",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1,name2=value2",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1,name2=value2\\",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1,name2",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1=one\\,two,name2=three\\,four",
|
||||||
|
map[string]interface{}{"name1": "one,two", "name2": "three,four"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1=one\\=two,name2=three\\=four",
|
||||||
|
map[string]interface{}{"name1": "one=two", "name2": "three=four"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1=one two three,name2=three two one",
|
||||||
|
map[string]interface{}{"name1": "one two three", "name2": "three two one"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outer.inner=value",
|
||||||
|
map[string]interface{}{"outer": map[string]interface{}{"inner": "value"}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outer.middle.inner=value",
|
||||||
|
map[string]interface{}{"outer": map[string]interface{}{"middle": map[string]interface{}{"inner": "value"}}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outer.inner1=value,outer.inner2=value2",
|
||||||
|
map[string]interface{}{"outer": map[string]interface{}{"inner1": "value", "inner2": "value2"}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outer.inner1=value,outer.middle.inner=value",
|
||||||
|
map[string]interface{}{
|
||||||
|
"outer": map[string]interface{}{
|
||||||
|
"inner1": "value",
|
||||||
|
"middle": map[string]interface{}{
|
||||||
|
"inner": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1.name2",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1.name2,name1.name3",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1.name2=",
|
||||||
|
expect: map[string]interface{}{"name1": map[string]interface{}{"name2": ""}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1.=name2",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1.,name2",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1={value1,value2}",
|
||||||
|
map[string]interface{}{"name1": []string{"value1", "value2"}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1={value1,value2},name2={value1,value2}",
|
||||||
|
map[string]interface{}{
|
||||||
|
"name1": []string{"value1", "value2"},
|
||||||
|
"name2": []string{"value1", "value2"},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1={1021,902}",
|
||||||
|
map[string]interface{}{"name1": []int{1021, 902}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name1.name2={value1,value2}",
|
||||||
|
map[string]interface{}{"name1": map[string]interface{}{"name2": []string{"value1", "value2"}}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
str: "name1={1021,902",
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got, err := Parse(tt.str)
|
||||||
|
if err != nil {
|
||||||
|
if tt.err {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Fatalf("%s: %s", tt.str, err)
|
||||||
|
}
|
||||||
|
if tt.err {
|
||||||
|
t.Errorf("%s: Expected error. Got nil", tt.str)
|
||||||
|
}
|
||||||
|
|
||||||
|
y1, err := yaml.Marshal(tt.expect)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
y2, err := yaml.Marshal(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error serializing parsed value: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(y1) != string(y2) {
|
||||||
|
t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.str, y1, y2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseInto(t *testing.T) {
|
||||||
|
got := map[string]interface{}{
|
||||||
|
"outer": map[string]interface{}{
|
||||||
|
"inner1": "overwrite",
|
||||||
|
"inner2": "value2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
input := "outer.inner1=value1,outer.inner3=value3"
|
||||||
|
expect := map[string]interface{}{
|
||||||
|
"outer": map[string]interface{}{
|
||||||
|
"inner1": "value1",
|
||||||
|
"inner2": "value2",
|
||||||
|
"inner3": "value3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ParseInto(input, got); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
y1, err := yaml.Marshal(expect)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
y2, err := yaml.Marshal(got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error serializing parsed value: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(y1) != string(y2) {
|
||||||
|
t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToYAML(t *testing.T) {
|
||||||
|
// The TestParse does the hard part. We just verify that YAML formatting is
|
||||||
|
// happening.
|
||||||
|
o, err := ToYAML("name=value")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expect := "name: value\n"
|
||||||
|
if o != expect {
|
||||||
|
t.Errorf("Expected %q, got %q", expect, o)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue