mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
646 lines
15 KiB
646 lines
15 KiB
/*
|
|
Copyright The Helm Authors.
|
|
|
|
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 chartutil
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
|
|
"github.com/golang/protobuf/ptypes/any"
|
|
|
|
kversion "k8s.io/apimachinery/pkg/version"
|
|
"k8s.io/helm/pkg/proto/hapi/chart"
|
|
"k8s.io/helm/pkg/timeconv"
|
|
"k8s.io/helm/pkg/version"
|
|
)
|
|
|
|
func TestReadValues(t *testing.T) {
|
|
doc := `# Test YAML parse
|
|
poet: "Coleridge"
|
|
title: "Rime of the Ancient Mariner"
|
|
stanza:
|
|
- "at"
|
|
- "length"
|
|
- "did"
|
|
- cross
|
|
- an
|
|
- Albatross
|
|
|
|
mariner:
|
|
with: "crossbow"
|
|
shot: "ALBATROSS"
|
|
|
|
water:
|
|
water:
|
|
where: "everywhere"
|
|
nor: "any drop to drink"
|
|
temperature: 1234567890
|
|
`
|
|
|
|
data, err := ReadValues([]byte(doc))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing bytes: %s", err)
|
|
}
|
|
matchValues(t, data)
|
|
|
|
tests := []string{`poet: "Coleridge"`, "# Just a comment", ""}
|
|
|
|
for _, tt := range tests {
|
|
data, err = ReadValues([]byte(tt))
|
|
if err != nil {
|
|
t.Fatalf("Error parsing bytes (%s): %s", tt, err)
|
|
}
|
|
if data == nil {
|
|
t.Errorf(`YAML string "%s" gave a nil map`, tt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToRenderValuesCaps(t *testing.T) {
|
|
|
|
chartValues := `
|
|
name: al Rashid
|
|
where:
|
|
city: Basrah
|
|
title: caliph
|
|
`
|
|
overideValues := `
|
|
name: Haroun
|
|
where:
|
|
city: Baghdad
|
|
date: 809 CE
|
|
`
|
|
|
|
c := &chart.Chart{
|
|
Metadata: &chart.Metadata{Name: "test"},
|
|
Templates: []*chart.Template{},
|
|
Values: &chart.Config{Raw: chartValues},
|
|
Dependencies: []*chart.Chart{
|
|
{
|
|
Metadata: &chart.Metadata{Name: "where"},
|
|
Values: &chart.Config{Raw: ""},
|
|
},
|
|
},
|
|
Files: []*any.Any{
|
|
{TypeUrl: "scheherazade/shahryar.txt", Value: []byte("1,001 Nights")},
|
|
},
|
|
}
|
|
v := &chart.Config{Raw: overideValues}
|
|
|
|
o := ReleaseOptions{
|
|
Name: "Seven Voyages",
|
|
Time: timeconv.Now(),
|
|
Namespace: "al Basrah",
|
|
IsInstall: true,
|
|
Revision: 5,
|
|
}
|
|
|
|
caps := &Capabilities{
|
|
APIVersions: DefaultVersionSet,
|
|
TillerVersion: version.GetVersionProto(),
|
|
KubeVersion: &kversion.Info{Major: "1"},
|
|
}
|
|
|
|
res, err := ToRenderValuesCaps(c, v, o, caps)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Ensure that the top-level values are all set.
|
|
if name := res["Chart"].(*chart.Metadata).Name; name != "test" {
|
|
t.Errorf("Expected chart name 'test', got %q", name)
|
|
}
|
|
relmap := res["Release"].(map[string]interface{})
|
|
if name := relmap["Name"]; name.(string) != "Seven Voyages" {
|
|
t.Errorf("Expected release name 'Seven Voyages', got %q", name)
|
|
}
|
|
if rev := relmap["Revision"]; rev.(int) != 5 {
|
|
t.Errorf("Expected release revision %d, got %q", 5, rev)
|
|
}
|
|
if relmap["IsUpgrade"].(bool) {
|
|
t.Error("Expected upgrade to be false.")
|
|
}
|
|
if !relmap["IsInstall"].(bool) {
|
|
t.Errorf("Expected install to be true.")
|
|
}
|
|
if data := res["Files"].(Files)["scheherazade/shahryar.txt"]; string(data) != "1,001 Nights" {
|
|
t.Errorf("Expected file '1,001 Nights', got %q", string(data))
|
|
}
|
|
if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") {
|
|
t.Error("Expected Capabilities to have v1 as an API")
|
|
}
|
|
if res["Capabilities"].(*Capabilities).TillerVersion.SemVer == "" {
|
|
t.Error("Expected Capabilities to have a Tiller version")
|
|
}
|
|
if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" {
|
|
t.Error("Expected Capabilities to have a Kube version")
|
|
}
|
|
|
|
var vals Values
|
|
vals = res["Values"].(Values)
|
|
|
|
if vals["name"] != "Haroun" {
|
|
t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals)
|
|
}
|
|
where := vals["where"].(map[string]interface{})
|
|
expects := map[string]string{
|
|
"city": "Baghdad",
|
|
"date": "809 CE",
|
|
"title": "caliph",
|
|
}
|
|
for field, expect := range expects {
|
|
if got := where[field]; got != expect {
|
|
t.Errorf("Expected %q, got %q (%v)", expect, got, where)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadValuesFile(t *testing.T) {
|
|
data, err := ReadValuesFile("./testdata/coleridge.yaml")
|
|
if err != nil {
|
|
t.Fatalf("Error reading YAML file: %s", err)
|
|
}
|
|
matchValues(t, data)
|
|
}
|
|
|
|
func ExampleValues() {
|
|
doc := `
|
|
title: "Moby Dick"
|
|
chapter:
|
|
one:
|
|
title: "Loomings"
|
|
two:
|
|
title: "The Carpet-Bag"
|
|
three:
|
|
title: "The Spouter Inn"
|
|
`
|
|
d, err := ReadValues([]byte(doc))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ch1, err := d.Table("chapter.one")
|
|
if err != nil {
|
|
panic("could not find chapter one")
|
|
}
|
|
fmt.Print(ch1["title"])
|
|
// Output:
|
|
// Loomings
|
|
}
|
|
|
|
func TestTable(t *testing.T) {
|
|
doc := `
|
|
title: "Moby Dick"
|
|
chapter:
|
|
one:
|
|
title: "Loomings"
|
|
two:
|
|
title: "The Carpet-Bag"
|
|
three:
|
|
title: "The Spouter Inn"
|
|
`
|
|
d, err := ReadValues([]byte(doc))
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse the White Whale: %s", err)
|
|
}
|
|
|
|
if _, err := d.Table("title"); err == nil {
|
|
t.Fatalf("Title is not a table.")
|
|
}
|
|
|
|
if _, err := d.Table("chapter"); err != nil {
|
|
t.Fatalf("Failed to get the chapter table: %s\n%v", err, d)
|
|
}
|
|
|
|
if v, err := d.Table("chapter.one"); err != nil {
|
|
t.Errorf("Failed to get chapter.one: %s", err)
|
|
} else if v["title"] != "Loomings" {
|
|
t.Errorf("Unexpected title: %s", v["title"])
|
|
}
|
|
|
|
if _, err := d.Table("chapter.three"); err != nil {
|
|
t.Errorf("Chapter three is missing: %s\n%v", err, d)
|
|
}
|
|
|
|
if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil {
|
|
t.Errorf("I think you mean 'Epilogue'")
|
|
}
|
|
}
|
|
|
|
func matchValues(t *testing.T, data map[string]interface{}) {
|
|
if data["poet"] != "Coleridge" {
|
|
t.Errorf("Unexpected poet: %s", data["poet"])
|
|
}
|
|
|
|
if o, err := ttpl("{{len .stanza}}", data); err != nil {
|
|
t.Errorf("len stanza: %s", err)
|
|
} else if o != "6" {
|
|
t.Errorf("Expected 6, got %s", o)
|
|
}
|
|
|
|
if o, err := ttpl("{{.mariner.shot}}", data); err != nil {
|
|
t.Errorf(".mariner.shot: %s", err)
|
|
} else if o != "ALBATROSS" {
|
|
t.Errorf("Expected that mariner shot ALBATROSS")
|
|
}
|
|
|
|
if o, err := ttpl("{{.water.water.where}}", data); err != nil {
|
|
t.Errorf(".water.water.where: %s", err)
|
|
} else if o != "everywhere" {
|
|
t.Errorf("Expected water water everywhere")
|
|
}
|
|
|
|
if o, err := ttpl("{{.water.water.temperature}}", data); err != nil {
|
|
t.Errorf(".water.water.temperature: %s", err)
|
|
} else if o != "1234567890" {
|
|
t.Errorf("Expected water water temperature: 1234567890, got: %s", o)
|
|
}
|
|
}
|
|
|
|
func ttpl(tpl string, v map[string]interface{}) (string, error) {
|
|
var b bytes.Buffer
|
|
tt := template.Must(template.New("t").Parse(tpl))
|
|
if err := tt.Execute(&b, v); err != nil {
|
|
return "", err
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
|
|
// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362
|
|
var testCoalesceValuesYaml = `
|
|
top: yup
|
|
bottom: null
|
|
right: Null
|
|
left: NULL
|
|
front: ~
|
|
back: ""
|
|
|
|
global:
|
|
name: Ishmael
|
|
subject: Queequeg
|
|
nested:
|
|
boat: true
|
|
|
|
pequod:
|
|
global:
|
|
name: Stinky
|
|
harpooner: Tashtego
|
|
nested:
|
|
boat: false
|
|
sail: true
|
|
ahab:
|
|
scope: whale
|
|
|
|
# test coalesce with nested null values
|
|
web:
|
|
livenessProbe:
|
|
httpGet: null
|
|
exec:
|
|
command:
|
|
- curl
|
|
- -f
|
|
- http://localhost:8080/api/v1/info
|
|
timeoutSeconds: null
|
|
readinessProbe:
|
|
httpGet: null
|
|
exec:
|
|
command:
|
|
- curl
|
|
- -f
|
|
- http://localhost:8080/api/v1/info
|
|
timeoutSeconds: null # catches the case where this wasn't defined in the original source...
|
|
`
|
|
|
|
func TestCoalesceValues(t *testing.T) {
|
|
tchart := "testdata/moby"
|
|
c, err := LoadDir(tchart)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tvals := &chart.Config{Raw: testCoalesceValuesYaml}
|
|
|
|
v, err := CoalesceValues(c, tvals)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
j, _ := json.MarshalIndent(v, "", " ")
|
|
t.Logf("Coalesced Values: %s", string(j))
|
|
|
|
tests := []struct {
|
|
tpl string
|
|
expect string
|
|
}{
|
|
{"{{.top}}", "yup"},
|
|
{"{{.back}}", ""},
|
|
{"{{.name}}", "moby"},
|
|
{"{{.global.name}}", "Ishmael"},
|
|
{"{{.global.subject}}", "Queequeg"},
|
|
{"{{.global.harpooner}}", "<no value>"},
|
|
{"{{.pequod.name}}", "pequod"},
|
|
{"{{.pequod.ahab.name}}", "ahab"},
|
|
{"{{.pequod.ahab.scope}}", "whale"},
|
|
{"{{.pequod.ahab.global.name}}", "Ishmael"},
|
|
{"{{.pequod.ahab.global.subject}}", "Queequeg"},
|
|
{"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
|
|
{"{{.pequod.global.name}}", "Ishmael"},
|
|
{"{{.pequod.global.subject}}", "Queequeg"},
|
|
{"{{.spouter.global.name}}", "Ishmael"},
|
|
{"{{.spouter.global.harpooner}}", "<no value>"},
|
|
|
|
{"{{.global.nested.boat}}", "true"},
|
|
{"{{.pequod.global.nested.boat}}", "true"},
|
|
{"{{.spouter.global.nested.boat}}", "true"},
|
|
{"{{.pequod.global.nested.sail}}", "true"},
|
|
{"{{.spouter.global.nested.sail}}", "<no value>"},
|
|
|
|
{"{{.web.livenessProbe.failureThreshold}}", "5"},
|
|
{"{{.web.livenessProbe.initialDelaySeconds}}", "10"},
|
|
{"{{.web.livenessProbe.periodSeconds}}", "15"},
|
|
{"{{.web.livenessProbe.exec}}", "map[command:[curl -f http://localhost:8080/api/v1/info]]"},
|
|
|
|
{"{{.web.readinessProbe.exec}}", "map[command:[curl -f http://localhost:8080/api/v1/info]]"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
|
|
t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
|
|
}
|
|
}
|
|
|
|
nullKeys := []string{"bottom", "right", "left", "front",
|
|
"web.livenessProbe.httpGet", "web.readinessProbe.httpGet", "web.livenessProbe.timeoutSeconds", "web.readinessProbe.timeoutSeconds"}
|
|
for _, nullKey := range nullKeys {
|
|
parts := strings.Split(nullKey, ".")
|
|
curMap := v
|
|
for partIdx, part := range parts {
|
|
nextVal, ok := curMap[part]
|
|
if partIdx == len(parts)-1 { // are we the last?
|
|
if ok {
|
|
t.Errorf("Expected key %q to be removed, still present", nullKey)
|
|
break
|
|
}
|
|
} else { // we are not the last
|
|
if !ok {
|
|
t.Errorf("Expected key %q to be removed, but partial parent path was not found", nullKey)
|
|
break
|
|
}
|
|
curMap, ok = nextVal.(map[string]interface{})
|
|
if !ok {
|
|
t.Errorf("Expected key %q to be removed, but partial parent path did not result in a map", nullKey)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCoalesceTables(t *testing.T) {
|
|
dst := map[string]interface{}{
|
|
"name": "Ishmael",
|
|
"address": map[string]interface{}{
|
|
"street": "123 Spouter Inn Ct.",
|
|
"city": "Nantucket",
|
|
},
|
|
"details": map[string]interface{}{
|
|
"friends": []string{"Tashtego"},
|
|
},
|
|
"boat": "pequod",
|
|
}
|
|
src := map[string]interface{}{
|
|
"occupation": "whaler",
|
|
"address": map[string]interface{}{
|
|
"state": "MA",
|
|
"street": "234 Spouter Inn Ct.",
|
|
},
|
|
"details": "empty",
|
|
"boat": map[string]interface{}{
|
|
"mast": true,
|
|
},
|
|
}
|
|
|
|
// What we expect is that anything in dst overrides anything in src, but that
|
|
// otherwise the values are coalesced.
|
|
dst = coalesceTables(dst, src, "")
|
|
|
|
if dst["name"] != "Ishmael" {
|
|
t.Errorf("Unexpected name: %s", dst["name"])
|
|
}
|
|
if dst["occupation"] != "whaler" {
|
|
t.Errorf("Unexpected occupation: %s", dst["occupation"])
|
|
}
|
|
|
|
addr, ok := dst["address"].(map[string]interface{})
|
|
if !ok {
|
|
t.Fatal("Address went away.")
|
|
}
|
|
|
|
if addr["street"].(string) != "123 Spouter Inn Ct." {
|
|
t.Errorf("Unexpected address: %v", addr["street"])
|
|
}
|
|
|
|
if addr["city"].(string) != "Nantucket" {
|
|
t.Errorf("Unexpected city: %v", addr["city"])
|
|
}
|
|
|
|
if addr["state"].(string) != "MA" {
|
|
t.Errorf("Unexpected state: %v", addr["state"])
|
|
}
|
|
|
|
if det, ok := dst["details"].(map[string]interface{}); !ok {
|
|
t.Fatalf("Details is the wrong type: %v", dst["details"])
|
|
} else if _, ok := det["friends"]; !ok {
|
|
t.Error("Could not find your friends. Maybe you don't have any. :-(")
|
|
}
|
|
|
|
if dst["boat"].(string) != "pequod" {
|
|
t.Errorf("Expected boat string, got %v", dst["boat"])
|
|
}
|
|
}
|
|
|
|
func TestCoalesceSubchart(t *testing.T) {
|
|
tchart := "testdata/moby"
|
|
c, err := LoadDir(tchart)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tvals := &chart.Config{}
|
|
|
|
v, err := CoalesceValues(c, tvals)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
j, _ := json.MarshalIndent(v, "", " ")
|
|
t.Logf("Coalesced Values: %s", string(j))
|
|
|
|
subchartValues, ok := v["spouter"].(map[string]interface{})
|
|
if !ok {
|
|
t.Errorf("Subchart values not found")
|
|
}
|
|
|
|
if _, ok := subchartValues["foo"]; ok {
|
|
t.Errorf("Expected key foo to be removed, still present")
|
|
}
|
|
}
|
|
|
|
func TestPathValue(t *testing.T) {
|
|
doc := `
|
|
title: "Moby Dick"
|
|
chapter:
|
|
one:
|
|
title: "Loomings"
|
|
two:
|
|
title: "The Carpet-Bag"
|
|
three:
|
|
title: "The Spouter Inn"
|
|
`
|
|
d, err := ReadValues([]byte(doc))
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse the White Whale: %s", err)
|
|
}
|
|
|
|
if v, err := d.PathValue("chapter.one.title"); err != nil {
|
|
t.Errorf("Got error instead of title: %s\n%v", err, d)
|
|
} else if v != "Loomings" {
|
|
t.Errorf("No error but got wrong value for title: %s\n%v", err, d)
|
|
}
|
|
if _, err := d.PathValue("chapter.one.doesntexist"); err == nil {
|
|
t.Errorf("Non-existent key should return error: %s\n%v", err, d)
|
|
}
|
|
if _, err := d.PathValue("chapter.doesntexist.one"); err == nil {
|
|
t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d)
|
|
}
|
|
if _, err := d.PathValue(""); err == nil {
|
|
t.Error("Asking for the value from an empty path should yield an error")
|
|
}
|
|
if v, err := d.PathValue("title"); err == nil {
|
|
if v != "Moby Dick" {
|
|
t.Errorf("Failed to return values for root key title")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValuesMergeInto(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
destination string
|
|
source string
|
|
result string
|
|
}{
|
|
"maps are merged": {
|
|
`
|
|
resources:
|
|
requests:
|
|
cpu: 400m
|
|
something: else
|
|
`,
|
|
`
|
|
resources:
|
|
requests:
|
|
cpu: 500m
|
|
`,
|
|
`
|
|
resources:
|
|
requests:
|
|
cpu: 500m
|
|
something: else
|
|
`,
|
|
},
|
|
"values are replaced": {
|
|
`
|
|
firstKey: firstValue
|
|
secondKey: secondValue
|
|
thirdKey: thirdValue
|
|
`,
|
|
`
|
|
firstKey: newFirstValue
|
|
thirdKey: newThirdValue
|
|
`,
|
|
`
|
|
firstKey: newFirstValue
|
|
secondKey: secondValue
|
|
thirdKey: newThirdValue
|
|
`,
|
|
},
|
|
"new values are added": {
|
|
`
|
|
existingKey: existingValue
|
|
`,
|
|
`
|
|
newKey: newValue
|
|
anotherNewKey:
|
|
nestedNewKey: nestedNewValue
|
|
`,
|
|
`
|
|
existingKey: existingValue
|
|
newKey: newValue
|
|
anotherNewKey:
|
|
nestedNewKey: nestedNewValue
|
|
`,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
d, err := ReadValues([]byte(tc.destination))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
s, err := ReadValues([]byte(tc.source))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
expectedRes, err := ReadValues([]byte(tc.result))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
d.MergeInto(s)
|
|
|
|
if !reflect.DeepEqual(expectedRes, d) {
|
|
t.Errorf("%s: Expected %v, but got %v", name, expectedRes, d)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOverriteTableItemWithNonTableValue(t *testing.T) {
|
|
// src has a table value for "foo"
|
|
src := map[string]interface{}{
|
|
"foo": map[string]interface{}{
|
|
"baz": "boz",
|
|
},
|
|
}
|
|
|
|
// dst has a non-table value for "foo"
|
|
dst := map[string]interface{}{
|
|
"foo": "bar",
|
|
}
|
|
|
|
// result - this may print a warning, but we has always "worked"
|
|
result := coalesceTables(dst, src, "")
|
|
expected := map[string]interface{}{
|
|
"foo": "bar",
|
|
}
|
|
|
|
if !reflect.DeepEqual(result, expected) {
|
|
t.Errorf("Expected %v, but got %v", expected, result)
|
|
}
|
|
}
|