Files
helmfile/pkg/state/state_test.go
T
Aditya Menon 9c70adc038 fix: resolve issues #2295, #2296, and #2297 (#2298)
* fix: resolve issues #2295, #2296, #2297 and OCI registry login

This PR fixes four related bugs affecting chart preparation, caching,
and OCI registry authentication.

Issue #2295: OCI chart cache conflicts with parallel helmfile processes
- Added filesystem-level locking using flock for cross-process sync
- Implements double-check locking pattern for efficiency
- Retry logic with 5-minute timeout and 3 retries
- Refactored into reusable acquireChartLock() helper function
- Added refresh marker coordination for cross-process cache management

Issue #2296: helmDefaults.skipDeps and helmDefaults.skipRefresh ignored
- Check both CLI options AND helmDefaults when deciding to skip repo sync

Issue #2297: Local chart + transformers causes panic
- Normalize local chart paths to absolute before calling chartify

OCI Registry Login URL Fix:
- Added extractRegistryHost() to extract just the registry host from URLs
- Fixed SyncRepos to use extracted host for OCI registry login
- e.g., "account.dkr.ecr.region.amazonaws.com/charts" ->
        "account.dkr.ecr.region.amazonaws.com"

Test Plan:
- Unit tests for issues #2295, #2296, #2297
- Unit tests for OCI registry login (extractRegistryHost, SyncRepos_OCI)
- Integration tests for issues #2295 and #2297
- All existing unit tests pass (including TestLint)

Fixes #2295
Fixes #2296
Fixes #2297

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: replace 60s timeout with reader-writer locks for OCI chart caching

Address PR review feedback from @champtar about the OCI chart caching
mechanism. The previous implementation used a 60-second timeout which
was arbitrary and caused race conditions when helm deployments took
longer (e.g., deployments triggering scaling up/down).

Changes:
- Replace 60s refresh marker timeout with proper reader-writer locks
- Use shared locks (RLock) when using cached charts (allows concurrent reads)
- Use exclusive locks (Lock) when refreshing/downloading charts
- Hold locks during entire helm operation lifecycle (not just during download)
- Add getNamedRWMutex() for in-process RW coordination
- Update PrepareCharts() to return locks map for lifecycle management
- Add chartLockReleaser in run.go to release locks after helm callback
- Remove unused mutexMap and getNamedMutex (replaced by RW versions)
- Add comprehensive tests for shared/exclusive lock behavior

This eliminates the race condition where one process could delete a
cached chart while another process's helm command was still using it.

Fixes review comment on PR #2298

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: prevent deadlock when multiple releases share the same chart

When multiple releases use the same OCI chart (e.g., same chart different
values), workers in PrepareCharts would deadlock:

1. Worker 1 acquires lock for chart/path, downloads, adds to cache
2. Worker 2 finds chart in cache, tries to acquire lock on same path
3. Worker 2 blocks waiting for Worker 1's lock
4. Collector waits for Worker 2's result
5. Worker 1's lock held until PrepareCharts finishes -> deadlock

The fix: when using the in-memory chart cache (which means another worker
in the same process already downloaded the chart), don't acquire another
lock. This is safe because:
- The in-memory cache is only used within a single helmfile process
- The tempDir cleanup is deferred until after helm callback completes
- Cross-process coordination is still handled by file locks during downloads

This fixes the "signal: killed" test failures in CI for:
- oci_chart_pull_direct
- oci_chart_pull_once
- oci_chart_pull_once2

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: resolve deadlock by releasing OCI chart locks immediately after download

This commit simplifies the OCI chart locking mechanism to fix deadlock
issues that occurred when multiple releases shared the same chart.

Problem:
When multiple releases used the same OCI chart, workers in PrepareCharts
would deadlock because:
1. Worker 1 acquires lock for chart/path, downloads chart
2. Worker 2 tries to acquire lock on same path, blocks waiting
3. PrepareCharts waits for all workers to complete
4. Worker 1's lock held until PrepareCharts finishes -> deadlock

Solution:
Release locks immediately after chart download completes. This is safe
because:
- The tempDir cleanup is deferred until after helm operations complete
  in withPreparedCharts(), so charts won't be deleted mid-use
- The in-memory chart cache prevents redundant downloads within a process
- Cross-process coordination via file locks still works during download

Changes:
- Remove chartLock field from chartPrepareResult struct
- Release locks immediately in getOCIChart() and forcedDownloadChart()
- Simplify PrepareCharts() by removing lock collection and release logic
- Update function signatures to return only (path, error)

This also fixes the "signal: killed" test failures in CI for:
- oci_chart_pull_direct
- oci_chart_pull_once
- oci_chart_pull_once2

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: add double-check locking for in-memory chart cache

When multiple workers concurrently process releases using the same chart,
they all check the in-memory cache before acquiring locks. If none have
populated the cache yet, all workers miss and try to download.

Previously, even after acquiring the exclusive lock, the code would
re-download the chart when needsRefresh=true (the default). This caused
multiple "Pulling" messages in tests like oci_chart_pull_once.

The fix adds a second in-memory cache check AFTER acquiring the lock.
This implements proper double-check locking:

1. Check cache (outside lock) → miss
2. Acquire lock
3. Check cache again (inside lock) → hit if another worker populated it
4. If still miss, download and add to cache

This ensures only one worker downloads the chart, while others use
the cached version populated by the first worker.

Changes:
- Add in-memory cache double-check in getOCIChart() after acquiring lock
- Add in-memory cache double-check in forcedDownloadChart() after acquiring lock

This fixes the oci_chart_pull_once and oci_chart_pull_direct test failures
where charts were being pulled multiple times instead of once.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: use callback to prevent redundant chart downloads within a process

When multiple workers concurrently process releases using the same chart,
they need to coordinate to avoid redundant downloads. The previous fix
set SkipRefresh=true for OCI charts, which prevented legitimate refresh
scenarios (e.g., floating tags).

This commit implements a better solution using a callback mechanism:

1. acquireChartLock() now accepts an optional skipRefreshCheck callback
2. Before deleting a cached chart for refresh, the callback is invoked
3. If the callback returns true (in-memory cache has the chart), skip refresh
4. This allows deduplication within a process while respecting cross-run refresh

The flow is now:
- Worker 1 downloads chart, adds to in-memory cache, releases lock
- Worker 2 acquires lock, sees needsRefresh=true, but callback sees
  in-memory cache is populated → uses cached instead of deleting

This correctly handles:
- Within-process deduplication: only one download per chart
- Cross-run refresh: respects --skip-refresh flag for floating tags
- Immutable versions: cached and reused as expected

Changes:
- Add skipRefreshCheck callback parameter to acquireChartLock()
- Update getOCIChart() to pass in-memory cache check callback
- Update forcedDownloadChart() to pass in-memory cache check callback
- Remove SkipRefresh=true workaround for OCI charts

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: address Copilot review comments on PR #2298

This commit addresses the automated review comments from GitHub Copilot:

1. pkg/state/state.go: Add nil check for logger in Release() method
   to prevent potential nil pointer dereference when logger is nil.

2. pkg/state/state.go: Fix misleading comment about "external callers"
   to accurately reflect that Logger() is used by the app package.

3. pkg/state/issue_2296_test.go: Add comment noting that boolPtr helper
   is already defined in skip_test.go (shared across test files).

