helmfile/pkg/app/app_test.go

4804 lines
134 KiB
Go

package app
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"sync"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/roboll/helmfile/pkg/remote"
"github.com/roboll/helmfile/pkg/exectest"
"gotest.tools/v3/assert"
"github.com/roboll/helmfile/pkg/helmexec"
"github.com/roboll/helmfile/pkg/state"
"github.com/roboll/helmfile/pkg/testhelper"
"github.com/variantdev/vals"
"go.uber.org/zap"
"gotest.tools/v3/env"
)
func appWithFs(app *App, files map[string]string) *App {
fs := testhelper.NewTestFs(files)
return injectFs(app, fs)
}
func injectFs(app *App, fs *testhelper.TestFs) *App {
app.readFile = fs.ReadFile
app.glob = fs.Glob
app.abs = fs.Abs
app.getwd = fs.Getwd
app.chdir = fs.Chdir
app.fileExistsAt = fs.FileExistsAt
app.fileExists = fs.FileExists
app.directoryExistsAt = fs.DirectoryExistsAt
return app
}
func expectNoCallsToHelm(app *App) {
expectNoCallsToHelmVersion(app, false)
}
func expectNoCallsToHelmVersion(app *App, isHelm3 bool) {
if app.helms != nil {
panic("invalid call to expectNoCallsToHelm")
}
app.helms = map[helmKey]helmexec.Interface{
createHelmKey(app.OverrideHelmBinary, app.OverrideKubeContext): &versionOnlyHelmExec{isHelm3: isHelm3},
}
}
func TestVisitDesiredStatesWithReleasesFiltered_ReleaseOrder(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
- helmfile.d/b*.yaml
`,
"/path/to/helmfile.d/a1.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/a2.yaml": `
releases:
- name: prometheus
chart: stable/prometheus
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
`,
}
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
app = injectFs(app, fs)
actualOrder := []string{}
noop := func(run *Run) (bool, []error) {
actualOrder = append(actualOrder, run.state.FilePath)
return false, []error{}
}
err := app.ForEachState(
noop,
SetFilter(true),
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
expectedOrder := []string{"a1.yaml", "a2.yaml", "b.yaml", "helmfile.yaml"}
if !reflect.DeepEqual(actualOrder, expectedOrder) {
t.Errorf("unexpected order of processed state files: expected=%v, actual=%v", expectedOrder, actualOrder)
}
}
func Noop(_ *Run) (bool, []error) {
return false, []error{}
}
func TestVisitDesiredStatesWithReleasesFiltered_EnvValuesFileOrder(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
default:
values:
- env.*.yaml
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/env.1.yaml": `FOO: 1
BAR: 2
`,
"/path/to/env.2.yaml": `BAR: 3
BAZ: 4
`,
}
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/env.*.yaml"] = []string{"/path/to/env.2.yaml", "/path/to/env.1.yaml"}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
app = injectFs(app, fs)
err := app.ForEachState(
Noop,
SetFilter(true),
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
expectedOrder := []string{"helmfile.yaml", "/path/to/env.1.yaml", "/path/to/env.2.yaml", "/path/to/env.1.yaml", "/path/to/env.2.yaml"}
actualOrder := fs.SuccessfulReads()
if !reflect.DeepEqual(actualOrder, expectedOrder) {
t.Errorf("unexpected order of processed state files: expected=%v, actual=%v", expectedOrder, actualOrder)
}
}
func TestVisitDesiredStatesWithReleasesFiltered_MissingEnvValuesFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
default:
values:
- env.*.yaml
releases:
- name: zipkin
chart: stable/zipkin
`,
}
fs := testhelper.NewTestFs(files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
app = injectFs(app, fs)
err := app.ForEachState(
Noop,
SetFilter(true),
)
if err == nil {
t.Fatal("expected error did not occur")
}
expected := "in ./helmfile.yaml: failed to read helmfile.yaml: environment values file matching \"env.*.yaml\" does not exist in \".\""
if err.Error() != expected {
t.Errorf("unexpected error: expected=%s, got=%v", expected, err)
}
}
func TestVisitDesiredStatesWithReleasesFiltered_Issue1008_MissingNonDefaultEnvInBase(t *testing.T) {
files := map[string]string{
"/path/to/base.yaml": `
helmDefaults:
wait: true
waitForJobs: true
`,
"/path/to/helmfile.yaml": `
bases:
- base.yaml
environments:
test:
releases:
- name: zipkin
chart: stable/zipkin
`,
}
fs := testhelper.NewTestFs(files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "test",
}
expectNoCallsToHelm(app)
app = injectFs(app, fs)
err := app.ForEachState(
Noop,
SetFilter(true),
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestVisitDesiredStatesWithReleasesFiltered_MissingEnvValuesFileHandler(t *testing.T) {
testcases := []struct {
name string
handler string
filePattern string
expectErr bool
}{
{name: "error handler with no files matching glob", handler: "Error", filePattern: "env.*.yaml", expectErr: true},
{name: "warn handler with no files matching glob", handler: "Warn", filePattern: "env.*.yaml", expectErr: false},
{name: "info handler with no files matching glob", handler: "Info", filePattern: "env.*.yaml", expectErr: false},
{name: "debug handler with no files matching glob", handler: "Debug", filePattern: "env.*.yaml", expectErr: false},
}
for i := range testcases {
testcase := testcases[i]
t.Run(testcase.name, func(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": fmt.Sprintf(`
environments:
default:
missingFileHandler: %s
values:
- %s
releases:
- name: zipkin
chart: stable/zipkin
`, testcase.handler, testcase.filePattern),
}
fs := testhelper.NewTestFs(files)
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
app = injectFs(app, fs)
err := app.ForEachState(
Noop,
SetFilter(true),
)
if testcase.expectErr && err == nil {
t.Fatal("expected error did not occur")
}
if !testcase.expectErr && err != nil {
t.Errorf("not error expected, but got: %v", err)
}
})
}
}
// See https://github.com/roboll/helmfile/issues/193
func TestVisitDesiredStatesWithReleasesFiltered(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
- helmfile.d/b*.yaml
`,
"/path/to/helmfile.d/a1.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/a2.yaml": `
releases:
- name: prometheus
chart: stable/prometheus
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
`,
}
testcases := []struct {
name string
expectErr bool
}{
{name: "prometheus", expectErr: false},
{name: "zipkin", expectErr: false},
{name: "grafana", expectErr: false},
{name: "elasticsearch", expectErr: true},
}
for _, testcase := range testcases {
fs := testhelper.NewTestFs(files)
fs.GlobFixtures["/path/to/helmfile.d/a*.yaml"] = []string{"/path/to/helmfile.d/a2.yaml", "/path/to/helmfile.d/a1.yaml"}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Selectors: []string{fmt.Sprintf("name=%s", testcase.name)},
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}
expectNoCallsToHelm(app)
app = injectFs(app, fs)
err := app.ForEachState(
Noop,
SetFilter(true),
)
if testcase.expectErr && err == nil {
t.Errorf("error expected but not happened for name=%s", testcase.name)
} else if !testcase.expectErr && err != nil {
t.Errorf("unexpected error for name=%s: %v", testcase.name, err)
}
}
}
// See https://github.com/roboll/helmfile/issues/320
func TestVisitDesiredStatesWithReleasesFiltered_UndefinedEnv(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
prod:
helmfiles:
- helmfile.d/a*.yaml
`,
"/path/to/helmfile.d/a1.yaml": `
environments:
prod:
releases:
- name: zipkin
chart: stable/zipkin
`,
}
testcases := []struct {
name string
expectErr bool
}{
{name: "undefined_env", expectErr: true},
{name: "default", expectErr: false},
{name: "prod", expectErr: false},
}
for _, testcase := range testcases {
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: testcase.name,
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
Noop,
SetFilter(true),
)
if testcase.expectErr && err == nil {
t.Errorf("error expected but not happened for environment=%s", testcase.name)
} else if !testcase.expectErr && err != nil {
t.Errorf("unexpected error for environment=%s: %v", testcase.name, err)
}
}
}
type ctxLogger struct {
label string
}
func (cl *ctxLogger) Write(b []byte) (int, error) {
return os.Stderr.Write(append([]byte(cl.label+":"), b...))
}
// See https://github.com/roboll/helmfile/issues/322
func TestVisitDesiredStatesWithReleasesFiltered_Selectors(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
- helmfile.d/b*.yaml
`,
"/path/to/helmfile.d/a1.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/a2.yaml": `
releases:
- name: prometheus
chart: stable/prometheus
`,
"/path/to/helmfile.d/b.yaml": `
helmDefaults:
tillerNamespace: zoo
releases:
- name: grafana
chart: stable/grafana
- name: foo
chart: charts/foo
labels:
duplicatedNs: yes
- name: foo
chart: charts/foo
labels:
duplicatedNs: yes
- name: grafana
chart: stable/grafana
- name: foo
chart: charts/foo
kubeContext: baz
labels:
duplicatedCtx: yes
- name: foo
chart: charts/foo
kubeContext: baz
labels:
duplicatedCtx: yes
- name: bar
chart: charts/foo
tillerNamespace: bar1
labels:
duplicatedOK: yes
- name: bar
chart: charts/foo
tillerNamespace: bar2
labels:
duplicatedOK: yes
`,
}
testcases := []struct {
label string
expectedCount int
expectErr bool
errMsg string
}{
{label: "name=prometheus", expectedCount: 1, expectErr: false},
{label: "name=", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name=. Expected label in form k=v or k!=v"},
{label: "name!=", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name!=. Expected label in form k=v or k!=v"},
{label: "name", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[0]: in /path/to/helmfile.d/a1.yaml: Malformed label: name. Expected label in form k=v or k!=v"},
// See https://github.com/roboll/helmfile/issues/193
{label: "duplicatedNs=yes", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[2]: in /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in namespace \"zoo\": there were 2 releases named \"foo\" matching specified selector"},
{label: "duplicatedCtx=yes", expectedCount: 0, expectErr: true, errMsg: "in ./helmfile.yaml: in .helmfiles[2]: in /path/to/helmfile.d/b.yaml: duplicate release \"foo\" found in namespace \"zoo\" in kubecontext \"baz\": there were 2 releases named \"foo\" matching specified selector"},
{label: "duplicatedOK=yes", expectedCount: 2, expectErr: false},
}
for i := range testcases {
testcase := testcases[i]
t.Run(testcase.label, func(t *testing.T) {
actual := []string{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r.Name)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(&ctxLogger{label: testcase.label}, "debug"),
Namespace: "",
Selectors: []string{testcase.label},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if testcase.expectErr {
if err == nil {
t.Errorf("error expected but not happened for selector %s", testcase.label)
} else if err.Error() != testcase.errMsg {
t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", testcase.errMsg, err.Error())
}
} else if !testcase.expectErr && err != nil {
t.Errorf("unexpected error for selector %s: %v", testcase.label, err)
}
if len(actual) != testcase.expectedCount {
t.Errorf("unexpected release count for selector %s: expected=%d, actual=%d", testcase.label, testcase.expectedCount, len(actual))
}
})
}
}
func TestVisitDesiredStatesWithReleasesFiltered_EmbeddedSelectors(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- path: helmfile.d/a*.yaml
selectors:
- name=prometheus
- name=zipkin
- helmfile.d/b*.yaml
- path: helmfile.d/c*.yaml
selectors: []
`,
"/path/to/helmfile.d/a1.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/a2.yaml": `
releases:
- name: prometheus
chart: stable/prometheus
`,
"/path/to/helmfile.d/a3.yaml": `
releases:
- name: mongodb
chart: stable/mongodb
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
- name: bar
chart: charts/foo
tillerNamespace: bar1
labels:
duplicatedOK: yes
- name: bar
chart: charts/foo
tillerNamespace: bar2
labels:
duplicatedOK: yes
`,
"/path/to/helmfile.d/c.yaml": `
releases:
- name: grafana
chart: stable/grafana
- name: postgresql
chart: charts/postgresql
labels:
whatever: yes
`,
}
//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
legacyTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
{label: "name=zipkin", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false},
{label: "name=grafana", expectedReleases: []string{"zipkin", "prometheus", "grafana", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "postgresql"}, expectErr: false},
}
runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st EmbeddedSelectors")
//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
desiredTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "duplicatedOK=yes", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
{label: "name=doesnotexists", expectedReleases: []string{"zipkin", "prometheus", "grafana", "bar", "bar", "grafana", "postgresql"}, expectErr: false},
}
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd EmbeddedSelectors")
}
func TestVisitDesiredStatesWithReleasesFiltered_InheritedSelectors_3leveldeep(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
releases:
- name: mongodb
chart: stable/mongodb
`,
"/path/to/helmfile.d/a.yaml": `
helmfiles:
- b*.yaml
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
`,
}
//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
legacyTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name!=grafana", expectedReleases: []string{"zipkin", "mongodb"}, expectErr: false},
}
runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st 3leveldeep")
//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
desiredTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name!=grafana", expectedReleases: []string{"grafana", "zipkin", "mongodb"}, expectErr: false},
}
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd 3leveldeep")
}
func TestVisitDesiredStatesWithReleasesFiltered_InheritedSelectors_inherits(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
- path: helmfile.d/a*.yaml
selectors:
- select=foo
releases:
- name: mongodb
chart: stable/mongodb
`,
"/path/to/helmfile.d/a.yaml": `
helmfiles:
- path: b*.yaml
selectorsInherited: true
releases:
- name: zipkin
chart: stable/zipkin
labels:
select: foo
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
- name: prometheus
chart: stable/prometheus
labels:
select: foo
`,
}
//Check with legacy behavior, that is when no explicit selector then sub-helmfiles inherits from command line selector
legacyTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name=grafana", expectedReleases: []string{"grafana", "prometheus", "zipkin"}, expectErr: false},
{label: "select!=foo", expectedReleases: []string{"grafana", "prometheus", "zipkin", "mongodb"}, expectErr: false},
}
runFilterSubHelmFilesTests(legacyTestcases, files, t, "1st inherits")
//Check with experimental behavior, that is when no explicit selector then sub-helmfiles do no inherit from any selector
desiredTestcases := []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}{
{label: "name=grafana", expectedReleases: []string{"grafana", "prometheus", "zipkin", "prometheus", "zipkin"}, expectErr: false},
{label: "select!=foo", expectedReleases: []string{"grafana", "prometheus", "zipkin", "prometheus", "zipkin", "mongodb"}, expectErr: false},
}
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
runFilterSubHelmFilesTests(desiredTestcases, files, t, "2nd inherits")
}
func runFilterSubHelmFilesTests(testcases []struct {
label string
expectedReleases []string
expectErr bool
errMsg string
}, files map[string]string, t *testing.T, testName string) {
t.Helper()
for _, testcase := range testcases {
actual := []string{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r.Name)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{testcase.label},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if testcase.expectErr {
if err == nil {
t.Errorf("[%s]error expected but not happened for selector %s", testName, testcase.label)
} else if err.Error() != testcase.errMsg {
t.Errorf("[%s]unexpected error message: expected=\"%s\", actual=\"%s\"", testName, testcase.errMsg, err.Error())
}
} else if !testcase.expectErr && err != nil {
t.Errorf("[%s]unexpected error for selector %s: %v", testName, testcase.label, err)
}
if !reflect.DeepEqual(actual, testcase.expectedReleases) {
t.Errorf("[%s]unexpected releases for selector %s: expected=%v, actual=%v", testName, testcase.label, testcase.expectedReleases, actual)
}
}
}
func TestVisitDesiredStatesWithReleasesFiltered_EmbeddedNestedStateAdditionalEnvValues(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- path: helmfile.d/a*.yaml
values:
- env.values.yaml
- helmfile.d/b*.yaml
- path: helmfile.d/c*.yaml
values:
- env.values.yaml
- tillerNs: INLINE_TILLER_NS_3
`,
"/path/to/helmfile.d/a1.yaml": `
environments:
default:
values:
- tillerNs: INLINE_TILLER_NS
ns: INLINE_NS
releases:
- name: foo
chart: stable/zipkin
tillerNamespace: {{ .Environment.Values.tillerNs }}
namespace: {{ .Environment.Values.ns }}
`,
"/path/to/helmfile.d/b.yaml": `
environments:
default:
values:
- tillerNs: INLINE_TILLER_NS
ns: INLINE_NS
releases:
- name: bar
chart: stable/grafana
tillerNamespace: {{ .Environment.Values.tillerNs }}
namespace: {{ .Environment.Values.ns }}
`,
"/path/to/helmfile.d/c.yaml": `
environments:
default:
values:
- tillerNs: INLINE_TILLER_NS
ns: INLINE_NS
releases:
- name: baz
chart: stable/envoy
tillerNamespace: {{ .Environment.Values.tillerNs }}
namespace: {{ .Environment.Values.ns }}
`,
"/path/to/env.values.yaml": `
tillerNs: INLINE_TILLER_NS_2
`,
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
processed := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
processed = append(processed, r)
}
return false, []error{}
}
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
type release struct {
chart string
tillerNs string
ns string
}
expectedReleases := map[string]release{
"foo": {"stable/zipkin", "INLINE_TILLER_NS_2", "INLINE_NS"},
"bar": {"stable/grafana", "INLINE_TILLER_NS", "INLINE_NS"},
"baz": {"stable/envoy", "INLINE_TILLER_NS_3", "INLINE_NS"},
}
for name := range processed {
actual := processed[name]
t.Run(actual.Name, func(t *testing.T) {
expected, ok := expectedReleases[actual.Name]
if !ok {
t.Fatalf("unexpected release processed: %v", actual)
}
if expected.chart != actual.Chart {
t.Errorf("unexpected chart: expected=%s, got=%s", expected.chart, actual.Chart)
}
if expected.tillerNs != actual.TillerNamespace {
t.Errorf("unexpected tiller namespace: expected=%s, got=%s", expected.tillerNs, actual.TillerNamespace)
}
if expected.ns != actual.Namespace {
t.Errorf("unexpected namespace: expected=%s, got=%s", expected.ns, actual.Namespace)
}
})
}
}
// See https://github.com/roboll/helmfile/issues/312
func TestVisitDesiredStatesWithReleasesFiltered_ReverseOrder(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
helmfiles:
- helmfile.d/a*.yaml
- helmfile.d/b*.yaml
`,
"/path/to/helmfile.d/a1.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
`,
"/path/to/helmfile.d/a2.yaml": `
releases:
- name: prometheus
chart: stable/prometheus
- name: elasticsearch
chart: stable/elasticsearch
`,
"/path/to/helmfile.d/b.yaml": `
releases:
- name: grafana
chart: stable/grafana
`,
}
expected := []string{"grafana", "elasticsearch", "prometheus", "zipkin"}
testcases := []struct {
reverse bool
expected []string
}{
{reverse: false, expected: []string{"zipkin", "prometheus", "elasticsearch", "grafana"}},
{reverse: true, expected: []string{"grafana", "elasticsearch", "prometheus", "zipkin"}},
}
for _, testcase := range testcases {
actual := []string{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r.Name)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
collectReleases,
SetReverse(testcase.reverse),
SetFilter(true),
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if !reflect.DeepEqual(testcase.expected, actual) {
t.Errorf("releases did not match: expected=%v actual=%v", expected, actual)
}
}
}
func TestVisitDesiredStatesWithReleasesFiltered_EnvironmentValueOverrides(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
environments:
default:
values:
- values.yaml
---
releases:
- name: {{ .Environment.Values.foo }}-{{ .Environment.Values.bar }}-{{ .Environment.Values.baz }}
chart: stable/zipkin
`,
"/path/to/values.yaml": `
foo: foo
bar: bar
baz: baz
`,
"/path/to/overrides.yaml": `
foo: "foo1"
bar: "bar1"
`,
}
testcases := []struct {
expected string
}{
{expected: "foo1-bar2-baz1"},
}
for _, testcase := range testcases {
actual := []string{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r.Name)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: "default",
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]interface{}{"bar": "bar2", "baz": "baz1"},
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(actual) != 1 {
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
}
if actual[0] != testcase.expected {
t.Errorf("unexpected result: expected=%s, got=%s", testcase.expected, actual[0])
}
}
}
func TestVisitDesiredStatesWithReleasesFiltered_StateValueOverrides(t *testing.T) {
envTmplExpr := "{{ .Values.x.foo }}-{{ .Values.x.bar }}-{{ .Values.x.baz }}-{{ .Values.x.hoge }}-{{ .Values.x.fuga }}-{{ .Values.x.a | first | pluck \"b\" | first | first | pluck \"c\" | first }}"
relTmplExpr := "\"{{`{{ .Values.x.foo }}-{{ .Values.x.bar }}-{{ .Values.x.baz }}-{{ .Values.x.hoge }}-{{ .Values.x.fuga }}-{{ .Values.x.a | first | pluck \\\"b\\\" | first | first | pluck \\\"c\\\" | first }}`}}\""
testcases := []struct {
expr, env, expected string
}{
{
expr: envTmplExpr,
env: "default",
expected: "foo-bar_default-baz_override-hoge_set-fuga_set-C",
},
{
expr: envTmplExpr,
env: "production",
expected: "foo-bar_production-baz_override-hoge_set-fuga_set-C",
},
{
expr: relTmplExpr,
env: "default",
expected: "foo-bar_default-baz_override-hoge_set-fuga_set-C",
},
{
expr: relTmplExpr,
env: "production",
expected: "foo-bar_production-baz_override-hoge_set-fuga_set-C",
},
}
for i := range testcases {
testcase := testcases[i]
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": fmt.Sprintf(`
# The top-level "values" are "base" values has inherited to state values with the lowest priority.
# The lowest priority results in environment-specific values to override values defined in the base.
values:
- values.yaml
environments:
default:
values:
- default.yaml
production:
values:
- production.yaml
---
releases:
- name: %s
chart: %s
namespace: %s
`, testcase.expr, testcase.expr, testcase.expr),
"/path/to/values.yaml": `
x:
foo: foo
bar: bar
baz: baz
hoge: hoge
fuga: fuga
a: []
`,
"/path/to/default.yaml": `
x:
bar: "bar_default"
baz: "baz_default"
a:
- b: []
`,
"/path/to/production.yaml": `
x:
bar: "bar_production"
baz: "baz_production"
a:
- b: []
`,
"/path/to/overrides.yaml": `
x:
baz: baz_override
hoge: hoge_override
a:
- b:
- c: C
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: testcase.env,
ValuesFiles: []string{"overrides.yaml"},
Set: map[string]interface{}{"x": map[string]interface{}{"hoge": "hoge_set", "fuga": "fuga_set"}},
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(actual) != 1 {
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
}
if actual[0].Name != testcase.expected {
t.Errorf("unexpected name: expected=%s, got=%s", testcase.expected, actual[0].Name)
}
if actual[0].Chart != testcase.expected {
t.Errorf("unexpected chart: expected=%s, got=%s", testcase.expected, actual[0].Chart)
}
if actual[0].Namespace != testcase.expected {
t.Errorf("unexpected namespace: expected=%s, got=%s", testcase.expected, actual[0].Namespace)
}
})
}
}
func TestVisitDesiredStatesWithReleasesFiltered_ChartAtAbsPath(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: myapp
chart: /path/to/mychart
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
Selectors: []string{},
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(actual) != 1 {
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
}
if actual[0].Name != "myapp" {
t.Errorf("unexpected name: expected=%s, got=%s", "myapp", actual[0].Name)
}
if actual[0].Chart != "/path/to/mychart" {
t.Errorf("unexpected chart: expected=%s, got=%s", "/path/to/mychart", actual[0].Chart)
}
}
func TestVisitDesiredStatesWithReleasesFiltered_RemoteTgzAsChart(t *testing.T) {
testcases := []struct {
expr, env, expected string
}{
{
expected: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
},
}
for i := range testcases {
testcase := testcases[i]
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: arangodb-crd
chart: https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Selectors: []string{},
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(actual) != 1 {
t.Errorf("unexpected number of processed releases: expected=1, got=%d", len(actual))
}
if actual[0].Chart != testcase.expected {
t.Errorf("unexpected chart: expected=%s, got=%s", testcase.expected, actual[0].Chart)
}
})
}
}
// See https://github.com/roboll/helmfile/issues/1213
func TestVisitDesiredStatesWithReleases_DuplicateReleasesHelm2(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
namespace: foo
chart: charts/foo
- name: foo
namespace: bar
chart: charts/foo
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app, false)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
expected := "in ./helmfile.yaml: duplicate release \"foo\" found: there were 2 releases named \"foo\" matching specified selector"
if err == nil {
t.Errorf("error expected but not happened")
} else if err.Error() != expected {
t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", expected, err.Error())
}
}
// See https://github.com/roboll/helmfile/issues/1213
func TestVisitDesiredStatesWithReleases_NoDuplicateReleasesHelm2(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
namespace: foo
tillerNamespace: tns1
chart: charts/foo
- name: foo
namespace: bar
tillerNamespace: tns2
chart: charts/foo
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app, false)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// See https://github.com/roboll/helmfile/issues/1213
func TestVisitDesiredStatesWithReleases_NoDuplicateReleasesHelm3(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
namespace: foo
chart: charts/foo
- name: foo
namespace: bar
chart: charts/foo
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app, true)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// See https://github.com/roboll/helmfile/issues/1213
func TestVisitDesiredStatesWithReleases_DuplicateReleasesHelm3(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
namespace: foo
chart: charts/foo
- name: foo
namespace: foo
chart: charts/foo
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app, true)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
expected := "in ./helmfile.yaml: duplicate release \"foo\" found in namespace \"foo\": there were 2 releases named \"foo\" matching specified selector"
if err == nil {
t.Errorf("error expected but not happened")
} else if err.Error() != expected {
t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", expected, err.Error())
}
}
func TestVisitDesiredStatesWithReleases_DuplicateReleasesInNsKubeContextHelm3(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
namespace: foo
chart: charts/foo
kubeContext: foo
- name: foo
namespace: foo
chart: charts/foo
kubeContext: foo
`,
}
actual := []state.ReleaseSpec{}
collectReleases := func(run *Run) (bool, []error) {
for _, r := range run.state.Releases {
actual = append(actual, r)
}
return false, []error{}
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Namespace: "",
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelmVersion(app, true)
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
expected := "in ./helmfile.yaml: duplicate release \"foo\" found in namespace \"foo\" in kubecontext \"foo\": there were 2 releases named \"foo\" matching specified selector"
if err == nil {
t.Errorf("error expected but not happened")
} else if err.Error() != expected {
t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", expected, err.Error())
}
}
func TestLoadDesiredStateFromYaml_DuplicateReleaseName(t *testing.T) {
yamlFile := "example/path/to/yaml/file"
yamlContent := []byte(`releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
`)
readFile := func(filename string) ([]byte, error) {
if filename != yamlFile {
return nil, fmt.Errorf("unexpected filename: %s", filename)
}
return yamlContent, nil
}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
readFile: readFile,
glob: filepath.Glob,
abs: filepath.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
expectNoCallsToHelm(app)
_, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestLoadDesiredStateFromYaml_Bases(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
- ../base.gotmpl
{{ readFile "templates.yaml" }}
releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
<<: *default
`
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
`,
"/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": `environments:
default:
values:
- environments/default/2.yaml
helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
directoryExistsAt: testFs.DirectoryExistsAt,
fileExistsAt: testFs.FileExistsAt,
fileExists: testFs.FileExists,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, "", app.readFile, app.directoryExistsAt, app.fileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if *st.Releases[1].MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[0].missingFileHandler: expected=Warn, got=%s", *st.Releases[1].MissingFileHandler)
}
if st.Releases[1].Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[0].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", st.Releases[1].Values[0])
}
if st.FilePath != yamlFile {
t.Errorf("unexpected filePath: expected=%s, got=%s", yamlFile, st.FilePath)
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
- ../base.gotmpl
---
helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
---
releases:
- name: myrelease0
chart: mychart0
---
{{ readFile "templates.yaml" }}
releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
<<: *default
`
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
`,
"/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": `environments:
default:
values:
- environments/default/2.yaml
helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
firstRelease := st.Releases[0]
if firstRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", firstRelease.Name)
}
secondRelease := st.Releases[1]
if secondRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", secondRelease.Name)
}
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if *secondRelease.MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *firstRelease.MissingFileHandler)
}
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_EnvvalsInheritanceToBaseTemplate(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
# "envvals inheritance"
# base.gotmpl should be able to reference environment values defined in the base.yaml and default/1.yaml
- ../base.gotmpl
---
releases:
- name: myrelease0
chart: mychart0
`
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
`,
"/path/to/base.gotmpl": `helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/1.yaml": `tillerNs: TILLER_NS
foo: FOO
`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_InlineEnvVals(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
# "envvals inheritance"
# base.gotmpl should be able to reference environment values defined in the base.yaml and default/1.yaml
- ../base.gotmpl
---
releases:
- name: myrelease0
chart: mychart0
`
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
default:
values:
- environments/default/1.yaml
- tillerNs: INLINE_TILLER_NS
`,
"/path/to/base.gotmpl": `helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/1.yaml": `tillerNs: TILLER_NS
foo: FOO
`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "INLINE_TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
if st.Releases[0].Name != "myrelease0" {
t.Errorf("unexpected releases[0].name: expected=myrelease0, got=%s", st.Releases[0].Name)
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithNonDefaultEnv(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `bases:
- ../base.yaml
---
bases:
- ../base.gotmpl
---
helmDefaults:
kubeContext: {{ .Environment.Values.foo }}
---
releases:
- name: myrelease0
chart: mychart0
---
{{ readFile "templates.yaml" }}
releases:
- name: myrelease1
chart: mychart1
labels:
stage: pre
foo: bar
- name: myrelease1
chart: mychart2
labels:
stage: post
<<: *default
`
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/base.yaml": `environments:
test:
values:
- environments/default/1.yaml
`,
"/path/to/yaml/environments/default/1.yaml": `foo: FOO`,
"/path/to/base.gotmpl": `environments:
test:
values:
- environments/default/2.yaml
helmDefaults:
tillerNamespace: {{ .Environment.Values.tillerNs }}
`,
"/path/to/yaml/environments/default/2.yaml": `tillerNs: TILLER_NS`,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
fileExists: testFs.FileExists,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "test",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(yamlFile)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.HelmDefaults.TillerNamespace != "TILLER_NS" {
t.Errorf("unexpected helmDefaults.tillerNamespace: expected=TILLER_NS, got=%s", st.HelmDefaults.TillerNamespace)
}
firstRelease := st.Releases[0]
if firstRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[1].name: expected=myrelease1, got=%s", firstRelease.Name)
}
secondRelease := st.Releases[1]
if secondRelease.Name != "myrelease1" {
t.Errorf("unexpected releases[2].name: expected=myrelease1, got=%s", secondRelease.Name)
}
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if *secondRelease.MissingFileHandler != "Warn" {
t.Errorf("unexpected releases[2].missingFileHandler: expected=Warn, got=%s", *firstRelease.MissingFileHandler)
}
if secondRelease.Values[0] != "{{`{{.Release.Name}}`}}/values.yaml" {
t.Errorf("unexpected releases[2].missingFileHandler: expected={{`{{.Release.Name}}`}}/values.yaml, got=%s", firstRelease.Values[0])
}
if st.HelmDefaults.KubeContext != "FOO" {
t.Errorf("unexpected helmDefaults.kubeContext: expected=FOO, got=%s", st.HelmDefaults.KubeContext)
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate_WithReverse(t *testing.T) {
yamlFile := "/path/to/yaml/file"
yamlContent := `
{{ readFile "templates.yaml" }}
releases:
- name: myrelease0
chart: mychart0
- name: myrelease1
chart: mychart1
<<: *default
---
{{ readFile "templates.yaml" }}
releases:
- name: myrelease2
chart: mychart2
- name: myrelease3
chart: mychart3
<<: *default
`
testFs := testhelper.NewTestFs(map[string]string{
yamlFile: yamlContent,
"/path/to/yaml/templates.yaml": `templates:
default: &default
missingFileHandler: Warn
values: ["` + "{{`" + `{{.Release.Name}}` + "`}}" + `/values.yaml"]
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(yamlFile, LoadOpts{Reverse: true})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.Releases[0].Name != "myrelease3" {
t.Errorf("unexpected releases[0].name: expected=myrelease3, got=%s", st.Releases[0].Name)
}
if st.Releases[1].Name != "myrelease2" {
t.Errorf("unexpected releases[0].name: expected=myrelease2, got=%s", st.Releases[1].Name)
}
if len(st.Releases) != 2 {
t.Errorf("unexpected number of releases: expected=2, got=%d", len(st.Releases))
}
}
// See https://github.com/roboll/helmfile/issues/615
func TestLoadDesiredStateFromYaml_MultiPartTemplate_NoMergeArrayInEnvVal(t *testing.T) {
statePath := "/path/to/helmfile.yaml"
stateContent := `
environments:
default:
values:
- foo: ["foo"]
---
environments:
default:
values:
- foo: ["FOO"]
- 1.yaml
---
environments:
default:
values:
- 2.yaml
---
releases:
- name: {{ .Environment.Values.foo | quote }}
chart: {{ .Environment.Values.bar | quote }}
`
testFs := testhelper.NewTestFs(map[string]string{
statePath: stateContent,
"/path/to/1.yaml": `bar: ["bar"]`,
"/path/to/2.yaml": `bar: ["BAR"]`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(statePath, LoadOpts{Reverse: true})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.Releases[0].Name != "[FOO]" {
t.Errorf("unexpected releases[0].name: expected=FOO, got=%s", st.Releases[0].Name)
}
if st.Releases[0].Chart != "[BAR]" {
t.Errorf("unexpected releases[0].chart: expected=BAR, got=%s", st.Releases[0].Chart)
}
}
// See https://github.com/roboll/helmfile/issues/623
func TestLoadDesiredStateFromYaml_MultiPartTemplate_MergeMapsVariousKeys(t *testing.T) {
type testcase struct {
overrideValues interface{}
expected string
}
testcases := []testcase{
{map[interface{}]interface{}{"foo": "FOO"}, `FOO`},
{map[interface{}]interface{}{"foo": map[interface{}]interface{}{"foo": "FOO"}}, `map[foo:FOO]`},
{map[interface{}]interface{}{"foo": map[string]interface{}{"foo": "FOO"}}, `map[foo:FOO]`},
{map[interface{}]interface{}{"foo": []interface{}{"foo"}}, `[foo]`},
{map[interface{}]interface{}{"foo": "FOO"}, `FOO`},
}
for i := range testcases {
tc := testcases[i]
statePath := "/path/to/helmfile.yaml"
stateContent := `
environments:
default:
values:
- 1.yaml
- 2.yaml
---
releases:
- name: {{ .Environment.Values.foo | quote }}
chart: {{ .Environment.Values.bar | quote }}
`
testFs := testhelper.NewTestFs(map[string]string{
statePath: stateContent,
"/path/to/1.yaml": `bar: ["bar"]`,
"/path/to/2.yaml": `bar: ["BAR"]`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
opts := LoadOpts{
CalleePath: statePath,
Environment: state.SubhelmfileEnvironmentSpec{
OverrideValues: []interface{}{tc.overrideValues},
},
Reverse: true,
}
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(statePath, opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if st.Releases[0].Name != tc.expected {
t.Errorf("unexpected releases[0].name: expected=%s, got=%s", tc.expected, st.Releases[0].Name)
}
if st.Releases[0].Chart != "[BAR]" {
t.Errorf("unexpected releases[0].chart: expected=BAR, got=%s", st.Releases[0].Chart)
}
}
}
func TestLoadDesiredStateFromYaml_MultiPartTemplate_SprigDictFuncs(t *testing.T) {
type testcase struct {
state string
expr string
expected string
}
stateInline := `
environments:
default:
values:
- foo: FOO
bar: { "baz": "BAZ" }
---
releases:
- name: %s
chart: stable/nginx
`
stateExternal := `
environments:
default:
values:
- 1.yaml
- 2.yaml
---
releases:
- name: %s
chart: stable/nginx
`
testcases := []testcase{
{stateInline, `{{ getOrNil "foo" .Environment.Values }}`, `FOO`},
{stateInline, `{{ getOrNil "baz" (getOrNil "bar" .Environment.Values) }}`, `BAZ`},
{stateInline, `{{ if hasKey .Environment.Values "foo" }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
{stateInline, `{{ if hasKey .Environment.Values "bar" }}{{ .Environment.Values.bar.baz }}{{ end }}`, `BAZ`},
{stateInline, `{{ if (keys .Environment.Values | has "foo") }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
// See https://github.com/roboll/helmfile/issues/624
// This fails when .Environment.Values.bar is not map[string]interface{}. At the time of #624 it was map[interface{}]interface{}, which sprig's dict funcs don't support.
{stateInline, `{{ if (keys .Environment.Values | has "bar") }}{{ if (keys .Environment.Values.bar | has "baz") }}{{ .Environment.Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
{stateExternal, `{{ getOrNil "foo" .Environment.Values }}`, `FOO`},
{stateExternal, `{{ getOrNil "baz" (getOrNil "bar" .Environment.Values) }}`, `BAZ`},
{stateExternal, `{{ if hasKey .Environment.Values "foo" }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
{stateExternal, `{{ if hasKey .Environment.Values "bar" }}{{ .Environment.Values.bar.baz }}{{ end }}`, `BAZ`},
{stateExternal, `{{ if (keys .Environment.Values | has "foo") }}{{ .Environment.Values.foo }}{{ end }}`, `FOO`},
// See https://github.com/roboll/helmfile/issues/624
{stateExternal, `{{ if (keys .Environment.Values | has "bar") }}{{ if (keys .Environment.Values.bar | has "baz") }}{{ .Environment.Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
// See https://github.com/roboll/helmfile/issues/643
{stateExternal, `{{ range $service := .Environment.Values.services }}{{ $service.name }}{{ if hasKey $service "something" }}{{ $service.something }}{{ end }}{{ end }}`, `xyfalse`},
// Same test with .Values
{stateInline, `{{ getOrNil "foo" .Values }}`, `FOO`},
{stateInline, `{{ getOrNil "baz" (getOrNil "bar" .Values) }}`, `BAZ`},
{stateInline, `{{ if hasKey .Values "foo" }}{{ .Values.foo }}{{ end }}`, `FOO`},
{stateInline, `{{ if hasKey .Values "bar" }}{{ .Values.bar.baz }}{{ end }}`, `BAZ`},
{stateInline, `{{ if (keys .Values | has "foo") }}{{ .Values.foo }}{{ end }}`, `FOO`},
// See https://github.com/roboll/helmfile/issues/624
// This fails when .Values.bar is not map[string]interface{}. At the time of #624 it was map[interface{}]interface{}, which sprig's dict funcs don't support.
{stateInline, `{{ if (keys .Values | has "bar") }}{{ if (keys .Values.bar | has "baz") }}{{ .Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
{stateExternal, `{{ getOrNil "foo" .Values }}`, `FOO`},
{stateExternal, `{{ getOrNil "baz" (getOrNil "bar" .Values) }}`, `BAZ`},
{stateExternal, `{{ if hasKey .Values "foo" }}{{ .Values.foo }}{{ end }}`, `FOO`},
{stateExternal, `{{ if hasKey .Values "bar" }}{{ .Values.bar.baz }}{{ end }}`, `BAZ`},
{stateExternal, `{{ if (keys .Values | has "foo") }}{{ .Values.foo }}{{ end }}`, `FOO`},
// See https://github.com/roboll/helmfile/issues/624
{stateExternal, `{{ if (keys .Values | has "bar") }}{{ if (keys .Values.bar | has "baz") }}{{ .Values.bar.baz }}{{ end }}{{ end }}`, `BAZ`},
// See https://github.com/roboll/helmfile/issues/643
{stateExternal, `{{ range $service := .Values.services }}{{ $service.name }}{{ if hasKey $service "something" }}{{ $service.something }}{{ end }}{{ end }}`, `xyfalse`},
}
for i := range testcases {
tc := testcases[i]
statePath := "/path/to/helmfile.yaml"
stateContent := fmt.Sprintf(tc.state, tc.expr)
testFs := testhelper.NewTestFs(map[string]string{
statePath: stateContent,
"/path/to/1.yaml": `foo: FOO`,
"/path/to/2.yaml": `bar: { "baz": "BAZ" }
services:
- name: "x"
- name: "y"
something: false
`,
})
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
readFile: testFs.ReadFile,
glob: testFs.Glob,
abs: testFs.Abs,
Env: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
}
app.remote = remote.NewRemote(app.Logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
expectNoCallsToHelm(app)
st, err := app.loadDesiredStateFromYaml(statePath, LoadOpts{Reverse: true})
if err != nil {
t.Fatalf("unexpected error at %d: %v", i, err)
}
if st.Releases[0].Name != tc.expected {
t.Errorf("unexpected releases[0].name at %d: expected=%s, got=%s", i, tc.expected, st.Releases[0].Name)
}
}
}
type configImpl struct {
selectors []string
set []string
output string
includeCRDs bool
skipCleanup bool
skipCRDs bool
skipDeps bool
skipNeeds bool
includeNeeds bool
}
func (a configImpl) Selectors() []string {
return a.selectors
}
func (c configImpl) Set() []string {
return c.set
}
func (c configImpl) Values() []string {
return []string{}
}
func (c configImpl) Args() string {
return "some args"
}
func (c configImpl) Validate() bool {
return true
}
func (c configImpl) SkipCleanup() bool {
return c.skipCleanup
}
func (c configImpl) SkipCRDs() bool {
return c.skipCRDs
}
func (c configImpl) SkipDeps() bool {
return c.skipDeps
}
func (c configImpl) SkipNeeds() bool {
return c.skipNeeds
}
func (c configImpl) IncludeNeeds() bool {
return c.includeNeeds
}
func (c configImpl) OutputDir() string {
return "output/subdir"
}
func (c configImpl) OutputDirTemplate() string {
return ""
}
func (c configImpl) IncludeCRDs() bool {
return c.includeCRDs
}
func (c configImpl) Concurrency() int {
return 1
}
func (c configImpl) EmbedValues() bool {
return false
}
func (c configImpl) Output() string {
return c.output
}
type applyConfig struct {
args string
values []string
retainValuesFiles bool
set []string
validate bool
skipCleanup bool
skipCRDs bool
skipDeps bool
skipNeeds bool
includeNeeds bool
includeTests bool
suppressSecrets bool
showSecrets bool
suppressDiff bool
noColor bool
context int
diffOutput string
concurrency int
detailedExitcode bool
interactive bool
skipDiffOnInstall bool
logger *zap.SugaredLogger
wait bool
waitForJobs bool
}
func (a applyConfig) Args() string {
return a.args
}
func (a applyConfig) Wait() bool {
return a.wait
}
func (a applyConfig) WaitForJobs() bool {
return a.waitForJobs
}
func (a applyConfig) Values() []string {
return a.values
}
func (a applyConfig) Set() []string {
return a.set
}
func (a applyConfig) Validate() bool {
return a.validate
}
func (a applyConfig) SkipCleanup() bool {
return a.skipCleanup
}
func (a applyConfig) SkipCRDs() bool {
return a.skipCRDs
}
func (a applyConfig) SkipDeps() bool {
return a.skipDeps
}
func (c applyConfig) SkipNeeds() bool {
return c.skipNeeds
}
func (c applyConfig) IncludeNeeds() bool {
return c.includeNeeds
}
func (a applyConfig) IncludeTests() bool {
return a.includeTests
}
func (a applyConfig) SuppressSecrets() bool {
return a.suppressSecrets
}
func (a applyConfig) ShowSecrets() bool {
return a.showSecrets
}
func (a applyConfig) SuppressDiff() bool {
return a.suppressDiff
}
func (a applyConfig) NoColor() bool {
return a.noColor
}
func (a applyConfig) Context() int {
return a.context
}
func (a applyConfig) DiffOutput() string {
return a.diffOutput
}
func (a applyConfig) Concurrency() int {
return a.concurrency
}
func (a applyConfig) DetailedExitcode() bool {
return a.detailedExitcode
}
func (a applyConfig) Interactive() bool {
return a.interactive
}
func (a applyConfig) Logger() *zap.SugaredLogger {
return a.logger
}
func (a applyConfig) RetainValuesFiles() bool {
return a.retainValuesFiles
}
func (a applyConfig) SkipDiffOnInstall() bool {
return a.skipDiffOnInstall
}
type depsConfig struct {
skipRepos bool
}
func (d depsConfig) SkipRepos() bool {
return d.skipRepos
}
func (d depsConfig) Args() string {
return ""
}
// Mocking the command-line runner
type mockRunner struct {
output []byte
err error
}
func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
return []byte{}, nil
}
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
return []byte{}, nil
}
func MockExecer(logger *zap.SugaredLogger, kubeContext string) helmexec.Interface {
execer := helmexec.New("helm", logger, kubeContext, &mockRunner{})
return execer
}
// mocking helmexec.Interface
type listKey struct {
filter string
flags string
}
type mockHelmExec struct {
templated []mockTemplates
repos []mockRepo
updateDepsCallbacks map[string]func(string) error
}
type mockTemplates struct {
name, chart string
flags []string
}
type mockRepo struct {
Name string
}
func (helm *mockHelmExec) TemplateRelease(name, chart string, flags ...string) error {
helm.templated = append(helm.templated, mockTemplates{name: name, chart: chart, flags: flags})
return nil
}
func (helm *mockHelmExec) ChartPull(chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) ChartExport(chart string, path string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) UpdateDeps(chart string) error {
return nil
}
func (helm *mockHelmExec) BuildDeps(name, chart string) error {
return nil
}
func (helm *mockHelmExec) SetExtraArgs(args ...string) {
return
}
func (helm *mockHelmExec) SetHelmBinary(bin string) {
return
}
func (helm *mockHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
helm.repos = append(helm.repos, mockRepo{Name: name})
return nil
}
func (helm *mockHelmExec) UpdateRepo() error {
return nil
}
func (helm *mockHelmExec) RegistryLogin(name string, username string, password string) error {
return nil
}
func (helm *mockHelmExec) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) DiffRelease(context helmexec.HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
return nil
}
func (helm *mockHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
return "", nil
}
func (helm *mockHelmExec) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
return "", nil
}
func (helm *mockHelmExec) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) IsHelm3() bool {
return false
}
func (helm *mockHelmExec) GetVersion() helmexec.Version {
return helmexec.Version{}
}
func (helm *mockHelmExec) IsVersionAtLeast(versionStr string) bool {
return false
}
func TestTemplate_SingleStateFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
repositories:
- name: stable
url: https://kubernetes-charts.storage.googleapis.com
- name: stable2
url: https://kubernetes-charts.storage.googleapis.com
releases:
- name: myrelease1
chart: stable/mychart1
labels:
group: one
- name: myrelease2
chart: stable/mychart2
labels:
group: one
- name: myrelease3
chart: stable2/mychart3
`,
}
var helm = &mockHelmExec{}
var wantReleases = []mockTemplates{
{name: "myrelease1", chart: "stable/mychart1", flags: []string{"--namespace", "testNamespace", "--set", "foo=a", "--set", "bar=b", "--output-dir", "output/subdir/helmfile-[a-z0-9]{8}-myrelease1"}},
{name: "myrelease2", chart: "stable/mychart2", flags: []string{"--namespace", "testNamespace", "--set", "foo=a", "--set", "bar=b", "--output-dir", "output/subdir/helmfile-[a-z0-9]{8}-myrelease2"}},
}
var wantRepos = []mockRepo{
{Name: "stable"},
{Name: "stable2"},
}
var buffer bytes.Buffer
logger := helmexec.NewLogger(&buffer, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
Namespace: "testNamespace",
valsRuntime: valsRuntime,
Selectors: []string{
"group=one",
},
}, files)
if err := app.Template(configImpl{set: []string{"foo=a", "bar=b"}, skipDeps: false}); err != nil {
t.Fatalf("%v", err)
}
if diff := cmp.Diff(wantRepos, helm.repos); diff != "" {
t.Errorf("unexpected add repo:\n%s", diff)
}
for i := range wantReleases {
if wantReleases[i].name != helm.templated[i].name {
t.Errorf("name = [%v], want %v", helm.templated[i].name, wantReleases[i].name)
}
if !strings.Contains(helm.templated[i].chart, wantReleases[i].chart) {
t.Errorf("chart = [%v], want %v", helm.templated[i].chart, wantReleases[i].chart)
}
for j := range wantReleases[i].flags {
if j == 7 {
matched, _ := regexp.Match(wantReleases[i].flags[j], []byte(helm.templated[i].flags[j]))
if !matched {
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
}
} else if wantReleases[i].flags[j] != helm.templated[i].flags[j] {
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
}
}
}
}
func TestTemplate_ApiVersions(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
apiVersions:
- helmfile.test/v1
- helmfile.test/v2
releases:
- name: myrelease1
chart: stable/mychart1
`,
}
var helm = &mockHelmExec{}
var wantReleases = []mockTemplates{
{name: "myrelease1", chart: "stable/mychart1", flags: []string{"--api-versions", "helmfile.test/v1", "--api-versions", "helmfile.test/v2", "--namespace", "testNamespace", "--output-dir", "output/subdir/helmfile-[a-z0-9]{8}-myrelease1"}},
}
var buffer bytes.Buffer
logger := helmexec.NewLogger(&buffer, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
Namespace: "testNamespace",
valsRuntime: valsRuntime,
}, files)
if err := app.Template(configImpl{}); err != nil {
t.Fatalf("%v", err)
}
for i := range wantReleases {
if wantReleases[i].name != helm.templated[i].name {
t.Errorf("name = [%v], want %v", helm.templated[i].name, wantReleases[i].name)
}
if !strings.Contains(helm.templated[i].chart, wantReleases[i].chart) {
t.Errorf("chart = [%v], want %v", helm.templated[i].chart, wantReleases[i].chart)
}
for j := range wantReleases[i].flags {
if j == 7 {
matched, _ := regexp.Match(wantReleases[i].flags[j], []byte(helm.templated[i].flags[j]))
if !matched {
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
}
} else if wantReleases[i].flags[j] != helm.templated[i].flags[j] {
t.Errorf("HelmState.TemplateReleases() = [%v], want %v", helm.templated[i].flags[j], wantReleases[i].flags[j])
}
}
}
}
func TestApply(t *testing.T) {
type fields struct {
skipNeeds bool
includeNeeds bool
}
testcases := []struct {
name string
loc string
fields fields
ns string
concurrency int
skipDiffOnInstall bool
error string
files map[string]string
selectors []string
lists map[exectest.ListKey]string
diffs map[exectest.DiffKey]error
upgraded []exectest.Release
deleted []exectest.Release
log string
}{
//
// complex test cases for smoke testing
//
{
name: "smoke",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: database
chart: charts/mysql
needs:
- logging
- name: frontend-v1
chart: charts/frontend
installed: false
needs:
- servicemesh
- logging
- backend-v1
- name: frontend-v2
chart: charts/frontend
needs:
- servicemesh
- logging
- backend-v2
- name: frontend-v3
chart: charts/frontend
needs:
- servicemesh
- logging
- backend-v2
- name: backend-v1
chart: charts/backend
installed: false
needs:
- servicemesh
- logging
- database
- anotherbackend
- name: backend-v2
chart: charts/backend
needs:
- servicemesh
- logging
- database
- anotherbackend
- name: anotherbackend
chart: charts/anotherbackend
needs:
- servicemesh
- logging
- database
- name: servicemesh
chart: charts/istio
needs:
- logging
- name: logging
chart: charts/fluent-bit
- name: front-proxy
chart: stable/envoy
`,
},
diffs: map[exectest.DiffKey]error{
// noop on frontend-v2
exectest.DiffKey{Name: "frontend-v2", Chart: "charts/frontend", Flags: "--kube-contextdefault--detailed-exitcode"}: nil,
// install frontend-v3
exectest.DiffKey{Name: "frontend-v3", Chart: "charts/frontend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
// upgrades
exectest.DiffKey{Name: "logging", Chart: "charts/fluent-bit", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "front-proxy", Chart: "stable/envoy", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "servicemesh", Chart: "charts/istio", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "database", Chart: "charts/mysql", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "backend-v2", Chart: "charts/backend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "anotherbackend", Chart: "charts/anotherbackend", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
// delete frontend-v1 and backend-v1
exectest.ListKey{Filter: "^frontend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
frontend-v1 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^backend-v1$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
backend-v1 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
`,
},
// Disable concurrency to avoid in-deterministic result
concurrency: 1,
upgraded: []exectest.Release{
{Name: "logging", Flags: []string{}},
{Name: "front-proxy", Flags: []string{}},
{Name: "database", Flags: []string{}},
{Name: "servicemesh", Flags: []string{}},
{Name: "anotherbackend", Flags: []string{}},
{Name: "backend-v2", Flags: []string{}},
{Name: "frontend-v3", Flags: []string{}},
},
deleted: []exectest.Release{
{Name: "frontend-v1", Flags: []string{}},
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: database
3: chart: charts/mysql
4: needs:
5: - logging
6: - name: frontend-v1
7: chart: charts/frontend
8: installed: false
9: needs:
10: - servicemesh
11: - logging
12: - backend-v1
13: - name: frontend-v2
14: chart: charts/frontend
15: needs:
16: - servicemesh
17: - logging
18: - backend-v2
19: - name: frontend-v3
20: chart: charts/frontend
21: needs:
22: - servicemesh
23: - logging
24: - backend-v2
25: - name: backend-v1
26: chart: charts/backend
27: installed: false
28: needs:
29: - servicemesh
30: - logging
31: - database
32: - anotherbackend
33: - name: backend-v2
34: chart: charts/backend
35: needs:
36: - servicemesh
37: - logging
38: - database
39: - anotherbackend
40: - name: anotherbackend
41: chart: charts/anotherbackend
42: needs:
43: - servicemesh
44: - logging
45: - database
46: - name: servicemesh
47: chart: charts/istio
48: needs:
49: - logging
50: - name: logging
51: chart: charts/fluent-bit
52: - name: front-proxy
53: chart: stable/envoy
54:
merged environment: &{default map[] map[]}
10 release(s) found in helmfile.yaml
Affected releases are:
anotherbackend (charts/anotherbackend) UPDATED
backend-v1 (charts/backend) DELETED
backend-v2 (charts/backend) UPDATED
database (charts/mysql) UPDATED
front-proxy (stable/envoy) UPDATED
frontend-v1 (charts/frontend) DELETED
frontend-v3 (charts/frontend) UPDATED
logging (charts/fluent-bit) UPDATED
servicemesh (charts/istio) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 frontend-v1
2 backend-v1
processing releases in group 1/2: frontend-v1
processing releases in group 2/2: backend-v1
processing 5 groups of releases in this order:
GROUP RELEASES
1 logging, front-proxy
2 database, servicemesh
3 anotherbackend
4 backend-v2
5 frontend-v3
processing releases in group 1/5: logging, front-proxy
getting deployed release version failed:unexpected list key: {^logging$ --kube-contextdefault--deleting--deployed--failed--pending}
getting deployed release version failed:unexpected list key: {^front-proxy$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/5: database, servicemesh
getting deployed release version failed:unexpected list key: {^database$ --kube-contextdefault--deleting--deployed--failed--pending}
getting deployed release version failed:unexpected list key: {^servicemesh$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 3/5: anotherbackend
getting deployed release version failed:unexpected list key: {^anotherbackend$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 4/5: backend-v2
getting deployed release version failed:unexpected list key: {^backend-v2$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 5/5: frontend-v3
getting deployed release version failed:unexpected list key: {^frontend-v3$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
logging charts/fluent-bit
front-proxy stable/envoy
database charts/mysql
servicemesh charts/istio
anotherbackend charts/anotherbackend
backend-v2 charts/backend
frontend-v3 charts/frontend
DELETED RELEASES:
NAME
frontend-v1
backend-v1
`,
},
//
// noop: no changes
//
{
name: "noop",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
- name: foo
chart: stable/mychart1
installed: false
needs:
- bar
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: nil,
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: ``,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{},
deleted: []exectest.Release{},
},
//
// install
//
{
name: "install",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: baz
chart: stable/mychart3
- name: foo
chart: stable/mychart1
needs:
- bar
- name: bar
chart: stable/mychart2
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{},
upgraded: []exectest.Release{
{Name: "baz", Flags: []string{}},
{Name: "bar", Flags: []string{}},
{Name: "foo", Flags: []string{}},
},
deleted: []exectest.Release{},
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: chart: stable/mychart3
4: - name: foo
5: chart: stable/mychart1
6: needs:
7: - bar
8: - name: bar
9: chart: stable/mychart2
10:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: chart: stable/mychart3
4: - name: foo
5: chart: stable/mychart1
6: needs:
7: - bar
8: - name: bar
9: chart: stable/mychart2
10:
merged environment: &{default map[] map[]}
3 release(s) found in helmfile.yaml
Affected releases are:
bar (stable/mychart2) UPDATED
baz (stable/mychart3) UPDATED
foo (stable/mychart1) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 baz, bar
2 foo
processing releases in group 1/2: baz, bar
getting deployed release version failed:unexpected list key: {^baz$ --kube-contextdefault--deleting--deployed--failed--pending}
getting deployed release version failed:unexpected list key: {^bar$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/2: foo
getting deployed release version failed:unexpected list key: {^foo$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
baz stable/mychart3
bar stable/mychart2
foo stable/mychart1
`,
},
//
// install with upgrade
//
{
name: "install-with-upgrade-with-validation-control",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: baz
chart: stable/mychart3
disableValidationOnInstall: true
- name: foo
chart: stable/mychart1
disableValidationOnInstall: true
needs:
- bar
- name: bar
chart: stable/mychart2
disableValidation: true
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--disable-validation--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: ``,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^baz$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "baz", Flags: []string{"--kube-context", "default"}},
{Name: "bar", Flags: []string{"--kube-context", "default"}},
{Name: "foo", Flags: []string{"--kube-context", "default"}},
},
deleted: []exectest.Release{},
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: chart: stable/mychart3
4: disableValidationOnInstall: true
5: - name: foo
6: chart: stable/mychart1
7: disableValidationOnInstall: true
8: needs:
9: - bar
10: - name: bar
11: chart: stable/mychart2
12: disableValidation: true
13:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: chart: stable/mychart3
4: disableValidationOnInstall: true
5: - name: foo
6: chart: stable/mychart1
7: disableValidationOnInstall: true
8: needs:
9: - bar
10: - name: bar
11: chart: stable/mychart2
12: disableValidation: true
13:
merged environment: &{default map[] map[]}
3 release(s) found in helmfile.yaml
Affected releases are:
bar (stable/mychart2) UPDATED
baz (stable/mychart3) UPDATED
foo (stable/mychart1) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 baz, bar
2 foo
processing releases in group 1/2: baz, bar
processing releases in group 2/2: foo
getting deployed release version failed:Failed to get the version for:mychart1
UPDATED RELEASES:
NAME CHART VERSION
baz stable/mychart3 3.1.0
bar stable/mychart2 3.1.0
foo stable/mychart1
`,
},
//
// install with upgrade and --skip-diff-on-install
//
{
name: "install-with-upgrade-with-skip-diff-on-install",
loc: location(),
skipDiffOnInstall: true,
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: baz
chart: stable/mychart3
disableValidationOnInstall: true
- name: foo
chart: stable/mychart1
disableValidationOnInstall: true
needs:
- bar
- name: bar
chart: stable/mychart2
disableValidation: true
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: ``,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^baz$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "baz", Flags: []string{"--kube-context", "default"}},
{Name: "bar", Flags: []string{"--kube-context", "default"}},
{Name: "foo", Flags: []string{"--kube-context", "default"}},
},
deleted: []exectest.Release{},
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: chart: stable/mychart3
4: disableValidationOnInstall: true
5: - name: foo
6: chart: stable/mychart1
7: disableValidationOnInstall: true
8: needs:
9: - bar
10: - name: bar
11: chart: stable/mychart2
12: disableValidation: true
13:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: chart: stable/mychart3
4: disableValidationOnInstall: true
5: - name: foo
6: chart: stable/mychart1
7: disableValidationOnInstall: true
8: needs:
9: - bar
10: - name: bar
11: chart: stable/mychart2
12: disableValidation: true
13:
merged environment: &{default map[] map[]}
3 release(s) found in helmfile.yaml
Affected releases are:
bar (stable/mychart2) UPDATED
baz (stable/mychart3) UPDATED
foo (stable/mychart1) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 baz, bar
2 foo
processing releases in group 1/2: baz, bar
processing releases in group 2/2: foo
getting deployed release version failed:Failed to get the version for:mychart1
UPDATED RELEASES:
NAME CHART VERSION
baz stable/mychart3 3.1.0
bar stable/mychart2 3.1.0
foo stable/mychart1
`,
},
//
// upgrades
//
{
name: "upgrade when foo needs bar",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
- name: foo
chart: stable/mychart1
needs:
- bar
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "bar", Flags: []string{}},
{Name: "foo", Flags: []string{}},
},
},
{
name: "upgrade when bar needs foo",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
chart: stable/mychart1
- name: bar
chart: stable/mychart2
needs:
- foo
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "foo", Flags: []string{}},
{Name: "bar", Flags: []string{}},
},
},
{
name: "upgrade when foo needs bar, with ns override",
loc: location(),
ns: "testNamespace",
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
- name: foo
chart: stable/mychart1
needs:
- bar
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "bar", Flags: []string{}},
{Name: "foo", Flags: []string{}},
},
},
{
name: "upgrade when bar needs foo, with ns override",
loc: location(),
ns: "testNamespace",
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
chart: stable/mychart1
- name: bar
chart: stable/mychart2
needs:
- foo
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--namespacetestNamespace--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "foo", Flags: []string{}},
{Name: "bar", Flags: []string{}},
},
},
{
name: "upgrade when ns1/foo needs ns2/bar",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
chart: stable/mychart1
namespace: ns1
needs:
- ns2/bar
- name: bar
chart: stable/mychart2
namespace: ns2
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "bar", Flags: []string{"--kube-context", "default", "--namespace", "ns2"}},
{Name: "foo", Flags: []string{"--kube-context", "default", "--namespace", "ns1"}},
},
},
{
name: "upgrade when ns2/bar needs ns1/foo",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
namespace: ns2
needs:
- ns1/foo
- name: foo
chart: stable/mychart1
namespace: ns1
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "foo", Flags: []string{"--kube-context", "default", "--namespace", "ns1"}},
{Name: "bar", Flags: []string{"--kube-context", "default", "--namespace", "ns2"}},
},
},
{
name: "upgrade when tns1/ns1/foo needs tns2/ns2/bar",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
chart: stable/mychart1
namespace: ns1
tillerNamespace: tns1
needs:
- tns2/ns2/bar
- name: bar
chart: stable/mychart2
namespace: ns2
tillerNamespace: tns2
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--tiller-namespacetns2--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--tiller-namespacetns1--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "bar", Flags: []string{"--tiller-namespace", "tns2", "--kube-context", "default", "--namespace", "ns2"}},
{Name: "foo", Flags: []string{"--tiller-namespace", "tns1", "--kube-context", "default", "--namespace", "ns1"}},
},
},
{
name: "upgrade when tns2/ns2/bar needs tns1/ns1/foo",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
namespace: ns2
tillerNamespace: tns2
needs:
- tns1/ns1/foo
- name: foo
chart: stable/mychart1
namespace: ns1
tillerNamespace: tns1
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--tiller-namespacetns2--kube-contextdefault--namespacens2--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--tiller-namespacetns1--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "foo", Flags: []string{"--tiller-namespace", "tns1", "--kube-context", "default", "--namespace", "ns1"}},
{Name: "bar", Flags: []string{"--tiller-namespace", "tns2", "--kube-context", "default", "--namespace", "ns2"}},
},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: chart: stable/mychart2
4: namespace: ns2
5: tillerNamespace: tns2
6: needs:
7: - tns1/ns1/foo
8: - name: foo
9: chart: stable/mychart1
10: namespace: ns1
11: tillerNamespace: tns1
12:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: chart: stable/mychart2
4: namespace: ns2
5: tillerNamespace: tns2
6: needs:
7: - tns1/ns1/foo
8: - name: foo
9: chart: stable/mychart1
10: namespace: ns1
11: tillerNamespace: tns1
12:
merged environment: &{default map[] map[]}
2 release(s) found in helmfile.yaml
Affected releases are:
bar (stable/mychart2) UPDATED
foo (stable/mychart1) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 tns1/ns1/foo
2 tns2/ns2/bar
processing releases in group 1/2: tns1/ns1/foo
getting deployed release version failed:unexpected list key: {^foo$ --tiller-namespacetns1--kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/2: tns2/ns2/bar
getting deployed release version failed:unexpected list key: {^bar$ --tiller-namespacetns2--kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
foo stable/mychart1
bar stable/mychart2
`,
},
//
// deletes: deleting all releases in the correct order
//
{
name: "delete foo and bar when foo needs bar",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
installed: false
- name: foo
chart: stable/mychart1
installed: false
needs:
- bar
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
},
deleted: []exectest.Release{
{Name: "foo", Flags: []string{}},
{Name: "bar", Flags: []string{}},
},
},
{
name: "delete foo and bar when bar needs foo",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
installed: false
needs:
- foo
- name: foo
chart: stable/mychart1
installed: false
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
},
deleted: []exectest.Release{
{Name: "bar", Flags: []string{}},
{Name: "foo", Flags: []string{}},
},
},
//
// upgrade and delete: upgrading one while deleting another
//
{
name: "delete foo when foo needs bar",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
- name: foo
chart: stable/mychart1
installed: false
needs:
- bar
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "bar", Flags: []string{}},
},
deleted: []exectest.Release{
{Name: "foo", Flags: []string{}},
},
},
{
name: "delete bar when foo needs bar",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
installed: false
- name: foo
chart: stable/mychart1
needs:
- bar
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "foo", Flags: []string{}},
},
deleted: []exectest.Release{
{Name: "bar", Flags: []string{}},
},
},
{
name: "delete foo when bar needs foo",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
chart: stable/mychart1
installed: false
- name: bar
chart: stable/mychart2
needs:
- foo
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "bar", Flags: []string{}},
},
deleted: []exectest.Release{
{Name: "foo", Flags: []string{}},
},
},
{
name: "delete bar when bar needs foo",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: foo
chart: stable/mychart1
- name: bar
chart: stable/mychart2
installed: false
needs:
- foo
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{
exectest.ListKey{Filter: "^foo$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart1-3.1.0 3.1.0 default
`,
exectest.ListKey{Filter: "^bar$", Flags: helmV2ListFlags}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
`,
},
upgraded: []exectest.Release{
{Name: "foo", Flags: []string{}},
},
deleted: []exectest.Release{
{Name: "bar", Flags: []string{}},
},
},
//
// upgrades with selector
//
{
// see https://github.com/roboll/helmfile/issues/919#issuecomment-549831747
name: "upgrades with good selector with --skip-needs=true",
loc: location(),
fields: fields{
skipNeeds: true,
},
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test"},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "external-secrets", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{
{Name: "external-secrets", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
{Name: "my-release", Flags: []string{"--kube-context", "default", "--namespace", "default"}},
},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
Affected releases are:
external-secrets (incubator/raw) UPDATED
my-release (incubator/raw) UPDATED
processing 2 groups of releases in this order:
GROUP RELEASES
1 default/external-secrets
2 default/my-release
processing releases in group 1/2: default/external-secrets
getting deployed release version failed:unexpected list key: {^external-secrets$ --kube-contextdefault--deleting--deployed--failed--pending}
processing releases in group 2/2: default/my-release
getting deployed release version failed:unexpected list key: {^my-release$ --kube-contextdefault--deleting--deployed--failed--pending}
UPDATED RELEASES:
NAME CHART VERSION
external-secrets incubator/raw
my-release incubator/raw
`,
},
{
// see https://github.com/roboll/helmfile/issues/919#issuecomment-549831747
name: "upgrades with good selector with --skip-needs=false --include-needs=true",
loc: location(),
fields: fields{
skipNeeds: false,
includeNeeds: true,
},
error: `in ./helmfile.yaml: release "default/external-secrets" depends on "kube-system/kubernetes-external-secrets" which does not match the selectors. Please add a selector like "--selector name=kubernetes-external-secrets", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies`,
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test"},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "external-secrets", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "my-release", Chart: "incubator/raw", Flags: "--kube-contextdefault--namespacedefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
upgraded: []exectest.Release{},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
err: release "default/external-secrets" depends on "kube-system/kubernetes-external-secrets" which does not match the selectors. Please add a selector like "--selector name=kubernetes-external-secrets", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies
`,
},
{
// see https://github.com/roboll/helmfile/issues/919#issuecomment-549831747
name: "upgrades with bad selector",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
{{ $mark := "a" }}
releases:
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
`,
},
selectors: []string{"app=test_non_existent"},
diffs: map[exectest.DiffKey]error{},
upgraded: []exectest.Release{},
error: "err: no releases found that matches specified selector(app=test_non_existent) and environment(default), in any helmfile",
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
0 release(s) matching app=test_non_existent found in helmfile.yaml
`,
},
//
// error cases
//
{
name: "non-existent release in needs",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: baz
namespace: ns1
chart: mychart3
- name: foo
chart: mychart1
needs:
- bar
`,
},
diffs: map[exectest.DiffKey]error{
exectest.DiffKey{Name: "baz", Chart: "mychart3", Flags: "--kube-contextdefault--namespacens1--detailed-exitcode"}: helmexec.ExitError{Code: 2},
exectest.DiffKey{Name: "foo", Chart: "mychart1", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
},
lists: map[exectest.ListKey]string{},
upgraded: []exectest.Release{},
deleted: []exectest.Release{},
concurrency: 1,
error: `in ./helmfile.yaml: "foo" depends on nonexistent release "bar"`,
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - bar
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - bar
9:
merged environment: &{default map[] map[]}
2 release(s) found in helmfile.yaml
err: "foo" depends on nonexistent release "bar"
`,
},
}
for i := range testcases {
tc := testcases[i]
t.Run(tc.name, func(t *testing.T) {
wantUpgrades := tc.upgraded
wantDeletes := tc.deleted
var helm = &exectest.Helm{
FailOnUnexpectedList: true,
FailOnUnexpectedDiff: true,
Lists: tc.lists,
Diffs: tc.diffs,
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
bs := &bytes.Buffer{}
func() {
logReader, logWriter := io.Pipe()
logFlushed := &sync.WaitGroup{}
// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
logFlushed.Add(1)
go func() {
scanner := bufio.NewScanner(logReader)
for scanner.Scan() {
bs.Write(scanner.Bytes())
bs.WriteString("\n")
}
logFlushed.Done()
}()
defer func() {
// This is here to avoid data-trace on bytes buffer `bs` to capture logs
if err := logWriter.Close(); err != nil {
panic(err)
}
logFlushed.Wait()
}()
logger := helmexec.NewLogger(logWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
valsRuntime: valsRuntime,
}, tc.files)
if tc.ns != "" {
app.Namespace = tc.ns
}
if tc.selectors != nil {
app.Selectors = tc.selectors
}
applyErr := app.Apply(applyConfig{
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
concurrency: tc.concurrency,
logger: logger,
skipDiffOnInstall: tc.skipDiffOnInstall,
skipNeeds: tc.fields.skipNeeds,
})
if tc.error == "" && applyErr != nil {
t.Fatalf("unexpected error for data defined at %s: %v", tc.loc, applyErr)
} else if tc.error != "" && applyErr == nil {
t.Fatalf("expected error did not occur for data defined at %s", tc.loc)
} else if tc.error != "" && applyErr != nil && tc.error != applyErr.Error() {
t.Fatalf("invalid error: expected %q, got %q", tc.error, applyErr.Error())
}
if len(wantUpgrades) > len(helm.Releases) {
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
}
for relIdx := range wantUpgrades {
if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
}
for flagIdx := range wantUpgrades[relIdx].Flags {
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
}
}
}
if len(wantDeletes) > len(helm.Deleted) {
t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
}
for relIdx := range wantDeletes {
if wantDeletes[relIdx].Name != helm.Deleted[relIdx].Name {
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Deleted[relIdx].Name, wantDeletes[relIdx].Name)
}
for flagIdx := range wantDeletes[relIdx].Flags {
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
}
}
}
}()
if tc.log != "" {
actual := bs.String()
diff, exists := testhelper.Diff(tc.log, actual, 3)
if exists {
t.Errorf("unexpected log for data defined %s:\nDIFF\n%s\nEOD", tc.loc, diff)
}
}
})
}
}
func TestDeps(t *testing.T) {
testcases := []struct {
name string
loc string
error string
files map[string]string
log string
charts []string
}{
//
// complex test cases for smoke testing
//
{
name: "smoke",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami/
releases:
- name: example
chart: /path/to/charts/example
`,
"/path/to/charts/example/Chart.yaml": `foo: FOO`,
},
log: `processing file "helmfile.yaml" in directory "."
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: repositories:
2: - name: bitnami
3: url: https://charts.bitnami.com/bitnami/
4: releases:
5: - name: example
6: chart: /path/to/charts/example
7:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: repositories:
2: - name: bitnami
3: url: https://charts.bitnami.com/bitnami/
4: releases:
5: - name: example
6: chart: /path/to/charts/example
7:
merged environment: &{default map[] map[]}
There are no repositories defined in your helmfile.yaml.
This means helmfile cannot update your dependencies or create a lock file.
See https://github.com/roboll/helmfile/issues/878 for more information.
`,
charts: []string{"/path/to/charts/example"},
},
}
for i := range testcases {
tc := testcases[i]
t.Run(tc.name, func(t *testing.T) {
var helm = &exectest.Helm{
DiffMutex: &sync.Mutex{},
ChartsMutex: &sync.Mutex{},
ReleasesMutex: &sync.Mutex{},
}
bs := &bytes.Buffer{}
func() {
logReader, logWriter := io.Pipe()
logFlushed := &sync.WaitGroup{}
// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
logFlushed.Add(1)
go func() {
scanner := bufio.NewScanner(logReader)
for scanner.Scan() {
bs.Write(scanner.Bytes())
bs.WriteString("\n")
}
logFlushed.Done()
}()
defer func() {
// This is here to avoid data-trace on bytes buffer `bs` to capture logs
if err := logWriter.Close(); err != nil {
panic(err)
}
logFlushed.Wait()
}()
logger := helmexec.NewLogger(logWriter, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
helms: map[helmKey]helmexec.Interface{
createHelmKey("helm", "default"): helm,
},
}, tc.files)
depsErr := app.Deps(depsConfig{
skipRepos: false,
})
if tc.error == "" && depsErr != nil {
t.Fatalf("unexpected error for data defined at %s: %v", tc.loc, depsErr)
} else if tc.error != "" && depsErr == nil {
t.Fatalf("expected error did not occur for data defined at %s", tc.loc)
} else if tc.error != "" && depsErr != nil && tc.error != depsErr.Error() {
t.Fatalf("invalid error: expected %q, got %q", tc.error, depsErr.Error())
}
if !reflect.DeepEqual(helm.Charts, tc.charts) {
t.Fatalf("expected charts %v, got %v", helm.Charts, tc.charts)
}
}()
if tc.log != "" {
actual := bs.String()
diff, exists := testhelper.Diff(tc.log, actual, 3)
if exists {
t.Errorf("unexpected log for data defined %s:\nDIFF\n%s\nEOD", tc.loc, diff)
}
}
})
}
}
func captureStdout(f func()) string {
reader, writer, err := os.Pipe()
if err != nil {
panic(err)
}
stdout := os.Stdout
defer func() {
os.Stdout = stdout
log.SetOutput(os.Stderr)
}()
os.Stdout = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
var buf bytes.Buffer
wg.Done()
io.Copy(&buf, reader)
out <- buf.String()
}()
wg.Wait()
f()
writer.Close()
return <-out
}
func TestPrint_SingleStateFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: myrelease1
chart: mychart1
- name: myrelease2
chart: mychart1
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
logger := helmexec.NewLogger(&buffer, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
out := captureStdout(func() {
err := app.PrintState(configImpl{})
assert.NilError(t, err)
})
assert.Assert(t, strings.Count(out, "---") == 1,
"state should contain '---' yaml doc separator:\n%s\n", out)
assert.Assert(t, strings.Contains(out, "helmfile.yaml"),
"state should contain source helmfile name:\n%s\n", out)
assert.Assert(t, strings.Contains(out, "name: myrelease1"),
"state should contain releases:\n%s\n", out)
}
func TestPrint_MultiStateFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
releases:
- name: myrelease1
chart: mychart1
- name: myrelease2
chart: mychart1
`,
"/path/to/helmfile.d/second.yaml": `
releases:
- name: myrelease3
chart: mychart1
- name: myrelease4
chart: mychart1
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
logger := helmexec.NewLogger(&buffer, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
out := captureStdout(func() {
err := app.PrintState(configImpl{})
assert.NilError(t, err)
})
assert.Assert(t, strings.Count(out, "---") == 2,
"state should contain '---' yaml doc separators:\n%s\n", out)
assert.Assert(t, strings.Contains(out, "second.yaml"),
"state should contain source helmfile name:\n%s\n", out)
assert.Assert(t, strings.Contains(out, "second.yaml"),
"state should contain source helmfile name:\n%s\n", out)
}
func TestList(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
commonLabels:
common: label
releases:
- name: myrelease1
chart: mychart1
installed: no
labels:
id: myrelease1
- name: myrelease2
chart: mychart1
`,
"/path/to/helmfile.d/second.yaml": `
releases:
- name: myrelease3
chart: mychart1
installed: yes
- name: myrelease4
chart: mychart1
labels:
id: myrelease1
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
logger := helmexec.NewLogger(&buffer, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
out := captureStdout(func() {
err := app.ListReleases(configImpl{})
assert.NilError(t, err)
})
expected := `NAME NAMESPACE ENABLED LABELS CHART VERSION
myrelease1 false common:label,id:myrelease1 mychart1
myrelease2 true common:label mychart1
myrelease3 true mychart1
myrelease4 true id:myrelease1 mychart1
`
assert.Equal(t, expected, out)
}
func TestListWithJsonOutput(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
releases:
- name: myrelease1
chart: mychart1
installed: no
labels:
id: myrelease1
- name: myrelease2
chart: mychart1
`,
"/path/to/helmfile.d/second.yaml": `
releases:
- name: myrelease3
chart: mychart1
installed: yes
- name: myrelease4
chart: mychart1
labels:
id: myrelease1
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
logger := helmexec.NewLogger(&buffer, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
glob: filepath.Glob,
abs: filepath.Abs,
OverrideKubeContext: "default",
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
out := captureStdout(func() {
err := app.ListReleases(configImpl{
output: "json",
})
assert.NilError(t, err)
})
expected := `[{"name":"myrelease1","namespace":"","enabled":false,"labels":"id:myrelease1","chart":"mychart1","version":""},{"name":"myrelease2","namespace":"","enabled":true,"labels":"","chart":"mychart1","version":""},{"name":"myrelease3","namespace":"","enabled":true,"labels":"","chart":"mychart1","version":""},{"name":"myrelease4","namespace":"","enabled":true,"labels":"id:myrelease1","chart":"mychart1","version":""}]
`
assert.Equal(t, expected, out)
}
func TestSetValuesTemplate(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: zipkin
chart: stable/zipkin
values:
- val2: "val2"
valuesTemplate:
- val1: '{{"{{ .Release.Name }}"}}'
set:
- name: "name"
value: "val"
setTemplate:
- name: name-{{"{{ .Release.Name }}"}}
value: val-{{"{{ .Release.Name }}"}}
`,
}
expectedValues := []interface{}{
map[interface{}]interface{}{"val1": "zipkin"},
map[interface{}]interface{}{"val2": "val2"}}
expectedSetValues := []state.SetValue{
state.SetValue{Name: "name-zipkin", Value: "val-zipkin"},
state.SetValue{Name: "name", Value: "val"}}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
Logger: helmexec.NewLogger(os.Stderr, "debug"),
Env: "default",
FileOrDir: "helmfile.yaml",
}, files)
expectNoCallsToHelm(app)
var specs []state.ReleaseSpec
collectReleases := func(run *Run) (bool, []error) {
specs = append(specs, run.state.Releases...)
return false, nil
}
err := app.ForEachState(
collectReleases,
SetFilter(true),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(specs) != 1 {
t.Fatalf("expected 1 release; got %d releases", len(specs))
}
actualValues := specs[0].Values
actualSetValues := specs[0].SetValues
if !reflect.DeepEqual(expectedValues, actualValues) {
t.Errorf("expected values: %v; got values: %v", expectedValues, actualValues)
}
if !reflect.DeepEqual(expectedSetValues, actualSetValues) {
t.Errorf("expected set: %v; got set: %v", expectedValues, actualValues)
}
}
func location() string {
_, fn, line, _ := runtime.Caller(1)
return fmt.Sprintf("%s:%d", filepath.Base(fn), line)
}