Commit ecef026b authored by Matt Butcher's avatar Matt Butcher Committed by GitHub

Merge pull request #2545 from technosophos/feat/set-list-index

feat(helm): support array index format for --set.
parents 9f9b3e87 c01c7318
...@@ -107,7 +107,6 @@ func DeploymentManifest(opts *Options) (string, error) { ...@@ -107,7 +107,6 @@ func DeploymentManifest(opts *Options) (string, error) {
// resource. // resource.
func ServiceManifest(namespace string) (string, error) { func ServiceManifest(namespace string) (string, error) {
obj := service(namespace) obj := service(namespace)
buf, err := yaml.Marshal(obj) buf, err := yaml.Marshal(obj)
return string(buf), err return string(buf), err
} }
......
...@@ -113,6 +113,11 @@ servers: ...@@ -113,6 +113,11 @@ servers:
port: 81 port: 81
``` ```
The above cannot be expressed with `--set` in Helm `<=2.4`. In Helm 2.5, the
accessing the port on foo is `--set servers[0].port=80`. Not only is it harder
for the user to figure out, but it is prone to errors if at some later time the
order of the `servers` is changed.
Easy to use: Easy to use:
```yaml ```yaml
...@@ -123,6 +128,8 @@ servers: ...@@ -123,6 +128,8 @@ servers:
port: 81 port: 81
``` ```
Accessing foo's port is much more obvious: `--set servers.foo.port=80`.
## Document 'values.yaml' ## Document 'values.yaml'
Every defined property in 'values.yaml' should be documented. The documentation string should begin with the name of the property that it describes, and then give at least a one-sentence description. Every defined property in 'values.yaml' should be documented. The documentation string should begin with the name of the property that it describes, and then give at least a one-sentence description.
......
...@@ -264,6 +264,22 @@ name: ...@@ -264,6 +264,22 @@ name:
- c - c
``` ```
As of Helm 2.5.0, it is possible to access list items using an array index syntax.
For example, `--set servers[0].port=80` becomes:
```yaml
servers:
- port: 80
```
Multiple values can be set this way. The line `--set servers[0].port=80,servers[0].host=example` becomes:
```yaml
servers:
- port: 80
host: example
```
Sometimes you need to use special characters in your `--set` lines. You can use Sometimes you need to use special characters in your `--set` lines. You can use
a backslash to escape the characters; `--set name=value1\,value2` will become: a backslash to escape the characters; `--set name=value1\,value2` will become:
...@@ -280,9 +296,9 @@ nodeSelector: ...@@ -280,9 +296,9 @@ nodeSelector:
kubernetes.io/role: master kubernetes.io/role: master
``` ```
The `--set` syntax is not as expressive as YAML, especially when it comes to Deeply nested datastructures can be difficult to express using `--set`. Chart
collections. And there is currently no method for expressing things such as "set designers are encouraged to consider the `--set` usage when designing the format
the third item in a list to...". of a `values.yaml` file.
### More Installation Methods ### More Installation Methods
......
...@@ -93,7 +93,7 @@ func runeSet(r []rune) map[rune]bool { ...@@ -93,7 +93,7 @@ func runeSet(r []rune) map[rune]bool {
} }
func (t *parser) key(data map[string]interface{}) error { func (t *parser) key(data map[string]interface{}) error {
stop := runeSet([]rune{'=', ',', '.'}) stop := runeSet([]rune{'=', '[', ',', '.'})
for { for {
switch k, last, err := runesUntil(t.sc, stop); { switch k, last, err := runesUntil(t.sc, stop); {
case err != nil: case err != nil:
...@@ -103,6 +103,23 @@ func (t *parser) key(data map[string]interface{}) error { ...@@ -103,6 +103,23 @@ func (t *parser) key(data map[string]interface{}) error {
return fmt.Errorf("key %q has no value", string(k)) return fmt.Errorf("key %q has no value", string(k))
//set(data, string(k), "") //set(data, string(k), "")
//return err //return err
case last == '[':
// We are in a list index context, so we need to set an index.
i, err := t.keyIndex()
if err != nil {
return fmt.Errorf("error parsing index: %s", err)
}
kk := string(k)
// Find or create target list
list := []interface{}{}
if _, ok := data[kk]; ok {
list = data[kk].([]interface{})
}
// Now we need to get the value after the ].
list, err = t.listItem(list, i)
set(data, kk, list)
return err
case last == '=': case last == '=':
//End of key. Consume =, Get value. //End of key. Consume =, Get value.
// FIXME: Get value list first // FIXME: Get value list first
...@@ -152,6 +169,71 @@ func set(data map[string]interface{}, key string, val interface{}) { ...@@ -152,6 +169,71 @@ func set(data map[string]interface{}, key string, val interface{}) {
data[key] = val data[key] = val
} }
func setIndex(list []interface{}, index int, val interface{}) []interface{} {
if len(list) <= index {
newlist := make([]interface{}, index+1)
copy(newlist, list)
list = newlist
}
list[index] = val
return list
}
func (t *parser) keyIndex() (int, error) {
// First, get the key.
stop := runeSet([]rune{']'})
v, _, err := runesUntil(t.sc, stop)
if err != nil {
return 0, err
}
// v should be the index
return strconv.Atoi(string(v))
}
func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) {
stop := runeSet([]rune{'[', '.', '='})
switch k, last, err := runesUntil(t.sc, stop); {
case len(k) > 0:
return list, fmt.Errorf("unexpected data at end of array index: %q", k)
case err != nil:
return list, err
case last == '=':
vl, e := t.valList()
switch e {
case nil:
return setIndex(list, i, vl), nil
case io.EOF:
return setIndex(list, i, ""), err
case ErrNotList:
v, e := t.val()
return setIndex(list, i, typedVal(v)), e
default:
return list, e
}
case last == '[':
// now we have a nested list. Read the index and handle.
i, err := t.keyIndex()
if err != nil {
return list, fmt.Errorf("error parsing index: %s", err)
}
// Now we need to get the value after the ].
list2, err := t.listItem(list, i)
return setIndex(list, i, list2), err
case last == '.':
// We have a nested object. Send to t.key
inner := map[string]interface{}{}
if len(list) > i {
inner = list[i].(map[string]interface{})
}
// Recurse
e := t.key(inner)
return setIndex(list, i, inner), e
default:
return nil, fmt.Errorf("parse error: unexpected token %v", last)
}
}
func (t *parser) val() ([]rune, error) { func (t *parser) val() ([]rune, error) {
stop := runeSet([]rune{','}) stop := runeSet([]rune{','})
v, _, err := runesUntil(t.sc, stop) v, _, err := runesUntil(t.sc, stop)
......
...@@ -21,6 +21,49 @@ import ( ...@@ -21,6 +21,49 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
) )
func TestSetIndex(t *testing.T) {
tests := []struct {
name string
initial []interface{}
expect []interface{}
add int
val int
}{
{
name: "short",
initial: []interface{}{0, 1},
expect: []interface{}{0, 1, 2},
add: 2,
val: 2,
},
{
name: "equal",
initial: []interface{}{0, 1},
expect: []interface{}{0, 2},
add: 1,
val: 2,
},
{
name: "long",
initial: []interface{}{0, 1, 2, 3, 4, 5},
expect: []interface{}{0, 1, 2, 4, 4, 5},
add: 3,
val: 4,
},
}
for _, tt := range tests {
got := setIndex(tt.initial, tt.add, tt.val)
if len(got) != len(tt.expect) {
t.Fatalf("%s: Expected length %d, got %d", tt.name, len(tt.expect), len(got))
}
if gg := got[tt.add].(int); gg != tt.val {
t.Errorf("%s, Expected value %d, got %d", tt.name, tt.val, gg)
}
}
}
func TestParseSet(t *testing.T) { func TestParseSet(t *testing.T) {
tests := []struct { tests := []struct {
str string str string
...@@ -155,6 +198,59 @@ func TestParseSet(t *testing.T) { ...@@ -155,6 +198,59 @@ func TestParseSet(t *testing.T) {
str: "name1={1021,902", str: "name1={1021,902",
err: true, err: true,
}, },
// List support
{
str: "list[0]=foo",
expect: map[string]interface{}{"list": []string{"foo"}},
},
{
str: "list[0].foo=bar",
expect: map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"foo": "bar"},
},
},
},
{
str: "list[0].foo=bar,list[0].hello=world",
expect: map[string]interface{}{
"list": []interface{}{
map[string]interface{}{"foo": "bar", "hello": "world"},
},
},
},
{
str: "list[0]=foo,list[1]=bar",
expect: map[string]interface{}{"list": []string{"foo", "bar"}},
},
{
str: "list[0]=foo,list[1]=bar,",
expect: map[string]interface{}{"list": []string{"foo", "bar"}},
},
{
str: "list[0]=foo,list[3]=bar",
expect: map[string]interface{}{"list": []interface{}{"foo", nil, nil, "bar"}},
},
{
str: "illegal[0]name.foo=bar",
err: true,
},
{
str: "noval[0]",
expect: map[string]interface{}{"noval": []interface{}{}},
},
{
str: "noval[0]=",
expect: map[string]interface{}{"noval": []interface{}{""}},
},
{
str: "nested[0][0]=1",
expect: map[string]interface{}{"nested": []interface{}{[]interface{}{1}}},
},
{
str: "nested[1][1]=1",
expect: map[string]interface{}{"nested": []interface{}{nil, []interface{}{nil, 1}}},
},
} }
for _, tt := range tests { for _, tt := range tests {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment