Files
helmfile/pkg/state/state_test.go
T
yxxhero c5eebc24bd fix: helmfile deps broken for OCI charts with underscores in path (#2648)
fix: helmfile deps broken for OCI charts with underscores in path (#954)

For OCI charts with multi-segment paths (e.g., myrepo/path_with_underscores/example),
helmfile was putting the full path as the dependency name in the generated Chart.yaml.
Helm then reconstructed the OCI reference using this name, and underscores in the
path caused issues with helm's OCI reference handling during dependency update.

Fix: move the chart path prefix into the repository URL and use only the chart
basename as the dependency name, matching Helm's recommended Chart.yaml format
for OCI dependencies:

  # Before (broken):
  dependencies:
  - name: path_with_underscores/example
    repository: oci://harbor.custom.com

  # After (fixed):
  dependencies:
  - name: example
    repository: oci://harbor.custom.com/path_with_underscores

The resulting OCI reference is identical, but the dependency name is now clean.

Includes backward-compatibility fallback for old lock files that used the full
path as the dependency name.

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-06-18 22:51:44 +08:00

6305 lines
154 KiB
Go

package state
import (
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/Masterminds/semver/v3"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/exectest"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/testhelper"
)
var logger = helmexec.NewLogger(io.Discard, "warn")
var valsRuntime, _ = vals.New(vals.Options{CacheSize: 32})
func injectFs(st *HelmState, fs *testhelper.TestFs) *HelmState {
st.fs = fs.ToFileSystem()
return st
}
func TestLabelParsing(t *testing.T) {
cases := []struct {
labelString string
expectedFilter LabelFilter
errorExected bool
}{
{"foo=bar", LabelFilter{positiveLabels: [][]string{{"foo", "bar"}}, negativeLabels: [][]string{}}, false},
{"foo!=bar", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{{"foo", "bar"}}}, false},
{"foo!=bar,baz=bat", LabelFilter{positiveLabels: [][]string{{"baz", "bat"}}, negativeLabels: [][]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
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",
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 and from spec",
fields: fieldsWithNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespaceFromFields,
},
{
name: "Spec and flag Has no a namespace",
fields: fieldsWithoutNamespace,
args: args{
spec: specWithoutNamespace,
},
want: specWithoutNamespace,
},
{
name: "Spec has no a namespace but from flag",
fields: fieldsWithNamespace,
args: args{
spec: specWithoutNamespace,
},
want: specWithNamespaceFromFields,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: tt.fields.BaseChartPath,
ReleaseSetSpec: ReleaseSetSpec{
OverrideNamespace: tt.fields.Namespace,
Repositories: tt.fields.Repositories,
Releases: tt.fields.Releases,
},
}
if state.ApplyOverrides(&tt.args.spec); !reflect.DeepEqual(tt.args.spec, tt.want) {
t.Errorf("HelmState.ApplyOverrides() = %v, want %v", tt.args.spec, tt.want)
}
})
}
}
func boolValue(v bool) *bool {
return &v
}
func TestHelmState_flagsForUpgrade(t *testing.T) {
enable := true
disable := false
postRendererDefault := "foo-default.sh"
postRendererRelease := "foo-release.sh"
some := func(v int) *int {
return &v
}
tests := []struct {
name string
version *semver.Version
defaults HelmSpec
release *ReleaseSpec
syncOpts *SyncOpts
want []string
wantErr 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",
"--namespace", "test-namespace",
},
},
{
name: "enable-dns",
defaults: HelmSpec{
EnableDNS: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
EnableDNS: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--enable-dns",
"--namespace", "test-namespace",
},
},
{
name: "enable-dns-from-default",
defaults: HelmSpec{
EnableDNS: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
EnableDNS: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "force-helm3",
defaults: HelmSpec{
Force: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
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-helm4",
defaults: HelmSpec{
Force: false,
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force-replace",
"--namespace", "test-namespace",
},
},
{
name: "force-from-default-helm3",
defaults: HelmSpec{
Force: true,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "force-from-default-helm4",
defaults: HelmSpec{
Force: true,
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "force-from-default-nil-force-helm3",
defaults: HelmSpec{
Force: true,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force",
"--namespace", "test-namespace",
},
},
{
name: "force-from-default-nil-force-helm4",
defaults: HelmSpec{
Force: true,
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force-replace",
"--namespace", "test-namespace",
},
},
{
name: "force-conflicts-helm4",
defaults: HelmSpec{
ForceConflicts: false,
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
ForceConflicts: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force-conflicts",
"--namespace", "test-namespace",
},
},
{
name: "force-conflicts-from-default-helm4",
defaults: HelmSpec{
ForceConflicts: true,
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force-conflicts",
"--namespace", "test-namespace",
},
},
{
name: "force-conflicts-helm3-error",
defaults: HelmSpec{
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
ForceConflicts: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
wantErr: "forceConflicts requires Helm 4 or greater (set via releases[].forceConflicts or helmDefaults.forceConflicts)",
},
{
name: "force-conflicts-from-default-helm3-error",
defaults: HelmSpec{
ForceConflicts: true,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
wantErr: "forceConflicts requires Helm 4 or greater (set via releases[].forceConflicts or helmDefaults.forceConflicts)",
},
{
name: "force-and-force-conflicts-mutually-exclusive-helm4",
defaults: HelmSpec{
CreateNamespace: &disable,
},
version: semver.MustParse("4.0.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &enable,
ForceConflicts: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
wantErr: "force and forceConflicts are mutually exclusive (check both releases[].force/forceConflicts and helmDefaults.force/forceConflicts)",
},
{
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",
"--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-for-jobs",
defaults: HelmSpec{
WaitForJobs: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
WaitForJobs: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--wait-for-jobs",
"--namespace", "test-namespace",
},
},
{
name: "devel",
defaults: HelmSpec{
Devel: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Wait: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--devel",
"--wait",
"--namespace", "test-namespace",
},
},
{
name: "devel-release",
defaults: HelmSpec{
Devel: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Devel: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--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",
"--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", "123s",
"--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", "123s",
"--namespace", "test-namespace",
},
},
{
name: "timeout-from-cli-flag",
defaults: HelmSpec{
Timeout: 123,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Timeout: some(456),
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Timeout: 789,
},
want: []string{
"--version", "0.1",
"--timeout", "789s",
"--namespace", "test-namespace",
},
},
{
name: "atomic",
defaults: HelmSpec{
Atomic: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Atomic: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--atomic",
"--namespace", "test-namespace",
},
},
{
name: "atomic-override-default",
defaults: HelmSpec{
Atomic: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Atomic: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "atomic-from-default",
defaults: HelmSpec{
Atomic: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--atomic",
"--namespace", "test-namespace",
},
},
{
name: "cleanup-on-fail",
defaults: HelmSpec{
CleanupOnFail: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
CleanupOnFail: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--cleanup-on-fail",
"--namespace", "test-namespace",
},
},
{
name: "cleanup-on-fail-override-default",
defaults: HelmSpec{
CleanupOnFail: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
CleanupOnFail: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "cleanup-on-fail-from-default",
defaults: HelmSpec{
CleanupOnFail: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--cleanup-on-fail",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-default-helm3.2",
defaults: HelmSpec{
Verify: false,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--create-namespace",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-disabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.0"),
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: "create-namespace-release-override-enabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &enable,
},
want: []string{
"--version", "0.1",
"--create-namespace",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-release-override-disabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-unsupported",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("2.16.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
wantErr: "releases[].createNamespace requires Helm 3.2.0 or greater",
},
{
name: "post-renderer-flags-use-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRenderer: &postRendererDefault,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererDefault,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-flags-use-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRenderer: &postRendererRelease,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererRelease,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-flags-use-release-prior-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRenderer: &postRendererDefault,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRenderer: &postRendererRelease,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererRelease,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--arg1", "--arg2"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--arg1",
"--post-renderer-args=--arg2",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release-prior-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-cli-overrides-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
syncOpts: &SyncOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--cli-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-release-overrides-cli",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
syncOpts: &SyncOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-short-flag-value",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"-v"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=-v",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-helmdefault-templated-with-release-name",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"{{ .Release.Name }}", "--chart={{ .Release.Chart }}"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "my-release",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=my-release",
"--post-renderer-args=--chart=test/chart",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-helmdefault-templated-with-namespace",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"{{ .Release.Namespace }}/{{ .Release.Name }}"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "my-release",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=test-namespace/my-release",
"--namespace", "test-namespace",
},
},
{
name: "description-from-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--description", "Release description from config",
"--namespace", "test-namespace",
},
},
{
name: "description-from-cli",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Description: "CLI description from --description flag",
},
want: []string{
"--version", "0.1",
"--description", "CLI description from --description flag",
"--namespace", "test-namespace",
},
},
{
name: "description-cli-overrides-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{
Description: "CLI description overrides config",
},
want: []string{
"--version", "0.1",
"--description", "CLI description overrides config",
"--namespace", "test-namespace",
},
},
{
name: "description-empty-string-not-passed",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "",
},
syncOpts: &SyncOpts{
Description: "",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "description-from-config-unsupported-version-3.1.0",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.1.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
wantErr: "releases[].description requires Helm 3.3.0 or greater",
},
{
name: "description-from-config-unsupported-version-3.2.4",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.4"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
wantErr: "releases[].description requires Helm 3.3.0 or greater",
},
{
name: "description-from-cli-unsupported-version",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Description: "CLI description from --description flag",
},
wantErr: "--description flag requires Helm 3.3.0 or greater",
},
{
name: "description-empty-on-old-version",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.1.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
// No description set
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
// No --description flag should appear
},
},
{
name: "description-from-config-supported-version-3.3.0",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.3.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
Description: "Release description from config",
},
syncOpts: &SyncOpts{},
want: []string{
"--version", "0.1",
"--description", "Release description from config",
"--namespace", "test-namespace",
},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: "./",
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
},
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Version: tt.version,
}
args, _, err := state.flagsForUpgrade(helm, tt.release, 0, tt.syncOpts)
if err != nil && tt.wantErr == "" {
t.Errorf("unexpected error flagsForUpgrade: %v", err)
}
if tt.wantErr != "" && (err == nil || err.Error() != tt.wantErr) {
t.Errorf("expected error '%v'; got '%v'", err, tt.wantErr)
}
if !reflect.DeepEqual(args, tt.want) {
t.Errorf("flagsForUpgrade returned = %v, want %v", args, tt.want)
}
})
}
}
func TestHelmState_flagsForTemplate(t *testing.T) {
enable := true
disable := false
postRendererDefault := "foo-default.sh"
postRendererRelease := "foo-release.sh"
tests := []struct {
name string
version *semver.Version
defaults HelmSpec
release *ReleaseSpec
templateOpts TemplateOpts
environments map[string]EnvironmentSpec
envName string
want []string
wantErr string
}{
{
name: "post-renderer-flags-use-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRenderer: &postRendererDefault,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererDefault,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-flags-use-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRenderer: &postRendererRelease,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererRelease,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-flags-use-release-prior-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRenderer: &postRendererDefault,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRenderer: &postRendererRelease,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererRelease,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--arg1", "--arg2"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--arg1",
"--post-renderer-args=--arg2",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-flags-use-release-prior-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-cli-overrides-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRendererArgs: []string{"--default-arg"},
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
templateOpts: TemplateOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--cli-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-release-overrides-cli",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"--release-arg"},
},
templateOpts: TemplateOpts{
PostRendererArgs: []string{"--cli-arg"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=--release-arg",
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-args-short-flag-value",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRendererArgs: []string{"-v"},
},
want: []string{
"--version", "0.1",
"--post-renderer-args=-v",
"--namespace", "test-namespace",
},
},
{
name: "kube-version-flag-should-be-used",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
templateOpts: TemplateOpts{
KubeVersion: "1.100",
},
want: []string{
"--version", "0.1",
"--kube-version", "1.100",
"--namespace", "test-namespace",
},
},
{
name: "kube-version-flag-should-be-respected",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
KubeVersion: "1.25",
},
templateOpts: TemplateOpts{
KubeVersion: "1.100",
},
want: []string{
"--version", "0.1",
"--kube-version", "1.100",
"--namespace", "test-namespace",
},
},
// Issue #2309: kube-context tests
{
name: "kube-context-from-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
KubeContext: "release-context",
},
want: []string{
"--version", "0.1",
"--kube-context", "release-context",
"--namespace", "test-namespace",
},
},
{
name: "kube-context-from-helmdefaults",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
KubeContext: "default-context",
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--kube-context", "default-context",
"--namespace", "test-namespace",
},
},
{
name: "kube-context-release-overrides-helmdefaults",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
KubeContext: "default-context",
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
KubeContext: "release-context",
},
want: []string{
"--version", "0.1",
"--kube-context", "release-context",
"--namespace", "test-namespace",
},
},
{
name: "kube-context-from-environment",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
environments: map[string]EnvironmentSpec{
"production": {KubeContext: "env-context"},
},
envName: "production",
want: []string{
"--version", "0.1",
"--kube-context", "env-context",
"--namespace", "test-namespace",
},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
envName := tt.envName
if envName == "" {
envName = "default"
}
environments := tt.environments
if environments == nil {
environments = make(map[string]EnvironmentSpec)
}
state := &HelmState{
basePath: "./",
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{*tt.release},
HelmDefaults: tt.defaults,
Environments: environments,
Env: environment.Environment{Name: envName},
},
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Version: tt.version,
}
args, _, err := state.flagsForTemplate(helm, tt.release, 0, &(tt.templateOpts))
if err != nil && tt.wantErr == "" {
t.Errorf("unexpected error flagsForUpgrade: %v", err)
}
if tt.wantErr != "" && (err == nil || err.Error() != tt.wantErr) {
t.Errorf("expected error '%v'; got '%v'", err, tt.wantErr)
}
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: true,
},
{
name: "parent local path",
args: args{
chart: "../examples",
},
want: true,
},
{
name: "parent-parent local path",
args: args{
chart: "../../",
},
want: true,
},
{
name: "absolute path",
args: args{
chart: "/foo/bar/baz",
},
want: true,
},
{
name: "remote chart in 3-level deep dir (e.g. ChartCenter)",
args: args{
chart: "center/bar/baz",
},
want: false,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
if got := isLocalChart(tt.args.chart); got != tt.want {
t.Errorf("%s(\"%s\") isLocalChart(): got %v, want %v", tt.name, tt.args.chart, 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: "/src",
chart: "./app",
},
want: "/src/app",
},
{
name: "construct local chart path, without leading dot",
args: args{
basePath: "/src",
chart: "published",
},
want: "/src/published",
},
{
name: "repo path",
args: args{
basePath: "/src",
chart: "remote/app",
},
want: "remote/app",
},
{
name: "chartcenter repo path",
args: args{
basePath: "/src",
chart: "center/stable/myapp",
},
want: "center/stable/myapp",
},
{
name: "construct local chart path, sibling dir",
args: args{
basePath: "/src",
chart: "../app",
},
want: "/app",
},
{
name: "construct local chart path, parent dir",
args: args{
basePath: "/src",
chart: "./..",
},
want: "/",
},
{
name: "too much parent levels",
args: args{
basePath: "/src",
chart: "../../app",
},
want: "/app",
},
}
for i := range tests {
tt := tests[i]
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
func TestHelmState_SyncRepos(t *testing.T) {
tests := []struct {
name string
repos []RepositorySpec
helm *exectest.Helm
envs map[string]string
want []string
}{
{
name: "normal repository",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "",
Password: "",
},
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "", "", "", "", "", "", "false", "false"},
},
{
name: "ACR hosted repository",
repos: []RepositorySpec{
{
Name: "name",
Managed: "acr",
},
},
helm: &exectest.Helm{},
want: []string{"name", "", "", "", "", "", "", "acr", "false", "false"},
},
{
name: "repository with cert and key",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "certfile",
KeyFile: "keyfile",
Username: "",
Password: "",
},
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "", "certfile", "keyfile", "", "", "", "false", "false"},
},
{
name: "repository with ca file",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CaFile: "cafile",
Username: "",
Password: "",
},
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "cafile", "", "", "", "", "", "false", "false"},
},
{
name: "repository with username and password",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "example_user",
Password: "example_password",
},
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "", "", "", "example_user", "example_password", "", "false", "false"},
},
{
name: "repository with username and password and pass-credentials",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "example_user",
Password: "example_password",
PassCredentials: true,
},
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "", "", "", "example_user", "example_password", "", "true", "false"},
},
{
name: "repository without username and password and environment with username and password",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "",
Password: "",
},
},
envs: map[string]string{
"NAME_USERNAME": "example_user",
"NAME_PASSWORD": "example_password",
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "", "", "", "example_user", "example_password", "", "false", "false"},
},
{
name: "repository with username and password and environment with username and password",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "example_user1",
Password: "example_password1",
},
},
envs: map[string]string{
"NAME_USERNAME": "example_user2",
"NAME_PASSWORD": "example_password2",
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "", "", "", "example_user1", "example_password1", "", "false", "false"},
},
{
name: "repository with skip-tls-verify",
repos: []RepositorySpec{
{
Name: "name",
URL: "http://example.com/",
CertFile: "",
KeyFile: "",
Username: "",
Password: "",
SkipTLSVerify: true,
},
},
helm: &exectest.Helm{},
want: []string{"name", "http://example.com/", "", "", "", "", "", "", "false", "true"},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.envs {
t.Setenv(k, v)
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: tt.repos,
},
}
if _, _ = state.SyncRepos(tt.helm, map[string]bool{}); !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) {
postRenderer := "foo.sh"
tests := []struct {
name string
releases []ReleaseSpec
helm *exectest.Helm
wantReleases []exectest.Release
wantErrorMsgs []string
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--reset-values"}}},
},
{
name: "escaped values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "someList",
Value: "a,b,c",
},
{
Name: "json",
Value: "{\"name\": \"john\"}",
},
},
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}", "--reset-values"}}},
},
{
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: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ", "--reset-values"}}},
},
{
name: "set single array value in an array",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "foo.bar[0]",
Values: []any{
"A",
"B",
},
},
},
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "foo.bar[0]={A,B}", "--reset-values"}}},
},
{
name: "set array of map values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "source.helm.parameters",
Values: []any{
map[string]any{"name": "demo"},
map[string]any{"version": "v2"},
},
},
},
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "source.helm.parameters={\\{\"name\":\"demo\"\\},\\{\"version\":\"v2\"\\}}", "--reset-values"}}},
},
{
name: "post renderer helm 3",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
PostRenderer: &postRenderer,
},
},
helm: &exectest.Helm{Helm3: true}, // Helm 3 keeps script paths unchanged
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--post-renderer", postRenderer, "--reset-values"}}},
},
{
name: "post renderer helm 4",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
PostRenderer: &postRenderer,
},
},
helm: &exectest.Helm{Helm4: true}, // Helm 4 converts script paths to plugin names
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--post-renderer", "foo", "--reset-values"}}}, // "foo.sh" -> "foo"
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
if errs := state.SyncReleases(&AffectedReleases{}, tt.helm, []string{}, 1); len(errs) > 0 {
if len(errs) != len(tt.wantErrorMsgs) {
t.Fatalf("Unexpected errors: %v\nExpected: %v", errs, tt.wantErrorMsgs)
}
var mismatch int
for i := range tt.wantErrorMsgs {
expected := tt.wantErrorMsgs[i]
actual := errs[i].Error()
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Unexpected error: expected=%v, got=%v", expected, actual)
}
}
if mismatch > 0 {
t.Fatalf("%d unexpected errors detected", mismatch)
}
}
if !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_SyncReleases_MissingValuesFileForUndesiredRelease(t *testing.T) {
no := false
tests := []struct {
name string
release ReleaseSpec
listResult string
expectedError string
}{
{
name: "should install",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
},
listResult: ``,
expectedError: ``,
},
{
name: "should upgrade",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: ``,
},
{
name: "should uninstall",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Installed: &no,
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: ``,
},
{
name: "should fail installing due to missing values file",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Values: []any{"noexistent.values.yaml"},
},
listResult: ``,
expectedError: `failed processing release foo: values file matching "noexistent.values.yaml" does not exist in "."`,
},
{
name: "should fail upgrading due to missing values file",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Values: []any{"noexistent.values.yaml"},
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: `failed processing release foo: values file matching "noexistent.values.yaml" does not exist in "."`,
},
{
name: "should uninstall even when there is a missing values file",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
Values: []any{"noexistent.values.yaml"},
Installed: &no,
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
expectedError: ``,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: ".",
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{tt.release},
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
fs := testhelper.NewTestFs(map[string]string{})
state = injectFs(state, fs)
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
//simulate the helm.list call result
helm.Lists[exectest.ListKey{Filter: "^" + tt.release.Name + "$"}] = tt.listResult
affectedReleases := AffectedReleases{}
errs := state.SyncReleases(&affectedReleases, helm, []string{}, 1)
if tt.expectedError != "" {
if len(errs) == 0 {
t.Fatalf("expected error not occurred: expected=%s, got none", tt.expectedError)
}
if len(errs) != 1 {
t.Fatalf("too many errors: expected %d, got %d: %v", 1, len(errs), errs)
}
err := errs[0]
if err.Error() != tt.expectedError {
t.Fatalf("unexpected error: expected=%s, got=%v", tt.expectedError, err)
}
} else {
if len(errs) > 0 {
t.Fatalf("unexpected error(s): expected=0, got=%d: %v", len(errs), errs)
}
}
})
}
}
func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
no := false
tests := []struct {
name string
releases []ReleaseSpec
installed []bool
wantAffected exectest.Affected
}{
{
name: "2 release",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo",
Chart: "foo",
},
{
Name: "releaseNameBar",
Chart: "bar",
},
},
wantAffected: exectest.Affected{
Upgraded: []*exectest.Release{
{Name: "releaseNameFoo", Flags: []string{}},
{Name: "releaseNameBar", Flags: []string{}},
},
Deleted: nil,
Failed: nil,
},
},
{
name: "2 removed",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo",
Chart: "foo",
Installed: &no,
},
{
Name: "releaseNameBar",
Chart: "foo",
Installed: &no,
},
},
installed: []bool{true, true},
wantAffected: exectest.Affected{
Upgraded: nil,
Deleted: []*exectest.Release{
{Name: "releaseNameFoo", Flags: []string{}},
{Name: "releaseNameBar", Flags: []string{}},
},
Failed: nil,
},
},
{
name: "2 errors",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo-error",
Chart: "foo",
},
{
Name: "releaseNameBar-error",
Chart: "foo",
},
},
wantAffected: exectest.Affected{
Upgraded: nil,
Deleted: nil,
Failed: []*exectest.Release{
{Name: "releaseNameFoo-error", Flags: []string{}},
{Name: "releaseNameBar-error", Flags: []string{}},
},
},
},
{
name: "1 removed, 1 new, 1 error",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo",
Chart: "foo",
},
{
Name: "releaseNameBar",
Chart: "foo",
Installed: &no,
},
{
Name: "releaseNameFoo-error",
Chart: "foo",
},
},
installed: []bool{true, true, true},
wantAffected: exectest.Affected{
Upgraded: []*exectest.Release{
{Name: "releaseNameFoo", Flags: []string{}},
},
Deleted: []*exectest.Release{
{Name: "releaseNameBar", Flags: []string{}},
},
Failed: []*exectest.Release{
{Name: "releaseNameFoo-error", Flags: []string{}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
//simulate the release is already installed
for i, release := range tt.releases {
if tt.installed != nil && tt.installed[i] {
helm.Lists[exectest.ListKey{Filter: "^" + release.Name + "$", Flags: "--uninstalling --deployed --failed --pending"}] = release.Name
}
}
affectedReleases := AffectedReleases{}
if err := state.SyncReleases(&affectedReleases, helm, []string{}, 1); err != nil {
if !testEq(affectedReleases.Failed, tt.wantAffected.Failed) {
t.Errorf("HelmState.SynchAffectedRelease() error failed for [%s] = %v, want %v", tt.name, affectedReleases.Failed, tt.wantAffected.Failed)
} //else expected error
}
if !testEq(affectedReleases.Upgraded, tt.wantAffected.Upgraded) {
t.Errorf("HelmState.SynchAffectedRelease() upgrade failed for [%s] = %v, want %v", tt.name, affectedReleases.Upgraded, tt.wantAffected.Upgraded)
}
if !testEq(affectedReleases.Deleted, tt.wantAffected.Deleted) {
t.Errorf("HelmState.SynchAffectedRelease() deleted failed for [%s] = %v, want %v", tt.name, affectedReleases.Deleted, tt.wantAffected.Deleted)
}
})
}
}
func TestHelmState_SyncReleasesAffectedReleasesWithReinstallIfForbidden(t *testing.T) {
no := false
tests := []struct {
name string
releases []ReleaseSpec
installed []bool
wantAffected exectest.Affected
}{
{
name: "2 new",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
{
Name: "releaseNameBar-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
},
wantAffected: exectest.Affected{
Upgraded: []*exectest.Release{
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
{Name: "releaseNameBar-forbidden", Flags: []string{}},
},
Reinstalled: nil,
Deleted: nil,
Failed: nil,
},
},
{
name: "1 removed, 1 new, 1 reinstalled first new",
releases: []ReleaseSpec{
{
Name: "releaseNameFoo-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
{
Name: "releaseNameBar",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
Installed: &no,
},
{
Name: "releaseNameFoo-forbidden",
Chart: "foo",
UpdateStrategy: "reinstallIfForbidden",
},
},
installed: []bool{true, true, true},
wantAffected: exectest.Affected{
Upgraded: []*exectest.Release{
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
},
Reinstalled: []*exectest.Release{
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
},
Deleted: []*exectest.Release{
{Name: "releaseNameBar", Flags: []string{}},
},
Failed: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
//simulate the release is already installed
for i, release := range tt.releases {
if tt.installed != nil && tt.installed[i] {
helm.Lists[exectest.ListKey{Filter: "^" + release.Name + "$", Flags: "--uninstalling --deployed --failed --pending"}] = release.Name
}
}
affectedReleases := AffectedReleases{}
if err := state.SyncReleases(&affectedReleases, helm, []string{}, 1); err != nil {
if !testEq(affectedReleases.Failed, tt.wantAffected.Failed) {
t.Errorf("HelmState.SyncReleases() error failed for [%s] = %v, want %v", tt.name, affectedReleases.Failed, tt.wantAffected.Failed)
} //else expected error
}
if !testEq(affectedReleases.Upgraded, tt.wantAffected.Upgraded) {
t.Errorf("HelmState.SyncReleases() upgrade failed for [%s] = %v, want %v", tt.name, affectedReleases.Upgraded, tt.wantAffected.Upgraded)
}
if !testEq(affectedReleases.Reinstalled, tt.wantAffected.Reinstalled) {
t.Errorf("HelmState.SyncReleases() reinstalled failed for [%s] = %v, want %v", tt.name, affectedReleases.Reinstalled, tt.wantAffected.Reinstalled)
}
if !testEq(affectedReleases.Deleted, tt.wantAffected.Deleted) {
t.Errorf("HelmState.SyncReleases() deleted failed for [%s] = %v, want %v", tt.name, affectedReleases.Deleted, tt.wantAffected.Deleted)
}
})
}
}
func testEq(a []*ReleaseSpec, b []*exectest.Release) bool {
// If one is nil, the other must also be nil.
if (a == nil) != (b == nil) {
return false
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].Name != b[i].Name {
return false
}
}
return true
}
func TestGetDeployedVersion(t *testing.T) {
tests := []struct {
name string
release ReleaseSpec
listResult string
installedVersion string
}{
{
name: "chart version",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-2.0.4 0.1.0 default`,
installedVersion: "2.0.4",
},
{
name: "chart version with a dash",
release: ReleaseSpec{
Name: "foo-bar",
Chart: "registry/foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-1.0.0-alpha.1 0.1.0 default`,
installedVersion: "1.0.0-alpha.1",
},
{
name: "chart version with dash and plus",
release: ReleaseSpec{
Name: "foo-bar",
Chart: "registry/foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-1.0.0-alpha+001 0.1.0 default`,
installedVersion: "1.0.0-alpha+001",
},
{
name: "chart version with dash and release with dash",
release: ReleaseSpec{
Name: "foo-bar",
Chart: "registry/foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo-bar-release 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar-1.0.0-alpha+001 0.1.0 default`,
installedVersion: "1.0.0-alpha+001",
},
{
name: "chart version from helm show chart",
release: ReleaseSpec{
Name: "foo",
Chart: "../../foo-bar",
},
listResult: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
foo 1 Wed Apr 17 17:39:04 2019 DEPLOYED foo-bar 0.1.0 default`,
installedVersion: "3.2.0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{tt.release},
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
// simulate the helm.list call result
helm.Lists[exectest.ListKey{Filter: "^" + tt.release.Name + "$", Flags: "--uninstalling --deployed --failed --pending"}] = tt.listResult
affectedReleases := AffectedReleases{}
state.SyncReleases(&affectedReleases, helm, []string{}, 1)
if state.Releases[0].installedVersion != tt.installedVersion {
t.Errorf("HelmState.TestGetDeployedVersion() failed for [%s] = %v, want %v", tt.name, state.Releases[0].installedVersion, tt.installedVersion)
}
})
}
}
func TestHelmState_DiffReleases(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *exectest.Helm
wantReleases []exectest.Release
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--reset-values"}}},
},
{
name: "escaped values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "someList",
Value: "a,b,c",
},
{
Name: "json",
Value: "{\"name\": \"john\"}",
},
},
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{
{Name: "releaseName", Flags: []string{"--set", "someList=a\\,b\\,c", "--set", "json=\\{\"name\": \"john\"\\}", "--reset-values"}},
},
},
{
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: &exectest.Helm{},
wantReleases: []exectest.Release{
{Name: "releaseName", Flags: []string{"--set", "foo=FOO", "--set-file", "bar=path/to/bar", "--set", "baz=BAZ", "--reset-values"}},
},
},
{
name: "set single array value in an array",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
SetValues: []SetValue{
{
Name: "foo.bar[0]",
Values: []any{
"A",
"B",
},
},
},
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{
{Name: "releaseName", Flags: []string{"--set", "foo.bar[0]={A,B}", "--reset-values"}},
},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
_, errs := state.DiffReleases(tt.helm, []string{}, 1, false, false, false, []string{}, false, false, false, false, false)
if len(errs) > 0 {
t.Errorf("unexpected error: %v", errs)
}
if !reflect.DeepEqual(tt.helm.Diffed, tt.wantReleases) {
t.Errorf("HelmState.DiffReleases() for [%s] = %v, want %v", tt.name, tt.helm.Releases, tt.wantReleases)
}
})
}
}
func TestHelmState_DiffFlags(t *testing.T) {
enable := true
disable := false
tests := []struct {
name string
defaults HelmSpec
releases []ReleaseSpec
helm *exectest.Helm
wantDiffFlags []string
}{
{
name: "release with api version and kubeversion",
defaults: HelmSpec{},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
KubeVersion: "1.21",
ApiVersions: []string{"helmfile.test/v1", "helmfile.test/v2"},
},
},
helm: &exectest.Helm{},
wantDiffFlags: []string{"--api-versions", "helmfile.test/v1", "--api-versions", "helmfile.test/v2", "--kube-version", "1.21"},
},
{
name: "release with kubeversion and plain http which is ignored",
defaults: HelmSpec{},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
KubeVersion: "1.21",
PlainHttp: true,
},
},
helm: &exectest.Helm{},
wantDiffFlags: []string{"--kube-version", "1.21"},
},
{
name: "release with enable-dns",
defaults: HelmSpec{EnableDNS: false},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
EnableDNS: &enable,
},
},
helm: &exectest.Helm{},
wantDiffFlags: []string{"--enable-dns"},
},
{
name: "release with disable-dns override",
defaults: HelmSpec{EnableDNS: true},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
EnableDNS: &disable,
},
},
helm: &exectest.Helm{},
wantDiffFlags: nil,
},
{
name: "release with enable-dns from default",
defaults: HelmSpec{EnableDNS: true},
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &exectest.Helm{},
wantDiffFlags: []string{"--enable-dns"},
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
HelmDefaults: tt.defaults,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
for j := range tt.releases {
flags, _, errs := state.flagsForDiff(tt.helm, &tt.releases[j], false, 1, nil)
if errs != nil {
t.Errorf("unexpected error: %v", errs)
}
if !reflect.DeepEqual(flags, tt.wantDiffFlags) {
t.Errorf("HelmState.flagsForDiff() for [%s][%s] = %v, want %v", tt.name, tt.releases[j].Name, flags, tt.wantDiffFlags)
}
}
})
}
}
func TestHelmState_SyncReleasesCleanup(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *exectest.Helm
expectedNumRemovedFiles int
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &exectest.Helm{},
expectedNumRemovedFiles: 0,
},
{
name: "inline values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []any{
map[any]any{
"someList": "a,b,c",
},
},
},
},
helm: &exectest.Helm{},
expectedNumRemovedFiles: 1,
},
{
name: "inline values and values file",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []any{
map[any]any{
"someList": "a,b,c",
},
"someFile",
},
},
},
helm: &exectest.Helm{},
expectedNumRemovedFiles: 2,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
testfs := testhelper.NewTestFs(map[string]string{
"/path/to/someFile": `foo: FOO`,
})
testfs.DeleteFile = func(f string) error {
numRemovedFiles += 1
return nil
}
state = injectFs(state, testfs)
if errs := state.SyncReleases(&AffectedReleases{}, tt.helm, []string{}, 1); len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if errs := state.Clean(); len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if numRemovedFiles != tt.expectedNumRemovedFiles {
t.Errorf("unexpected number of removed files: expected %d, got %d", tt.expectedNumRemovedFiles, numRemovedFiles)
}
})
}
}
func TestHelmState_DiffReleasesCleanup(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *exectest.Helm
expectedNumRemovedFiles int
}{
{
name: "normal release",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
},
},
helm: &exectest.Helm{},
expectedNumRemovedFiles: 0,
},
{
name: "inline values",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []any{
map[any]any{
"someList": "a,b,c",
},
},
},
},
helm: &exectest.Helm{},
expectedNumRemovedFiles: 1,
},
{
name: "inline values and values file",
releases: []ReleaseSpec{
{
Name: "releaseName",
Chart: "foo",
Values: []any{
map[any]any{
"someList": "a,b,c",
},
"someFile",
},
},
},
helm: &exectest.Helm{},
expectedNumRemovedFiles: 2,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
numRemovedFiles := 0
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
}
testfs := testhelper.NewTestFs(map[string]string{
"/path/to/someFile": `foo: bar
`,
})
testfs.DeleteFile = func(f string) error {
numRemovedFiles += 1
return nil
}
state = injectFs(state, testfs)
if _, errs := state.DiffReleases(tt.helm, []string{}, 1, false, false, false, []string{}, false, false, false, false, false); len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if errs := state.Clean(); len(errs) > 0 {
t.Errorf("unexpected errors: %v", errs)
}
if numRemovedFiles != tt.expectedNumRemovedFiles {
t.Errorf("unexpected number of removed files: expected %d, got %d", tt.expectedNumRemovedFiles, numRemovedFiles)
}
})
}
}
func TestHelmState_UpdateDeps(t *testing.T) {
helm := &exectest.Helm{
UpdateDepsCallbacks: map[string]func(string) error{},
}
var generatedDir string
tempDir := func(dir, prefix string) (string, error) {
var err error
generatedDir, err = os.MkdirTemp(dir, prefix)
if err != nil {
return "", err
}
// nolint: unparam
helm.UpdateDepsCallbacks[generatedDir] = func(chart string) error {
content := []byte(`dependencies:
- name: envoy
repository: https://kubernetes-charts.storage.googleapis.com
version: 1.5.0
- name: envoy
repository: https://kubernetes-charts.storage.googleapis.com
version: 1.4.0
digest: sha256:8194b597c85bb3d1fee8476d4a486e952681d5c65f185ad5809f2118bc4079b5
generated: 2019-05-16T15:42:45.50486+09:00
`)
filename := filepath.Join(generatedDir, "Chart.lock")
logger.Debugf("test: writing %s: %s", filename, content)
return os.WriteFile(filename, content, 0644)
}
return generatedDir, nil
}
logger := helmexec.NewLogger(io.Discard, "debug")
basePath := filepath.ToSlash(t.TempDir())
state := &HelmState{
basePath: basePath,
FilePath: filepath.Join(basePath, "helmfile.yaml"),
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "/example",
},
{
Chart: "./example",
},
{
Chart: "published/deeper",
},
{
Chart: "stable/envoy",
Version: "1.5.0",
},
{
Chart: "stable/envoy",
Version: "1.4.0",
},
},
Repositories: []RepositorySpec{
{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
},
},
},
tempDir: tempDir,
logger: logger,
}
fs := testhelper.NewTestFs(map[string]string{
"/example/Chart.yaml": `foo: FOO`,
filepath.Join(basePath, "example/Chart.yaml"): `foo: FOO`,
})
fs.Cwd = basePath
state = injectFs(state, fs)
errs := state.UpdateDeps(helm, false)
want := []string{"/example", "./example", generatedDir}
if !reflect.DeepEqual(helm.Charts, want) {
t.Errorf("HelmState.UpdateDeps() = %v, want %v", helm.Charts, want)
}
if len(errs) != 0 {
t.Errorf("HelmState.UpdateDeps() - unexpected %d errors: %v", len(errs), errs)
}
resolved, err := state.ResolveDeps()
if err != nil {
t.Errorf("HelmState.ResolveDeps() - unexpected error: %v", err)
}
if resolved.Releases[3].Version != "1.5.0" {
t.Errorf("HelmState.ResolveDeps() - unexpected version number: expected=1.5.0, got=%s", resolved.Releases[5].Version)
}
if resolved.Releases[4].Version != "1.4.0" {
t.Errorf("HelmState.ResolveDeps() - unexpected version number: expected=1.4.0, got=%s", resolved.Releases[6].Version)
}
}
func TestHelmState_UpdateDeps_OCIUnderscores(t *testing.T) {
helm := &exectest.Helm{
UpdateDepsCallbacks: map[string]func(string) error{},
}
var generatedDir string
var generatedChartYaml string
tempDir := func(dir, prefix string) (string, error) {
var err error
generatedDir, err = os.MkdirTemp(dir, prefix)
if err != nil {
return "", err
}
helm.UpdateDepsCallbacks[generatedDir] = func(string) error {
// Read the Chart.yaml that helmfile generated to verify its content
chartYamlBytes, readErr := os.ReadFile(filepath.Join(generatedDir, "Chart.yaml"))
if readErr == nil {
generatedChartYaml = string(chartYamlBytes)
}
// Simulate helm writing Chart.lock with the basename as dependency name
content := []byte(`dependencies:
- name: example
repository: oci://harbor.custom.com/path_with_underscores
version: 1.0.0
digest: sha256:abc123def456
generated: 2023-08-01T23:04:02Z
`)
return os.WriteFile(filepath.Join(generatedDir, "Chart.lock"), content, 0644)
}
return generatedDir, nil
}
logger := helmexec.NewLogger(io.Discard, "debug")
basePath := filepath.ToSlash(t.TempDir())
state := &HelmState{
basePath: basePath,
FilePath: filepath.Join(basePath, "helmfile.yaml"),
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Name: "example",
Chart: "myrepo/path_with_underscores/example",
Version: "1.0.0",
Namespace: "myns",
},
},
Repositories: []RepositorySpec{
{
Name: "myrepo",
URL: "harbor.custom.com",
OCI: true,
},
},
},
tempDir: tempDir,
logger: logger,
}
fs := testhelper.NewTestFs(map[string]string{})
fs.Cwd = basePath
state = injectFs(state, fs)
errs := state.UpdateDeps(helm, false)
if len(errs) != 0 {
t.Fatalf("HelmState.UpdateDeps() - unexpected %d errors: %v", len(errs), errs)
}
// Verify the generated Chart.yaml has basename as name and path prefix in repository
assert.Contains(t, generatedChartYaml, "name: example")
assert.Contains(t, generatedChartYaml, "repository: oci://harbor.custom.com/path_with_underscores")
// It should NOT contain the full path as the name
assert.NotContains(t, generatedChartYaml, "name: path_with_underscores/example")
// Verify the lock file was written with the correct resolved version
resolved, err := state.ResolveDeps()
assert.NoError(t, err)
assert.Equal(t, "1.0.0", resolved.Releases[0].Version)
}
func TestHelmState_ResolveDeps_OCIUnderscores_BackwardCompat(t *testing.T) {
logger := helmexec.NewLogger(io.Discard, "debug")
basePath := filepath.ToSlash(t.TempDir())
// Old-format lock file using the full path as the dependency name
oldLockContent := `version: 0.155.0
dependencies:
- name: path_with_underscores/example
repository: oci://harbor.custom.com
version: 1.0.0
digest: sha256:abc123
generated: 2023-08-01T23:04:02Z
`
lockPath := filepath.Join(basePath, "helmfile.lock")
err := os.WriteFile(lockPath, []byte(oldLockContent), 0644)
require.NoError(t, err)
state := &HelmState{
basePath: basePath,
FilePath: filepath.Join(basePath, "helmfile.yaml"),
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Name: "example",
Chart: "myrepo/path_with_underscores/example",
Version: "1.0.0",
},
},
Repositories: []RepositorySpec{
{
Name: "myrepo",
URL: "harbor.custom.com",
OCI: true,
},
},
},
logger: logger,
}
fs := testhelper.NewTestFs(map[string]string{})
fs.Cwd = basePath
state = injectFs(state, fs)
resolved, err := state.ResolveDeps()
require.NoError(t, err)
assert.Equal(t, "1.0.0", resolved.Releases[0].Version,
"old lock file with full-path name should still resolve via backward-compat fallback")
}
func TestHelmState_ResolveDeps_NoLockFile(t *testing.T) {
logger := helmexec.NewLogger(io.Discard, "debug")
state := &HelmState{
basePath: "/src",
FilePath: "/src/helmfile.yaml",
ReleaseSetSpec: ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "./..",
},
{
Chart: "../examples",
},
{
Chart: "../../helmfile",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: "stable/envoy",
},
},
Repositories: []RepositorySpec{
{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
},
},
},
logger: logger,
fs: &filesystem.FileSystem{
ReadFile: func(f string) ([]byte, error) {
if f != filepath.Join("/src", "helmfile.lock") {
return nil, fmt.Errorf("stub: unexpected file: %s", f)
}
return nil, os.ErrNotExist
},
},
}
_, err := state.ResolveDeps()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestHelmState_ResolveDeps_NoLockFile_WithCustomLockFile(t *testing.T) {
logger := helmexec.NewLogger(io.Discard, "debug")
state := &HelmState{
basePath: "/src",
FilePath: "/src/helmfile.yaml",
ReleaseSetSpec: ReleaseSetSpec{
LockFile: "custom-lock-file",
Releases: []ReleaseSpec{
{
Chart: "./..",
},
{
Chart: "../examples",
},
{
Chart: "../../helmfile",
},
{
Chart: "published",
},
{
Chart: "published/deeper",
},
{
Chart: "stable/envoy",
},
},
Repositories: []RepositorySpec{
{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
},
},
},
logger: logger,
fs: &filesystem.FileSystem{
ReadFile: func(f string) ([]byte, error) {
if f != filepath.Join("/src", "custom-lock-file") {
return nil, fmt.Errorf("stub: unexpected file: %s", f)
}
return nil, os.ErrNotExist
},
},
}
_, err := state.ResolveDeps()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestHelmState_ReleaseStatuses(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *exectest.Helm
want []exectest.Release
wantErr bool
}{
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "releaseA",
},
},
helm: &exectest.Helm{},
want: []exectest.Release{
{Name: "releaseA", Flags: []string{}},
},
},
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "error",
},
},
helm: &exectest.Helm{},
wantErr: true,
},
{
name: "complain missing values file for desired release",
releases: []ReleaseSpec{
{
Name: "error",
Values: []any{
"foo.yaml",
},
},
},
helm: &exectest.Helm{},
wantErr: true,
},
{
name: "should not complain missing values file for undesired release",
releases: []ReleaseSpec{
{
Name: "error",
Values: []any{
"foo.yaml",
},
Installed: boolValue(false),
},
},
helm: &exectest.Helm{},
wantErr: false,
},
}
for i := range tests {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
fs: &filesystem.FileSystem{
FileExists: func(f string) (bool, error) {
if f != "foo.yaml" {
return false, fmt.Errorf("unexpected file: %s", f)
}
return true, nil
},
ReadFile: func(f string) ([]byte, error) {
if f != "foo.yaml" {
return nil, fmt.Errorf("unexpected file: %s", f)
}
return []byte{}, nil
},
},
}
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, f)
}
}
func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
tests := []struct {
name string
cleanup bool
releases []ReleaseSpec
helm *exectest.Helm
want []exectest.Release
wantErr bool
}{
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "releaseA",
},
},
helm: &exectest.Helm{},
want: []exectest.Release{{Name: "releaseA", Flags: []string{"--timeout", "1s"}}},
},
{
name: "do cleanup",
cleanup: true,
releases: []ReleaseSpec{
{
Name: "releaseB",
},
},
helm: &exectest.Helm{},
want: []exectest.Release{{Name: "releaseB", Flags: []string{"--timeout", "1s"}}},
},
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "error",
},
},
helm: &exectest.Helm{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
}
errs := state.TestReleases(tt.helm, tt.cleanup, 1, 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)
}
})
}
}
func TestConditionEnabled(t *testing.T) {
tests := []struct {
name string
condition string
values map[string]any
want bool
wantErr bool
}{
{
name: "enabled",
condition: "foo.enabled",
values: map[string]any{
"foo": map[string]any{
"enabled": true,
},
},
want: true,
},
{
name: "disabled",
condition: "foo.enabled",
values: map[string]any{
"foo": map[string]any{
"enabled": false,
},
},
want: false,
},
{
name: "typo in condition",
condition: "fooo.enabled",
values: map[string]any{
"foo": map[string]any{
"enabled": true,
},
},
want: false,
wantErr: true,
},
{
name: "missing enabled",
condition: "foo.enabled",
values: map[string]any{
"foo": map[string]any{
"something else": false,
},
},
want: false,
},
{
name: "foo nil",
condition: "foo.enabled",
values: map[string]any{
"foo": nil,
},
want: false,
wantErr: true,
},
{
name: "foo missing",
condition: "foo.enabled",
values: map[string]any{},
want: false,
wantErr: true,
},
{
name: "wrong suffix",
condition: "services.foo_enabled",
want: false,
wantErr: true,
},
{
name: "too short condition",
condition: "rnd42",
want: false,
wantErr: true,
},
{
name: "nested values",
condition: "rnd42.really.enabled",
values: map[string]any{
"rnd42": map[string]any{
"really": map[string]any{
"enabled": true,
},
},
},
want: true,
wantErr: false,
},
{
name: "nested values enabled missing",
condition: "rnd42.really.ok",
want: false,
wantErr: true,
},
{
name: "nested values unknown key",
condition: "rnd42.unknown.enabled",
values: map[string]any{
"rnd42": map[string]any{
"really": map[string]any{
"enabled": true,
},
},
},
want: false,
wantErr: true,
},
{
name: "nested values invalid type",
condition: "rnd42.invalid.enabled",
values: map[string]any{
"rnd42": map[string]any{
"invalid": "hello",
"really": map[string]any{
"enabled": true,
},
},
},
want: false,
wantErr: true,
},
{
name: "empty",
condition: "",
want: true,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
res, err := ConditionEnabled(ReleaseSpec{Condition: tt.condition}, tt.values)
if tt.wantErr {
if err == nil {
t.Errorf("ConditionEnabled() for %s expected err response", tt.name)
}
return
}
if err != nil {
t.Errorf("ConditionEnabled() for %s unexpected err %v", tt.name, err)
}
if res != tt.want {
t.Errorf("ConditionEnabled() for %s = %v, want %v", tt.name, res, tt.want)
}
})
}
}
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 i := range tests {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
},
logger: logger,
RenderedValues: map[string]any{},
}
state.Selectors = []string{tt.labels}
errs := state.FilterReleases(false)
if (errs != nil) != tt.wantErr {
t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
}
t.Run(tt.name, f)
}
}
func TestHelmState_Delete(t *testing.T) {
tests := []struct {
name string
deleted []exectest.Release
wantErr bool
desired *bool
installed bool
purge bool
flags string
namespace string
kubeContext string
defKubeContext string
deleteWait bool
deleteTimeout int
}{
{
name: "delete wait enabled",
deleteWait: true,
wantErr: false,
desired: boolValue(true),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--wait"}}},
},
{
name: "delete wait with deleteTimeout",
deleteWait: true,
deleteTimeout: 800,
wantErr: false,
desired: boolValue(true),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--wait", "--timeout", "800s"}}},
},
{
name: "desired and installed (purge=false)",
wantErr: false,
desired: boolValue(true),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired(default) and installed (purge=false)",
wantErr: false,
desired: nil,
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired(default) and installed (purge=false) but error",
wantErr: true,
desired: nil,
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired and installed (purge=true)",
wantErr: false,
desired: boolValue(true),
installed: true,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired but not installed (purge=false)",
wantErr: false,
desired: boolValue(true),
installed: false,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired but not installed (purge=true)",
wantErr: false,
desired: boolValue(true),
installed: false,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "installed but filtered (purge=false)",
wantErr: false,
desired: boolValue(false),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "installed but filtered (purge=true)",
wantErr: false,
desired: boolValue(false),
installed: true,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "not installed, and filtered (purge=false)",
wantErr: false,
desired: boolValue(false),
installed: false,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "not installed, and filtered (purge=true)",
wantErr: false,
desired: boolValue(false),
installed: false,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "with kubecontext",
wantErr: false,
desired: nil,
installed: true,
purge: true,
kubeContext: "ctx",
flags: "--kube-contextctx",
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--kube-context", "ctx"}}},
},
{
name: "with default kubecontext",
wantErr: false,
desired: nil,
installed: true,
purge: true,
defKubeContext: "defctx",
flags: "--kube-contextdefctx",
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--kube-context", "defctx"}}},
},
{
name: "with non-default and default kubecontexts",
wantErr: false,
desired: nil,
installed: true,
purge: true,
kubeContext: "ctx",
defKubeContext: "defctx",
flags: "--kube-contextctx",
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--kube-context", "ctx"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name := "releaseA"
if tt.wantErr {
name = "releaseA-error"
}
release := ReleaseSpec{
Name: name,
Installed: tt.desired,
Namespace: tt.namespace,
KubeContext: tt.kubeContext,
}
releases := []ReleaseSpec{
release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: tt.defKubeContext,
DeleteWait: tt.deleteWait,
DeleteTimeout: tt.deleteTimeout,
},
Releases: releases,
},
logger: logger,
RenderedValues: map[string]any{},
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
Deleted: []exectest.Release{},
}
if tt.installed {
helm.Lists[exectest.ListKey{Filter: "^" + name + "$", Flags: tt.flags}] = name
}
affectedReleases := AffectedReleases{}
errs := state.DeleteReleases(&affectedReleases, helm, 1, tt.purge, "")
if errs != nil {
if !tt.wantErr || len(affectedReleases.DeleteFailed) != 1 || affectedReleases.DeleteFailed[0].Name != release.Name {
t.Errorf("DeleteReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
} else if !(reflect.DeepEqual(tt.deleted, helm.Deleted) && (len(affectedReleases.Deleted) == len(tt.deleted))) {
t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.Deleted)
}
})
}
}
func TestDiffpareSyncReleases(t *testing.T) {
tests := []struct {
name string
flags []string
diffOptions *DiffOpts
helmDefaults *HelmSpec
}{
{
name: "reuse-values",
flags: []string{"--reuse-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "reset-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{},
helmDefaults: &HelmSpec{},
},
{
name: "default-reuse-values",
flags: []string{"--reuse-values"},
diffOptions: &DiffOpts{},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "force-reset-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "both-reset-reuse-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "both-reset-reuse-default-reuse-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
}
for _, tt := range tests {
release := ReleaseSpec{
Name: tt.name,
}
releases := []ReleaseSpec{
release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
HelmDefaults: *tt.helmDefaults,
},
logger: logger,
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
results, es := state.prepareDiffReleases(helm, []string{}, 1, false, false, false, []string{}, false, false, false, tt.diffOptions)
require.Len(t, es, 0)
require.Len(t, results, 1)
r := results[0]
require.Equal(t, tt.flags, r.flags)
}
}
func TestPrepareSyncReleases(t *testing.T) {
tests := []struct {
name string
flags []string
syncOptions *SyncOpts
helmDefaults *HelmSpec
}{
{
name: "reuse-values",
flags: []string{"--reuse-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "reset-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{},
helmDefaults: &HelmSpec{},
},
{
name: "reuse-default-values",
flags: []string{"--reuse-values"},
syncOptions: &SyncOpts{},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "force-reset-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "both-reset-reuse-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "both-reset-reuse-default-reuse-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
}
for _, tt := range tests {
release := ReleaseSpec{
Name: tt.name,
}
releases := []ReleaseSpec{
release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
HelmDefaults: *tt.helmDefaults,
},
logger: logger,
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
results, es := state.prepareSyncReleases(helm, []string{}, 1, tt.syncOptions)
require.Len(t, es, 0)
require.Len(t, results, 1)
r := results[0]
require.Equal(t, tt.flags, r.flags)
}
}
func TestReverse(t *testing.T) {
num := 8
st := &HelmState{}
for i := 0; i < num; i++ {
name := fmt.Sprintf("%d", i)
st.Helmfiles = append(st.Helmfiles, SubHelmfileSpec{
Path: name,
})
st.Releases = append(st.Releases, ReleaseSpec{
Name: name,
})
}
st.Reverse()
for i := 0; i < num; i++ {
j := num - 1 - i
want := fmt.Sprintf("%d", j)
if got := st.Helmfiles[i].Path; got != want {
t.Errorf("sub-helmfile at %d has incorrect path: want %q, got %q", i, want, got)
}
if got := st.Releases[i].Name; got != want {
t.Errorf("release at %d has incorrect name: want %q, got %q", i, want, got)
}
}
}
func Test_gatherUsernamePassword(t *testing.T) {
type args struct {
repoName string
username string
password string
}
tests := []struct {
name string
args args
envUsernameKey string
envUsernameValue string
envPasswordKey string
envPasswordValue string
wantUsername string
wantPassword string
}{
{
name: "pass username/password from args",
args: args{
repoName: "myRegistry",
username: "username1",
password: "password1",
},
wantUsername: "username1",
wantPassword: "password1",
},
{
name: "repoName does not contain hyphen, read username/password from environment variables",
args: args{
repoName: "myRegistry",
},
envUsernameKey: "MYREGISTRY_USERNAME",
envUsernameValue: "username2",
envPasswordKey: "MYREGISTRY_PASSWORD",
envPasswordValue: "password2",
wantUsername: "username2",
wantPassword: "password2",
},
{
name: "repoName contain hyphen, read username/password from environment variables",
args: args{
repoName: "my-registry",
},
envUsernameKey: "MY_REGISTRY_USERNAME",
envUsernameValue: "username3",
envPasswordKey: "MY_REGISTRY_PASSWORD",
envPasswordValue: "password3",
wantUsername: "username3",
wantPassword: "password3",
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
if tt.envUsernameKey != "" && tt.envUsernameValue != "" {
t.Setenv(tt.envUsernameKey, tt.envUsernameValue)
}
if tt.envPasswordKey != "" && tt.envPasswordValue != "" {
t.Setenv(tt.envPasswordKey, tt.envPasswordValue)
}
gotUsername, gotPassword := gatherUsernamePassword(tt.args.repoName, tt.args.username, tt.args.password)
if gotUsername != tt.wantUsername || gotPassword != tt.wantPassword {
t.Errorf("gatherUsernamePassword() = got username/password %v/%v, want username/password %v/%v", gotUsername, gotPassword, tt.wantUsername, tt.wantPassword)
}
})
}
}
func Test_extractRegistryHost(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "ECR with chart path",
url: "123456789012.dkr.ecr.us-east-1.amazonaws.com/helm-charts",
want: "123456789012.dkr.ecr.us-east-1.amazonaws.com",
},
{
name: "GHCR with nested path",
url: "ghcr.io/deliveryhero/helm-charts",
want: "ghcr.io",
},
{
name: "registry with port and path",
url: "registry.example.com:5000/helm-charts",
want: "registry.example.com:5000",
},
{
name: "registry without path",
url: "registry.example.com",
want: "registry.example.com",
},
{
name: "with oci:// prefix and path",
url: "oci://ghcr.io/charts/nginx",
want: "ghcr.io",
},
{
name: "with https:// prefix and path",
url: "https://registry.example.com/helm-charts",
want: "registry.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extractRegistryHost(tt.url); got != tt.want {
t.Errorf("extractRegistryHost() = %v, want %v", got, tt.want)
}
})
}
}
func TestHelmState_SyncRepos_OCI(t *testing.T) {
tests := []struct {
name string
repos []RepositorySpec
wantRegistryLoginHost string
}{
{
name: "OCI registry with chart path should extract host only",
repos: []RepositorySpec{
{
Name: "ecr",
URL: "123456789012.dkr.ecr.us-east-1.amazonaws.com/helm-charts",
OCI: true,
Username: "AWS",
Password: "token",
},
},
wantRegistryLoginHost: "123456789012.dkr.ecr.us-east-1.amazonaws.com",
},
{
name: "OCI registry without path should pass URL as-is",
repos: []RepositorySpec{
{
Name: "ghcr",
URL: "ghcr.io",
OCI: true,
Username: "user",
Password: "pass",
},
},
wantRegistryLoginHost: "ghcr.io",
},
{
name: "OCI registry with nested path",
repos: []RepositorySpec{
{
Name: "docker",
URL: "registry-1.docker.io/bitnamicharts",
OCI: true,
Username: "user",
Password: "pass",
},
},
wantRegistryLoginHost: "registry-1.docker.io",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
helm := &exectest.Helm{}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: tt.repos,
},
}
_, err := state.SyncRepos(helm, map[string]bool{})
if err != nil {
t.Errorf("SyncRepos() error = %v", err)
return
}
if helm.RegistryLoginHost != tt.wantRegistryLoginHost {
t.Errorf("RegistryLogin was called with host = %q, want %q", helm.RegistryLoginHost, tt.wantRegistryLoginHost)
}
})
}
}
func TestGenerateOutputFilePath(t *testing.T) {
tests := []struct {
envName string
filePath string
releaseName string
outputFileTemplate string
wantErr bool
expected string
}{
{
envName: "dev",
releaseName: "release1",
filePath: "/path/to/helmfile.yaml",
outputFileTemplate: "helmfile-{{ .Environment.Name }}.yaml",
expected: "helmfile-dev.yaml",
},
{
envName: "error",
releaseName: "release2",
filePath: "helmfile.yaml",
outputFileTemplate: "helmfile-{{ .Environment.Name",
wantErr: true,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.envName, func(t *testing.T) {
st := &HelmState{
FilePath: tt.envName,
ReleaseSetSpec: ReleaseSetSpec{
Env: environment.Environment{
Name: tt.envName,
},
},
}
ra := &ReleaseSpec{
Name: tt.releaseName,
}
got, err := st.GenerateOutputFilePath(ra, tt.outputFileTemplate)
if tt.wantErr {
require.Errorf(t, err, "GenerateOutputFilePath() error = %v, want error", err)
} else {
require.NoError(t, err, "GenerateOutputFilePath() error = %v, want nil", err)
}
require.Equalf(t, got, tt.expected, "GenerateOutputFilePath() got = %v, want %v", got, tt.expected)
})
}
}
func TestFullFilePath(t *testing.T) {
fs := testhelper.NewTestFs(map[string]string{})
tests := []struct {
basePath string
filePath string
fs *filesystem.FileSystem
expected string
}{
{
basePath: ".",
filePath: "helmfile.yaml",
expected: "helmfile.yaml",
},
{
basePath: "./test-1/",
filePath: "helmfile.yaml",
expected: "test-1/helmfile.yaml",
},
{
basePath: "/test-2/",
filePath: "helmfile.yaml",
expected: "/test-2/helmfile.yaml",
},
{
basePath: "./test-3/",
filePath: "helmfile.yaml",
fs: fs.ToFileSystem(),
expected: "/path/to/test-3/helmfile.yaml",
},
{
basePath: "/test-4/",
filePath: "helmfile.yaml",
fs: fs.ToFileSystem(),
expected: "/path/to/test-4/helmfile.yaml",
},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
st := &HelmState{
basePath: tt.basePath,
FilePath: tt.filePath,
fs: tt.fs,
}
actual, err := st.FullFilePath()
require.Equalf(t, actual, tt.expected, "FullFilePath() got = %v, want %v", actual, tt.expected)
require.Equalf(t, err, nil, "error %v", err)
})
}
}
func TestGetOCIQualifiedChartName(t *testing.T) {
devel := true
tests := []struct {
state HelmState
expected []struct {
qualifiedChartName string
chartName string
chartVersion string
}
helmVersion string
wantErr bool
}{
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "0.1.2",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name:0.1.2", "chart-name", "0.1.2"},
},
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "latest",
},
},
},
},
helmVersion: "3.13.3",
wantErr: true,
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "latest",
},
},
},
},
helmVersion: "3.7.0",
wantErr: true, // Now rejects "latest" for all Helm versions
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/chart-path",
OCI: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chart-name",
Version: "0.1.2",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name:0.1.2", "chart-name", "0.1.2"},
},
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Devel: &devel,
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name", "chart-name", ""},
},
},
// Digest in version field
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "2.0.0@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Digest-only in version field
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
// Version tag in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name:2.0.0",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name", "chart-name", "2.0.0"},
},
},
// Digest in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
// Version + digest in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name:2.0.0@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Port with digest in URL
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry:5000/chart-path/chart-name@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry:5000/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
// Digest in URL + version field
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name@sha256:abc123",
Version: "2.0.0",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Repo-aliased OCI chart with digest in version
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/chart-path",
OCI: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chart-name",
Version: "2.0.0@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", "2.0.0"},
},
},
// Repo-aliased OCI chart with digest-only version
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/chart-path",
OCI: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chart-name",
Version: "@sha256:abc123",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name@sha256:abc123", "chart-name", ""},
},
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%+v", tt.expected), func(t *testing.T) {
for i, r := range tt.state.Releases {
qualifiedChartName, chartName, chartVersion, err := tt.state.getOCIQualifiedChartName(&r)
if tt.wantErr {
require.Error(t, err, "getOCIQualifiedChartName() error = nil, want error")
return
}
require.NoError(t, err, "getOCIQualifiedChartName() error = %v, want nil", err)
if len(tt.expected) > 0 {
require.Equalf(t, qualifiedChartName, tt.expected[i].qualifiedChartName, "qualifiedChartName got = %v, want %v", qualifiedChartName, tt.expected[i].qualifiedChartName)
require.Equalf(t, chartName, tt.expected[i].chartName, "chartName got = %v, want %v", chartName, tt.expected[i].chartName)
require.Equalf(t, chartVersion, tt.expected[i].chartVersion, "chartVersion got = %v, want %v", chartVersion, tt.expected[i].chartVersion)
}
}
})
}
}
func TestGenerateChartPath(t *testing.T) {
tests := []struct {
testName string
chartName string
release *ReleaseSpec
outputDir string
outputDirTemplate string
wantErr bool
expected string
}{
{
testName: "PathGeneratedWithGivenOutputDirAndDefaultReleaseVersion",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseVersion",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", Version: "0.0.0"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-name/chart-name/0.0.0",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseNamespace",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", Namespace: "release-namespace"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-namespace/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseKubeContext",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", KubeContext: "kube-context"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/kube-context/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenReleaseNamespaceAndGivenReleaseKubeContext",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name", Namespace: "release-namespace", KubeContext: "kube-context"},
outputDir: "/output-dir",
wantErr: false,
expected: "/output-dir/release-namespace/kube-context/release-name/chart-name/latest",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenOutputDirTemplateWithFieldNameOutputDir",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}",
wantErr: false,
expected: "/output-dir",
},
{
testName: "PathGeneratedWithGivenOutputDirAndGivenOutputDirTemplateWithFieldNamesOutputDirAndReleaseName",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}/{{ .Release.Name }}",
wantErr: false,
expected: "/output-dir/release-name",
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithFieldNamesOutputDir",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "{{ .OutputDir }}",
wantErr: false,
expected: "",
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithFieldNameReleaseName",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "{{ .Release.Name }}",
wantErr: false,
expected: "release-name",
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithStringAndFieldNameReleaseName",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "./charts/{{ .Release.Name }}",
wantErr: false,
expected: "./charts/release-name",
},
{
testName: "ErrorReturnedWithGivenInvalidOutputDirTemplate",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDirTemplate: "{{ .OutputDir }",
wantErr: true,
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithEnvironmentName",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}/{{ .Environment.Name }}",
wantErr: false,
expected: "/output-dir/test-env",
},
{
testName: "PathGeneratedWithGivenOutputDirTemplateWithEnvironmentValues",
chartName: "chart-name",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}/{{ .Environment.Values.cluster.name }}",
wantErr: false,
expected: "/output-dir/my-test-cluster",
},
}
st := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Env: environment.Environment{
Name: "test-env",
Values: map[string]any{
"cluster": map[string]any{
"name": "my-test-cluster",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
got, err := st.generateChartPath(tt.chartName, tt.outputDir, tt.release, tt.outputDirTemplate)
if tt.wantErr {
require.Errorf(t, err, "GenerateChartPath() error \"%v\", want error", err)
} else {
require.NoError(t, err, "GenerateChartPath() error \"%v\", want no error", err)
}
require.Equalf(t, tt.expected, got, "GenerateChartPath() got \"%v\", want \"%v\"", got, tt.expected)
})
}
}
func TestGenerateOutputDir(t *testing.T) {
tests := []struct {
testName string
release *ReleaseSpec
outputDir string
outputDirTemplate string
wantErr bool
expected string
}{
{
testName: "PathGeneratedWithEnvironmentName",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}/{{ .Environment.Name }}/{{ .Release.Name }}",
wantErr: false,
expected: "/output-dir/test-env/release-name",
},
{
testName: "PathGeneratedWithEnvironmentValues",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}/{{ .Environment.Values.cluster.name }}/{{ .Release.Name }}",
wantErr: false,
expected: "/output-dir/my-test-cluster/release-name",
},
{
testName: "PathGeneratedWithEnvironmentKubeContext",
release: &ReleaseSpec{Name: "release-name"},
outputDir: "/output-dir",
outputDirTemplate: "{{ .OutputDir }}/{{ .Environment.KubeContext }}/{{ .Release.Name }}",
wantErr: false,
expected: "/output-dir/test-kubecontext/release-name",
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
st := &HelmState{
FilePath: "test.yaml",
ReleaseSetSpec: ReleaseSetSpec{
Env: environment.Environment{
Name: "test-env",
KubeContext: "test-kubecontext",
Values: map[string]any{
"cluster": map[string]any{
"name": "my-test-cluster",
},
},
},
},
}
got, err := st.GenerateOutputDir(tt.outputDir, tt.release, tt.outputDirTemplate)
if tt.wantErr {
require.Errorf(t, err, "GenerateOutputDir() error \"%v\", want error", err)
} else {
require.NoError(t, err, "GenerateOutputDir() error \"%v\", want no error", err)
}
require.Equalf(t, tt.expected, got, "GenerateOutputDir() got \"%v\", want \"%v\"", got, tt.expected)
})
}
}
func TestCommonDiffFlags(t *testing.T) {
tests := []struct {
name string
// stripTrailingCR is a flag to strip trailing carriage returns from the output
stripTrailingCR bool
expected []string
}{
{
name: "stripTrailingCR enabled",
stripTrailingCR: true,
expected: []string{
"--strip-trailing-cr",
},
},
{
name: "stripTrailingCR disenabled",
expected: []string{},
},
}
for _, tt := range tests {
st := &HelmState{}
result := st.commonDiffFlags(false, tt.stripTrailingCR, false, []string{}, false, false, false, &DiffOpts{})
require.Equal(t, tt.expected, result)
}
}
func TestAppendChartDownloadFlags(t *testing.T) {
tests := []struct {
name string
spec *ReleaseSetSpec
expected []string
}{
{
name: "SkipTLSVerify in repo",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "testrepo",
URL: "registry/repo-path",
SkipTLSVerify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "testrepo/chartname",
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "PlainHttp in repo, SkipTLSVerify everywhere",
spec: &ReleaseSetSpec{
HelmDefaults: HelmSpec{
InsecureSkipTLSVerify: true,
},
Repositories: []RepositorySpec{
{
Name: "testrepo",
URL: "registry/repo-path",
SkipTLSVerify: true,
PlainHttp: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "testrepo/chartname",
InsecureSkipTLSVerify: true,
},
},
},
expected: []string{
"--plain-http",
},
},
{
name: "SkipTLSVerify in defaults",
spec: &ReleaseSetSpec{
HelmDefaults: HelmSpec{
InsecureSkipTLSVerify: true,
},
Releases: []ReleaseSpec{
{
Chart: "chartname",
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "SkipTLSVerify in release",
spec: &ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "chartname",
InsecureSkipTLSVerify: true,
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "PlainHttp in defaults",
spec: &ReleaseSetSpec{
HelmDefaults: HelmSpec{
PlainHttp: true,
},
Releases: []ReleaseSpec{
{
Chart: "chartname",
},
},
},
expected: []string{
"--plain-http",
},
},
{
name: "PlainHttp in release",
spec: &ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "chartname",
PlainHttp: true,
},
},
},
expected: []string{
"--plain-http",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: *tt.spec,
}
result := st.appendChartDownloadFlags([]string{}, &st.Releases[0])
require.Equal(t, tt.expected, result)
})
}
}
func TestNeedsPlainHttp(t *testing.T) {
tests := []struct {
name string
release *ReleaseSpec
repo *RepositorySpec
defaults HelmSpec
expected bool
}{
{
name: "PlainHttp in Release",
release: &ReleaseSpec{
PlainHttp: true,
},
expected: true,
},
{
name: "PlainHttp in Repository",
repo: &RepositorySpec{
PlainHttp: true,
},
expected: true,
},
{
name: "PlainHttp in HelmDefaults",
defaults: HelmSpec{
PlainHttp: true,
},
expected: true,
},
{
name: "PlainHttp not set",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: tt.defaults,
},
}
require.Equal(t, tt.expected, st.needsPlainHttp(tt.release, tt.repo))
})
}
}
func TestNeedsInsecureSkipTLSVerify(t *testing.T) {
tests := []struct {
name string
release *ReleaseSpec
repo *RepositorySpec
defaults HelmSpec
expected bool
}{
{
name: "InsecureSkipTLSVerify in Release",
release: &ReleaseSpec{
InsecureSkipTLSVerify: true,
},
expected: true,
},
{
name: "SkipTLSVerify in Repository",
repo: &RepositorySpec{
SkipTLSVerify: true,
},
expected: true,
},
{
name: "InsecureSkipTLSVerify in HelmDefaults",
defaults: HelmSpec{
InsecureSkipTLSVerify: true,
},
expected: true,
},
{
name: "InsecureSkipTLSVerify not set",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: tt.defaults,
},
}
require.Equal(t, tt.expected, st.needsInsecureSkipTLSVerify(tt.release, tt.repo))
})
}
}
func TestHideChartURL(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"http://username:password@example.com/", "http://---:---@example.com/"},
{"http://example.com@", "http://---:---@"},
{"https://username:password@example.com/", "https://---:---@example.com/"},
{"https://username:@password@example.com/", "https://---:---@example.com/"},
{"https://username::password@example.com/", "https://---:---@example.com/"},
{"https://username:httpd@example.com/", "https://---:---@example.com/"},
{"https://username:httpsd@example.com/", "https://---:---@example.com/"},
{"https://example.com/", "https://example.com/"},
}
for _, test := range tests {
result, _ := hideChartCredentials(test.input)
if result != test.expected {
t.Errorf("For input '%s', expected '%s', but got '%s'", test.input, test.expected, result)
}
}
}
func Test_appendExtraDiffFlags(t *testing.T) {
tests := []struct {
name string
inputFlags []string
inputOpts *DiffOpts
inputDefaults []string
expected []string
}{
{
name: "Skipping default flags, because diffOpts is provided",
inputFlags: []string{"aaaaa"},
inputOpts: &DiffOpts{
DiffArgs: "-bbbb --cccc",
},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-bbbb", "--cccc"},
},
{
name: "Use default flags, because diffOpts is not provided",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-dddd", "--eeee"},
},
{
name: "Don't add non-flag arguments",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-d=ddd", "non-flag", "--eeee"},
expected: []string{"aaaaa", "-d=ddd", "--eeee"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := (&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
DiffArgs: test.inputDefaults,
},
},
}).appendExtraDiffFlags(test.inputFlags, test.inputOpts)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("For input %v %v, expected %v, but got %v", test.inputFlags, test.inputOpts, test.expected, result)
}
})
}
}
func Test_appendExtraSyncFlags(t *testing.T) {
tests := []struct {
name string
inputFlags []string
inputOpts *SyncOpts
inputDefaults []string
expected []string
}{
{
name: "Skipping default flags, because diffOpts is provided",
inputFlags: []string{"aaaaa"},
inputOpts: &SyncOpts{
SyncArgs: "-bbbb --cccc",
},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-bbbb", "--cccc"},
},
{
name: "Use default flags, because diffOpts is not provided",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-dddd", "--eeee"},
},
{
name: "Don't add non-flag arguments",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-d=ddd", "non-flag", "--eeee"},
expected: []string{"aaaaa", "-d=ddd", "--eeee"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := (&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
SyncArgs: test.inputDefaults,
},
},
}).appendExtraSyncFlags(test.inputFlags, test.inputOpts)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("For input %v %v, expected %v, but got %v", test.inputFlags, test.inputOpts, test.expected, result)
}
})
}
}
func TestHelmState_appendApiVersionsFlags(t *testing.T) {
tests := []struct {
name string
kubeVersion string
flags []string
expectedFlags []string
releaseKubeVersion string
releaseApiVersion []string
stateKubeVersion string
stateApiVersion []string
}{
{
name: "no kubeVersion is set",
expectedFlags: []string{},
},
{
name: "flags are kept",
flags: []string{"--flag1", "--flag2"},
expectedFlags: []string{"--flag1", "--flag2"},
},
{
name: "kubeVersion is set",
kubeVersion: "1.18.0",
expectedFlags: []string{"--kube-version", "1.18.0"},
},
{
name: "kubeVersion is set from release",
releaseKubeVersion: "1.19.0",
expectedFlags: []string{"--kube-version", "1.19.0"},
},
{
name: "kubeVersion from release hasn't priority",
kubeVersion: "1.18.0",
releaseKubeVersion: "1.19.0",
expectedFlags: []string{"--kube-version", "1.18.0"},
},
{
name: "kubeVersion is set from state",
stateKubeVersion: "1.18.0",
expectedFlags: []string{"--kube-version", "1.18.0"},
},
{
name: "kubeVersion from state hasn't priority",
stateKubeVersion: "1.18.0",
releaseKubeVersion: "1.19.0",
expectedFlags: []string{"--kube-version", "1.19.0"},
},
{
name: "kubeVersion priority",
stateKubeVersion: "1.18.0",
releaseKubeVersion: "1.19.0",
kubeVersion: "1.20.0",
expectedFlags: []string{"--kube-version", "1.20.0"},
},
{
name: "api-version are set from state",
stateApiVersion: []string{"v1,v2"},
expectedFlags: []string{"--api-versions", "v1,v2"},
},
{
name: "api-version are set from release",
releaseApiVersion: []string{"v1,v2"},
expectedFlags: []string{"--api-versions", "v1,v2"},
},
{
name: "api-version priority",
stateApiVersion: []string{"v1"},
releaseApiVersion: []string{"v2"},
expectedFlags: []string{"--api-versions", "v2"},
},
{
name: "api-version multiple values",
stateApiVersion: []string{"v1", "v2"},
expectedFlags: []string{"--api-versions", "v1", "--api-versions", "v2"},
},
{
name: "All kubeVersion and api-version are set",
kubeVersion: "1.18.0",
stateKubeVersion: "1.19.0",
releaseKubeVersion: "1.20.0",
stateApiVersion: []string{"v1"},
releaseApiVersion: []string{"v2"},
flags: []string{"--previous-flag-1", "--previous-flag-2"},
expectedFlags: []string{"--previous-flag-1", "--previous-flag-2", "--api-versions", "v2", "--kube-version", "1.18.0"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.expectedFlags == nil {
test.expectedFlags = []string{}
}
if test.flags == nil {
test.flags = []string{}
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
KubeVersion: test.stateKubeVersion,
ApiVersions: test.stateApiVersion,
},
}
r := &ReleaseSpec{
KubeVersion: test.releaseKubeVersion,
ApiVersions: test.releaseApiVersion,
}
result := state.appendApiVersionsFlags(test.flags, r, test.kubeVersion)
assert.Equal(t, test.expectedFlags, result)
})
}
}
func TestGetOCIChartPath(t *testing.T) {
tests := []struct {
name string
tempDir string
release *ReleaseSpec
chartName string
chartVersion string
outputDirTemplate string
expectedPath string
expectedErr bool
}{
{
name: "OCI chart with template",
tempDir: "charts",
release: &ReleaseSpec{
Name: "karpenter",
Chart: "karpenter/karpenter",
},
chartName: "karpenter",
chartVersion: "0.37.0",
outputDirTemplate: "{{ .OutputDir }}/",
expectedPath: "charts/",
expectedErr: false,
},
{
name: "OCI chart with template containing unknown values",
tempDir: "charts",
release: &ReleaseSpec{
Name: "karpenter",
Chart: "karpenter/karpenter",
},
chartName: "karpenter",
chartVersion: "0.37.0",
outputDirTemplate: "{{ .SomethingThatDoesNotExist }}/",
expectedPath: "",
expectedErr: true,
},
{
name: "OCI chart without template",
tempDir: "charts",
release: &ReleaseSpec{
Name: "karpenter",
Chart: "karpenter/karpenter",
},
chartName: "karpenter",
chartVersion: "0.37.0",
expectedPath: "charts/karpenter/karpenter/0.37.0",
expectedErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{}
path, err := st.getOCIChartPath(tt.tempDir, tt.release, tt.chartName, tt.chartVersion, tt.outputDirTemplate)
if tt.expectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedPath, path)
}
})
}
}
func TestHelmState_chartOCIFlags(t *testing.T) {
tests := []struct {
name string
spec *ReleaseSetSpec
expected []string
}{
{
name: "CaFile enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
CaFile: "cafile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--ca-file",
"cafile",
},
},
{
name: "CertFile enabled and KeyFile disabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
CertFile: "certfile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{},
},
{
name: "CertFile disabled and KeyFile enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
KeyFile: "keyfile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{},
},
{
name: "CertFile and KeyFile enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
CertFile: "certfile",
KeyFile: "keyfile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--cert-file",
"certfile",
"--key-file",
"keyfile",
},
},
{
name: "SkipTLSVerify enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
SkipTLSVerify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "Verify enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
Verify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--verify",
},
},
{
name: "Keyring enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
Keyring: "keyring.pgp",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--keyring",
"keyring.pgp",
},
},
{
name: "PlainHttp enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
PlainHttp: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--plain-http",
},
},
{
name: "PlainHttp and TLS options enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
PlainHttp: true,
CaFile: "cafile",
CertFile: "certfile",
KeyFile: "keyfile",
SkipTLSVerify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--plain-http",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: *tt.spec,
}
flags := st.chartOCIFlags(&tt.spec.Releases[0])
require.Equal(t, tt.expected, flags)
})
}
}
func TestIsOCIChart(t *testing.T) {
cases := []struct {
st *HelmState
chart string
expected bool
}{
{&HelmState{}, "oci://myrepo/mychart", true},
{&HelmState{}, "oci://myrepo/mychart:1.0.0", true},
{&HelmState{}, "myrepo/mychart", false},
{&HelmState{}, "myrepo/mychart:1.0.0", false},
{
&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "ocirepo",
URL: "ocirepo.com",
OCI: true,
},
},
},
},
"ocirepo/chart",
true,
},
{
&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "nonocirepo",
URL: "nonocirepo.com",
},
},
},
},
"nonocirepo/chart",
false,
},
}
for _, c := range cases {
actual := c.st.IsOCIChart(c.chart)
if actual != c.expected {
t.Errorf("IsOCIChart(%s) = %t; expected %t", c.chart, actual, c.expected)
}
}
}
func TestAppendVerifyFlags(t *testing.T) {
st := &HelmState{}
tests := []struct {
name string
repo []RepositorySpec
helmDefaults HelmSpec
release *ReleaseSpec
expected []string
}{
{
name: "Release with true verify flag",
release: &ReleaseSpec{Verify: boolValue(true)},
repo: nil,
helmDefaults: HelmSpec{},
expected: []string{"--verify"},
},
{
name: "Release with false verify flag",
release: &ReleaseSpec{Verify: boolValue(false)},
repo: nil,
helmDefaults: HelmSpec{},
expected: []string(nil),
},
{
name: "Repository with verify flag",
helmDefaults: HelmSpec{},
repo: []RepositorySpec{
{
Name: "myrepo",
Verify: true,
},
},
release: &ReleaseSpec{
Chart: "myrepo/mychart",
},
expected: []string{"--verify"},
},
{
name: "Helm defaults with verify flag",
repo: nil,
helmDefaults: HelmSpec{
Verify: true,
},
release: &ReleaseSpec{},
expected: []string{"--verify"},
},
{
name: "No verify flag",
repo: nil,
helmDefaults: HelmSpec{},
release: &ReleaseSpec{},
expected: []string(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st.ReleaseSetSpec.Repositories = tt.repo
st.ReleaseSetSpec.HelmDefaults = tt.helmDefaults
flags := st.appendVerifyFlags(nil, tt.release)
assert.Equal(t, tt.expected, flags)
})
}
}
// TestHelmState_setStringFlags tests the setStringFlags method
func TestHelmState_setStringFlags(t *testing.T) {
tests := []struct {
name string
setStringValues []SetValue
want []string
wantErr bool
}{
{
name: "single value",
setStringValues: []SetValue{
{
Name: "key",
Value: "value",
},
},
want: []string{"--set-string", "key=value"},
wantErr: false,
},
{
name: "multiple values",
setStringValues: []SetValue{
{
Name: "key",
Values: []any{"value1", "value2"},
},
},
want: []string{"--set-string", "key={value1,value2}"},
wantErr: false,
},
{
name: "rendered value error",
setStringValues: []SetValue{
{
Name: "key",
Value: "ref+echo://value",
},
},
want: []string{"--set-string", "key=value"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
valsRuntime: valsRuntime,
}
got, err := st.setStringFlags(tt.setStringValues)
if (err != nil) != tt.wantErr {
t.Errorf("setStringFlags() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("setStringFlags() got = %v, want %v", got, tt.want)
}
})
}
}
func TestPrepareDiffReleases_ValueControlReleaseOverride(t *testing.T) {
tests := []struct {
flags []string
diffOptions *DiffOpts
helmDefaults *HelmSpec
release *ReleaseSpec
}{
{
flags: []string{"--reuse-values"},
diffOptions: &DiffOpts{},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reuse-values-from-release",
ReuseValues: boolValue(true),
},
},
{
flags: []string{"--reuse-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reuse-values-from-cli",
ReuseValues: boolValue(false),
},
},
{
flags: []string{"--reuse-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
release: &ReleaseSpec{
Name: "reuse-values-all",
ReuseValues: boolValue(true),
},
},
{
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
release: &ReleaseSpec{
Name: "reset-values-from-helm-defaults",
ReuseValues: boolValue(false),
},
},
{
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reset-values-from-release",
ReuseValues: boolValue(false),
},
},
{
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{
ResetValues: true,
},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reset-values-cli-overrides-release",
ReuseValues: boolValue(true),
},
},
}
for _, tt := range tests {
releases := []ReleaseSpec{
*tt.release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
HelmDefaults: *tt.helmDefaults,
},
logger: logger,
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
results, es := state.prepareDiffReleases(helm, []string{}, 1, false, false, false, []string{}, false, false, false, tt.diffOptions)
require.Len(t, es, 0)
require.Len(t, results, 1)
r := results[0]
require.Equal(t, tt.flags, r.flags, "Wrong value control flag for release %s", r.release.Name)
}
}
func TestPrepareSyncReleases_ValueControlReleaseOverride(t *testing.T) {
tests := []struct {
flags []string
syncOptions *SyncOpts
helmDefaults *HelmSpec
release *ReleaseSpec
}{
{
flags: []string{"--reuse-values"},
syncOptions: &SyncOpts{},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reuse-values-from-release",
ReuseValues: boolValue(true),
},
},
{
flags: []string{"--reuse-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reuse-values-from-cli",
ReuseValues: boolValue(false),
},
},
{
flags: []string{"--reuse-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
release: &ReleaseSpec{
Name: "reuse-values-all",
ReuseValues: boolValue(true),
},
},
{
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
release: &ReleaseSpec{
Name: "reset-values-from-helm-defaults",
ReuseValues: boolValue(false),
},
},
{
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reset-values-from-release",
ReuseValues: boolValue(false),
},
},
{
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{
ResetValues: true,
},
helmDefaults: &HelmSpec{},
release: &ReleaseSpec{
Name: "reset-values-cli-overrides-release",
ReuseValues: boolValue(true),
},
},
}
for _, tt := range tests {
releases := []ReleaseSpec{
*tt.release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
HelmDefaults: *tt.helmDefaults,
},
logger: logger,
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
results, es := state.prepareSyncReleases(helm, []string{}, 1, tt.syncOptions)
require.Len(t, es, 0)
require.Len(t, results, 1)
r := results[0]
require.Equal(t, tt.flags, r.flags, "Wrong value control flag for release %s", r.release.Name)
}
}
func TestChartCacheKey(t *testing.T) {
st := &HelmState{}
// Test case 1: release with version
release1 := &ReleaseSpec{
Chart: "stable/nginx",
Version: "1.2.3",
}
key1 := st.getChartCacheKey(release1)
expected1 := ChartCacheKey{Chart: "stable/nginx", Version: "1.2.3"}
if key1 != expected1 {
t.Errorf("Expected %+v, got %+v", expected1, key1)
}
// Test case 2: release without version
release2 := &ReleaseSpec{
Chart: "stable/nginx",
}
key2 := st.getChartCacheKey(release2)
expected2 := ChartCacheKey{Chart: "stable/nginx", Version: ""}
if key2 != expected2 {
t.Errorf("Expected %+v, got %+v", expected2, key2)
}
}
func TestChartCache(t *testing.T) {
st := &HelmState{}
// Create a test key
key := ChartCacheKey{Chart: "stable/test", Version: "1.0.0"}
path := "/tmp/test-chart"
// Initially, chart should not be in cache
_, exists := st.checkChartCache(key)
if exists {
t.Error("Chart should not be in cache initially")
}
// Add to cache
st.addToChartCache(key, path)
// Now chart should be in cache
cachedPath, exists := st.checkChartCache(key)
if !exists {
t.Error("Chart should be in cache after adding")
}
if cachedPath != path {
t.Errorf("Expected path %s, got %s", path, cachedPath)
}
}
// TestHelmState_getKubeContext tests the kube-context resolution logic
// Issue #2309: This helper is used to pass kube-context to chartify
func TestHelmState_getKubeContext(t *testing.T) {
tests := []struct {
name string
defaults HelmSpec
environments map[string]EnvironmentSpec
envName string
release *ReleaseSpec
want string
}{
{
name: "returns empty when no context configured",
defaults: HelmSpec{},
release: &ReleaseSpec{Name: "test"},
want: "",
},
{
name: "returns release kubeContext when set",
defaults: HelmSpec{
KubeContext: "default-context",
},
release: &ReleaseSpec{
Name: "test",
KubeContext: "release-context",
},
want: "release-context",
},
{
name: "returns helmDefaults kubeContext when release not set",
defaults: HelmSpec{
KubeContext: "default-context",
},
release: &ReleaseSpec{
Name: "test",
},
want: "default-context",
},
{
name: "environment overrides helmDefaults when release not set",
defaults: HelmSpec{
KubeContext: "default-context",
},
environments: map[string]EnvironmentSpec{
"production": {KubeContext: "env-context"},
},
envName: "production",
release: &ReleaseSpec{
Name: "test",
},
want: "env-context",
},
{
name: "release overrides environment",
defaults: HelmSpec{
KubeContext: "default-context",
},
environments: map[string]EnvironmentSpec{
"production": {KubeContext: "env-context"},
},
envName: "production",
release: &ReleaseSpec{
Name: "test",
KubeContext: "release-context",
},
want: "release-context",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
envName := tt.envName
if envName == "" {
envName = "default"
}
environments := tt.environments
if environments == nil {
environments = make(map[string]EnvironmentSpec)
}
state := &HelmState{
basePath: "./",
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: tt.defaults,
Environments: environments,
Env: environment.Environment{
Name: envName,
},
},
}
got := state.getKubeContext(tt.release)
if got != tt.want {
t.Errorf("getKubeContext() = %v, want %v", got, tt.want)
}
})
}
}
// resolveOCIAdhocDepChart should rewrite a release `dependencies[].chart` value
// that uses the named-repo prefix form into a full oci:// URL whenever the
// matching `repositories:` entry has `oci: true`. All other inputs must pass
// through unchanged so we never disturb existing behavior.
func TestResolveOCIAdhocDepChart(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{Name: "ociregistry", URL: "registry.example.com:5000/charts", OCI: true},
{Name: "ociregistry-trailing", URL: "registry.example.com:5000/charts/", OCI: true},
{Name: "stable", URL: "https://charts.helm.sh/stable"},
},
},
}
tests := []struct {
name string
chart string
wantOK bool
wantChart string
}{
{
name: "named OCI repo prefix is rewritten to oci:// URL",
chart: "ociregistry/redis",
wantOK: true,
wantChart: "oci://registry.example.com:5000/charts/redis",
},
{
name: "trailing slash on repo URL does not produce a double slash",
chart: "ociregistry-trailing/redis",
wantOK: true,
wantChart: "oci://registry.example.com:5000/charts/redis",
},
{
name: "non-OCI repo prefix is left alone for chartify's helm-repo-list path",
chart: "stable/nginx",
wantOK: false,
},
{
name: "explicit oci:// URL is left alone (already in chartify's OCI branch)",
chart: "oci://registry.example.com:5000/charts/redis",
wantOK: false,
},
{
name: "unknown repo prefix is left alone",
chart: "unknownrepo/something",
wantOK: false,
},
{
name: "single-segment chart (no slash) is left alone",
chart: "localchart",
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := state.resolveOCIAdhocDepChart(tt.chart)
if ok != tt.wantOK {
t.Errorf("ok: want %v, got %v", tt.wantOK, ok)
}
if tt.wantOK && got != tt.wantChart {
t.Errorf("rewritten chart: want %q, got %q", tt.wantChart, got)
}
if !tt.wantOK && got != "" {
t.Errorf("expected empty rewrite when ok=false, got %q", got)
}
})
}
}