mirror of https://github.com/helm/helm
Merge bba555aa3f
into 5f4b260ae5
commit
30a7941a07
@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright 2017 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 engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
)
|
||||
|
||||
type expansionState struct {
|
||||
origTop chartutil.Values
|
||||
origVals chartutil.Values
|
||||
engine *Engine
|
||||
prepTpl *preparedTemplate
|
||||
locks map[string]bool
|
||||
valCache map[string]interface{}
|
||||
rendBufs []*bytes.Buffer
|
||||
rendDepth int
|
||||
}
|
||||
|
||||
// ExpandValues will expand all templates found in .Values. The usual .Values structure will be available
|
||||
// via a new `tval` function instead; eg. {{ .Values.foo.bar }} can be accessed via {{ tval foo.bar }}.
|
||||
// This permits recursive expansion; eg. a.b='{{a.c}}', a.c='{{a.d}}', a.d='d '=> a.b='d'. If .Values is
|
||||
// used directly, no recursive expansion will occur. Also, all string values are expanded, so if literal
|
||||
// {{ }} characters are required, they must be escaped; eg. {{ "{{ }}" }}. Values of any other type (eg.
|
||||
// numeric) will remain unchanged.
|
||||
func (engine *Engine) ExpandValues(top chartutil.Values) (chartutil.Values, error) {
|
||||
vals, err := top.Table("Values")
|
||||
if err != nil {
|
||||
return top, nil
|
||||
}
|
||||
|
||||
prepTpl, err := engine.renderPrepare()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error during templated value expansion: %s", err.Error())
|
||||
}
|
||||
|
||||
state := expansionState{
|
||||
origTop: top, origVals: vals, engine: engine, prepTpl: prepTpl, locks: map[string]bool{},
|
||||
valCache: map[string]interface{}{}, rendBufs: []*bytes.Buffer{}, rendDepth: 0,
|
||||
}
|
||||
prepTpl.funcMap["tval"] = state.tvalImpl
|
||||
|
||||
var expVals chartutil.Values
|
||||
expVals, err = state.expandMapVal(vals, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error during templated value expansion: %s", err.Error())
|
||||
}
|
||||
|
||||
newTop := make(chartutil.Values, len(top))
|
||||
for k, v := range top {
|
||||
newTop[k] = v
|
||||
}
|
||||
newTop["Values"] = expVals
|
||||
return newTop, nil
|
||||
}
|
||||
|
||||
func (state *expansionState) tvalImpl(path string) (interface{}, error) {
|
||||
// Lock state is only checked here. This is the only place in which we can jump to some
|
||||
// other subtree of .Values, and if it's unlocked then it's fine to recurse into it. The
|
||||
// only time we need to check for another lock is on any further jump (ie. tval call).
|
||||
if _, locked := state.locks[path]; locked {
|
||||
return "", fmt.Errorf("Cyclic reference to %q", path)
|
||||
}
|
||||
val, err := state.origVals.PathValue(path)
|
||||
if err != nil {
|
||||
if val, err = state.origVals.Table(path); err != nil { // PathValue() will not return maps
|
||||
return "", fmt.Errorf("Value %q does not exist", path)
|
||||
}
|
||||
}
|
||||
return state.expandVal(val, path)
|
||||
}
|
||||
|
||||
func joinPath(parent string, child string) string {
|
||||
if len(parent) == 0 {
|
||||
return child
|
||||
}
|
||||
return parent + "." + child
|
||||
}
|
||||
|
||||
func (state *expansionState) expandMapVal(vals map[string]interface{}, path string) (map[string]interface{}, error) {
|
||||
state.locks[path] = true
|
||||
defer delete(state.locks, path)
|
||||
|
||||
newVals := make(map[string]interface{}, len(vals))
|
||||
for key, val := range vals {
|
||||
newVal, err := state.expandVal(val, joinPath(path, key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newVals[key] = newVal
|
||||
}
|
||||
return newVals, nil
|
||||
}
|
||||
|
||||
func (state *expansionState) expandArrayVal(vals []interface{}, path string) ([]interface{}, error) {
|
||||
state.locks[path] = true
|
||||
defer delete(state.locks, path)
|
||||
|
||||
newVals := make([]interface{}, len(vals))
|
||||
for i, val := range vals {
|
||||
newVal, err := state.expandVal(val, joinPath(path, strconv.Itoa(i)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newVals[i] = newVal
|
||||
}
|
||||
return newVals, nil
|
||||
}
|
||||
|
||||
func (state *expansionState) expandVal(val interface{}, path string) (interface{}, error) {
|
||||
state.locks[path] = true
|
||||
defer delete(state.locks, path)
|
||||
|
||||
switch typedVal := val.(type) {
|
||||
case chartutil.Values:
|
||||
return state.expandMapVal(typedVal, path)
|
||||
case map[string]interface{}:
|
||||
return state.expandMapVal(typedVal, path)
|
||||
case []interface{}:
|
||||
return state.expandArrayVal(typedVal, path)
|
||||
case string:
|
||||
return state.renderVal(path, typedVal)
|
||||
default:
|
||||
return val, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (state *expansionState) renderVal(path string, val string) (interface{}, error) {
|
||||
if !strings.Contains(val, "{{") {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// We only cache string values that we have actually resolved. It's probably not worthwhile
|
||||
// for anything else as either the value is trivial to retrieve (if there are no template
|
||||
// expressions) or it is some structure (eg. a list) that will not usually be used directly
|
||||
// (and is straightforward to reconstruct from cached values).
|
||||
if precalc, found := state.valCache[path]; found {
|
||||
return precalc, nil
|
||||
}
|
||||
|
||||
tplName := joinPath("__expand", path)
|
||||
tpl := state.prepTpl.tpl.New(tplName).Funcs(state.prepTpl.funcMap)
|
||||
if _, err := tpl.Parse(val); err != nil {
|
||||
return nil, fmt.Errorf("Parse error in value %q: %s", path, err)
|
||||
}
|
||||
|
||||
if state.rendDepth == len(state.rendBufs) {
|
||||
state.rendBufs = append(state.rendBufs, new(bytes.Buffer))
|
||||
}
|
||||
state.rendDepth++
|
||||
rendered, err := state.engine.renderSingle(tpl, tplName, state.origTop, state.rendBufs[state.rendDepth-1])
|
||||
state.rendDepth--
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error expanding value %q: %s", path, err.Error())
|
||||
}
|
||||
state.valCache[path] = rendered
|
||||
return rendered, nil
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright 2017 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 engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
)
|
||||
|
||||
func expandValuesHelper(input string) (chartutil.Values, error) {
|
||||
var vals chartutil.Values
|
||||
vals = chartutil.FromYaml(input)
|
||||
if err, found := vals["Error"]; found {
|
||||
return nil, fmt.Errorf("Unexpected YAML parse failure: %s", err.(string))
|
||||
}
|
||||
expanded, err := New().ExpandValues(chartutil.Values{"Values": vals})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Expansion failed: %s", err.Error())
|
||||
}
|
||||
return expanded, nil
|
||||
}
|
||||
|
||||
func checkFullExpansionHelper(input string, expected string) error {
|
||||
expanded, err := expandValuesHelper(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output := chartutil.ToYaml(expanded["Values"])
|
||||
if "\n"+output != expected {
|
||||
return fmt.Errorf("Unexpected result from ExpandValues().\nGot: %s\nExpected: %s", output, expected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSingleValue(input string, path string, expected string) error {
|
||||
expanded, err := expandValuesHelper(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
val, err := expanded.PathValue("Values." + path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val != expected {
|
||||
return fmt.Errorf("Unexpected expansion result for %q.\nGot: %s\nExpected: %s", path, val, expected)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestTemplateValueExpansion(t *testing.T) {
|
||||
input := `
|
||||
a:
|
||||
aa: '{{ tval "b.ba.baa" }}2' # 12
|
||||
ab: '{{ tval "a.aa" }}3' # 123
|
||||
b:
|
||||
ba:
|
||||
baa: 1
|
||||
bb: '0{{ tval "a.ab" -}} 4 {{- tval "b.bc" }}' # 012345
|
||||
bc: 5
|
||||
bd: '{{ index (tval "d") 1 }}' # L1
|
||||
c:
|
||||
ca: '{{ substr 2 5 (tval "b.bb") }}' # 234
|
||||
cb: '{{ .Values.b.ba.baa }}' # Can still access things this way (and get 1 here)
|
||||
cc: '{{ .Values.a.aa }}' # ... but there will be no recursion, so this will just be '{{ tval.b.ba }}2'
|
||||
d:
|
||||
- L0
|
||||
- 'L{{ tval "b.ba.baa" }}' # L1
|
||||
e:
|
||||
bool: true # Will just be left alone
|
||||
float: 1.234 # "
|
||||
`
|
||||
|
||||
expected := `
|
||||
a:
|
||||
aa: "12"
|
||||
ab: "123"
|
||||
b:
|
||||
ba:
|
||||
baa: 1
|
||||
bb: "012345"
|
||||
bc: 5
|
||||
bd: L1
|
||||
c:
|
||||
ca: "234"
|
||||
cb: "1"
|
||||
cc: '{{ tval "b.ba.baa" }}2'
|
||||
d:
|
||||
- L0
|
||||
- L1
|
||||
e:
|
||||
bool: true
|
||||
float: 1.234
|
||||
`
|
||||
if err := checkFullExpansionHelper(input, expected); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
input = `
|
||||
a:
|
||||
- aa:
|
||||
aaa: '{{ (tval "b").ba.baa }}'
|
||||
aab: '{{ tval "b.ba.bac" }}'
|
||||
b:
|
||||
ba:
|
||||
baa: 1
|
||||
bab:
|
||||
- unused
|
||||
- left
|
||||
- right
|
||||
bac: 2
|
||||
c:
|
||||
ca: >-
|
||||
{{
|
||||
print
|
||||
(index (tval "b.ba").bab (atoi (index (tval "a") 0).aa.aaa))
|
||||
(index (tval "d") (atoi (index (tval "a") 0).aa.aaa)).db.dba.dbaa
|
||||
}}
|
||||
d:
|
||||
- da:
|
||||
- unused
|
||||
- db:
|
||||
dba:
|
||||
dbaa: '{{ index (tval "b.ba.bab") (atoi (index (tval "a") 0).aa.aab) }}'
|
||||
`
|
||||
if err := checkSingleValue(input, "c.ca", "leftright"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
input = `
|
||||
a:
|
||||
aa: cc
|
||||
b:
|
||||
bb: '{{ (index (tval "c") (tval "a.aa")).x.xx }}'
|
||||
c:
|
||||
cc:
|
||||
x:
|
||||
xx: '{{ tval "c.cd.x.xx" }}'
|
||||
cd:
|
||||
x:
|
||||
xx: good
|
||||
`
|
||||
if err := checkSingleValue(input, "b.bb", "good"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateValueExpansionErrors(t *testing.T) {
|
||||
checkExpError := func(input string, errRegex string) {
|
||||
if _, err := expandValuesHelper(input); err == nil {
|
||||
t.Errorf("Expected error matching %q but expansion succeeded", errRegex)
|
||||
} else if !regexp.MustCompile("(?i)" + errRegex).MatchString(err.Error()) {
|
||||
t.Errorf("Expected error matching %q but got %q", errRegex, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
inputYaml := "a: { b: '{{ tval \"a.b\" }}' }"
|
||||
checkExpError(inputYaml, `cyclic reference to "a.b"`)
|
||||
|
||||
inputYaml = "a: { b: '{{ tval \"c.d\" }}' }\nc: { d: '{{ tval \"a.b\" }}' }"
|
||||
checkExpError(inputYaml, `cyclic reference`)
|
||||
inputYaml = "a: { b: '{{ tval \"c.d\" }}' }\nc: { d: '{{ tval \"e.f\" }}' }\ne: { f: '{{ tval \"g.h\" }}' }\ng: { h: '{{ tval \"a.b\" }}' }"
|
||||
checkExpError(inputYaml, `cyclic reference`)
|
||||
|
||||
inputYaml = "a: { b: '{{ tval \"c.d\" }}' }"
|
||||
checkExpError(inputYaml, `value "c.d" does not exist`)
|
||||
|
||||
inputYaml = "a: { b: '{{ tval \"c.d\" }}' }\nc: { d: '{{ invalid }}' }"
|
||||
checkExpError(inputYaml, `function "invalid" not defined`) // This one happens during parse, so we don't catch it so specifically
|
||||
checkExpError(inputYaml, `parse error in value "c.d"`) // The (decorated) template will still be reported though
|
||||
|
||||
inputYaml = "a: { b: '{{ tval \"c.d\" }}' }\nc: { d: '{{ substr \"bad arg\" 0 0 }}' }"
|
||||
checkExpError(inputYaml, `error expanding value "c.d"`)
|
||||
}
|
Loading…
Reference in new issue