4. test/integration/test-cases/oci-parallel-pull.sh: Replace hardcoded
   /tmp paths with a dedicated temp directory for test outputs. Add
   cleanup for the output directory in the cleanup function.

5. test/integration/test-cases/issue-2297-local-chart-transformers.sh:
   Add cleanup trap to remove temp directory on exit, preventing
   leftover files from accumulating.

6. Remove dead code: The chartLocks map in PrepareCharts was always
   empty since locks are released immediately after download. Removed
   the unused return value and corresponding handling in run.go to
   improve code clarity and maintainability.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: make oci-parallel-pull test resilient to registry issues

The integration test was intermittently failing in CI due to Docker Hub
rate limiting or network issues. These failures are not helmfile bugs.

Changes:
- Add is_registry_error() function to detect external registry issues
  (rate limits, network timeouts, connection refused, etc.)
- Check for the race condition bug (issue #2295) first and fail fast
- If other failures occur, check if they're registry-related
- Skip test gracefully when registry issues are detected instead of
  failing CI on external infrastructure problems

This ensures the test still catches the actual race condition bug while
not causing false failures due to Docker Hub rate limits in CI.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: make oci-parallel-pull test resilient to registry issues

The integration test was failing in CI for two reasons:

1. Docker Hub rate limiting or network issues causing helmfile to fail
2. The test script exits early due to `set -e` when `wait` returns non-zero

Changes:
- Use `wait $pid || exit=$?` pattern to capture exit codes without triggering
  set -e. When wait returns non-zero, the || branch captures the exit code
  into the variable, preventing script termination.
- Add is_registry_error() function to detect external registry issues
  (rate limits, network timeouts, connection refused, etc.)
- Check for the race condition bug (issue #2295) first and fail fast
- Skip test gracefully when registry issues are detected instead of
  failing CI on external infrastructure problems

This ensures the test still catches the actual race condition bug while
not causing false failures due to Docker Hub rate limits in CI.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: address PR #2298 review - reinitialize fileLock after release

Address Copilot review comments:

1. pkg/state/state.go: Reinitialize fileLock after releasing shared lock
   When upgrading from shared to exclusive lock, the fileLock needs to be
   reinitialized with flock.New() after calling Release(). This ensures
   a fresh flock object is used for the exclusive lock acquisition.

2. test/integration/test-cases/oci-parallel-pull.sh: Add lock file
   verification warning if no lock files are found, to ensure the
   locking mechanism is actually being tested.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: address PR #2298 Copilot review comments (round 4)

Address 8 Copilot review comments:

1. pkg/state/state.go: Release in-process mutex during retry backoff
   to avoid blocking other goroutines for up to 90 seconds.

2. pkg/state/state.go: Include chartPath in shared lock error message
   for better debugging.

3. pkg/state/state.go: Document that extractRegistryHost does not handle
   URLs with query parameters or fragments (uncommon for OCI registries).

4. pkg/state/state.go: Document that skipRefreshCheck callback should be
   fast and non-blocking since it runs while holding exclusive lock.

5. oci-parallel-pull.sh: Use case-insensitive grep (-i flag) to catch
   error variations like "I/O timeout".

6. helmfile.yaml: Expand comment explaining why library charts can't be
   used for this test (they can't be templated by Helm).

Skipped (with justification):
- PrepareChartKey helper: Only 2 usages with different source structs
- Context reuse in retry: Per-attempt contexts provide clearer semantics

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: address PR #2298 Copilot review comments (round 5)

1. Make race condition detection grep more robust (oci-parallel-pull.sh)
   - Use case-insensitive extended regex (-iqE)
   - Add multiple pattern variations to catch different tar/helm versions

2. Remove unused Logger() method from HelmState (state.go)
   - Method was never called; all lock releases use st.logger directly

3. Add clarifying comments for lock retry behavior (state.go)
   - Document why file system errors are retried but timeouts are not
   - Explain flock returns (false, nil) on context deadline exceeded

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: clarify lock file check is informational only

Lock files are ephemeral and may be cleaned up immediately after
helmfile processes complete. Update comments and warning message
to make clear their absence doesn't indicate locking wasn't used.

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

* fix: add HELM_BIN env var to Dockerfiles

The helm-git plugin requires HELM_BIN environment variable to be set.
Without it, the plugin fails with "HELM_BIN: parameter not set".

Add HELM_BIN=/usr/local/bin/helm to all Dockerfile variants.

Fixes #2303

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>

---------

Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
2025-11-27 22:13:03 +08:00

4916 lines
117 KiB
Go

package state
import (
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/Masterminds/semver/v3"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/exectest"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/testhelper"
)
var logger = helmexec.NewLogger(io.Discard, "warn")
var valsRuntime, _ = vals.New(vals.Options{CacheSize: 32})
func injectFs(st *HelmState, fs *testhelper.TestFs) *HelmState {
st.fs = fs.ToFileSystem()
return st
}
func TestLabelParsing(t *testing.T) {
cases := []struct {
labelString string
expectedFilter LabelFilter
errorExected bool
}{
{"foo=bar", LabelFilter{positiveLabels: [][]string{{"foo", "bar"}}, negativeLabels: [][]string{}}, false},
{"foo!=bar", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{{"foo", "bar"}}}, false},
{"foo!=bar,baz=bat", LabelFilter{positiveLabels: [][]string{{"baz", "bat"}}, negativeLabels: [][]string{{"foo", "bar"}}}, false},
{"foo", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{}}, true},
{"foo!=bar=baz", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{}}, true},
{"=bar", LabelFilter{positiveLabels: [][]string{}, negativeLabels: [][]string{}}, true},
}
for idx, c := range cases {
filter, err := ParseLabels(c.labelString)
if err != nil && !c.errorExected {
t.Errorf("[%d] Didn't expect an error parsing labels: %s", idx, err)
} else if err == nil && c.errorExected {
t.Errorf("[%d] Expected %s to result in an error but got none", idx, c.labelString)
} else if !reflect.DeepEqual(filter, c.expectedFilter) {
t.Errorf("[%d] parsed label did not result in expected filter: %v, expected: %v", idx, filter, c.expectedFilter)
}
}
}
func TestHelmState_applyDefaultsTo(t *testing.T) {
type fields struct {
BaseChartPath string
Context string
Namespace string
Repositories []RepositorySpec
Releases []ReleaseSpec
}
type args struct {
spec ReleaseSpec
}
verify := false
specWithNamespace := ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &verify,
Name: "test-charts",
Namespace: "test-namespace",
Values: nil,
SetValues: nil,
EnvValues: nil,
}
specWithoutNamespace := specWithNamespace
specWithoutNamespace.Namespace = ""
specWithNamespaceFromFields := specWithNamespace
specWithNamespaceFromFields.Namespace = "test-namespace-field"
fieldsWithNamespace := fields{
BaseChartPath: ".",
Context: "test_context",
Namespace: specWithNamespaceFromFields.Namespace,
Repositories: nil,
Releases: []ReleaseSpec{
specWithNamespace,
},
}
fieldsWithoutNamespace := fieldsWithNamespace
fieldsWithoutNamespace.Namespace = ""
tests := []struct {
name string
fields fields
args args
want ReleaseSpec
}{
{
name: "Has a namespace from spec",
fields: fieldsWithoutNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespace,
},
{
name: "Has a namespace from flags and from spec",
fields: fieldsWithNamespace,
args: args{
spec: specWithNamespace,
},
want: specWithNamespaceFromFields,
},
{
name: "Spec and flag Has no a namespace",
fields: fieldsWithoutNamespace,
args: args{
spec: specWithoutNamespace,
},
want: specWithoutNamespace,
},
{
name: "Spec has no a namespace but from flag",
fields: fieldsWithNamespace,
args: args{
spec: specWithoutNamespace,
},
want: specWithNamespaceFromFields,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
basePath: tt.fields.BaseChartPath,
ReleaseSetSpec: ReleaseSetSpec{
OverrideNamespace: tt.fields.Namespace,
Repositories: tt.fields.Repositories,
Releases: tt.fields.Releases,
},
}
if state.ApplyOverrides(&tt.args.spec); !reflect.DeepEqual(tt.args.spec, tt.want) {
t.Errorf("HelmState.ApplyOverrides() = %v, want %v", tt.args.spec, tt.want)
}
})
}
}
func boolValue(v bool) *bool {
return &v
}
func TestHelmState_flagsForUpgrade(t *testing.T) {
enable := true
disable := false
postRendererDefault := "foo-default.sh"
postRendererRelease := "foo-release.sh"
some := func(v int) *int {
return &v
}
tests := []struct {
name string
version *semver.Version
defaults HelmSpec
release *ReleaseSpec
syncOpts *SyncOpts
want []string
wantErr string
}{
{
name: "no-options",
defaults: HelmSpec{
Verify: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "verify",
defaults: HelmSpec{
Verify: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--verify",
"--namespace", "test-namespace",
},
},
{
name: "verify-from-default",
defaults: HelmSpec{
Verify: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "enable-dns",
defaults: HelmSpec{
EnableDNS: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
EnableDNS: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--enable-dns",
"--namespace", "test-namespace",
},
},
{
name: "enable-dns-from-default",
defaults: HelmSpec{
EnableDNS: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
EnableDNS: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "force",
defaults: HelmSpec{
Force: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--force",
"--namespace", "test-namespace",
},
},
{
name: "force-from-default",
defaults: HelmSpec{
Force: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Force: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "recreate-pods",
defaults: HelmSpec{
RecreatePods: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
RecreatePods: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--recreate-pods",
"--namespace", "test-namespace",
},
},
{
name: "recreate-pods-from-default",
defaults: HelmSpec{
RecreatePods: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
RecreatePods: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "wait",
defaults: HelmSpec{
Wait: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Wait: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--wait",
"--namespace", "test-namespace",
},
},
{
name: "wait-for-jobs",
defaults: HelmSpec{
WaitForJobs: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
WaitForJobs: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--wait-for-jobs",
"--namespace", "test-namespace",
},
},
{
name: "devel",
defaults: HelmSpec{
Devel: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Wait: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--devel",
"--wait",
"--namespace", "test-namespace",
},
},
{
name: "devel-release",
defaults: HelmSpec{
Devel: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Devel: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "wait-from-default",
defaults: HelmSpec{
Wait: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Wait: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "timeout",
defaults: HelmSpec{
Timeout: 0,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Timeout: some(123),
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--timeout", "123s",
"--namespace", "test-namespace",
},
},
{
name: "timeout-from-default",
defaults: HelmSpec{
Timeout: 123,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Timeout: nil,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--timeout", "123s",
"--namespace", "test-namespace",
},
},
{
name: "timeout-from-cli-flag",
defaults: HelmSpec{
Timeout: 123,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Timeout: some(456),
Name: "test-charts",
Namespace: "test-namespace",
},
syncOpts: &SyncOpts{
Timeout: 789,
},
want: []string{
"--version", "0.1",
"--timeout", "789s",
"--namespace", "test-namespace",
},
},
{
name: "atomic",
defaults: HelmSpec{
Atomic: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Atomic: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--atomic",
"--namespace", "test-namespace",
},
},
{
name: "atomic-override-default",
defaults: HelmSpec{
Atomic: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Atomic: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "atomic-from-default",
defaults: HelmSpec{
Atomic: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--atomic",
"--namespace", "test-namespace",
},
},
{
name: "cleanup-on-fail",
defaults: HelmSpec{
CleanupOnFail: false,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
CleanupOnFail: &enable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--cleanup-on-fail",
"--namespace", "test-namespace",
},
},
{
name: "cleanup-on-fail-override-default",
defaults: HelmSpec{
CleanupOnFail: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
CleanupOnFail: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "cleanup-on-fail-from-default",
defaults: HelmSpec{
CleanupOnFail: true,
},
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--cleanup-on-fail",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-default-helm3.2",
defaults: HelmSpec{
Verify: false,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--create-namespace",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-disabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-release-override-enabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &disable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &enable,
},
want: []string{
"--version", "0.1",
"--create-namespace",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-release-override-disabled-helm3.2",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.2.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--namespace", "test-namespace",
},
},
{
name: "create-namespace-unsupported",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("2.16.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
},
wantErr: "releases[].createNamespace requires Helm 3.2.0 or greater",
},
{
name: "post-renderer-flags-use-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRenderer: &postRendererDefault,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererDefault,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-flags-use-release",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRenderer: &postRendererRelease,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererRelease,
"--namespace", "test-namespace",
},
},
{
name: "post-renderer-flags-use-release-prior-helmdefault",
defaults: HelmSpec{
Verify: false,
CreateNamespace: &enable,
PostRenderer: &postRendererDefault,
},
version: semver.MustParse("3.10.0"),
release: &ReleaseSpec{
Chart: "test/chart",
Version: "0.1",
Verify: &disable,
Name: "test-charts",
Namespace: "test-namespace",
CreateNamespace: &disable,
PostRenderer: &postRendererRelease,
},
want: []string{
"--version", "0.1",
"--post-renderer", postRendererRelease,
"--namespace", "test-namespace",
},
},
}
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
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: "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",
},
},
}
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.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: []string{
"A",
"B",
},
},
},
},
},
helm: &exectest.Helm{},
wantReleases: []exectest.Release{{Name: "releaseName", Flags: []string{"--set", "foo.bar[0]={A,B}", "--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: []string{
"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 := "/src"
state := &HelmState{
basePath: basePath,
FilePath: "/src/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`,
"/src/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_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 != "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 != "custom-lock-file" {
return nil, fmt.Errorf("stub: unexpected file: %s", f)
}
return nil, os.ErrNotExist
},
},
}
_, err := state.ResolveDeps()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestHelmState_ReleaseStatuses(t *testing.T) {
tests := []struct {
name string
releases []ReleaseSpec
helm *exectest.Helm
want []exectest.Release
wantErr bool
}{
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "releaseA",
},
},
helm: &exectest.Helm{},
want: []exectest.Release{
{Name: "releaseA", Flags: []string{}},
},
},
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "error",
},
},
helm: &exectest.Helm{},
wantErr: true,
},
{
name: "complain missing values file for desired release",
releases: []ReleaseSpec{
{
Name: "error",
Values: []any{
"foo.yaml",
},
},
},
helm: &exectest.Helm{},
wantErr: true,
},
{
name: "should not complain missing values file for undesired release",
releases: []ReleaseSpec{
{
Name: "error",
Values: []any{
"foo.yaml",
},
Installed: boolValue(false),
},
},
helm: &exectest.Helm{},
wantErr: false,
},
}
for i := range tests {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
fs: &filesystem.FileSystem{
FileExists: func(f string) (bool, error) {
if f != "foo.yaml" {
return false, fmt.Errorf("unexpected file: %s", f)
}
return true, nil
},
ReadFile: func(f string) ([]byte, error) {
if f != "foo.yaml" {
return nil, fmt.Errorf("unexpected file: %s", f)
}
return []byte{}, nil
},
},
}
errs := state.ReleaseStatuses(tt.helm, 1)
if (errs != nil) != tt.wantErr {
t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
if !reflect.DeepEqual(tt.helm.Releases, tt.want) {
t.Errorf("HelmState.ReleaseStatuses() for [%s] = %v, want %v", tt.name, tt.helm.Releases, tt.want)
}
}
t.Run(tt.name, f)
}
}
func TestHelmState_TestReleasesNoCleanUp(t *testing.T) {
tests := []struct {
name string
cleanup bool
releases []ReleaseSpec
helm *exectest.Helm
want []exectest.Release
wantErr bool
}{
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "releaseA",
},
},
helm: &exectest.Helm{},
want: []exectest.Release{{Name: "releaseA", Flags: []string{"--timeout", "1s"}}},
},
{
name: "do cleanup",
cleanup: true,
releases: []ReleaseSpec{
{
Name: "releaseB",
},
},
helm: &exectest.Helm{},
want: []exectest.Release{{Name: "releaseB", Flags: []string{"--timeout", "1s"}}},
},
{
name: "happy path",
releases: []ReleaseSpec{
{
Name: "error",
},
},
helm: &exectest.Helm{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: tt.releases,
},
logger: logger,
}
errs := state.TestReleases(tt.helm, tt.cleanup, 1, 1)
if (errs != nil) != tt.wantErr {
t.Errorf("TestReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
if !reflect.DeepEqual(tt.helm.Releases, tt.want) {
t.Errorf("HelmState.TestReleases() for [%s] = %v, want %v", tt.name, tt.helm.Releases, tt.want)
}
})
}
}
func TestConditionEnabled(t *testing.T) {
tests := []struct {
name string
condition string
values map[string]any
want bool
wantErr bool
}{
{
name: "enabled",
condition: "foo.enabled",
values: map[string]any{
"foo": map[string]any{
"enabled": true,
},
},
want: true,
},
{
name: "disabled",
condition: "foo.enabled",
values: map[string]any{
"foo": map[string]any{
"enabled": false,
},
},
want: false,
},
{
name: "typo in condition",
condition: "fooo.enabled",
values: map[string]any{
"foo": map[string]any{
"enabled": true,
},
},
want: false,
wantErr: true,
},
{
name: "missing enabled",
condition: "foo.enabled",
values: map[string]any{
"foo": map[string]any{
"something else": false,
},
},
want: false,
},
{
name: "foo nil",
condition: "foo.enabled",
values: map[string]any{
"foo": nil,
},
want: false,
wantErr: true,
},
{
name: "foo missing",
condition: "foo.enabled",
values: map[string]any{},
want: false,
wantErr: true,
},
{
name: "wrong suffix",
condition: "services.foo_enabled",
want: false,
wantErr: true,
},
{
name: "too short condition",
condition: "rnd42",
want: false,
wantErr: true,
},
{
name: "nested values",
condition: "rnd42.really.enabled",
values: map[string]any{
"rnd42": map[string]any{
"really": map[string]any{
"enabled": true,
},
},
},
want: true,
wantErr: false,
},
{
name: "nested values enabled missing",
condition: "rnd42.really.ok",
want: false,
wantErr: true,
},
{
name: "nested values unknown key",
condition: "rnd42.unknown.enabled",
values: map[string]any{
"rnd42": map[string]any{
"really": map[string]any{
"enabled": true,
},
},
},
want: false,
wantErr: true,
},
{
name: "nested values invalid type",
condition: "rnd42.invalid.enabled",
values: map[string]any{
"rnd42": map[string]any{
"invalid": "hello",
"really": map[string]any{
"enabled": true,
},
},
},
want: false,
wantErr: true,
},
{
name: "empty",
condition: "",
want: true,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
res, err := ConditionEnabled(ReleaseSpec{Condition: tt.condition}, tt.values)
if tt.wantErr {
if err == nil {
t.Errorf("ConditionEnabled() for %s expected err response", tt.name)
}
return
}
if err != nil {
t.Errorf("ConditionEnabled() for %s unexpected err %v", tt.name, err)
}
if res != tt.want {
t.Errorf("ConditionEnabled() for %s = %v, want %v", tt.name, res, tt.want)
}
})
}
}
func TestHelmState_NoReleaseMatched(t *testing.T) {
releases := []ReleaseSpec{
{
Name: "releaseA",
Labels: map[string]string{
"foo": "bar",
},
},
}
tests := []struct {
name string
labels string
wantErr bool
}{
{
name: "happy path",
labels: "foo=bar",
wantErr: false,
},
{
name: "name does not exist",
labels: "name=releaseB",
wantErr: false,
},
{
name: "label does not match anything",
labels: "foo=notbar",
wantErr: false,
},
}
for i := range tests {
tt := tests[i]
f := func(t *testing.T) {
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
},
logger: logger,
RenderedValues: map[string]any{},
}
state.Selectors = []string{tt.labels}
errs := state.FilterReleases(false)
if (errs != nil) != tt.wantErr {
t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
}
t.Run(tt.name, f)
}
}
func TestHelmState_Delete(t *testing.T) {
tests := []struct {
name string
deleted []exectest.Release
wantErr bool
desired *bool
installed bool
purge bool
flags string
namespace string
kubeContext string
defKubeContext string
deleteWait bool
deleteTimeout int
}{
{
name: "delete wait enabled",
deleteWait: true,
wantErr: false,
desired: boolValue(true),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--wait"}}},
},
{
name: "delete wait with deleteTimeout",
deleteWait: true,
deleteTimeout: 800,
wantErr: false,
desired: boolValue(true),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--wait", "--timeout", "800s"}}},
},
{
name: "desired and installed (purge=false)",
wantErr: false,
desired: boolValue(true),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired(default) and installed (purge=false)",
wantErr: false,
desired: nil,
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired(default) and installed (purge=false) but error",
wantErr: true,
desired: nil,
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired and installed (purge=true)",
wantErr: false,
desired: boolValue(true),
installed: true,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired but not installed (purge=false)",
wantErr: false,
desired: boolValue(true),
installed: false,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "desired but not installed (purge=true)",
wantErr: false,
desired: boolValue(true),
installed: false,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "installed but filtered (purge=false)",
wantErr: false,
desired: boolValue(false),
installed: true,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "installed but filtered (purge=true)",
wantErr: false,
desired: boolValue(false),
installed: true,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "not installed, and filtered (purge=false)",
wantErr: false,
desired: boolValue(false),
installed: false,
purge: false,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "not installed, and filtered (purge=true)",
wantErr: false,
desired: boolValue(false),
installed: false,
purge: true,
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{}}},
},
{
name: "with kubecontext",
wantErr: false,
desired: nil,
installed: true,
purge: true,
kubeContext: "ctx",
flags: "--kube-contextctx",
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--kube-context", "ctx"}}},
},
{
name: "with default kubecontext",
wantErr: false,
desired: nil,
installed: true,
purge: true,
defKubeContext: "defctx",
flags: "--kube-contextdefctx",
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--kube-context", "defctx"}}},
},
{
name: "with non-default and default kubecontexts",
wantErr: false,
desired: nil,
installed: true,
purge: true,
kubeContext: "ctx",
defKubeContext: "defctx",
flags: "--kube-contextctx",
deleted: []exectest.Release{{Name: "releaseA", Flags: []string{"--kube-context", "ctx"}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name := "releaseA"
if tt.wantErr {
name = "releaseA-error"
}
release := ReleaseSpec{
Name: name,
Installed: tt.desired,
Namespace: tt.namespace,
KubeContext: tt.kubeContext,
}
releases := []ReleaseSpec{
release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
KubeContext: tt.defKubeContext,
DeleteWait: tt.deleteWait,
DeleteTimeout: tt.deleteTimeout,
},
Releases: releases,
},
logger: logger,
RenderedValues: map[string]any{},
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
Deleted: []exectest.Release{},
}
if tt.installed {
helm.Lists[exectest.ListKey{Filter: "^" + name + "$", Flags: tt.flags}] = name
}
affectedReleases := AffectedReleases{}
errs := state.DeleteReleases(&affectedReleases, helm, 1, tt.purge, "")
if errs != nil {
if !tt.wantErr || len(affectedReleases.DeleteFailed) != 1 || affectedReleases.DeleteFailed[0].Name != release.Name {
t.Errorf("DeleteReleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr)
return
}
} else if !(reflect.DeepEqual(tt.deleted, helm.Deleted) && (len(affectedReleases.Deleted) == len(tt.deleted))) {
t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.Deleted)
}
})
}
}
func TestDiffpareSyncReleases(t *testing.T) {
tests := []struct {
name string
flags []string
diffOptions *DiffOpts
helmDefaults *HelmSpec
}{
{
name: "reuse-values",
flags: []string{"--reuse-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "reset-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{},
helmDefaults: &HelmSpec{},
},
{
name: "default-reuse-values",
flags: []string{"--reuse-values"},
diffOptions: &DiffOpts{},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "force-reset-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "both-reset-reuse-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "both-reset-reuse-default-reuse-values",
flags: []string{"--reset-values"},
diffOptions: &DiffOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
}
for _, tt := range tests {
release := ReleaseSpec{
Name: tt.name,
}
releases := []ReleaseSpec{
release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
HelmDefaults: *tt.helmDefaults,
},
logger: logger,
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
results, es := state.prepareDiffReleases(helm, []string{}, 1, false, false, false, []string{}, false, false, false, tt.diffOptions)
require.Len(t, es, 0)
require.Len(t, results, 1)
r := results[0]
require.Equal(t, tt.flags, r.flags)
}
}
func TestPrepareSyncReleases(t *testing.T) {
tests := []struct {
name string
flags []string
syncOptions *SyncOpts
helmDefaults *HelmSpec
}{
{
name: "reuse-values",
flags: []string{"--reuse-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "reset-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{},
helmDefaults: &HelmSpec{},
},
{
name: "reuse-default-values",
flags: []string{"--reuse-values"},
syncOptions: &SyncOpts{},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "force-reset-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
{
name: "both-reset-reuse-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{},
},
{
name: "both-reset-reuse-default-reuse-values",
flags: []string{"--reset-values"},
syncOptions: &SyncOpts{
ReuseValues: true,
ResetValues: true,
},
helmDefaults: &HelmSpec{
ReuseValues: true,
},
},
}
for _, tt := range tests {
release := ReleaseSpec{
Name: tt.name,
}
releases := []ReleaseSpec{
release,
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Releases: releases,
HelmDefaults: *tt.helmDefaults,
},
logger: logger,
valsRuntime: valsRuntime,
}
helm := &exectest.Helm{
Lists: map[exectest.ListKey]string{},
}
results, es := state.prepareSyncReleases(helm, []string{}, 1, tt.syncOptions)
require.Len(t, es, 0)
require.Len(t, results, 1)
r := results[0]
require.Equal(t, tt.flags, r.flags)
}
}
func TestReverse(t *testing.T) {
num := 8
st := &HelmState{}
for i := 0; i < num; i++ {
name := fmt.Sprintf("%d", i)
st.Helmfiles = append(st.Helmfiles, SubHelmfileSpec{
Path: name,
})
st.Releases = append(st.Releases, ReleaseSpec{
Name: name,
})
}
st.Reverse()
for i := 0; i < num; i++ {
j := num - 1 - i
want := fmt.Sprintf("%d", j)
if got := st.Helmfiles[i].Path; got != want {
t.Errorf("sub-helmfile at %d has incorrect path: want %q, got %q", i, want, got)
}
if got := st.Releases[i].Name; got != want {
t.Errorf("release at %d has incorrect name: want %q, got %q", i, want, got)
}
}
}
func Test_gatherUsernamePassword(t *testing.T) {
type args struct {
repoName string
username string
password string
}
tests := []struct {
name string
args args
envUsernameKey string
envUsernameValue string
envPasswordKey string
envPasswordValue string
wantUsername string
wantPassword string
}{
{
name: "pass username/password from args",
args: args{
repoName: "myRegistry",
username: "username1",
password: "password1",
},
wantUsername: "username1",
wantPassword: "password1",
},
{
name: "repoName does not contain hyphen, read username/password from environment variables",
args: args{
repoName: "myRegistry",
},
envUsernameKey: "MYREGISTRY_USERNAME",
envUsernameValue: "username2",
envPasswordKey: "MYREGISTRY_PASSWORD",
envPasswordValue: "password2",
wantUsername: "username2",
wantPassword: "password2",
},
{
name: "repoName contain hyphen, read username/password from environment variables",
args: args{
repoName: "my-registry",
},
envUsernameKey: "MY_REGISTRY_USERNAME",
envUsernameValue: "username3",
envPasswordKey: "MY_REGISTRY_PASSWORD",
envPasswordValue: "password3",
wantUsername: "username3",
wantPassword: "password3",
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
if tt.envUsernameKey != "" && tt.envUsernameValue != "" {
t.Setenv(tt.envUsernameKey, tt.envUsernameValue)
}
if tt.envPasswordKey != "" && tt.envPasswordValue != "" {
t.Setenv(tt.envPasswordKey, tt.envPasswordValue)
}
gotUsername, gotPassword := gatherUsernamePassword(tt.args.repoName, tt.args.username, tt.args.password)
if gotUsername != tt.wantUsername || gotPassword != tt.wantPassword {
t.Errorf("gatherUsernamePassword() = got username/password %v/%v, want username/password %v/%v", gotUsername, gotPassword, tt.wantUsername, tt.wantPassword)
}
})
}
}
func Test_extractRegistryHost(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "ECR with chart path",
url: "123456789012.dkr.ecr.us-east-1.amazonaws.com/helm-charts",
want: "123456789012.dkr.ecr.us-east-1.amazonaws.com",
},
{
name: "GHCR with nested path",
url: "ghcr.io/deliveryhero/helm-charts",
want: "ghcr.io",
},
{
name: "registry with port and path",
url: "registry.example.com:5000/helm-charts",
want: "registry.example.com:5000",
},
{
name: "registry without path",
url: "registry.example.com",
want: "registry.example.com",
},
{
name: "with oci:// prefix and path",
url: "oci://ghcr.io/charts/nginx",
want: "ghcr.io",
},
{
name: "with https:// prefix and path",
url: "https://registry.example.com/helm-charts",
want: "registry.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extractRegistryHost(tt.url); got != tt.want {
t.Errorf("extractRegistryHost() = %v, want %v", got, tt.want)
}
})
}
}
func TestHelmState_SyncRepos_OCI(t *testing.T) {
tests := []struct {
name string
repos []RepositorySpec
wantRegistryLoginHost string
}{
{
name: "OCI registry with chart path should extract host only",
repos: []RepositorySpec{
{
Name: "ecr",
URL: "123456789012.dkr.ecr.us-east-1.amazonaws.com/helm-charts",
OCI: true,
Username: "AWS",
Password: "token",
},
},
wantRegistryLoginHost: "123456789012.dkr.ecr.us-east-1.amazonaws.com",
},
{
name: "OCI registry without path should pass URL as-is",
repos: []RepositorySpec{
{
Name: "ghcr",
URL: "ghcr.io",
OCI: true,
Username: "user",
Password: "pass",
},
},
wantRegistryLoginHost: "ghcr.io",
},
{
name: "OCI registry with nested path",
repos: []RepositorySpec{
{
Name: "docker",
URL: "registry-1.docker.io/bitnamicharts",
OCI: true,
Username: "user",
Password: "pass",
},
},
wantRegistryLoginHost: "registry-1.docker.io",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
helm := &exectest.Helm{}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: tt.repos,
},
}
_, err := state.SyncRepos(helm, map[string]bool{})
if err != nil {
t.Errorf("SyncRepos() error = %v", err)
return
}
if helm.RegistryLoginHost != tt.wantRegistryLoginHost {
t.Errorf("RegistryLogin was called with host = %q, want %q", helm.RegistryLoginHost, tt.wantRegistryLoginHost)
}
})
}
}
func TestGenerateOutputFilePath(t *testing.T) {
tests := []struct {
envName string
filePath string
releaseName string
outputFileTemplate string
wantErr bool
expected string
}{
{
envName: "dev",
releaseName: "release1",
filePath: "/path/to/helmfile.yaml",
outputFileTemplate: "helmfile-{{ .Environment.Name }}.yaml",
expected: "helmfile-dev.yaml",
},
{
envName: "error",
releaseName: "release2",
filePath: "helmfile.yaml",
outputFileTemplate: "helmfile-{{ .Environment.Name",
wantErr: true,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.envName, func(t *testing.T) {
st := &HelmState{
FilePath: tt.envName,
ReleaseSetSpec: ReleaseSetSpec{
Env: environment.Environment{
Name: tt.envName,
},
},
}
ra := &ReleaseSpec{
Name: tt.releaseName,
}
got, err := st.GenerateOutputFilePath(ra, tt.outputFileTemplate)
if tt.wantErr {
require.Errorf(t, err, "GenerateOutputFilePath() error = %v, want error", err)
} else {
require.NoError(t, err, "GenerateOutputFilePath() error = %v, want nil", err)
}
require.Equalf(t, got, tt.expected, "GenerateOutputFilePath() got = %v, want %v", got, tt.expected)
})
}
}
func TestFullFilePath(t *testing.T) {
fs := testhelper.NewTestFs(map[string]string{})
tests := []struct {
basePath string
filePath string
fs *filesystem.FileSystem
expected string
}{
{
basePath: ".",
filePath: "helmfile.yaml",
expected: "helmfile.yaml",
},
{
basePath: "./test-1/",
filePath: "helmfile.yaml",
expected: "test-1/helmfile.yaml",
},
{
basePath: "/test-2/",
filePath: "helmfile.yaml",
expected: "/test-2/helmfile.yaml",
},
{
basePath: "./test-3/",
filePath: "helmfile.yaml",
fs: fs.ToFileSystem(),
expected: "/path/to/test-3/helmfile.yaml",
},
{
basePath: "/test-4/",
filePath: "helmfile.yaml",
fs: fs.ToFileSystem(),
expected: "/path/to/test-4/helmfile.yaml",
},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
st := &HelmState{
basePath: tt.basePath,
FilePath: tt.filePath,
fs: tt.fs,
}
actual, err := st.FullFilePath()
require.Equalf(t, actual, tt.expected, "FullFilePath() got = %v, want %v", actual, tt.expected)
require.Equalf(t, err, nil, "error %v", err)
})
}
}
func TestGetOCIQualifiedChartName(t *testing.T) {
devel := true
tests := []struct {
state HelmState
expected []struct {
qualifiedChartName string
chartName string
chartVersion string
}
helmVersion string
wantErr bool
}{
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "0.1.2",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name:0.1.2", "chart-name", "0.1.2"},
},
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "latest",
},
},
},
},
helmVersion: "3.13.3",
wantErr: true,
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Version: "latest",
},
},
},
},
helmVersion: "3.7.0",
wantErr: true, // Now rejects "latest" for all Helm versions
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/chart-path",
OCI: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chart-name",
Version: "0.1.2",
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name:0.1.2", "chart-name", "0.1.2"},
},
},
{
state: HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{},
Releases: []ReleaseSpec{
{
Chart: "oci://registry/chart-path/chart-name",
Devel: &devel,
},
},
},
},
helmVersion: "3.13.3",
expected: []struct {
qualifiedChartName string
chartName string
chartVersion string
}{
{"registry/chart-path/chart-name", "chart-name", ""},
},
},
}
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,
},
}
for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
got, err := 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 TestCommonDiffFlags(t *testing.T) {
tests := []struct {
name string
// stripTrailingCR is a flag to strip trailing carriage returns from the output
stripTrailingCR bool
expected []string
}{
{
name: "stripTrailingCR enabled",
stripTrailingCR: true,
expected: []string{
"--strip-trailing-cr",
},
},
{
name: "stripTrailingCR disenabled",
expected: []string{},
},
}
for _, tt := range tests {
st := &HelmState{}
result := st.commonDiffFlags(false, tt.stripTrailingCR, false, []string{}, false, false, false, &DiffOpts{})
require.Equal(t, tt.expected, result)
}
}
func TestAppendChartDownloadFlags(t *testing.T) {
tests := []struct {
name string
spec *ReleaseSetSpec
expected []string
}{
{
name: "SkipTLSVerify in repo",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "testrepo",
URL: "registry/repo-path",
SkipTLSVerify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "testrepo/chartname",
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "PlainHttp in repo, SkipTLSVerify everywhere",
spec: &ReleaseSetSpec{
HelmDefaults: HelmSpec{
InsecureSkipTLSVerify: true,
},
Repositories: []RepositorySpec{
{
Name: "testrepo",
URL: "registry/repo-path",
SkipTLSVerify: true,
PlainHttp: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "testrepo/chartname",
InsecureSkipTLSVerify: true,
},
},
},
expected: []string{
"--plain-http",
},
},
{
name: "SkipTLSVerify in defaults",
spec: &ReleaseSetSpec{
HelmDefaults: HelmSpec{
InsecureSkipTLSVerify: true,
},
Releases: []ReleaseSpec{
{
Chart: "chartname",
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "SkipTLSVerify in release",
spec: &ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "chartname",
InsecureSkipTLSVerify: true,
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "PlainHttp in defaults",
spec: &ReleaseSetSpec{
HelmDefaults: HelmSpec{
PlainHttp: true,
},
Releases: []ReleaseSpec{
{
Chart: "chartname",
},
},
},
expected: []string{
"--plain-http",
},
},
{
name: "PlainHttp in release",
spec: &ReleaseSetSpec{
Releases: []ReleaseSpec{
{
Chart: "chartname",
PlainHttp: true,
},
},
},
expected: []string{
"--plain-http",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: *tt.spec,
}
result := st.appendChartDownloadFlags([]string{}, &st.Releases[0])
require.Equal(t, tt.expected, result)
})
}
}
func TestNeedsPlainHttp(t *testing.T) {
tests := []struct {
name string
release *ReleaseSpec
repo *RepositorySpec
defaults HelmSpec
expected bool
}{
{
name: "PlainHttp in Release",
release: &ReleaseSpec{
PlainHttp: true,
},
expected: true,
},
{
name: "PlainHttp in Repository",
repo: &RepositorySpec{
PlainHttp: true,
},
expected: true,
},
{
name: "PlainHttp in HelmDefaults",
defaults: HelmSpec{
PlainHttp: true,
},
expected: true,
},
{
name: "PlainHttp not set",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: tt.defaults,
},
}
require.Equal(t, tt.expected, st.needsPlainHttp(tt.release, tt.repo))
})
}
}
func TestNeedsInsecureSkipTLSVerify(t *testing.T) {
tests := []struct {
name string
release *ReleaseSpec
repo *RepositorySpec
defaults HelmSpec
expected bool
}{
{
name: "InsecureSkipTLSVerify in Release",
release: &ReleaseSpec{
InsecureSkipTLSVerify: true,
},
expected: true,
},
{
name: "SkipTLSVerify in Repository",
repo: &RepositorySpec{
SkipTLSVerify: true,
},
expected: true,
},
{
name: "InsecureSkipTLSVerify in HelmDefaults",
defaults: HelmSpec{
InsecureSkipTLSVerify: true,
},
expected: true,
},
{
name: "InsecureSkipTLSVerify not set",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: tt.defaults,
},
}
require.Equal(t, tt.expected, st.needsInsecureSkipTLSVerify(tt.release, tt.repo))
})
}
}
func TestHideChartURL(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"http://username:password@example.com/", "http://---:---@example.com/"},
{"http://example.com@", "http://---:---@"},
{"https://username:password@example.com/", "https://---:---@example.com/"},
{"https://username:@password@example.com/", "https://---:---@example.com/"},
{"https://username::password@example.com/", "https://---:---@example.com/"},
{"https://username:httpd@example.com/", "https://---:---@example.com/"},
{"https://username:httpsd@example.com/", "https://---:---@example.com/"},
{"https://example.com/", "https://example.com/"},
}
for _, test := range tests {
result, _ := hideChartCredentials(test.input)
if result != test.expected {
t.Errorf("For input '%s', expected '%s', but got '%s'", test.input, test.expected, result)
}
}
}
func Test_appendExtraDiffFlags(t *testing.T) {
tests := []struct {
name string
inputFlags []string
inputOpts *DiffOpts
inputDefaults []string
expected []string
}{
{
name: "Skipping default flags, because diffOpts is provided",
inputFlags: []string{"aaaaa"},
inputOpts: &DiffOpts{
DiffArgs: "-bbbb --cccc",
},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-bbbb", "--cccc"},
},
{
name: "Use default flags, because diffOpts is not provided",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-dddd", "--eeee"},
},
{
name: "Don't add non-flag arguments",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-d=ddd", "non-flag", "--eeee"},
expected: []string{"aaaaa", "-d=ddd", "--eeee"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := (&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
DiffArgs: test.inputDefaults,
},
},
}).appendExtraDiffFlags(test.inputFlags, test.inputOpts)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("For input %v %v, expected %v, but got %v", test.inputFlags, test.inputOpts, test.expected, result)
}
})
}
}
func Test_appendExtraSyncFlags(t *testing.T) {
tests := []struct {
name string
inputFlags []string
inputOpts *SyncOpts
inputDefaults []string
expected []string
}{
{
name: "Skipping default flags, because diffOpts is provided",
inputFlags: []string{"aaaaa"},
inputOpts: &SyncOpts{
SyncArgs: "-bbbb --cccc",
},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-bbbb", "--cccc"},
},
{
name: "Use default flags, because diffOpts is not provided",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-dddd", "--eeee"},
expected: []string{"aaaaa", "-dddd", "--eeee"},
},
{
name: "Don't add non-flag arguments",
inputFlags: []string{"aaaaa"},
inputDefaults: []string{"-d=ddd", "non-flag", "--eeee"},
expected: []string{"aaaaa", "-d=ddd", "--eeee"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := (&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
HelmDefaults: HelmSpec{
SyncArgs: test.inputDefaults,
},
},
}).appendExtraSyncFlags(test.inputFlags, test.inputOpts)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("For input %v %v, expected %v, but got %v", test.inputFlags, test.inputOpts, test.expected, result)
}
})
}
}
func TestHelmState_appendApiVersionsFlags(t *testing.T) {
tests := []struct {
name string
kubeVersion string
flags []string
expectedFlags []string
releaseKubeVersion string
releaseApiVersion []string
stateKubeVersion string
stateApiVersion []string
}{
{
name: "no kubeVersion is set",
expectedFlags: []string{},
},
{
name: "flags are kept",
flags: []string{"--flag1", "--flag2"},
expectedFlags: []string{"--flag1", "--flag2"},
},
{
name: "kubeVersion is set",
kubeVersion: "1.18.0",
expectedFlags: []string{"--kube-version", "1.18.0"},
},
{
name: "kubeVersion is set from release",
releaseKubeVersion: "1.19.0",
expectedFlags: []string{"--kube-version", "1.19.0"},
},
{
name: "kubeVersion from release hasn't priority",
kubeVersion: "1.18.0",
releaseKubeVersion: "1.19.0",
expectedFlags: []string{"--kube-version", "1.18.0"},
},
{
name: "kubeVersion is set from state",
stateKubeVersion: "1.18.0",
expectedFlags: []string{"--kube-version", "1.18.0"},
},
{
name: "kubeVersion from state hasn't priority",
stateKubeVersion: "1.18.0",
releaseKubeVersion: "1.19.0",
expectedFlags: []string{"--kube-version", "1.19.0"},
},
{
name: "kubeVersion priority",
stateKubeVersion: "1.18.0",
releaseKubeVersion: "1.19.0",
kubeVersion: "1.20.0",
expectedFlags: []string{"--kube-version", "1.20.0"},
},
{
name: "api-version are set from state",
stateApiVersion: []string{"v1,v2"},
expectedFlags: []string{"--api-versions", "v1,v2"},
},
{
name: "api-version are set from release",
releaseApiVersion: []string{"v1,v2"},
expectedFlags: []string{"--api-versions", "v1,v2"},
},
{
name: "api-version priority",
stateApiVersion: []string{"v1"},
releaseApiVersion: []string{"v2"},
expectedFlags: []string{"--api-versions", "v2"},
},
{
name: "api-version multiple values",
stateApiVersion: []string{"v1", "v2"},
expectedFlags: []string{"--api-versions", "v1", "--api-versions", "v2"},
},
{
name: "All kubeVersion and api-version are set",
kubeVersion: "1.18.0",
stateKubeVersion: "1.19.0",
releaseKubeVersion: "1.20.0",
stateApiVersion: []string{"v1"},
releaseApiVersion: []string{"v2"},
flags: []string{"--previous-flag-1", "--previous-flag-2"},
expectedFlags: []string{"--previous-flag-1", "--previous-flag-2", "--api-versions", "v2", "--kube-version", "1.18.0"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.expectedFlags == nil {
test.expectedFlags = []string{}
}
if test.flags == nil {
test.flags = []string{}
}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
KubeVersion: test.stateKubeVersion,
ApiVersions: test.stateApiVersion,
},
}
r := &ReleaseSpec{
KubeVersion: test.releaseKubeVersion,
ApiVersions: test.releaseApiVersion,
}
result := state.appendApiVersionsFlags(test.flags, r, test.kubeVersion)
assert.Equal(t, test.expectedFlags, result)
})
}
}
func TestGetOCIChartPath(t *testing.T) {
tests := []struct {
name string
tempDir string
release *ReleaseSpec
chartName string
chartVersion string
outputDirTemplate string
expectedPath string
expectedErr bool
}{
{
name: "OCI chart with template",
tempDir: "charts",
release: &ReleaseSpec{
Name: "karpenter",
Chart: "karpenter/karpenter",
},
chartName: "karpenter",
chartVersion: "0.37.0",
outputDirTemplate: "{{ .OutputDir }}/",
expectedPath: "charts/",
expectedErr: false,
},
{
name: "OCI chart with template containing unknown values",
tempDir: "charts",
release: &ReleaseSpec{
Name: "karpenter",
Chart: "karpenter/karpenter",
},
chartName: "karpenter",
chartVersion: "0.37.0",
outputDirTemplate: "{{ .SomethingThatDoesNotExist }}/",
expectedPath: "",
expectedErr: true,
},
{
name: "OCI chart without template",
tempDir: "charts",
release: &ReleaseSpec{
Name: "karpenter",
Chart: "karpenter/karpenter",
},
chartName: "karpenter",
chartVersion: "0.37.0",
expectedPath: "charts/karpenter/karpenter/0.37.0",
expectedErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{}
path, err := st.getOCIChartPath(tt.tempDir, tt.release, tt.chartName, tt.chartVersion, tt.outputDirTemplate)
if tt.expectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expectedPath, path)
}
})
}
}
func TestHelmState_chartOCIFlags(t *testing.T) {
tests := []struct {
name string
spec *ReleaseSetSpec
expected []string
}{
{
name: "CaFile enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
CaFile: "cafile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--ca-file",
"cafile",
},
},
{
name: "CertFile enabled and KeyFile disabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
CertFile: "certfile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{},
},
{
name: "CertFile disabled and KeyFile enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
KeyFile: "keyfile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{},
},
{
name: "CertFile and KeyFile enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
CertFile: "certfile",
KeyFile: "keyfile",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--cert-file",
"certfile",
"--key-file",
"keyfile",
},
},
{
name: "SkipTLSVerify enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
SkipTLSVerify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--insecure-skip-tls-verify",
},
},
{
name: "Verify enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
Verify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--verify",
},
},
{
name: "Keyring enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
Keyring: "keyring.pgp",
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--keyring",
"keyring.pgp",
},
},
{
name: "PlainHttp enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
PlainHttp: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--plain-http",
},
},
{
name: "PlainHttp and TLS options enabled",
spec: &ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "oci-repo",
URL: "registry/repo-path",
OCI: true,
PlainHttp: true,
CaFile: "cafile",
CertFile: "certfile",
KeyFile: "keyfile",
SkipTLSVerify: true,
},
},
Releases: []ReleaseSpec{
{
Chart: "oci-repo/chartname",
},
},
},
expected: []string{
"--plain-http",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
ReleaseSetSpec: *tt.spec,
}
flags := st.chartOCIFlags(&tt.spec.Releases[0])
require.Equal(t, tt.expected, flags)
})
}
}
func TestIsOCIChart(t *testing.T) {
cases := []struct {
st *HelmState
chart string
expected bool
}{
{&HelmState{}, "oci://myrepo/mychart", true},
{&HelmState{}, "oci://myrepo/mychart:1.0.0", true},
{&HelmState{}, "myrepo/mychart", false},
{&HelmState{}, "myrepo/mychart:1.0.0", false},
{
&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "ocirepo",
URL: "ocirepo.com",
OCI: true,
},
},
},
},
"ocirepo/chart",
true,
},
{
&HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: []RepositorySpec{
{
Name: "nonocirepo",
URL: "nonocirepo.com",
},
},
},
},
"nonocirepo/chart",
false,
},
}
for _, c := range cases {
actual := c.st.IsOCIChart(c.chart)
if actual != c.expected {
t.Errorf("IsOCIChart(%s) = %t; expected %t", c.chart, actual, c.expected)
}
}
}
func TestAppendVerifyFlags(t *testing.T) {
st := &HelmState{}
tests := []struct {
name string
repo []RepositorySpec
helmDefaults HelmSpec
release *ReleaseSpec
expected []string
}{
{
name: "Release with true verify flag",
release: &ReleaseSpec{Verify: boolValue(true)},
repo: nil,
helmDefaults: HelmSpec{},
expected: []string{"--verify"},
},
{
name: "Release with false verify flag",
release: &ReleaseSpec{Verify: boolValue(false)},
repo: nil,
helmDefaults: HelmSpec{},
expected: []string(nil),
},
{
name: "Repository with verify flag",
helmDefaults: HelmSpec{},
repo: []RepositorySpec{
{
Name: "myrepo",
Verify: true,
},
},
release: &ReleaseSpec{
Chart: "myrepo/mychart",
},
expected: []string{"--verify"},
},
{
name: "Helm defaults with verify flag",
repo: nil,
helmDefaults: HelmSpec{
Verify: true,
},
release: &ReleaseSpec{},
expected: []string{"--verify"},
},
{
name: "No verify flag",
repo: nil,
helmDefaults: HelmSpec{},
release: &ReleaseSpec{},
expected: []string(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st.ReleaseSetSpec.Repositories = tt.repo
st.ReleaseSetSpec.HelmDefaults = tt.helmDefaults
flags := st.appendVerifyFlags(nil, tt.release)
assert.Equal(t, tt.expected, flags)
})
}
}
// TestHelmState_setStringFlags tests the setStringFlags method
func TestHelmState_setStringFlags(t *testing.T) {
tests := []struct {
name string
setStringValues []SetValue
want []string
wantErr bool
}{
{
name: "single value",
setStringValues: []SetValue{
{
Name: "key",
Value: "value",
},
},
want: []string{"--set-string", "key=value"},
wantErr: false,
},
{
name: "multiple values",
setStringValues: []SetValue{
{
Name: "key",
Values: []string{"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)
}
}