helmfile/state/state_test.go

898 lines
20 KiB
Go

package state
import (
"os"
"reflect"
"testing"
"errors"
"github.com/roboll/helmfile/helmexec"
"strings"
)
var logger = helmexec.NewLogger(os.Stdout, "warn")
func TestLabelParsing(t *testing.T) {
cases := []struct {
labelString string
expectedFilter LabelFilter
errorExected bool
}{
{"foo=bar", LabelFilter{positiveLabels: [][]string{[]string{"foo", "bar"}}, negativeLabels: [][]string{}}, false},
{"foo!=bar", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{[]string{"foo", "bar"}}}, false},
{"foo!=bar,baz=bat", LabelFilter{positiveLabels: [][]string{[]string{"baz", "bat"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}}, false},
{"foo", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{}}, true},
{"foo!=bar=baz", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{}}, true},
{"=bar", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{}}, true},
}
for idx, c := range cases {
filter, err := ParseLabels(c.labelString)
if err != nil && !c.errorExected {
t.Errorf("[%d] Didn't expect an error parsing labels: %s", idx, err)
} else if err == nil && c.errorExected {
t.Errorf("[%d] Expected %s to result in an error but got none", idx, c.labelString)
} else if !reflect.DeepEqual(filter, c.expectedFilter) {
t.Errorf("[%d] parsed label did not result in expected filter: %v, expected: %v", idx, filter, c.expectedFilter)
}
}
}
func TestHelmState_applyDefaultsTo(t *testing.T) {
type fields struct {
BaseChartPath string
Context string
DeprecatedReleases []ReleaseSpec
Namespace string
Repositories []RepositorySpec
Releases []ReleaseSpec
}
type args struct {
spec ReleaseSpec
}
verify := false
specWithNamespace := ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &verify,
Name: "test-charts",
Namespace: "test-namespace",
Values: nil,
SetValues: nil,
EnvValues: nil,
}
specWithoutNamespace := specWithNamespace
specWithoutNamespace.Namespace = ""
specWithNamespaceFromFields := specWithNamespace
specWithNamespaceFromFields.Namespace = "test-namespace-field"
fieldsWithNamespace := fields{
BaseChartPath: ".",
Context: "test_context",
DeprecatedReleases: nil,
Namespace: specWithNamespaceFromFields.Namespace,
Repositories: nil,
Releases: []ReleaseSpec{
specWithNamespace,
},
}
fieldsWithoutNamespace := fieldsWithNamespace
fieldsWithoutNamespace.Namespace = ""
tests := []struct {
name string
fields fields
args args
want ReleaseSpec
}{
{
name: "Has a namespace from spec",
fields: fieldsWithoutNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespace,
},
{
name: "Has a namespace from flags",
fields: fieldsWithoutNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespace,
},
{
name: "Has a namespace from flags and from spec",
fields: fieldsWithNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespaceFromFields,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: tt.fields.BaseChartPath,
Context: tt.fields.Context,
DeprecatedReleases: tt.fields.DeprecatedReleases,
Namespace: tt.fields.Namespace,
Repositories: tt.fields.Repositories,
Releases: tt.fields.Releases,
}
if state.applyDefaultsTo(&tt.args.spec); !reflect.DeepEqual(tt.args.spec, tt.want) {
t.Errorf("HelmState.applyDefaultsTo() = %v, want %v", tt.args.spec, tt.want)
}
})
}
}
func TestHelmState_flagsForUpgrade(t *testing.T) {
enable := true
disable := false
some := func(v int) *int {
return &v
}
tests := []struct {
name string
defaults HelmSpec
release *ReleaseSpec
want []string
}{
{
name: "no-options",
defaults: HelmSpec{
Verify: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "verify",
defaults: HelmSpec{
Verify: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--verify",
"--namespace", "test-namespace",
},
},
{
name: "verify-from-default",
defaults: HelmSpec{
Verify: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--verify",
"--namespace", "test-namespace",
},
},
{
name: "force",
defaults: HelmSpec{
Force: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force",
"--namespace", "test-namespace",
},
},
{
name: "force-from-default",
defaults: HelmSpec{
Force: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force",
"--namespace", "test-namespace",
},
},
{
name: "recreate-pods",
defaults: HelmSpec{
RecreatePods: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
RecreatePods: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--recreate-pods",
"--namespace", "test-namespace",
},
},
{
name: "recreate-pods-from-default",
defaults: HelmSpec{
RecreatePods: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
RecreatePods: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--recreate-pods",
"--namespace", "test-namespace",
},
},
{
name: "wait",
defaults: HelmSpec{
Wait: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Wait: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--wait",
"--namespace", "test-namespace",
},
},
{
name: "wait-from-default",
defaults: HelmSpec{
Wait: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Wait: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--wait",
"--namespace", "test-namespace",
},
},
{
name: "timeout",
defaults: HelmSpec{
Timeout: 0,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Timeout: some(123),
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--timeout", "123",
"--namespace", "test-namespace",
},
},
{
name: "timeout-from-default",
defaults: HelmSpec{
Timeout: 123,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Timeout: nil,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--timeout", "123",
"--namespace", "test-namespace",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: "./",
Context: "default",
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
}
helm := helmexec.New(logger, "default")
args, err := state.flagsForUpgrade(helm, tt.release)
if err != nil {
t.Errorf("unexpected error flagsForUpgade: %v", err)
}
if !reflect.DeepEqual(args, tt.want) {
t.Errorf("flagsForUpgrade returned = %v, want %v", args, tt.want)
}
})
}
}
func Test_isLocalChart(t *testing.T) {
type args struct {
chart string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "local chart",
args: args{
chart: "./",
},
want: true,
},
{
name: "repo chart",
args: args{
chart: "stable/genius",
},
want: false,
},
{
name: "empty",
args: args{
chart: "",
},
want: false,
},
{
name: "parent local path",
args: args{
chart: "../examples",
},
want: true,
},
{
name: "parent-parent local path",
args: args{
chart: "../../",
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isLocalChart(tt.args.chart); got != tt.want {
t.Errorf("pathExists() = %v, want %v", got, tt.want)
}
})
}
}
func Test_normalizeChart(t *testing.T) {
type args struct {
basePath string
chart string
}
tests := []struct {
name string
args args
want string
}{
{
name: "construct local chart path",
args: args{
basePath: "/Users/jane/code/deploy/charts",
chart: "./app",
},
want: "/Users/jane/code/deploy/charts/app",
},
{
name: "repo path",
args: args{
basePath: "/Users/jane/code/deploy/charts",
chart: "remote/app",
},
want: "remote/app",
},
{
name: "construct local chart path, parent dir",
args: args{
basePath: "/Users/jane/code/deploy/charts",
chart: "../app",
},
want: "/Users/jane/code/deploy/app",
},
{
name: "too much parent levels",
args: args{
basePath: "/src",
chart: "../../app",
},
want: "/app",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := normalizeChart(tt.args.basePath, tt.args.chart); got != tt.want {
t.Errorf("normalizeChart() = %v, want %v", got, tt.want)
}
})
}
}
// mocking helmexec.Interface
type mockHelmExec struct {
charts []string
repo []string
releases []mockRelease
}
type mockRelease struct {
name string
flags []string
}
func (helm *mockHelmExec) UpdateDeps(chart string) error {
if strings.Contains(chart, "error") {
return errors.New("error")
}
helm.charts = append(helm.charts, chart)
return nil
}
func (helm *mockHelmExec) SetExtraArgs(args ...string) {
return
}
func (helm *mockHelmExec) SetHelmBinary(bin string) {
return
}
func (helm *mockHelmExec) AddRepo(name, repository, certfile, keyfile, username, password string) error {
helm.repo = []string{name, repository, certfile, keyfile, username, password}
return nil
}
func (helm *mockHelmExec) UpdateRepo() error {
return nil
}
func (helm *mockHelmExec) SyncRelease(name, chart string, flags ...string) error {
if strings.Contains(name, "error") {
return errors.New("error")
}
helm.releases = append(helm.releases, mockRelease{name: name, flags: flags})
helm.charts = append(helm.charts, chart)
return nil
}
func (helm *mockHelmExec) DiffRelease(name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) ReleaseStatus(release string) error {
if strings.Contains(release, "error") {
return errors.New("error")
}
helm.releases = append(helm.releases, mockRelease{name: release, flags: []string{}})
return nil
}
func (helm *mockHelmExec) DeleteRelease(name string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) DecryptSecret(name string) (string, error) {
return "", nil
}
func (helm *mockHelmExec) TestRelease(name string, flags ...string) error {
if strings.Contains(name, "error") {
return errors.New("error")
}
helm.releases = append(helm.releases, mockRelease{name: name, flags: flags})
return nil
}
func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) Lint(chart string, flags ...string) error {
return nil
}
func TestHelmState_SyncRepos(t *testing.T) {
tests := []struct {
name string
repos []RepositorySpec
helm *mockHelmExec
envs map[string]string
want []string
}{
{
name: "normal repository",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "",
Password: "",
},
},
helm: &mockHelmExec{},
want: []string{"name", "http://example.com/", "", "", "", ""},
},
{
name: "repository with cert and key",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "certfile",
KeyFile: "keyfile",
Username: "",
Password: "",
},
},
helm: &mockHelmExec{},
want: []string{"name", "http://example.com/", "certfile", "keyfile", "", ""},
},
{
name: "repository with username and password",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "example_user",
Password: "example_password",
},
},
helm: &mockHelmExec{},
want: []string{"name", "http://example.com/", "", "", "example_user", "example_password"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.envs {
err := os.Setenv(k, v)
if err != nil {
t.Error("HelmState.SyncRepos() could not set env var for testing")
}
}
state := &HelmState{
Repositories: tt.repos,
}
if _ = state.SyncRepos(tt.helm); !reflect.DeepEqual(tt.helm.repo, tt.want) {
t.Errorf("HelmState.SyncRepos() for [%s] = %v, want %v", tt.name, tt.helm.repo, tt.want)
}
})
}
}
func TestHelmState_SyncReleases(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *mockHelmExec
wantReleases []mockRelease
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{}}},
},
{
name: "escaped values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "someList",
Value: "a,b,c",
},
{
Name: "json",
Value: "{\"name\": \"john\"}",
},
},
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}"}}},
},
{
name: "set single value from file",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "foo",
Value: "FOO",
},
{
Name: "bar",
File: "path/to/bar",
},
{
Name: "baz",
Value: "BAZ",
},
},
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ"}}},
},
{
name: "set single array value in an array",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "foo.bar[0]",
Values: []string{
"A",
"B",
},
},
},
},
},
helm: &mockHelmExec{},
wantReleases: []mockRelease{{"releaseName", []string{"--set", "foo.bar[0]={A,B}"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
}
if _ = state.SyncReleases(tt.helm, []string{}, 1); !reflect.DeepEqual(tt.helm.releases, tt.wantReleases) {
t.Errorf("HelmState.SyncReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.wantReleases)
}
})
}
}
func TestHelmState_UpdateDeps(t *testing.T) {
state := &HelmState{
basePath: "/src",
Releases: []ReleaseSpec{
{
Chart: "./..",
},
{
Chart: "../examples",
},
{
Chart: "../../helmfile",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: ".error",
},
},
}
want := []string{"/", "/examples", "/helmfile"}
helm := &mockHelmExec{}
errs := state.UpdateDeps(helm)
if !reflect.DeepEqual(helm.charts, want) {
t.Errorf("HelmState.UpdateDeps() = %v, want %v", helm.charts, want)
}
if len(errs) != 0 {
t.Errorf("HelmState.UpdateDeps() - no errors, but got: %v", len(errs))
}
}
func TestHelmState_ReleaseStatuses(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *mockHelmExec
want []mockRelease
wantErr bool
}{
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "releaseA",
},
},
helm: &mockHelmExec{},
want: []mockRelease{{"releaseA", []string{}}},
},
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "error",
},
},
helm: &mockHelmExec{},
wantErr: true,
},
}
for _, tt := range tests {
i := func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
}
errs := state.ReleaseStatuses(tt.helm, 1)
if (errs != nil) != tt.wantErr {
t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
if !reflect.DeepEqual(tt.helm.releases, tt.want) {
t.Errorf("HelmState.ReleaseStatuses() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.want)
}
}
t.Run(tt.name, i)
}
}
func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
tests := []struct {
name string
cleanup bool
releases []ReleaseSpec
helm *mockHelmExec
want []mockRelease
wantErr bool
}{
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "releaseA",
},
},
helm: &mockHelmExec{},
want: []mockRelease{{"releaseA", []string{"--timeout", "1"}}},
},
{
name: "do cleanup",
cleanup: true,
releases: []ReleaseSpec{
{
Name: "releaseB",
},
},
helm: &mockHelmExec{},
want: []mockRelease{{"releaseB", []string{"--cleanup", "--timeout", "1"}}},
},
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "error",
},
},
helm: &mockHelmExec{},
wantErr: true,
},
}
for _, tt := range tests {
i := func(t *testing.T) {
state := &HelmState{
Releases: tt.releases,
}
errs := state.TestReleases(tt.helm, tt.cleanup, 1)
if (errs != nil) != tt.wantErr {
t.Errorf("TestReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
if !reflect.DeepEqual(tt.helm.releases, tt.want) {
t.Errorf("HelmState.TestReleases() for [%s] = %v, want %v", tt.name, tt.helm.releases, tt.want)
}
}
t.Run(tt.name, i)
}
}
func TestHelmState_NoReleaseMatched(t *testing.T) {
releases := []ReleaseSpec{
{
Name: "releaseA",
Labels: map[string]string{
"foo": "bar",
},
},
}
tests := []struct {
name string
labels string
wantErr bool
}{
{
name: "happy path",
labels: "foo=bar",
wantErr: false,
},
{
name: "name does not exist",
labels: "name=releaseB",
wantErr: false,
},
{
name: "label does not match anything",
labels: "foo=notbar",
wantErr: false,
},
}
for _, tt := range tests {
i := func(t *testing.T) {
state := &HelmState{
Releases: releases,
logger: logger,
}
errs := state.FilterReleases([]string{tt.labels})
if (errs != nil) != tt.wantErr {
t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
}
t.Run(tt.name, i)
}
}