mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 22:10:54 +02:00
* fix: resolve OCI version constraints before deriving the shared chart cache path When an OCI release uses a semver constraint (e.g. `~1`, `^2.0.0`, `*`), `getOCIChartPath` currently derives the on-disk cache directory from the raw constraint string via `safeVersionPath`, which substitutes constraint characters (`~`, `^`, `>`, `<`, `!`, `|`, `=`, ` `, `,`, `*`) with `_`. So `version: ~1` becomes `.../mychart/_1/` on disk. `acquireChartLock` then refuses to refresh anything under the shared cache dir to avoid race conditions between concurrent processes, so once the constraint is first resolved and written to `_1/`, every subsequent render on that machine (or that container replica) returns the pinned tarball regardless of newer matching tags being published. In multi-pod deployments like ArgoCD's argocd-repo-server this shows up as intermittent stale renders: different pods populate their caches at different moments and serve different snapshots of the same `~1` release forever. Fix: for OCI releases whose `version` looks like a constraint, run `helm show chart <ref> --version <constraint> [flags]` and use the returned metadata.Version as the effective version for all downstream cache-key and path derivation. Helm already resolves the constraint against the registry and returns the concrete matching Chart.yaml. Callers get a content-addressable cache path (`.../mychart/1.0.1/`) that naturally invalidates when the constraint resolves to a new version. Exact-version releases and non-OCI releases skip the extra call. Adds an opt-out `resolveOCIVersions` field on `helmDefaults` and `ReleaseSpec` (both default true). If the resolution call fails transiently, the resolver logs a warning and falls back to the pre-fix behavior so a network hiccup doesn't break rendering. Adds `ShowChartWithFlags` to helmexec.Interface so the existing `ShowChart` API stays backwards compatible. Resolves #2766 Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com> * refactor: move ShowChartWithFlags to a ChartInspector capability interface Address Copilot review feedback on PR #2768: adding a method to the exported helmexec.Interface is a source-breaking change for every third-party implementation and mock of that interface, even though ShowChart itself stayed backward-compatible. Move ShowChartWithFlags off Interface and onto a new capability interface, helmexec.ChartInspector, following the same pattern used by the existing DependencyUpdater capability interface. The concrete execer and the exectest.Helm test stub still satisfy it (they already have the method); the OCI resolver in state.HelmState now type-asserts and falls back to the pre-fix caching behavior when the capability is absent, so downstream callers with their own helmexec.Interface implementations keep compiling untouched. Adds TestResolveOCIConstraintVersion_ChartInspectorFallback that exercises the type-assertion path with a helm value that satisfies Interface but deliberately does not satisfy ChartInspector. Reverts ShowChartWithFlags additions from testutil.noCallHelmExec and app_test.mockHelmExec since Interface no longer requires them. Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com> * fix: detect wildcard-segment semver constraints (1.x, 1.X) as constraints Address Copilot review feedback on PR #2768: the previous isVersionConstraint implementation scanned the input for operator characters (~, ^, >, <, !, |, =, space, comma, *). Masterminds/semver also accepts wildcard-segment constraints like "1.x", "1.X", "1.x.x", and "1.2.X" that contain no operator characters. Those would slip past the classifier, bypass OCI constraint resolution, and remain cached forever under the raw ".../mychart/1.x/" path — the same stale-cache bug the PR is meant to fix. Replace the character scan with a semver-parser-based check: a value is a constraint iff Masterminds/semver rejects it as a NewVersion but accepts it as a NewConstraint. This correctly: - Recognizes wildcard forms (1.x, 1.X, 1.x.x, 1.2.x, v1.x). - Preserves exact versions where "x" appears in prerelease metadata ("1.0.0-alpha.x") or build metadata ("1.0.0+x", "1.0.0+build.x.1") without misclassifying them, which a naive "add x to the scanned charset" fix would have gotten wrong. - Continues to classify values that are neither a version nor a constraint (empty string, "latest", junk) as non-constraints; helm handles those elsewhere. Removes the now-unused versionConstraintChars string constant. Expands TestIsVersionConstraint with 8 wildcard cases and 3 prerelease /build metadata cases containing "x", plus 2 non-parseable inputs. Adds a "wildcard segment constraint resolves to concrete version" subtest to TestResolveOCIConstraintVersion so the end-to-end pipeline is exercised for a version string that has no operator characters. Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com> * test: add getOCIChart integration test proving cache-path/pull-flag wiring Address Copilot review feedback on PR #2768. The existing unit test exercised resolveOCIConstraintVersion in isolation but did not prove that its output was propagated into the downstream cache key, cache path, and `helm chart pull --version` flag. Add a targeted integration test that: 1. Calls getOCIChart with a constraint release (`~1`) and a helm mock whose ShowChartWithFlags returns Chart.yaml version 1.0.1. 2. Asserts helm chart pull receives `--version 1.0.1`, not `~1`. 3. Asserts the destination path passed to helm chart pull contains the resolved-version segment (`/1.0.1/`) and does NOT contain the raw-constraint segment (`/_1/`). 4. Reads back the on-disk Chart.yaml under the cache path to confirm resolved version, path, and flag agree end to end. Add a second test that runs the same release twice with different resolver outputs (1.0.1, then 1.0.2 — simulating a newly published matching tag) and asserts the two resolutions land in distinct cache directories. This is the promise of the fix: once the raw constraint is out of the path, a new matching tag stops silently reusing the previously-resolved cache entry. The integration test flushed out a real correctness gap in the initial fix: getOCIChart resolved release.Version and chartVersion but did NOT recompute the qualified OCI ref that getOCIQualifiedChartName built pre-resolution. Helm was therefore receiving `oci://<repo>/<chart>:<constraint>` alongside a `--version <resolved>` flag — at best redundant, at worst rejected by future Helm versions. Fixed by re-invoking getOCIQualifiedChartName on the mutated release copy so the embedded tag also carries the resolved value. Isolates the shared helmfile cache via `t.Setenv(HELMFILE_CACHE_HOME, t.TempDir())` so the OutputDirTemplate == "" code path (which writes into remote.CacheDir) does not touch the user's real `~/.cache/helmfile` during test runs. Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com> * refactor: flatten OCI constraint-resolution wiring in getOCIChart Address review feedback on PR #2768: - Extract the inline resolve/requalify block from getOCIChart into applyOCIConstraintResolution, keeping getOCIChart flat (guard-clause style) and making the resolution wiring independently testable. The helper returns the (possibly updated) release, qualified chart name, and chart version; every failure mode returns its inputs unchanged. - On a re-qualify failure after a successful resolution, fall back to the pre-fix behavior entirely (raw constraint in cache key, ref, AND --version flag) instead of the previous half-resolved mix (resolved version in the cache key, raw constraint in the path and flag), which could desynchronize the in-process cache key from the on-disk path. - Build the 'helm show chart' ref by reusing parseOCIChartRef instead of re-implementing its last-slash/last-colon tag-splitting inline. Same behavior for all realistic refs (registry ports preserved), and it also handles the digest suffix should one ever reach this point. - Drop --devel from the resolver flags: helm documents --devel as ignored whenever --version is set, and --version is always passed on this path. No behavior change intended beyond the requalify-failure fallback (which cannot realistically trigger) and the removal of the inert --devel flag. Signed-off-by: yxxhero <aiopsclub@163.com> * fix: classify partial semver versions (1, 1.2) as OCI constraints Address review feedback on PR #2768: Masterminds' lenient parser accepts partial versions like "1" or "1.2" as versions, so the previous classifier (NewVersion fails && NewConstraint succeeds) treated them as exact pins. But helm's OCI resolution — registry.GetTagMatchingVersionOrConstraint — honors a version string as an exact pin ONLY when a registry tag literally equals it; otherwise it parses the string as a constraint, and "1"/"1.2" float across 1.x.y/1.2.y tags. Caching those under their raw spelling reproduces the stale-cache bug of issue #2766, just with a narrower trigger. Replace the NewVersion probe with isFullSemver, which additionally requires the whole major.minor.patch triple to be spelled out (optional v prefix, prerelease, and build metadata all still count as exact when the core is fully qualified). When a registry does carry a literal tag equal to the version string, the resolver's metadata.Version == chartVersion path reports no change, so literal-tag pins keep today's behavior. TestIsVersionConstraint: "1"/"1.0" flip to constraints, joined by new v1.2/0/v1 cases and a 1.2.3 exact case. TestResolveOCIConstraintVersion gains a "partial version resolves" subtest. Docs updated to describe the parser-based classification instead of "constraint characters". Signed-off-by: yxxhero <aiopsclub@163.com> * fix: skip OCI constraint resolution under skipRefresh Address review feedback on PR #2768: the resolver ran even under --skip-refresh, so offline and cache-only workflows gained a 'helm show chart' registry attempt per constraint-versioned OCI release. It degraded gracefully (warn + fallback), but added registry-timeout latency and warning noise per release. skipOCIConstraintResolution now suppresses resolution when any of the skipRefresh levels is set — CLI --skip-refresh (forced), per-release skipRefresh, or helmDefaults.skipRefresh — with the same precedence the other skipRefresh consumers in prepareChartForRelease use. Skipped runs fall back to the constraint-keyed cache path, i.e. they reuse whatever a previous non-skipped run resolved, which is what 'skip checking for updates to cached charts' means for constraint versions. The existing issue #2766 integration tests flip their opts to SkipRefresh: false since they assert resolution happens. New coverage: TestSkipOCIConstraintResolution (tri-state precedence table) and TestGetOCIChart_SkipRefreshSkipsConstraintResolution (no inspector call, raw constraint in --version and cache path). Signed-off-by: yxxhero <aiopsclub@163.com> * perf: memoize OCI constraint resolution per chart+constraint Address review feedback on PR #2768: resolution ran before the in-process chart-cache fast path and was not memoized, so every constraint-versioned OCI release paid its own 'helm show chart' registry round-trip on every render — including N releases sharing the same chart+constraint, whose parallel workers could even resolve to different versions if the registry changed between their lookups. Memoize successful resolutions in resolvedOCIConstraints keyed by (chart ref, constraint), mirroring the downloadedCharts pattern: - Releases sharing a chart+constraint cost one round-trip per process and consistently use one resolved version per run. - Only successful resolutions are memoized; failures may be transient. - Flags are not part of the key: they govern TLS/verification/registry credentials, not which tag a constraint matches (--devel is already omitted as it is ignored whenever --version is set). - Concurrent misses may both hit the registry; last write wins, harmlessly. resetResolvedOCIConstraintsForTest is added alongside the existing resetChartCacheForTest and wired into the issue #2766 tests — notably ResolvesToDifferentVersionsPicksSeparateCachePaths, which reuses the same chart+constraint across its two runs and would otherwise be served the first resolution from the memo (which is exactly the intended per-process semantics). New coverage: TestResolveOCIConstraintVersion_Memoized (memo hit skips the registry, different constraint is a different key) and TestGetOCIChart_SharedConstraintResolvedOncePerProcess (two releases, one inspector call, one pull, same path). Signed-off-by: yxxhero <aiopsclub@163.com> * test: cover URL-embedded OCI constraint resolution Address review feedback on PR #2768: the existing integration tests only exercised the repo-aliased spelling (chart: myrepo/mychart, version: '~1') and the version-field spelling. The chart-URL spelling (chart: oci://<registry>/<chart>:~1) takes a different branch in getOCIQualifiedChartName — the URL version is deliberately NOT embedded into the qualified ref and flows through --version only — so its re-qualification after constraint resolution (release.Version mutated to the resolved value, versionInURL still the constraint) was untested. TestGetOCIChart_URLEmbeddedConstraintResolves asserts the resolver receives the URL-embedded constraint, helm chart pull receives the resolved version via --version with a tag-less ref, and the cache path carries the resolved version segment instead of the raw constraint. Also gofmt-aligns the test tables added in earlier commits and drops a redundant 1.2.3 test case that tripped goconst. Signed-off-by: yxxhero <aiopsclub@163.com> * docs: note empty-version OCI releases are unaffected by resolveOCIVersions Releases with no version: at all keep their pre-existing semantics: helm picks the latest tag at pull time and helmfile caches it under a version-less shared-cache path. Document the limitation alongside the other resolveOCIVersions scope notes. Signed-off-by: yxxhero <aiopsclub@163.com> --------- Signed-off-by: Samuel Archambault <samuel.archambault@getmaintainx.com> Signed-off-by: yxxhero <aiopsclub@163.com> Co-authored-by: Samuel Archambault <samuel.archambault@getmaintainx.com> Co-authored-by: yxxhero <aiopsclub@163.com>
7233 lines
182 KiB
Go
7233 lines
182 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"
|
|
chart "helm.sh/helm/v4/pkg/chart/v2"
|
|
|
|
"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"
|
|
"github.com/helmfile/helmfile/pkg/yaml"
|
|
)
|
|
|
|
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,
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("3.10.0"),
|
|
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-helm4",
|
|
defaults: HelmSpec{
|
|
Atomic: false,
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("4.0.0"),
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
Atomic: &enable,
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
want: []string{
|
|
"--version", "0.1",
|
|
"--rollback-on-failure",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "atomic-override-default",
|
|
defaults: HelmSpec{
|
|
Atomic: true,
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("3.10.0"),
|
|
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,
|
|
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",
|
|
"--atomic",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "atomic-from-default-helm4",
|
|
defaults: HelmSpec{
|
|
Atomic: 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",
|
|
"--rollback-on-failure",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "rollback-on-failure",
|
|
defaults: HelmSpec{
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("4.0.0"),
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
RollbackOnFailure: &enable,
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
want: []string{
|
|
"--version", "0.1",
|
|
"--rollback-on-failure",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "rollback-on-failure-from-default",
|
|
defaults: HelmSpec{
|
|
RollbackOnFailure: 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",
|
|
"--rollback-on-failure",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "rollback-on-failure-override-default",
|
|
defaults: HelmSpec{
|
|
RollbackOnFailure: true,
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("4.0.0"),
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
RollbackOnFailure: &disable,
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
want: []string{
|
|
"--version", "0.1",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "rollback-on-failure-helm3-error",
|
|
defaults: HelmSpec{
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("3.10.0"),
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
RollbackOnFailure: &enable,
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
wantErr: "rollbackOnFailure requires Helm 4 or greater (set via releases[].rollbackOnFailure or helmDefaults.rollbackOnFailure)",
|
|
},
|
|
{
|
|
name: "rollback-on-failure-from-default-helm3-error",
|
|
defaults: HelmSpec{
|
|
RollbackOnFailure: true,
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("3.10.0"),
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
wantErr: "rollbackOnFailure requires Helm 4 or greater (set via releases[].rollbackOnFailure or helmDefaults.rollbackOnFailure)",
|
|
},
|
|
{
|
|
name: "atomic-and-rollback-on-failure-mutually-exclusive-helm4",
|
|
defaults: HelmSpec{
|
|
CreateNamespace: &disable,
|
|
},
|
|
version: semver.MustParse("4.0.0"),
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
Atomic: &enable,
|
|
RollbackOnFailure: &enable,
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
wantErr: "atomic and rollbackOnFailure are mutually exclusive (check both releases[].atomic/rollbackOnFailure and helmDefaults.atomic/rollbackOnFailure)",
|
|
},
|
|
{
|
|
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",
|
|
},
|
|
},
|
|
{
|
|
name: "template-args appended to helm template (issue #1833 lookup support)",
|
|
version: semver.MustParse("3.10.0"),
|
|
defaults: HelmSpec{
|
|
Verify: false,
|
|
},
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
templateOpts: TemplateOpts{
|
|
TemplateArgs: "--dry-run=server --enable-dns",
|
|
},
|
|
want: []string{
|
|
"--version", "0.1",
|
|
"--dry-run=server",
|
|
"--enable-dns",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "empty template-args are not appended",
|
|
version: semver.MustParse("3.10.0"),
|
|
defaults: HelmSpec{
|
|
Verify: false,
|
|
},
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
want: []string{
|
|
"--version", "0.1",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "helmDefaults.templateArgs used when no CLI template-args",
|
|
version: semver.MustParse("3.10.0"),
|
|
defaults: HelmSpec{
|
|
Verify: false,
|
|
TemplateArgs: []string{"--dry-run=server", "--enable-dns"},
|
|
},
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
want: []string{
|
|
"--version", "0.1",
|
|
"--dry-run=server",
|
|
"--enable-dns",
|
|
"--namespace", "test-namespace",
|
|
},
|
|
},
|
|
{
|
|
name: "CLI template-args overrides helmDefaults.templateArgs",
|
|
version: semver.MustParse("3.10.0"),
|
|
defaults: HelmSpec{
|
|
Verify: false,
|
|
TemplateArgs: []string{"--enable-dns"},
|
|
},
|
|
release: &ReleaseSpec{
|
|
Chart: "test/chart",
|
|
Version: "0.1",
|
|
Name: "test-charts",
|
|
Namespace: "test-namespace",
|
|
},
|
|
templateOpts: TemplateOpts{
|
|
TemplateArgs: "--dry-run=server",
|
|
},
|
|
want: []string{
|
|
"--version", "0.1",
|
|
"--dry-run=server",
|
|
"--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,
|
|
},
|
|
{
|
|
name: "condition true literal enabled",
|
|
condition: "true",
|
|
values: map[string]any{},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "condition false literal disabled",
|
|
condition: "false",
|
|
values: map[string]any{},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "condition true literal takes precedence over values lookup",
|
|
condition: "true",
|
|
values: map[string]any{
|
|
"foo": map[string]any{
|
|
"enabled": false,
|
|
},
|
|
},
|
|
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 TestReleaseSpecConditionUnmarshalsBoolLiteral(t *testing.T) {
|
|
var got ReleaseSpec
|
|
require.NoError(t, yaml.Unmarshal([]byte("condition: true"), &got))
|
|
assert.Equal(t, "true", got.Condition)
|
|
|
|
enabled, err := ConditionEnabled(got, map[string]any{})
|
|
require.NoError(t, err)
|
|
assert.True(t, enabled)
|
|
}
|
|
|
|
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 TestPrepareDiffReleases_SkipDiffValidationOnInstall(t *testing.T) {
|
|
installedListOutput := "NAME\tNAMESPACE\tREVISION\tSTATUS\nfoo\tdefault\t1\tdeployed"
|
|
listFlags := "--uninstalling --deployed --failed --pending"
|
|
|
|
tests := []struct {
|
|
name string
|
|
skipDiffValidationOnInstall bool
|
|
perReleaseDisableValidation bool
|
|
installed bool
|
|
wantDisableValidation bool
|
|
}{
|
|
{
|
|
name: "flag-set-not-installed",
|
|
skipDiffValidationOnInstall: true,
|
|
installed: false,
|
|
wantDisableValidation: true,
|
|
},
|
|
{
|
|
name: "flag-set-already-installed",
|
|
skipDiffValidationOnInstall: true,
|
|
installed: true,
|
|
wantDisableValidation: false,
|
|
},
|
|
{
|
|
name: "flag-not-set-not-installed",
|
|
skipDiffValidationOnInstall: false,
|
|
installed: false,
|
|
wantDisableValidation: false,
|
|
},
|
|
{
|
|
name: "per-release-flag-not-installed",
|
|
perReleaseDisableValidation: true,
|
|
installed: false,
|
|
wantDisableValidation: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
release := ReleaseSpec{
|
|
Name: "foo",
|
|
Chart: "stable/foo",
|
|
}
|
|
if tt.perReleaseDisableValidation {
|
|
release.DisableValidationOnInstall = boolValue(true)
|
|
}
|
|
st := &HelmState{
|
|
ReleaseSetSpec: ReleaseSetSpec{
|
|
Releases: []ReleaseSpec{release},
|
|
HelmDefaults: HelmSpec{},
|
|
},
|
|
logger: logger,
|
|
valsRuntime: valsRuntime,
|
|
}
|
|
lists := map[exectest.ListKey]string{}
|
|
if tt.installed {
|
|
lists[exectest.ListKey{Filter: "^foo$", Flags: listFlags}] = installedListOutput
|
|
}
|
|
helm := &exectest.Helm{Lists: lists}
|
|
|
|
results, errs := st.prepareDiffReleases(helm, []string{}, 1, false, false, false, []string{}, false, false, false, &DiffOpts{
|
|
SkipDiffValidationOnInstall: tt.skipDiffValidationOnInstall,
|
|
})
|
|
|
|
require.Len(t, errs, 0)
|
|
require.Len(t, results, 1)
|
|
|
|
flags := results[0].flags
|
|
hasDisableValidation := false
|
|
for _, f := range flags {
|
|
if f == "--disable-validation" {
|
|
hasDisableValidation = true
|
|
break
|
|
}
|
|
}
|
|
require.Equal(t, tt.wantDisableValidation, hasDisableValidation,
|
|
"--disable-validation presence mismatch for case %q; flags: %v", tt.name, 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 TestHelmState_SyncRepos_OCIOnly(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
repos []RepositorySpec
|
|
opts []SyncOption
|
|
wantRegistryLoginHost string
|
|
wantRepoSet bool
|
|
}{
|
|
{
|
|
name: "WithOCIOnly logs into OCI registry",
|
|
repos: []RepositorySpec{
|
|
{
|
|
Name: "ociregistry",
|
|
URL: "quay.io/myorg",
|
|
OCI: true,
|
|
Username: "user",
|
|
Password: "pass",
|
|
},
|
|
},
|
|
opts: []SyncOption{WithOCIOnly()},
|
|
wantRegistryLoginHost: "quay.io",
|
|
wantRepoSet: false,
|
|
},
|
|
{
|
|
name: "WithOCIOnly skips non-OCI repo",
|
|
repos: []RepositorySpec{
|
|
{
|
|
Name: "stable",
|
|
URL: "https://charts.helm.sh/stable",
|
|
},
|
|
},
|
|
opts: []SyncOption{WithOCIOnly()},
|
|
wantRegistryLoginHost: "",
|
|
wantRepoSet: false,
|
|
},
|
|
{
|
|
name: "without options processes non-OCI repo via AddRepo",
|
|
repos: []RepositorySpec{
|
|
{
|
|
Name: "stable",
|
|
URL: "https://charts.helm.sh/stable",
|
|
},
|
|
},
|
|
opts: nil,
|
|
wantRegistryLoginHost: "",
|
|
wantRepoSet: true,
|
|
},
|
|
}
|
|
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{}, tt.opts...)
|
|
if err != nil {
|
|
t.Errorf("SyncRepos() error = %v", err)
|
|
return
|
|
}
|
|
if tt.wantRegistryLoginHost != "" && helm.RegistryLoginHost != tt.wantRegistryLoginHost {
|
|
t.Errorf("RegistryLogin host = %q, want %q", helm.RegistryLoginHost, tt.wantRegistryLoginHost)
|
|
}
|
|
if tt.wantRegistryLoginHost == "" && helm.RegistryLoginHost != "" {
|
|
t.Errorf("RegistryLogin should not have been called, got host = %q", helm.RegistryLoginHost)
|
|
}
|
|
if len(helm.Repo) > 0 != tt.wantRepoSet {
|
|
t.Errorf("AddRepo called = %v, want %v", len(helm.Repo) > 0, tt.wantRepoSet)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
inputTemplateDefaults []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"},
|
|
},
|
|
{
|
|
name: "TemplateArgs are appended after DiffArgs (issue #1833 lookup support)",
|
|
inputFlags: []string{"aaaaa"},
|
|
inputOpts: &DiffOpts{
|
|
DiffArgs: "-bbbb",
|
|
TemplateArgs: "--dry-run=server",
|
|
},
|
|
expected: []string{"aaaaa", "-bbbb", "--dry-run=server"},
|
|
},
|
|
{
|
|
name: "TemplateArgs are appended even without DiffArgs",
|
|
inputFlags: []string{"aaaaa"},
|
|
inputOpts: &DiffOpts{
|
|
TemplateArgs: "--dry-run=server --enable-dns",
|
|
},
|
|
expected: []string{"aaaaa", "--dry-run=server", "--enable-dns"},
|
|
},
|
|
{
|
|
name: "Empty TemplateArgs are not appended",
|
|
inputFlags: []string{"aaaaa"},
|
|
inputOpts: &DiffOpts{
|
|
DiffArgs: "-bbbb",
|
|
TemplateArgs: "",
|
|
},
|
|
expected: []string{"aaaaa", "-bbbb"},
|
|
},
|
|
{
|
|
name: "helmDefaults.templateArgs used when no CLI TemplateArgs",
|
|
inputFlags: []string{"aaaaa"},
|
|
inputTemplateDefaults: []string{"--dry-run=server", "--enable-dns"},
|
|
expected: []string{"aaaaa", "--dry-run=server", "--enable-dns"},
|
|
},
|
|
{
|
|
name: "CLI TemplateArgs overrides helmDefaults.templateArgs",
|
|
inputFlags: []string{"aaaaa"},
|
|
inputOpts: &DiffOpts{TemplateArgs: "--dry-run=server"},
|
|
inputTemplateDefaults: []string{"--enable-dns"},
|
|
expected: []string{"aaaaa", "--dry-run=server"},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
result := (&HelmState{
|
|
ReleaseSetSpec: ReleaseSetSpec{
|
|
HelmDefaults: HelmSpec{
|
|
DiffArgs: test.inputDefaults,
|
|
TemplateArgs: test.inputTemplateDefaults,
|
|
},
|
|
},
|
|
}).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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsVersionConstraint checks the semver-parser-based constraint detector.
|
|
// Exact semvers (including with a "v" prefix, prerelease metadata that may
|
|
// contain "x", or build metadata that may contain "x") are not constraints.
|
|
// Anything the Masterminds/semver parser accepts as a constraint — operator
|
|
// forms AND wildcard segment forms (1.x, 1.X) — is a constraint. Values that
|
|
// are neither (empty, "latest", junk) return false: helm handles those
|
|
// separately elsewhere.
|
|
func TestIsVersionConstraint(t *testing.T) {
|
|
tests := []struct {
|
|
version string
|
|
want bool
|
|
}{
|
|
// exact versions — never constraints
|
|
{"1.0.1", false},
|
|
{"v1.0.1", false},
|
|
{"1.0.0-rc.1", false},
|
|
{"1.0.0+build.1", false},
|
|
// exact versions where "x" appears in prerelease or build metadata:
|
|
// must NOT be misclassified as wildcard constraints
|
|
{"1.0.0-alpha.x", false},
|
|
{"1.0.0+x", false},
|
|
{"1.0.0+build.x.1", false},
|
|
// operator constraints
|
|
{"~1", true},
|
|
{"~1.0", true},
|
|
{"^1", true},
|
|
{"^2.0.0", true},
|
|
{"*", true},
|
|
{">=1.0.0", true},
|
|
{">=1.0.0 <2.0.0", true}, // whitespace-separated range
|
|
{">1.0", true},
|
|
{"<2.0", true},
|
|
{"!=1.0.0", true},
|
|
{"1.0.0 || 2.0.0", true},
|
|
{"1.0.0,2.0.0", true},
|
|
// wildcard-segment constraints — the case that a character scan missed
|
|
{"1.x", true},
|
|
{"1.X", true},
|
|
{"1.x.x", true},
|
|
{"1.X.X", true},
|
|
{"1.2.x", true},
|
|
{"1.2.X", true},
|
|
{"v1.x", true},
|
|
// partial semvers — Masterminds' parser accepts them as versions, but
|
|
// helm's OCI resolution (registry.GetTagMatchingVersionOrConstraint)
|
|
// only treats a version string as an exact pin when a registry tag
|
|
// literally equals it; otherwise "1"/"1.2" float as ranges. They must
|
|
// therefore be classified as constraints and resolved before caching.
|
|
{"1", true},
|
|
{"1.2", true},
|
|
{"v1.2", true},
|
|
{"0", true},
|
|
{"v1", true},
|
|
// neither a valid version nor a valid constraint — handled elsewhere,
|
|
// resolver skips them so the raw string keeps flowing to helm.
|
|
{"", false},
|
|
{"latest", false},
|
|
{"not-a-version", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.version, func(t *testing.T) {
|
|
require.Equal(t, tt.want, isVersionConstraint(tt.version))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResolveOCIConstraintVersion exercises the pre-cache constraint resolver.
|
|
// The stubbed helm implementation stands in for `helm show chart ... --version
|
|
// <constraint>` and returns whatever Chart.yaml version the test wants; the
|
|
// resolver must forward the returned version back to the caller so downstream
|
|
// cache-key derivation uses the concrete tag rather than the raw constraint.
|
|
func TestResolveOCIConstraintVersion(t *testing.T) {
|
|
const (
|
|
releaseName = "app"
|
|
chartRef = "myrepo/app"
|
|
qualified = "registry.example.com/charts/app"
|
|
resolvedVersion = "1.0.1"
|
|
)
|
|
baseRepositories := []RepositorySpec{
|
|
{Name: "myrepo", URL: "registry.example.com/charts", OCI: true},
|
|
}
|
|
|
|
newState := func(defaults HelmSpec) *HelmState {
|
|
return &HelmState{
|
|
ReleaseSetSpec: ReleaseSetSpec{
|
|
HelmDefaults: defaults,
|
|
Repositories: baseRepositories,
|
|
},
|
|
logger: logger,
|
|
valsRuntime: valsRuntime,
|
|
}
|
|
}
|
|
|
|
trueVal := true
|
|
falseVal := false
|
|
|
|
tests := []struct {
|
|
name string
|
|
defaults HelmSpec
|
|
release ReleaseSpec
|
|
qualifiedRef string
|
|
version string
|
|
stubbedResolved string
|
|
stubbedErr error
|
|
expectHelmCalled bool
|
|
expectVersion string
|
|
expectChanged bool
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "constraint resolves to concrete version",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "~1"},
|
|
qualifiedRef: qualified,
|
|
version: "~1",
|
|
stubbedResolved: resolvedVersion,
|
|
expectHelmCalled: true,
|
|
expectVersion: resolvedVersion,
|
|
expectChanged: true,
|
|
},
|
|
{
|
|
// Wildcard-segment constraint has no operator character but must
|
|
// still be detected as a constraint and resolved (regression
|
|
// coverage for the semver-parser-based isVersionConstraint fix).
|
|
name: "wildcard segment constraint resolves to concrete version",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "1.x"},
|
|
qualifiedRef: qualified,
|
|
version: "1.x",
|
|
stubbedResolved: resolvedVersion,
|
|
expectHelmCalled: true,
|
|
expectVersion: resolvedVersion,
|
|
expectChanged: true,
|
|
},
|
|
{
|
|
// Partial semver ("1.2") parses as a version but floats as a range
|
|
// in helm's OCI tag matching, so it must also be resolved.
|
|
name: "partial version resolves to concrete version",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "1.2"},
|
|
qualifiedRef: qualified + ":1.2",
|
|
version: "1.2",
|
|
stubbedResolved: resolvedVersion,
|
|
expectHelmCalled: true,
|
|
expectVersion: resolvedVersion,
|
|
expectChanged: true,
|
|
},
|
|
{
|
|
name: "exact version bypasses resolver",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: resolvedVersion},
|
|
qualifiedRef: qualified + ":" + resolvedVersion,
|
|
version: resolvedVersion,
|
|
expectHelmCalled: false,
|
|
expectVersion: resolvedVersion,
|
|
expectChanged: false,
|
|
},
|
|
{
|
|
name: "empty version bypasses resolver",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef},
|
|
qualifiedRef: qualified,
|
|
version: "",
|
|
expectHelmCalled: false,
|
|
expectChanged: false,
|
|
},
|
|
{
|
|
name: "digest-pinned ref bypasses resolver even with constraint version",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "~1"},
|
|
qualifiedRef: qualified + "@sha256:deadbeef",
|
|
version: "~1",
|
|
expectHelmCalled: false,
|
|
expectVersion: "~1",
|
|
expectChanged: false,
|
|
},
|
|
{
|
|
name: "opt-out via HelmDefaults keeps raw constraint",
|
|
defaults: HelmSpec{ResolveOCIVersions: &falseVal},
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "~1"},
|
|
qualifiedRef: qualified,
|
|
version: "~1",
|
|
expectHelmCalled: false,
|
|
expectVersion: "~1",
|
|
expectChanged: false,
|
|
},
|
|
{
|
|
name: "opt-out at release level wins over helmDefaults",
|
|
defaults: HelmSpec{ResolveOCIVersions: &trueVal},
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "~1", ResolveOCIVersions: &falseVal},
|
|
qualifiedRef: qualified,
|
|
version: "~1",
|
|
expectHelmCalled: false,
|
|
expectChanged: false,
|
|
},
|
|
{
|
|
name: "resolver returns empty version is an error",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "~1"},
|
|
qualifiedRef: qualified,
|
|
version: "~1",
|
|
stubbedResolved: "",
|
|
expectHelmCalled: true,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "resolver returns same version as constraint reports no change",
|
|
release: ReleaseSpec{Name: releaseName, Chart: chartRef, Version: "~1"},
|
|
qualifiedRef: qualified,
|
|
version: "~1",
|
|
stubbedResolved: "~1",
|
|
expectHelmCalled: true,
|
|
expectVersion: "~1",
|
|
expectChanged: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Several subtests share the same chart+constraint; keep the
|
|
// in-process resolution memo out of the picture.
|
|
resetResolvedOCIConstraintsForTest()
|
|
called := false
|
|
var gotFlags []string
|
|
helm := &exectest.Helm{
|
|
ShowChartWithFlagsFunc: func(chartPath string, flags ...string) (chart.Metadata, error) {
|
|
called = true
|
|
gotFlags = flags
|
|
if tt.stubbedErr != nil {
|
|
return chart.Metadata{}, tt.stubbedErr
|
|
}
|
|
return chart.Metadata{Version: tt.stubbedResolved}, nil
|
|
},
|
|
}
|
|
st := newState(tt.defaults)
|
|
resolved, changed, err := st.resolveOCIConstraintVersion(&tt.release, helm, tt.qualifiedRef, tt.version)
|
|
|
|
require.Equalf(t, tt.expectHelmCalled, called, "helm.ShowChartWithFlags call expectation mismatch")
|
|
if tt.expectHelmCalled {
|
|
// The resolver must pass --version <constraint> so helm can
|
|
// resolve against the registry rather than a cached index.
|
|
require.Contains(t, gotFlags, "--version")
|
|
require.Contains(t, gotFlags, tt.version)
|
|
}
|
|
if tt.expectErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expectChanged, changed)
|
|
if tt.expectVersion != "" {
|
|
require.Equal(t, tt.expectVersion, resolved)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSkipOCIConstraintResolution checks the tri-state skip logic: the CLI
|
|
// --skip-refresh flag forces skipping; otherwise an explicit per-release
|
|
// skipRefresh wins; otherwise helmDefaults.skipRefresh decides.
|
|
func TestSkipOCIConstraintResolution(t *testing.T) {
|
|
falseVal, trueVal := false, true
|
|
tests := []struct {
|
|
name string
|
|
opts ChartPrepareOptions
|
|
release ReleaseSpec
|
|
defaults HelmSpec
|
|
want bool
|
|
}{
|
|
{
|
|
name: "CLI flag forces skip",
|
|
opts: ChartPrepareOptions{SkipRefresh: true},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no flags set resolves",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "release-level skipRefresh skips",
|
|
release: ReleaseSpec{SkipRefresh: &trueVal},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "release-level skipRefresh=false beats helmDefaults=true",
|
|
release: ReleaseSpec{SkipRefresh: &falseVal},
|
|
defaults: HelmSpec{SkipRefresh: true},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "helmDefaults.skipRefresh applies when release is unset",
|
|
defaults: HelmSpec{SkipRefresh: true},
|
|
want: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
st := &HelmState{ReleaseSetSpec: ReleaseSetSpec{HelmDefaults: tt.defaults}}
|
|
require.Equal(t, tt.want, st.skipOCIConstraintResolution(&tt.release, tt.opts))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResolveOCIConstraintVersion_Memoized verifies the in-process resolution
|
|
// memo: the second lookup of the same chart+constraint must not hit the
|
|
// registry again, while a different constraint on the same chart is a distinct
|
|
// key and does.
|
|
func TestResolveOCIConstraintVersion_Memoized(t *testing.T) {
|
|
resetResolvedOCIConstraintsForTest()
|
|
|
|
const (
|
|
repoURL = "registry.example.com/charts"
|
|
chartRef = "myrepo/memo"
|
|
qualified = repoURL + "/memo"
|
|
)
|
|
calls := 0
|
|
helm := &exectest.Helm{
|
|
ShowChartWithFlagsFunc: func(_ string, flags ...string) (chart.Metadata, error) {
|
|
calls++
|
|
return chart.Metadata{Version: "1.0.1"}, nil
|
|
},
|
|
}
|
|
st := &HelmState{
|
|
ReleaseSetSpec: ReleaseSetSpec{
|
|
Repositories: []RepositorySpec{{Name: "myrepo", URL: repoURL, OCI: true}},
|
|
},
|
|
logger: logger,
|
|
valsRuntime: valsRuntime,
|
|
}
|
|
|
|
first, changed, err := st.resolveOCIConstraintVersion(&ReleaseSpec{Name: "app", Chart: chartRef, Version: "~1"}, helm, qualified, "~1")
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.Equal(t, "1.0.1", first)
|
|
require.Equal(t, 1, calls)
|
|
|
|
// Same chart+constraint: served from the memo, no second registry call.
|
|
second, changed, err := st.resolveOCIConstraintVersion(&ReleaseSpec{Name: "app2", Chart: chartRef, Version: "~1"}, helm, qualified, "~1")
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.Equal(t, "1.0.1", second)
|
|
require.Equal(t, 1, calls, "the second lookup must be served from the in-process memo")
|
|
|
|
// A different constraint on the same chart is a different memo key.
|
|
_, _, err = st.resolveOCIConstraintVersion(&ReleaseSpec{Name: "app3", Chart: chartRef, Version: "^1"}, helm, qualified, "^1")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, calls)
|
|
}
|
|
|
|
// noOpChartInspector is a helmexec.Interface implementation that intentionally
|
|
// does NOT satisfy helmexec.ChartInspector. It exists to prove that
|
|
// resolveOCIConstraintVersion degrades gracefully when a third-party helm
|
|
// implementation predates the ShowChartWithFlags capability, instead of
|
|
// requiring every downstream mock to grow the new method.
|
|
type noOpChartInspector struct {
|
|
helmexec.Interface
|
|
}
|
|
|
|
// TestResolveOCIConstraintVersion_ChartInspectorFallback confirms that a helm
|
|
// implementation lacking the ChartInspector capability causes the resolver to
|
|
// return the raw constraint unchanged with no error, keeping backward
|
|
// compatibility for third-party helmexec.Interface implementations.
|
|
func TestResolveOCIConstraintVersion_ChartInspectorFallback(t *testing.T) {
|
|
resetResolvedOCIConstraintsForTest()
|
|
const (
|
|
repoName = "myrepo"
|
|
repoURL = "registry.example.com/charts"
|
|
chartRef = "myrepo/fallbackchart"
|
|
qualified = "registry.example.com/charts/fallbackchart"
|
|
)
|
|
st := &HelmState{
|
|
ReleaseSetSpec: ReleaseSetSpec{
|
|
Repositories: []RepositorySpec{
|
|
{Name: repoName, URL: repoURL, OCI: true},
|
|
},
|
|
},
|
|
logger: logger,
|
|
valsRuntime: valsRuntime,
|
|
}
|
|
release := &ReleaseSpec{Name: "fallback", Chart: chartRef, Version: "~1"}
|
|
helm := &noOpChartInspector{}
|
|
|
|
// Sanity: noOpChartInspector satisfies Interface but not ChartInspector.
|
|
var _ helmexec.Interface = helm
|
|
_, isInspector := any(helm).(helmexec.ChartInspector)
|
|
require.False(t, isInspector, "test setup: noOpChartInspector must NOT implement ChartInspector")
|
|
|
|
resolved, changed, err := st.resolveOCIConstraintVersion(release, helm, qualified, "~1")
|
|
require.NoError(t, err)
|
|
require.False(t, changed, "no ChartInspector capability => must not change version")
|
|
require.Equal(t, "~1", resolved, "no ChartInspector capability => must return raw constraint")
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestChartFetchFlags verifies that chartFetchFlags (used to prefetch shared
|
|
// remote charts, see issue #2741) matches the chart-acquisition flags
|
|
// flagsForUpgrade applies for a normal (non-prefetched) remote chart: version,
|
|
// verify, keyring, and TLS/plain-http flags.
|
|
func TestChartFetchFlags(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
repo []RepositorySpec
|
|
helmDefaults HelmSpec
|
|
release *ReleaseSpec
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "exact version",
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart", Version: "1.2.3"},
|
|
expected: []string{"--version", "1.2.3"},
|
|
},
|
|
{
|
|
name: "version range",
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart", Version: ">=1.0.0 <2.0.0"},
|
|
expected: []string{"--version", ">=1.0.0 <2.0.0"},
|
|
},
|
|
{
|
|
name: "no version means latest",
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart"},
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "devel release",
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart", Devel: boolValue(true)},
|
|
expected: []string{"--devel"},
|
|
},
|
|
{
|
|
name: "release-level verify and keyring",
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart", Verify: boolValue(true), Keyring: "/keys/release.gpg"},
|
|
expected: []string{"--verify", "--keyring", "/keys/release.gpg"},
|
|
},
|
|
{
|
|
name: "repo-level verify and keyring",
|
|
repo: []RepositorySpec{{Name: "myrepo", Verify: true, Keyring: "/keys/repo.gpg"}},
|
|
release: &ReleaseSpec{
|
|
Chart: "myrepo/mychart",
|
|
},
|
|
expected: []string{"--verify", "--keyring", "/keys/repo.gpg"},
|
|
},
|
|
{
|
|
name: "helmDefaults-level verify and keyring",
|
|
helmDefaults: HelmSpec{Verify: true, Keyring: "/keys/default.gpg"},
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart"},
|
|
expected: []string{"--verify", "--keyring", "/keys/default.gpg"},
|
|
},
|
|
{
|
|
name: "release-level plain http",
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart", PlainHttp: true},
|
|
expected: []string{"--plain-http"},
|
|
},
|
|
{
|
|
name: "release-level insecure skip tls verify",
|
|
release: &ReleaseSpec{Chart: "myrepo/mychart", InsecureSkipTLSVerify: true},
|
|
expected: []string{"--insecure-skip-tls-verify"},
|
|
},
|
|
{
|
|
name: "OCI chart skips verify and keyring but keeps download flags",
|
|
repo: []RepositorySpec{{Name: "myrepo", OCI: true, Verify: true, Keyring: "/keys/repo.gpg"}},
|
|
release: &ReleaseSpec{
|
|
Chart: "myrepo/mychart",
|
|
PlainHttp: true,
|
|
},
|
|
expected: []string{"--plain-http"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
st := &HelmState{}
|
|
st.ReleaseSetSpec.Repositories = tt.repo
|
|
st.ReleaseSetSpec.HelmDefaults = tt.helmDefaults
|
|
flags := st.chartFetchFlags(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)
|
|
}
|
|
})
|
|
}
|
|
}
|