mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 12:34:23 +02:00
* fix: update helm plugins via uninstall+reinstall to honor pinned version (#2726) helm plugin update re-installs a plugin from its cached source WITHOUT the --version flag, so it does not reliably install the pinned version helmfile requests. It reports success (exit 0) while re-downloading the unchanged cached source, leaving 'helm plugin list' showing the old version. This affected 'helmfile init --force' which reported a successful diff/secrets plugin update while the old version remained installed. The reporter's workaround was to uninstall the plugin before running init. UpdatePlugin now uninstalls the existing plugin and reinstalls the exact pinned version via AddPlugin (which passes --version). The unreliable 'plugin update' command is no longer used for the general path; this mirrors the uninstall+reinstall strategy already used for helm-secrets on Helm 4. Fixes #2726 Refs #2548 Signed-off-by: yxxhero <aiopsclub@163.com> * fix: only ignore 'not found' uninstall errors in UpdatePlugin Address review feedback: UpdatePlugin ignored all uninstall errors, which could mask real failures (permissions, broken Helm) behind a less informative 'plugin already exists' error from the subsequent install. Now only the expected 'not found' case (plugin removed concurrently, reported by both Helm 3 'Plugin: <name> not found' and Helm 4 'plugin: <name> not found') is ignored and install proceeds. Any other uninstall error is returned. Adds tests for both branches. Signed-off-by: yxxhero <aiopsclub@163.com> * fix: extract pluginCmd constant to satisfy goconst in tests golangci-lint (goconst, min-occurrences: 8) flagged the "plugin" string literal in exec_test.go after the new UpdatePlugin tests pushed the package- wide count from 7 (under threshold, passing on main) to 14. Introduce a pluginCmd constant and use it for all helm sub-command checks in the UpdatePlugin tests. No behavior change; drops the literal count back below the threshold so CI goconst passes. Signed-off-by: yxxhero <aiopsclub@163.com> * fix: match helm plugin-absent error precisely; guard against plugin update regression Address review feedback on UpdatePlugin: 1. The uninstall error check used strings.Contains(err, "not found"), which is too broad: it also matches unrelated failures such as a missing helm binary ("executable file not found") or an uninstall hook failing with "sh: ...: not found". Those would be silently treated as "plugin absent" and logged/proceeded past, masking the real problem. Match only helm's specific plugin-absent message instead, via a case-insensitive regex that covers both majors: Helm 4: "plugin: <name> not found" Helm 3: "Plugin: <name> not found" All other uninstall failures (permissions, missing binary, hook errors) are now surfaced. 2. The init test mock had no "plugin update" branch, so a production regression to calling helm plugin update would go undetected (false negative). Add an explicit case that records and fails on "plugin update". Also adds a test for the missing-binary case and makes the plugin-absent test table-driven across Helm 3/4 formats. Extracts installCmd to keep goconst (min-occurrences: 8) satisfied after the new tests. Signed-off-by: yxxhero <aiopsclub@163.com> --------- Signed-off-by: yxxhero <aiopsclub@163.com>
2206 lines
72 KiB
Go
2206 lines
72 KiB
Go
package helmexec
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tj/assert"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// pluginCmd is the "plugin" helm subcommand used repeatedly across these tests.
|
|
// Extracted as constants so the repeated string literals do not trip goconst
|
|
// (min-occurrences: 8) once additional plugin tests are added.
|
|
const (
|
|
pluginCmd = "plugin"
|
|
installCmd = "install"
|
|
)
|
|
|
|
// Mocking the command-line runner
|
|
|
|
type mockRunner struct {
|
|
output []byte
|
|
versionOutput []byte // if set, returned for "helm version --short" probe; overrides default Helm 4 fallback
|
|
err error
|
|
// failCount is the number of times Execute/ExecuteStdIn fail (with
|
|
// errTransientNet) before succeeding. Used to exercise retry logic.
|
|
failCount int
|
|
calls int
|
|
// stdins records the stdin content passed to each ExecuteStdIn call,
|
|
// so tests can assert that retries still pass the full payload (e.g. the
|
|
// password is not consumed by a prior attempt).
|
|
stdins []string
|
|
// execArgs records the args passed to each Execute/ExecuteStdIn call, so
|
|
// tests can assert that retries don't accumulate duplicated flags.
|
|
execArgs [][]string
|
|
}
|
|
|
|
var errTransientNet = fmt.Errorf("transient network error")
|
|
|
|
func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
|
|
mock.calls++
|
|
mock.execArgs = append(mock.execArgs, append([]string{}, args...))
|
|
if stdin != nil {
|
|
b, _ := io.ReadAll(stdin)
|
|
mock.stdins = append(mock.stdins, string(b))
|
|
}
|
|
if mock.failCount > 0 && mock.calls <= mock.failCount {
|
|
return nil, errTransientNet
|
|
}
|
|
return mock.output, mock.err
|
|
}
|
|
|
|
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
|
if strings.Join(args, " ") == "version --short" {
|
|
if mock.versionOutput != nil {
|
|
return mock.versionOutput, nil
|
|
}
|
|
if len(mock.output) == 0 {
|
|
return []byte("v4.0.1+g12500dd"), nil
|
|
}
|
|
}
|
|
mock.calls++
|
|
mock.execArgs = append(mock.execArgs, append([]string{}, args...))
|
|
if mock.failCount > 0 && mock.calls <= mock.failCount {
|
|
return nil, errTransientNet
|
|
}
|
|
return mock.output, mock.err
|
|
}
|
|
|
|
func MockExecer(logger *zap.SugaredLogger, kubeconfig, kubeContext string) (*execer, error) {
|
|
execer, err := New("helm", HelmExecOptions{}, logger, kubeconfig, kubeContext, &mockRunner{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return execer, nil
|
|
}
|
|
|
|
// Test methods
|
|
|
|
func TestNewHelmExec(t *testing.T) {
|
|
buffer := bytes.NewBufferString("something")
|
|
helm, err := MockExecer(NewLogger(buffer, "debug"), "config", "dev")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if helm.kubeContext != "dev" {
|
|
t.Error("helmexec.New() - kubeContext")
|
|
}
|
|
if buffer.String() != "something" {
|
|
t.Error("helmexec.New() - changed buffer")
|
|
}
|
|
if len(helm.extra) != 0 {
|
|
t.Error("helmexec.New() - extra args not empty")
|
|
}
|
|
}
|
|
|
|
func Test_SetExtraArgs(t *testing.T) {
|
|
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
helm.SetExtraArgs()
|
|
if len(helm.extra) != 0 {
|
|
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
|
|
}
|
|
helm.SetExtraArgs("foo")
|
|
if !reflect.DeepEqual(helm.extra, []string{"foo"}) {
|
|
t.Error("helmexec.SetExtraArgs() - one extra argument missing")
|
|
}
|
|
helm.SetExtraArgs("alpha", "beta")
|
|
if !reflect.DeepEqual(helm.extra, []string{"alpha", "beta"}) {
|
|
t.Error("helmexec.SetExtraArgs() - two extra arguments missing (overwriting the previous value)")
|
|
}
|
|
}
|
|
|
|
func Test_SetHelmBinary(t *testing.T) {
|
|
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if helm.helmBinary != "helm" {
|
|
t.Error("helmexec.command - default command is not helm")
|
|
}
|
|
helm.SetHelmBinary("foo")
|
|
if helm.helmBinary != "foo" {
|
|
t.Errorf("helmexec.SetHelmBinary() - actual = %s expect = foo", helm.helmBinary)
|
|
}
|
|
}
|
|
|
|
func Test_SetEnableLiveOutput(t *testing.T) {
|
|
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if helm.options.EnableLiveOutput {
|
|
t.Error("helmexec.options.EnableLiveOutput should not be enabled by default")
|
|
}
|
|
helm.SetEnableLiveOutput(true)
|
|
if !helm.options.EnableLiveOutput {
|
|
t.Errorf("helmexec.SetEnableLiveOutput() - actual = %t expect = true", helm.options.EnableLiveOutput)
|
|
}
|
|
}
|
|
|
|
func Test_AddRepo_Helm_Version(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
disableForceUpdate bool
|
|
expected string
|
|
}{
|
|
{
|
|
name: "Helm 3.2.0 (before force-update)",
|
|
version: "3.2.0",
|
|
expected: `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
|
|
`,
|
|
},
|
|
{
|
|
name: "Helm 3.3.2 (force-update added)",
|
|
version: "3.3.2",
|
|
expected: `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
|
|
`,
|
|
},
|
|
{
|
|
name: "Helm 3.19.2 (with force-update)",
|
|
version: "3.19.2",
|
|
expected: `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
|
|
`,
|
|
},
|
|
{
|
|
name: "Helm 3.19.2 (force-update disabled)",
|
|
version: "3.19.2",
|
|
disableForceUpdate: true,
|
|
expected: `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
|
|
`,
|
|
},
|
|
{
|
|
name: "Helm 4.0.1 (force-update needed)",
|
|
version: "4.0.1",
|
|
expected: `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
|
|
`,
|
|
},
|
|
{
|
|
name: "Helm 4.0.1 (force-update disabled)",
|
|
version: "4.0.1",
|
|
disableForceUpdate: true,
|
|
expected: `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse(tt.version),
|
|
logger: logger,
|
|
kubeconfig: "config",
|
|
kubeContext: "dev",
|
|
runner: &mockRunner{},
|
|
options: HelmExecOptions{DisableForceUpdate: tt.disableForceUpdate},
|
|
}
|
|
err := helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "", false, false)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expected, buffer.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_AddRepo(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Test case with certfile and keyfile
|
|
buffer.Reset()
|
|
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "", false, false)
|
|
expected := `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test case with cafile
|
|
buffer.Reset()
|
|
err = helm.AddRepo("myRepo", "https://repo.example.com/", "ca.crt", "", "", "", "", "", false, false)
|
|
expected = `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --ca-file ca.crt
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test case with no certfile or cafile
|
|
buffer.Reset()
|
|
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "", "", false, false)
|
|
expected = `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test case with managed "acr"
|
|
buffer.Reset()
|
|
err = helm.AddRepo("acrRepo", "", "", "", "", "", "", "acr", false, false)
|
|
expected = `Adding repo acrRepo (acr)
|
|
exec: az acr helm repo add --name acrRepo
|
|
exec: az acr helm repo add --name acrRepo:
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test case with unknown managed type
|
|
buffer.Reset()
|
|
err = helm.AddRepo("otherRepo", "", "", "", "", "", "", "unknown", false, false)
|
|
expected = `ERROR: unknown type 'unknown' for repository otherRepo
|
|
`
|
|
if err == nil {
|
|
t.Errorf("expected error for unknown managed type, got nil")
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test case with username and password (using password-stdin)
|
|
buffer.Reset()
|
|
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", false, false)
|
|
expected = `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --username example_user --password-stdin
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test case with username, password, and pass-credentials
|
|
buffer.Reset()
|
|
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", true, false)
|
|
expected = `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --pass-credentials --username example_user --password-stdin
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
actual := buffer.String()
|
|
if actual != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
|
|
// Test case with skipTLSVerify
|
|
buffer.Reset()
|
|
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "", "", false, true)
|
|
expected = `Adding repo myRepo https://repo.example.com/
|
|
exec: helm --kubeconfig config --kube-context dev repo add myRepo https://repo.example.com/ --force-update --insecure-skip-tls-verify
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
normalize := func(s string) string {
|
|
return strings.Join(strings.Fields(s), " ")
|
|
}
|
|
|
|
actual = normalize(buffer.String())
|
|
expected = normalize(expected)
|
|
|
|
if actual != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
|
|
// Test case with empty name
|
|
buffer.Reset()
|
|
err = helm.AddRepo("", "https://repo.example.com/", "", "", "", "", "", "", false, false)
|
|
expected = `empty field name`
|
|
|
|
if err != nil && err.Error() != "empty field name" {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
actual = strings.TrimSpace(buffer.String())
|
|
if actual != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
}
|
|
|
|
func Test_UpdateRepo(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.UpdateRepo()
|
|
expected := `Updating repo
|
|
exec: helm --kubeconfig config --kube-context dev repo update
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_RegistryLogin(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse("v3.12.0"),
|
|
logger: logger,
|
|
kubeconfig: "config",
|
|
kubeContext: "dev",
|
|
runner: &mockRunner{},
|
|
}
|
|
err := helm.RegistryLogin("repo.example.com", "example_user", "example_password", "example_ca", "example_cert", "example_key", true)
|
|
expected := `Logging in to registry
|
|
exec: helm --kubeconfig config --kube-context dev registry login repo.example.com --cert-file example_cert --key-file example_key --ca-file example_ca --insecure --username example_user --password-stdin
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.RegistryLogin()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test helm version prior to v3.12.0, without support for TLS
|
|
buffer.Reset()
|
|
helm.version = semver.MustParse("v3.11.0")
|
|
|
|
err = helm.RegistryLogin("repo.example.com", "example_user", "example_password", "example_ca", "example_cert", "example_key", true)
|
|
expected = `Logging in to registry
|
|
exec: helm --kubeconfig config --kube-context dev registry login repo.example.com --insecure --username example_user --password-stdin
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.RegistryLogin()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
// newRetryExecer builds an execer with the given RepoRetry option and a
|
|
// mockRunner that fails failCount times before succeeding.
|
|
func newRetryExecer(t *testing.T, repoRetry, failCount int) (*execer, *mockRunner) {
|
|
t.Helper()
|
|
logger := NewLogger(os.Stdout, "debug")
|
|
runner := &mockRunner{failCount: failCount}
|
|
helm, err := New("helm", HelmExecOptions{RepoRetry: repoRetry}, logger, "config", "dev", runner)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
return helm, runner
|
|
}
|
|
|
|
// withTestBackoff shrinks repoRetryBaseBackoff for the test's duration so retry
|
|
// tests don't sleep for real. The restore is via t.Cleanup so it always runs.
|
|
func withTestBackoff(t *testing.T) {
|
|
t.Helper()
|
|
original := repoRetryBaseBackoff
|
|
repoRetryBaseBackoff = time.Millisecond
|
|
t.Cleanup(func() { repoRetryBaseBackoff = original })
|
|
}
|
|
|
|
// retryTestCase is the shared shape for the AddRepo/UpdateRepo/RegistryLogin
|
|
// retry table tests.
|
|
type retryTestCase struct {
|
|
name string
|
|
repoRetry int
|
|
failCount int
|
|
wantErr bool
|
|
wantCalls int // expected number of exec (non-version) calls
|
|
managed string
|
|
}
|
|
|
|
func runRetryCases(t *testing.T, run func(*testing.T, *execer, *mockRunner, retryTestCase), cases []retryTestCase) {
|
|
t.Helper()
|
|
withTestBackoff(t)
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
helm, runner := newRetryExecer(t, tt.repoRetry, tt.failCount)
|
|
run(t, helm, runner, tt)
|
|
if runner.calls != tt.wantCalls {
|
|
t.Errorf("runner.calls = %d, want %d", runner.calls, tt.wantCalls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_AddRepo_Retry(t *testing.T) {
|
|
runRetryCases(t, func(t *testing.T, helm *execer, runner *mockRunner, tt retryTestCase) {
|
|
err := helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "user", "pass", tt.managed, false, false)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("AddRepo() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
// For the normal (non-acr) path, each attempt's args must contain
|
|
// --username exactly once: guards against the username/password flags
|
|
// accumulating across retries.
|
|
if tt.managed == "" {
|
|
for i, a := range runner.execArgs {
|
|
if c := strings.Count(strings.Join(a, " "), "--username"); c != 1 {
|
|
t.Errorf("AddRepo() attempt %d: --username count = %d, want 1 (args=%v)", i+1, c, a)
|
|
}
|
|
}
|
|
}
|
|
}, []retryTestCase{
|
|
{name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2},
|
|
{name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2},
|
|
{name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1},
|
|
{name: "acr managed retries", repoRetry: 3, failCount: 2, wantErr: false, wantCalls: 3, managed: "acr"},
|
|
})
|
|
}
|
|
|
|
func Test_UpdateRepo_Retry(t *testing.T) {
|
|
runRetryCases(t, func(t *testing.T, helm *execer, _ *mockRunner, tt retryTestCase) {
|
|
err := helm.UpdateRepo()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("UpdateRepo() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
}, []retryTestCase{
|
|
{name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2},
|
|
{name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2},
|
|
{name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1},
|
|
})
|
|
}
|
|
|
|
func Test_RegistryLogin_Retry(t *testing.T) {
|
|
runRetryCases(t, func(t *testing.T, helm *execer, runner *mockRunner, tt retryTestCase) {
|
|
err := helm.RegistryLogin("repo.example.com", "user", "pass", "", "", "", false)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("RegistryLogin() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
// Every attempt must carry the full password (regression guard:
|
|
// the password buffer must not be consumed by a prior attempt).
|
|
for i, s := range runner.stdins {
|
|
if s != "pass\n" {
|
|
t.Errorf("RegistryLogin() attempt %d stdin = %q, want %q", i+1, s, "pass\n")
|
|
}
|
|
}
|
|
}, []retryTestCase{
|
|
{name: "succeeds after one retry", repoRetry: 2, failCount: 1, wantErr: false, wantCalls: 2},
|
|
{name: "retries exhausted", repoRetry: 1, failCount: 2, wantErr: true, wantCalls: 2},
|
|
{name: "retry disabled runs once", repoRetry: 0, failCount: 1, wantErr: true, wantCalls: 1},
|
|
})
|
|
}
|
|
|
|
// Test_Retry_LargeCount_NoOverflow verifies that a large --repo-retries value
|
|
// doesn't overflow the backoff duration (the shift exponent must be capped).
|
|
func Test_Retry_LargeCount_NoOverflow(t *testing.T) {
|
|
withTestBackoff(t)
|
|
// failCount > repoRetry so every attempt fails; attempt indices exceed the
|
|
// shift cap of 5, exercising the overflow guard without panicking.
|
|
const retries = 10
|
|
helm, runner := newRetryExecer(t, retries, retries+1)
|
|
err := helm.UpdateRepo()
|
|
if err == nil {
|
|
t.Fatal("UpdateRepo() expected error, got nil")
|
|
}
|
|
if runner.calls != retries+1 { // 1 initial + retries
|
|
t.Errorf("runner.calls = %d, want %d", runner.calls, retries+1)
|
|
}
|
|
}
|
|
|
|
// Test_SleepCtx_Canceled verifies sleepCtx returns promptly (and false) when
|
|
// the runner's context is already canceled, rather than blocking for the full
|
|
// duration.
|
|
func Test_SleepCtx_Canceled(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // cancel immediately
|
|
helm := &execer{
|
|
runner: &ShellRunner{Ctx: ctx},
|
|
logger: NewLogger(io.Discard, "debug"),
|
|
}
|
|
start := time.Now()
|
|
completed := helm.sleepCtx(30 * time.Second)
|
|
elapsed := time.Since(start)
|
|
if completed {
|
|
t.Error("sleepCtx() = true with a canceled context; want false")
|
|
}
|
|
if elapsed > time.Second {
|
|
t.Errorf("sleepCtx blocked for %v with a canceled context; expected prompt return", elapsed)
|
|
}
|
|
}
|
|
|
|
// Test_SleepCtx_NoContext verifies sleepCtx falls back to time.Sleep (returning
|
|
// true) when the runner has no context (e.g. mockRunner).
|
|
func Test_SleepCtx_NoContext(t *testing.T) {
|
|
helm := &execer{
|
|
runner: &mockRunner{},
|
|
logger: NewLogger(io.Discard, "debug"),
|
|
}
|
|
start := time.Now()
|
|
completed := helm.sleepCtx(5 * time.Millisecond)
|
|
elapsed := time.Since(start)
|
|
if !completed {
|
|
t.Error("sleepCtx() = false without a context; want true")
|
|
}
|
|
if elapsed < 4*time.Millisecond {
|
|
t.Errorf("sleepCtx returned in %v without a context; expected to sleep ~5ms", elapsed)
|
|
}
|
|
}
|
|
|
|
// Test_Retry_AbortsOnCanceledContext verifies that retryRepoOp stops retrying
|
|
// once the runner's context is canceled, rather than spinning into a tight
|
|
// loop of rapid helm invocations.
|
|
func Test_Retry_AbortsOnCanceledContext(t *testing.T) {
|
|
withTestBackoff(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
runner := &mockRunner{failCount: 100} // always fails
|
|
helm := &execer{
|
|
options: HelmExecOptions{RepoRetry: 100},
|
|
version: semver.MustParse("v4.0.1"),
|
|
runner: &ShellRunner{Ctx: ctx},
|
|
logger: NewLogger(io.Discard, "debug"),
|
|
}
|
|
// Cancel deterministically inside the op after the first attempt runs, so
|
|
// the retry loop must bail out at sleepCtx — no timing-based flakiness.
|
|
calls := 0
|
|
_, err := helm.retryRepoOp("test", func() ([]byte, error) {
|
|
calls++
|
|
out, e := runner.Execute("helm", []string{"repo", "update"}, nil, false)
|
|
if calls == 1 {
|
|
cancel()
|
|
}
|
|
return out, e
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
// The context is canceled during the first attempt; sleepCtx must observe
|
|
// it and abort, so only the first attempt should run.
|
|
if calls > 2 {
|
|
t.Errorf("calls = %d after cancellation; expected <= 2 (no tight loop)", calls)
|
|
}
|
|
}
|
|
|
|
func Test_SyncRelease(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.SyncRelease(HelmContext{}, "release", "chart", "default", "--timeout 10", "--wait", "--wait-for-jobs")
|
|
expected := `Upgrading release=release, chart=chart, namespace=default
|
|
exec: helm --kubeconfig config --kube-context dev upgrade --install release chart --timeout 10 --wait --wait-for-jobs --history-max 0
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
err = helm.SyncRelease(HelmContext{}, "release", "chart", "default")
|
|
expected = `Upgrading release=release, chart=chart, namespace=default
|
|
exec: helm --kubeconfig config --kube-context dev upgrade --install release chart --history-max 0
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
err = helm.SyncRelease(HelmContext{}, "release", "https://example_user:example_password@repo.example.com/chart.tgz", "default")
|
|
expected = `Upgrading release=release, chart=https://example_user:xxxxx@repo.example.com/chart.tgz, namespace=default
|
|
exec: helm --kubeconfig config --kube-context dev upgrade --install release https://example_user:example_password@repo.example.com/chart.tgz --history-max 0
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_UpdateDeps(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.UpdateDeps("./chart/foo")
|
|
expected := `Updating dependency ./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
helm.SetExtraArgs("--verify")
|
|
err = helm.UpdateDeps("./chart/foo")
|
|
// --verify is a dependency-specific flag and should be preserved
|
|
expected = `Updating dependency ./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo --verify
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_BuildDeps(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a")}
|
|
helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.BuildDeps("foo", "./chart/foo", []string{"--skip-refresh"}...)
|
|
expected := `Building dependency release=foo, chart=./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --skip-refresh
|
|
v3.2.4+ge29ce2a
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
err = helm.BuildDeps("foo", "./chart/foo")
|
|
expected = `Building dependency release=foo, chart=./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
|
|
v3.2.4+ge29ce2a
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
helm.SetExtraArgs("--verify")
|
|
err = helm.BuildDeps("foo", "./chart/foo", []string{"--skip-refresh"}...)
|
|
// --verify is a dependency-specific flag and should be preserved
|
|
expected = `Building dependency release=foo, chart=./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --skip-refresh --verify
|
|
v3.2.4+ge29ce2a
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94")}
|
|
helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm2Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.BuildDeps("foo", "./chart/foo")
|
|
expected = `Building dependency release=foo, chart=./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
|
|
Client: v2.16.1+ge13bc94
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test that --dry-run flag is filtered out (not supported by helm dependency build)
|
|
buffer.Reset()
|
|
helm3Runner = mockRunner{output: []byte("v3.2.4+ge29ce2a")}
|
|
helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
helm.SetExtraArgs("--dry-run=server")
|
|
err = helm.BuildDeps("foo", "./chart/foo")
|
|
expected = `Building dependency release=foo, chart=./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
|
|
v3.2.4+ge29ce2a
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.BuildDeps() with --dry-run should filter it out\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
// Test that global flags (--debug) and dependency flags (--verify) are preserved,
|
|
// while template-specific flags (--dry-run) are filtered out
|
|
buffer.Reset()
|
|
helm3Runner = mockRunner{output: []byte("v3.2.4+ge29ce2a")}
|
|
helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
helm.SetExtraArgs("--debug", "--dry-run=server", "--verify", "--wait")
|
|
err = helm.BuildDeps("foo", "./chart/foo")
|
|
expected = `Building dependency release=foo, chart=./chart/foo
|
|
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --debug --verify
|
|
v3.2.4+ge29ce2a
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.BuildDeps() should preserve global and dependency flags\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_DecryptSecret(t *testing.T) {
|
|
// Set secrets plugin version to 4.7.4
|
|
if err := os.Setenv("HELM_PLUGINS", "../../test/plugins/secrets/4.7.4"); err != nil {
|
|
t.Errorf("failed to set environment HELM_PLUGINS error: %s", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Unsetenv("HELM_PLUGINS"); err != nil {
|
|
t.Errorf("failed to unset environment HELM_PLUGINS error: %s", err)
|
|
}
|
|
}()
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
tmpFilePath := "path/to/temp/file"
|
|
helm.writeTempFile = func(content []byte) (string, error) {
|
|
return tmpFilePath, nil
|
|
}
|
|
|
|
_, err = helm.DecryptSecret(HelmContext{}, "secretName")
|
|
if err != nil {
|
|
t.Errorf("Error: %v", err)
|
|
}
|
|
cwd, err := filepath.Abs(".")
|
|
if err != nil {
|
|
t.Errorf("Error: %v", err)
|
|
}
|
|
// Run again for caching
|
|
_, err = helm.DecryptSecret(HelmContext{}, "secretName")
|
|
|
|
expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName
|
|
Decrypting secret %s/secretName
|
|
exec: helm --kubeconfig config --kube-context dev secrets decrypt %s/secretName
|
|
Decrypted %s/secretName into %s
|
|
Preparing to decrypt secret %s/secretName
|
|
Found secret in cache %s/secretName
|
|
Decrypted %s/secretName into %s
|
|
`, cwd, cwd, cwd, cwd, tmpFilePath, cwd, cwd, cwd, tmpFilePath)
|
|
if err != nil {
|
|
if _, ok := err.(*os.PathError); ok {
|
|
} else {
|
|
t.Errorf("Error: %v", err)
|
|
}
|
|
}
|
|
if d := cmp.Diff(expected, buffer.String()); d != "" {
|
|
t.Errorf("helmexec.DecryptSecret(): want (-), got (+):\n%s", d)
|
|
}
|
|
}
|
|
|
|
func Test_DecryptSecretWithGotmpl(t *testing.T) {
|
|
// Set secrets plugin version to 4.7.4
|
|
if err := os.Setenv("HELM_PLUGINS", "../../test/plugins/secrets/4.7.4"); err != nil {
|
|
t.Errorf("failed to set environment HELM_PLUGINS error: %s", err)
|
|
}
|
|
defer func() {
|
|
if err := os.Unsetenv("HELM_PLUGINS"); err != nil {
|
|
t.Errorf("failed to unset environment HELM_PLUGINS error: %s", err)
|
|
}
|
|
}()
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
tmpFilePath := "path/to/temp/file"
|
|
helm.writeTempFile = func(content []byte) (string, error) {
|
|
return tmpFilePath, nil
|
|
}
|
|
|
|
secretName := "secretName.yaml.gotmpl"
|
|
_, err = helm.DecryptSecret(HelmContext{}, secretName)
|
|
if err != nil {
|
|
t.Errorf("Error: %v", err)
|
|
}
|
|
cwd, err := filepath.Abs(".")
|
|
if err != nil {
|
|
t.Errorf("Error: %v", err)
|
|
}
|
|
|
|
expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName.yaml.gotmpl
|
|
Decrypting secret %s/secretName.yaml.gotmpl
|
|
exec: helm --kubeconfig config --kube-context dev secrets decrypt %s/secretName.yaml.gotmpl
|
|
Decrypted %s/secretName.yaml.gotmpl into %s
|
|
`, cwd, cwd, cwd, cwd, tmpFilePath)
|
|
if err != nil {
|
|
t.Errorf("Error: %v", err)
|
|
}
|
|
if d := cmp.Diff(expected, buffer.String()); d != "" {
|
|
t.Errorf("helmexec.DecryptSecret(): want (-), got (+):\n%s", d)
|
|
}
|
|
}
|
|
|
|
func Test_DiffRelease(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--timeout 10", "--wait", "--wait-for-jobs")
|
|
expected := `Comparing release=release, chart=chart, namespace=default
|
|
|
|
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --timeout 10 --wait --wait-for-jobs
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false)
|
|
expected = `Comparing release=release, chart=chart, namespace=default
|
|
|
|
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
err = helm.DiffRelease(HelmContext{}, "release", "https://example_user:example_password@repo.example.com/chart.tgz", "default", false)
|
|
expected = `Comparing release=release, chart=https://example_user:xxxxx@repo.example.com/chart.tgz, namespace=default
|
|
|
|
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release https://example_user:example_password@repo.example.com/chart.tgz
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_DiffRelease_ColorFlagHelm4(t *testing.T) {
|
|
// Test that --color and --no-color flags are removed and HELM_DIFF_COLOR env var is set for Helm 4
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
|
|
// MockExecer creates a Helm 4 execer by default (returns v4.0.1)
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify it's Helm 4
|
|
if !helm.IsHelm4() {
|
|
t.Errorf("expected Helm 4, got version: %v", helm.GetVersion())
|
|
}
|
|
|
|
// Test with --color flag
|
|
buffer.Reset()
|
|
err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--color", "--context", "3")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
// The --color flag should be removed and --context should remain
|
|
expected := `Comparing release=release, chart=chart, namespace=default
|
|
|
|
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --context 3
|
|
`
|
|
actual := buffer.String()
|
|
if actual != expected {
|
|
t.Errorf("helmexec.DiffRelease() with --color\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
|
|
// Verify --color flag was removed
|
|
if strings.Contains(actual, "--color") {
|
|
t.Errorf("--color flag should have been removed, but got: %v", actual)
|
|
}
|
|
|
|
// Test with --no-color flag
|
|
buffer.Reset()
|
|
err = helm.DiffRelease(HelmContext{}, "release", "chart", "default", false, "--no-color", "--context", "3")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
// The --no-color flag should be removed and --context should remain
|
|
expected = `Comparing release=release, chart=chart, namespace=default
|
|
|
|
exec: helm --kubeconfig config --kube-context dev diff upgrade --allow-unreleased release chart --context 3
|
|
`
|
|
actual = buffer.String()
|
|
if actual != expected {
|
|
t.Errorf("helmexec.DiffRelease() with --no-color\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
|
|
// Verify --no-color flag was removed
|
|
if strings.Contains(actual, "--no-color") {
|
|
t.Errorf("--no-color flag should have been removed, but got: %v", actual)
|
|
}
|
|
}
|
|
|
|
func Test_FilterColorFlagsForHelm4(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
inputFlags []string
|
|
expectedFlags []string
|
|
expectedEnvKey string
|
|
expectedEnvVal string
|
|
}{
|
|
{
|
|
name: "color flag",
|
|
inputFlags: []string{"--color", "--context", "3"},
|
|
expectedFlags: []string{"--context", "3"},
|
|
expectedEnvKey: "HELM_DIFF_COLOR",
|
|
expectedEnvVal: "true",
|
|
},
|
|
{
|
|
name: "no-color flag",
|
|
inputFlags: []string{"--no-color", "--context", "3"},
|
|
expectedFlags: []string{"--context", "3"},
|
|
expectedEnvKey: "HELM_DIFF_COLOR",
|
|
expectedEnvVal: "false",
|
|
},
|
|
{
|
|
name: "no color flags",
|
|
inputFlags: []string{"--context", "3", "--detailed-exitcode"},
|
|
expectedFlags: []string{"--context", "3", "--detailed-exitcode"},
|
|
expectedEnvKey: "",
|
|
expectedEnvVal: "",
|
|
},
|
|
{
|
|
name: "color flag with other flags",
|
|
inputFlags: []string{"--detailed-exitcode", "--color", "--suppress", "secret"},
|
|
expectedFlags: []string{"--detailed-exitcode", "--suppress", "secret"},
|
|
expectedEnvKey: "HELM_DIFF_COLOR",
|
|
expectedEnvVal: "true",
|
|
},
|
|
{
|
|
name: "both color and no-color flags (first wins with defensive check)",
|
|
inputFlags: []string{"--color", "--no-color", "--context", "3"},
|
|
expectedFlags: []string{"--context", "3"},
|
|
expectedEnvKey: "HELM_DIFF_COLOR",
|
|
expectedEnvVal: "true", // Changed: first flag wins due to defensive check
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
env := make(map[string]string)
|
|
actualFlags := helm.filterColorFlagsForHelm4(tt.inputFlags, env)
|
|
|
|
if !reflect.DeepEqual(actualFlags, tt.expectedFlags) {
|
|
t.Errorf("filterColorFlagsForHelm4() flags\nactual = %v\nexpect = %v", actualFlags, tt.expectedFlags)
|
|
}
|
|
|
|
if tt.expectedEnvKey != "" {
|
|
if env[tt.expectedEnvKey] != tt.expectedEnvVal {
|
|
t.Errorf("filterColorFlagsForHelm4() env[%v]\nactual = %v\nexpect = %v",
|
|
tt.expectedEnvKey, env[tt.expectedEnvKey], tt.expectedEnvVal)
|
|
}
|
|
} else if len(env) > 0 {
|
|
t.Errorf("filterColorFlagsForHelm4() expected no env vars, but got: %v", env)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_DeleteRelease(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.DeleteRelease(HelmContext{}, "release")
|
|
expected := `Deleting release
|
|
exec: helm --kubeconfig config --kube-context dev delete release
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
func Test_DeleteRelease_Flags(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.DeleteRelease(HelmContext{}, "release", "--purge")
|
|
expected := `Deleting release
|
|
exec: helm --kubeconfig config --kube-context dev delete release --purge
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_TestRelease(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.TestRelease(HelmContext{}, "release")
|
|
expected := `Testing release
|
|
exec: helm --kubeconfig config --kube-context dev test release
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
func Test_TestRelease_Flags(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
|
|
expected := `Testing release
|
|
exec: helm --kubeconfig config --kube-context dev test release --cleanup --timeout 60
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_ReleaseStatus(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.ReleaseStatus(HelmContext{}, "myRelease")
|
|
expected := `Getting status myRelease
|
|
exec: helm --kubeconfig config --kube-context dev status myRelease
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_exec(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "", "")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
env := map[string]string{}
|
|
_, err = helm.exec([]string{"version"}, env, nil)
|
|
expected := `exec: helm version
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
helm, err = MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
ret, _ := helm.exec([]string{"diff"}, env, nil)
|
|
if len(ret) != 0 {
|
|
t.Error("helmexec.exec() - expected empty return value")
|
|
}
|
|
|
|
buffer.Reset()
|
|
helm, err = MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
_, err = helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env, nil)
|
|
expected = `exec: helm --kubeconfig config --kube-context dev diff release chart --timeout 10 --wait --wait-for-jobs
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
_, err = helm.exec([]string{"version"}, env, nil)
|
|
expected = `exec: helm --kubeconfig config --kube-context dev version
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
helm.SetExtraArgs("foo")
|
|
_, err = helm.exec([]string{"version"}, env, nil)
|
|
expected = `exec: helm --kubeconfig config --kube-context dev version foo
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
helm, err = MockExecer(logger, "", "")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
helm.SetHelmBinary("overwritten")
|
|
_, err = helm.exec([]string{"version"}, env, nil)
|
|
expected = `exec: overwritten version
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_Lint(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.Lint("release", "path/to/chart", "--values", "file.yml")
|
|
expected := `Linting release=release, chart=path/to/chart
|
|
exec: helm --kubeconfig config --kube-context dev lint path/to/chart --values file.yml
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_Fetch(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
|
|
expected := `Fetching chart
|
|
exec: helm --kubeconfig config --kube-context dev fetch chart --version 1.2.3 --untar --untardir /tmp/dir
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.Fetch()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
err = helm.Fetch("https://example_user:example_password@repo.example.com/chart.tgz", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
|
|
expected = `Fetching https://example_user:xxxxx@repo.example.com/chart.tgz
|
|
exec: helm --kubeconfig config --kube-context dev fetch https://example_user:example_password@repo.example.com/chart.tgz --version 1.2.3 --untar --untardir /tmp/dir
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.Fetch()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_ChartPull(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
tests := []struct {
|
|
name string
|
|
helmBin string
|
|
helmVersion string
|
|
chartName string
|
|
chartPath string
|
|
chartFlags []string
|
|
listResult string
|
|
}{
|
|
{
|
|
name: "less then v3.7.0",
|
|
helmBin: "helm",
|
|
helmVersion: "v3.6.0",
|
|
chartName: "chart",
|
|
chartPath: "path1",
|
|
chartFlags: []string{"--untar", "--untardir", "/tmp/dir"},
|
|
listResult: `Pulling chart
|
|
exec: helm --kubeconfig config --kube-context dev chart pull chart
|
|
`,
|
|
},
|
|
{
|
|
name: "more then v3.7.0",
|
|
helmBin: "helm",
|
|
helmVersion: "v3.10.0",
|
|
chartName: "repo/helm-charts:0.14.0",
|
|
chartPath: "path1",
|
|
chartFlags: []string{"--untardir", "/tmp/dir", "--version", "0.14.0"},
|
|
listResult: `Pulling repo/helm-charts:0.14.0
|
|
exec: helm --kubeconfig config --kube-context dev pull oci://repo/helm-charts --destination path1 --untar --untardir /tmp/dir --version 0.14.0
|
|
`,
|
|
},
|
|
{
|
|
name: "more then v3.7.0 with rc",
|
|
helmBin: "helm",
|
|
helmVersion: "v3.14.0-rc.1+g69dcc92",
|
|
chartName: "repo/helm-charts",
|
|
chartPath: "path1",
|
|
chartFlags: []string{"--untardir", "/tmp/dir", "--devel"},
|
|
listResult: `Pulling repo/helm-charts
|
|
exec: helm --kubeconfig config --kube-context dev pull oci://repo/helm-charts --destination path1 --untar --untardir /tmp/dir --devel
|
|
`,
|
|
},
|
|
}
|
|
for i := range tests {
|
|
tt := tests[i]
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
buffer.Reset()
|
|
helm, err := New(tt.helmBin, HelmExecOptions{}, logger, "config", "dev", &mockRunner{output: []byte(tt.helmVersion)})
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.ChartPull(tt.chartName, tt.chartPath, tt.chartFlags...)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
isMatch, _ := regexp.MatchString(tt.listResult, buffer.String())
|
|
if !isMatch {
|
|
t.Errorf("helmexec.ChartPull()\nactual = %v\nexpect = %v", buffer.String(), tt.listResult)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ChartExport(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
tests := []struct {
|
|
name string
|
|
helmBin string
|
|
helmVersion string
|
|
chartName string
|
|
chartPath string
|
|
listResult string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "",
|
|
helmBin: "helm",
|
|
helmVersion: "v3.6.0",
|
|
chartName: "chart",
|
|
chartPath: "path1",
|
|
listResult: `Exporting chart
|
|
exec: helm --kubeconfig config --kube-context dev chart export chart --destination path1
|
|
`,
|
|
expectedError: "",
|
|
},
|
|
}
|
|
for i := range tests {
|
|
tt := tests[i]
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
buffer.Reset()
|
|
helm := &execer{
|
|
helmBinary: tt.helmBin,
|
|
version: semver.MustParse(tt.helmVersion),
|
|
logger: logger,
|
|
kubeconfig: "config",
|
|
kubeContext: "dev",
|
|
runner: &mockRunner{},
|
|
}
|
|
err := helm.ChartExport(tt.chartName, tt.chartPath)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != tt.listResult {
|
|
t.Errorf("helmexec.ChartExport()\nactual = %v\nexpect = %v", buffer.String(), tt.listResult)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var logLevelTests = map[string]string{
|
|
"debug": `Adding repo myRepo https://repo.example.com/
|
|
exec: helm repo add myRepo https://repo.example.com/ --force-update --username example_user --password-stdin
|
|
`,
|
|
"info": `Adding repo myRepo https://repo.example.com/
|
|
`,
|
|
"warn": ``,
|
|
}
|
|
|
|
func Test_LogLevels(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
for logLevel, expected := range logLevelTests {
|
|
buffer.Reset()
|
|
logger := NewLogger(&buffer, logLevel)
|
|
helm, err := MockExecer(logger, "", "")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "", false, false)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
actual := buffer.String()
|
|
|
|
if strings.Contains(actual, "--password-stdin") {
|
|
expected = strings.Replace(expected, "--password example_password", "--password-stdin", 1)
|
|
}
|
|
|
|
if actual != expected {
|
|
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Test_mergeEnv(t *testing.T) {
|
|
actual := env2map(mergeEnv([]string{"A=1", "B=c=d", "E=2"}, map[string]string{"B": "3", "F": "4"}))
|
|
expected := map[string]string{"A": "1", "B": "3", "E": "2", "F": "4"}
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Errorf("mergeEnv()\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
}
|
|
|
|
func Test_Template(t *testing.T) {
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm, err := MockExecer(logger, "config", "dev")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
err = helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
|
|
expected := `Templating release=release, chart=path/to/chart
|
|
exec: helm --kubeconfig config --kube-context dev template release path/to/chart --values file.yml
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
|
|
buffer.Reset()
|
|
err = helm.TemplateRelease("release", "https://example_user:example_password@repo.example.com/chart.tgz", "--values", "file.yml")
|
|
expected = `Templating release=release, chart=https://example_user:xxxxx@repo.example.com/chart.tgz
|
|
exec: helm --kubeconfig config --kube-context dev template release https://example_user:example_password@repo.example.com/chart.tgz --values file.yml
|
|
`
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if buffer.String() != expected {
|
|
t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
|
}
|
|
}
|
|
|
|
func Test_Template_PostRendererWithOutputDir(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
postRendererFlag string
|
|
}{
|
|
{"separate flags", "--post-renderer"},
|
|
{"combined flag", "--post-renderer=/bin/echo"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
|
|
// Use Helm 3 version for the version probe so the Helm 3 workaround is applied.
|
|
// The workaround is not needed for Helm 4, which natively applies --post-renderer to --output-dir output.
|
|
runner := &mockRunner{versionOutput: []byte("v3.20.0")}
|
|
helm, err := New("helm", HelmExecOptions{}, logger, "config", "dev", runner)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
runner.output = []byte("apiVersion: v1\nkind: Namespace\n")
|
|
|
|
var flags []string
|
|
if tt.postRendererFlag == "--post-renderer" {
|
|
flags = []string{"--post-renderer", "/bin/echo", "--output-dir", tmpDir, "--values", "file.yml"}
|
|
} else {
|
|
flags = []string{tt.postRendererFlag, "--output-dir", tmpDir, "--values", "file.yml"}
|
|
}
|
|
|
|
err = helm.TemplateRelease("myrelease", "path/to/chart", flags...)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
outputPath := filepath.Join(tmpDir, "templates", "myrelease.yaml")
|
|
data, err := os.ReadFile(outputPath)
|
|
if err != nil {
|
|
t.Fatalf("expected output file %s to exist: %v", outputPath, err)
|
|
}
|
|
|
|
expected := "apiVersion: v1\nkind: Namespace\n\n"
|
|
if string(data) != expected {
|
|
t.Errorf("output file content:\nactual=%q\nexpect=%q", string(data), expected)
|
|
}
|
|
|
|
outputLog := buffer.String()
|
|
if strings.Contains(outputLog, "--output-dir") {
|
|
t.Errorf("helm should NOT have been called with --output-dir, got: %s", outputLog)
|
|
}
|
|
if !strings.Contains(outputLog, "--post-renderer") {
|
|
t.Errorf("helm should have been called with --post-renderer, got: %s", outputLog)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_IsHelm3(t *testing.T) {
|
|
helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
|
|
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if !helm.IsHelm3() {
|
|
t.Error("helmexec.IsHelm3() - Failed to detect Helm 3")
|
|
}
|
|
|
|
helm4Runner := mockRunner{output: []byte("v4.0.1+g12500dd\n")}
|
|
helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm4Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if helm.IsHelm3() {
|
|
t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 4 version")
|
|
}
|
|
}
|
|
|
|
func Test_IsHelm4(t *testing.T) {
|
|
helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
|
|
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if helm.IsHelm4() {
|
|
t.Error("helmexec.IsHelm4() - Detected Helm 4 with Helm 3 version")
|
|
}
|
|
|
|
helm4Runner := mockRunner{output: []byte("v4.0.1+g12500dd\n")}
|
|
helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm4Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if !helm.IsHelm4() {
|
|
t.Error("helmexec.IsHelm4() - Failed to detect Helm 4")
|
|
}
|
|
}
|
|
|
|
func Test_GetPluginVersion(t *testing.T) {
|
|
v3ExpectedVersion := "3.15.0"
|
|
v4ExpectedVersion := "4.7.4"
|
|
v3PluginDirPath := "../../test/plugins/secrets/3.15.0"
|
|
v4PluginDirPath := "../../test/plugins/secrets/4.7.4"
|
|
|
|
v3SecretPluginVersion, err := GetPluginVersion("secrets", v3PluginDirPath)
|
|
require.NoError(t, err)
|
|
|
|
if v3SecretPluginVersion.String() != v3ExpectedVersion {
|
|
t.Errorf("secrets v3 plugin version is %v, expected %v", v3SecretPluginVersion.String(), v3ExpectedVersion)
|
|
}
|
|
|
|
v4SecretPluginVersion, err := GetPluginVersion("secrets", v4PluginDirPath)
|
|
require.NoError(t, err)
|
|
if v4SecretPluginVersion.String() != v4ExpectedVersion {
|
|
t.Errorf("secrets v4 plugin version is %v, expected %v", v4SecretPluginVersion.String(), v4ExpectedVersion)
|
|
}
|
|
}
|
|
|
|
func Test_GetPluginVersion_XDGPaths(t *testing.T) {
|
|
v3ExpectedVersion := "3.15.0"
|
|
v4ExpectedVersion := "4.7.4"
|
|
v3PluginDirPath := "../../test/plugins/secrets/3.15.0"
|
|
v4PluginDirPath := "../../test/plugins/secrets/4.7.4"
|
|
|
|
sep := string(os.PathListSeparator)
|
|
xdgPaths := "nonexistent/path" + sep + v3PluginDirPath + sep + "another/nonexistent"
|
|
|
|
pluginVersion, err := GetPluginVersion("secrets", xdgPaths)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, v3ExpectedVersion, pluginVersion.String())
|
|
|
|
xdgPathsV4 := v4PluginDirPath + sep + "nonexistent/path"
|
|
pluginVersion, err = GetPluginVersion("secrets", xdgPathsV4)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, v4ExpectedVersion, pluginVersion.String())
|
|
|
|
_, err = GetPluginVersion("nonexistent-plugin", xdgPaths)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "plugin nonexistent-plugin not installed")
|
|
}
|
|
|
|
func Test_GetPluginVersion_Symlink(t *testing.T) {
|
|
// Simulate a nix/devbox environment where the plugin directory is a symlink
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a real plugin directory with plugin.yaml
|
|
realPluginDir := filepath.Join(tmpDir, "real", "helm-diff")
|
|
require.NoError(t, os.MkdirAll(realPluginDir, 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(realPluginDir, "plugin.yaml"), []byte(`name: "diff"
|
|
version: "3.12.0"
|
|
`), 0o644))
|
|
|
|
// Create a plugins directory where helm-diff is a symlink (like nix/devbox)
|
|
symlinkPluginsDir := filepath.Join(tmpDir, "plugins")
|
|
require.NoError(t, os.MkdirAll(symlinkPluginsDir, 0o755))
|
|
require.NoError(t, os.Symlink(realPluginDir, filepath.Join(symlinkPluginsDir, "helm-diff")))
|
|
|
|
version, err := GetPluginVersion("diff", symlinkPluginsDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "3.12.0", version.String())
|
|
}
|
|
|
|
func Test_GetVersion(t *testing.T) {
|
|
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
|
|
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
ver := helm.GetVersion()
|
|
if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 {
|
|
t.Errorf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver)
|
|
}
|
|
|
|
helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")}
|
|
helm, err = New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm3Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
ver = helm.GetVersion()
|
|
if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 {
|
|
t.Errorf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver)
|
|
}
|
|
}
|
|
|
|
func Test_IsVersionAtLeast(t *testing.T) {
|
|
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
|
|
helm, err := New("helm", HelmExecOptions{}, NewLogger(os.Stdout, "info"), "", "dev", &helm2Runner)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if !helm.IsVersionAtLeast("2.1.0") {
|
|
t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1")
|
|
}
|
|
|
|
if helm.IsVersionAtLeast("2.19.0") {
|
|
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 2.19")
|
|
}
|
|
|
|
if helm.IsVersionAtLeast("3.2.0") {
|
|
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 3.2")
|
|
}
|
|
}
|
|
|
|
func Test_resolveOciChart(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
chartPath string
|
|
ociChartURL string
|
|
ociChartTag string
|
|
}{
|
|
{
|
|
name: "normal",
|
|
chartPath: "chart/nginx:v1",
|
|
ociChartURL: "oci://chart/nginx",
|
|
ociChartTag: "v1",
|
|
},
|
|
{
|
|
name: "contains the port",
|
|
chartPath: "chart:5000/nginx:v1",
|
|
ociChartURL: "oci://chart:5000/nginx",
|
|
ociChartTag: "v1",
|
|
},
|
|
{
|
|
name: "no tag",
|
|
chartPath: "chart:5000/nginx",
|
|
ociChartURL: "oci://chart:5000/nginx",
|
|
ociChartTag: "",
|
|
},
|
|
{
|
|
name: "digest only",
|
|
chartPath: "ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
|
|
ociChartURL: "oci://ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
|
|
ociChartTag: "",
|
|
},
|
|
{
|
|
name: "version and digest",
|
|
chartPath: "ghcr.io/nginxinc/charts/nginx-ingress:2.0.0@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
|
|
ociChartURL: "oci://ghcr.io/nginxinc/charts/nginx-ingress@sha256:87ad282a8e7cc31913ce0543de2933ddb3f3eba80d6e5285f33b62ed720fc085",
|
|
ociChartTag: "2.0.0",
|
|
},
|
|
{
|
|
name: "port with digest",
|
|
chartPath: "registry:5000/chart@sha256:abc123",
|
|
ociChartURL: "oci://registry:5000/chart@sha256:abc123",
|
|
ociChartTag: "",
|
|
},
|
|
{
|
|
name: "port with version and digest",
|
|
chartPath: "registry:5000/chart:1.0.0@sha256:abc123",
|
|
ociChartURL: "oci://registry:5000/chart@sha256:abc123",
|
|
ociChartTag: "1.0.0",
|
|
},
|
|
}
|
|
for i := range tests {
|
|
tt := tests[i]
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
url, tag := resolveOciChart(tt.chartPath)
|
|
if tt.ociChartURL != url || tt.ociChartTag != tag {
|
|
actual := fmt.Sprintf("ociChartURL->%s ociChartTag->%s", url, tag)
|
|
expected := fmt.Sprintf("ociChartURL->%s ociChartTag->%s", tt.ociChartURL, tt.ociChartTag)
|
|
t.Errorf("resolveOciChart()\nactual = %v\nexpect = %v", actual, expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ShowChart(t *testing.T) {
|
|
showChartRunner := mockRunner{output: []byte("name: my-chart\nversion: 3.2.0\n")}
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse("3.3.2"),
|
|
logger: NewLogger(os.Stdout, "info"),
|
|
kubeContext: "dev",
|
|
runner: &showChartRunner,
|
|
}
|
|
|
|
metadata, err := helm.ShowChart("my-chart")
|
|
if err != nil {
|
|
t.Errorf("helmexec.ShowChart() - unexpected error: %v", err)
|
|
}
|
|
if metadata.Name != "my-chart" {
|
|
t.Errorf("helmexec.ShowChart() - expected chart name was %s, received: %s", "my-chart", metadata.Name)
|
|
}
|
|
if metadata.Version != "3.2.0" {
|
|
t.Errorf("helmexec.ShowChart() - expected chart version was %s, received: %s", "3.2.0", metadata.Version)
|
|
}
|
|
}
|
|
|
|
func Test_SetDisableForceUpdate(t *testing.T) {
|
|
helm, err := MockExecer(NewLogger(os.Stdout, "info"), "config", "dev")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if helm.options.DisableForceUpdate {
|
|
t.Error("helmexec.options.DisableForceUpdate should not be enabled by default")
|
|
}
|
|
helm.SetDisableForceUpdate(true)
|
|
if !helm.options.DisableForceUpdate {
|
|
t.Errorf("helmexec.SetDisableForceUpdate() - actual = %t expect = true", helm.options.DisableForceUpdate)
|
|
}
|
|
}
|
|
|
|
func TestParseHelmVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
want *semver.Version
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "helm 2",
|
|
version: "Client: v2.16.1+ge13bc94\n",
|
|
want: semver.MustParse("v2.16.1+ge13bc94"),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "helm 3",
|
|
version: "Client: v3.2.4+ge29ce2a\n",
|
|
want: semver.MustParse("v3.2.4+ge29ce2a"),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "helm 3 with os arch and build info",
|
|
version: "Client v3.7.1+7.el8+g8f33223\n",
|
|
want: semver.MustParse("v3.7.1+7.el8"),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty version",
|
|
version: "",
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid version",
|
|
version: "oooooo",
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "version without v prefix",
|
|
version: "3.2.4",
|
|
want: semver.MustParse("v3.2.4"),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "version without v prefix with build info",
|
|
version: "3.2.4+ge29ce2a",
|
|
want: semver.MustParse("v3.2.4+ge29ce2a"),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "version without v prefix with spaces",
|
|
version: " 3.2.4 ",
|
|
want: semver.MustParse("v3.2.4"),
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := parseHelmVersion(tt.version)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("parseHelmVersion() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("parseHelmVersion() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_helmSecretsRequiresSplitInstall(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
want bool
|
|
}{
|
|
{name: "below threshold", version: "v4.6.9", want: false},
|
|
{name: "exactly at threshold", version: "v4.7.0", want: true},
|
|
{name: "above threshold single digit minor", version: "v4.8.0", want: true},
|
|
{name: "above threshold double digit minor (v4.10.0+)", version: "v4.10.0", want: true},
|
|
{name: "pre-release below threshold", version: "v4.7.0-beta.1", want: false},
|
|
{name: "invalid version string", version: "not-a-version", want: false},
|
|
{name: "empty string", version: "", want: false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := helmSecretsRequiresSplitInstall(tt.version)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
type funcRunner struct {
|
|
execute func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error)
|
|
}
|
|
|
|
func (r *funcRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) {
|
|
return r.execute(cmd, args, env, false)
|
|
}
|
|
|
|
func (r *funcRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
|
return r.execute(cmd, args, env, enableLiveOutput)
|
|
}
|
|
|
|
func Test_UpdatePlugin_Helm4SecretsUsesUninstallReinstall(t *testing.T) {
|
|
var calledArgs [][]string
|
|
runner := &funcRunner{
|
|
execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
|
calledArgs = append(calledArgs, append([]string(nil), args...))
|
|
return []byte{}, nil
|
|
},
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse("4.0.0"),
|
|
logger: logger,
|
|
kubeconfig: "config",
|
|
kubeContext: "dev",
|
|
runner: runner,
|
|
}
|
|
|
|
err := helm.UpdatePlugin("secrets", "https://github.com/jkroepke/helm-secrets", "v4.7.0")
|
|
require.NoError(t, err)
|
|
|
|
// Verify that "plugin update" was NOT called (the Helm 4 secrets path should skip it).
|
|
for _, args := range calledArgs {
|
|
for i, a := range args {
|
|
if a == pluginCmd && i+1 < len(args) && args[i+1] == "update" {
|
|
t.Errorf("expected 'plugin update' to not be called for Helm 4 secrets, but it was: %v", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify that uninstall was called for all three split plugins.
|
|
checkUninstall := func(name string) {
|
|
for _, args := range calledArgs {
|
|
for i, a := range args {
|
|
if a == pluginCmd && i+2 < len(args) && args[i+1] == "uninstall" && args[i+2] == name {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.Errorf("expected 'plugin uninstall %s' to be called", name)
|
|
}
|
|
checkUninstall("secrets")
|
|
checkUninstall("secrets-getter")
|
|
checkUninstall("secrets-post-renderer")
|
|
|
|
// Verify that install was called for all three split plugin tarballs.
|
|
checkInstall := func(urlSubstring string) {
|
|
for _, args := range calledArgs {
|
|
for i, a := range args {
|
|
if a == pluginCmd && i+2 < len(args) && args[i+1] == installCmd && strings.Contains(args[i+2], urlSubstring) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.Errorf("expected 'plugin install' for %q to be called", urlSubstring)
|
|
}
|
|
checkInstall("secrets-4.7.0.tgz")
|
|
checkInstall("secrets-getter-4.7.0.tgz")
|
|
checkInstall("secrets-post-renderer-4.7.0.tgz")
|
|
}
|
|
|
|
// Test_UpdatePlugin_GeneralPathUsesUninstallReinstall verifies that for a regular
|
|
// plugin, UpdatePlugin does NOT rely on the unreliable `helm plugin update`
|
|
// command. Instead it must uninstall the existing plugin and reinstall the exact
|
|
// pinned version. See issues #2726 and #2548: `helm plugin update` reports
|
|
// success but leaves `helm plugin list` showing the old version because it
|
|
// re-installs from the cached source without the --version flag.
|
|
func Test_UpdatePlugin_GeneralPathUsesUninstallReinstall(t *testing.T) {
|
|
var calledArgs [][]string
|
|
runner := &funcRunner{
|
|
execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
|
calledArgs = append(calledArgs, append([]string(nil), args...))
|
|
return []byte{}, nil
|
|
},
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse("3.16.4"),
|
|
logger: logger,
|
|
runner: runner,
|
|
}
|
|
|
|
err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
|
|
require.NoError(t, err)
|
|
|
|
// `plugin update` must never be used: it does not honor --version and silently
|
|
// leaves the old version installed.
|
|
for _, args := range calledArgs {
|
|
for i, a := range args {
|
|
if a == pluginCmd && i+1 < len(args) && args[i+1] == "update" {
|
|
t.Errorf("expected 'plugin update' to not be called, but it was: %v", args)
|
|
}
|
|
}
|
|
}
|
|
|
|
// `plugin uninstall diff` must be called to clear the stale version.
|
|
uninstalled := false
|
|
for _, args := range calledArgs {
|
|
for i, a := range args {
|
|
if a == pluginCmd && i+2 < len(args) && args[i+1] == "uninstall" && args[i+2] == "diff" {
|
|
uninstalled = true
|
|
}
|
|
}
|
|
}
|
|
require.True(t, uninstalled, "expected 'plugin uninstall diff' to be called")
|
|
|
|
// `plugin install <repo> --version <pinned>` must be called with the exact
|
|
// requested version so the installed plugin is updated to it.
|
|
installed := false
|
|
for _, args := range calledArgs {
|
|
for i, a := range args {
|
|
if a == pluginCmd && i+1 < len(args) && args[i+1] == installCmd {
|
|
hasRepo := false
|
|
hasVersion := false
|
|
for _, arg := range args[i+2:] {
|
|
if arg == "https://github.com/databus23/helm-diff" {
|
|
hasRepo = true
|
|
}
|
|
if arg == "v3.15.10" {
|
|
hasVersion = true
|
|
}
|
|
}
|
|
if hasRepo && hasVersion {
|
|
installed = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
require.True(t, installed, "expected 'plugin install' with the pinned version to be called")
|
|
}
|
|
|
|
// Test_UpdatePlugin_NotFoundUninstallProceedsToInstall ensures that when the
|
|
// plugin is already absent (helm reports its "plugin not found" message),
|
|
// UpdatePlugin still proceeds to install the pinned version rather than treating
|
|
// it as an error. Both helm major formats are covered.
|
|
func Test_UpdatePlugin_NotFoundUninstallProceedsToInstall(t *testing.T) {
|
|
// Helm 4 reports "plugin: <name> not found"; Helm 3 reports "Plugin: <name> not found".
|
|
for _, tc := range []struct {
|
|
name string
|
|
version string
|
|
msg string
|
|
}{
|
|
{name: "helm4", version: "4.2.3", msg: "plugin: diff not found"},
|
|
{name: "helm3", version: "3.16.4", msg: "Plugin: diff not found"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var calledArgs [][]string
|
|
runner := &funcRunner{
|
|
execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
|
calledArgs = append(calledArgs, append([]string(nil), args...))
|
|
if len(args) >= 2 && args[0] == pluginCmd && args[1] == "uninstall" {
|
|
return nil, ExitError{Message: tc.msg, Code: 1}
|
|
}
|
|
return []byte{}, nil
|
|
},
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse(tc.version),
|
|
logger: logger,
|
|
runner: runner,
|
|
}
|
|
|
|
err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
|
|
require.NoError(t, err, "a plugin-absent uninstall error should be ignored and install should proceed")
|
|
|
|
// install with the pinned version must still be attempted
|
|
installed := false
|
|
for _, args := range calledArgs {
|
|
for i, a := range args {
|
|
if a == pluginCmd && i+1 < len(args) && args[i+1] == installCmd {
|
|
for _, arg := range args[i+2:] {
|
|
if arg == "v3.15.10" {
|
|
installed = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
require.True(t, installed, "expected install to proceed after a plugin-absent uninstall")
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test_UpdatePlugin_RealUninstallFailureReturnsError ensures that a genuine
|
|
// uninstall failure (permissions, broken Helm, etc.) is surfaced instead of
|
|
// being ignored, which would otherwise mask the root cause behind a confusing
|
|
// "plugin already exists" error from the subsequent install.
|
|
func Test_UpdatePlugin_RealUninstallFailureReturnsError(t *testing.T) {
|
|
installCalled := false
|
|
runner := &funcRunner{
|
|
execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
|
if len(args) >= 2 && args[0] == pluginCmd && args[1] == "uninstall" {
|
|
return nil, ExitError{Message: "permission denied", Code: 1}
|
|
}
|
|
if len(args) >= 2 && args[0] == pluginCmd && args[1] == installCmd {
|
|
installCalled = true
|
|
}
|
|
return []byte{}, nil
|
|
},
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse("3.16.4"),
|
|
logger: logger,
|
|
runner: runner,
|
|
}
|
|
|
|
err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
|
|
require.Error(t, err, "a real uninstall failure should be returned")
|
|
assert.Contains(t, err.Error(), "uninstall")
|
|
assert.False(t, installCalled, "install must not be attempted after a real uninstall failure")
|
|
}
|
|
|
|
// Test_UpdatePlugin_ExecutableNotFoundUninstallErrorIsNotSwallowed guards against
|
|
// an overly broad "not found" check: when helm itself is missing the runner
|
|
// surfaces an error containing "executable file not found", which must NOT be
|
|
// mistaken for an absent plugin (otherwise UpdatePlugin would silently log and
|
|
// proceed to install, masking the real problem). Only the helm-specific
|
|
// "plugin: <name> not found" message is tolerated.
|
|
func Test_UpdatePlugin_ExecutableNotFoundUninstallErrorIsNotSwallowed(t *testing.T) {
|
|
installCalled := false
|
|
runner := &funcRunner{
|
|
execute: func(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) {
|
|
if len(args) >= 2 && args[0] == pluginCmd && args[1] == "uninstall" {
|
|
// Mimics helmfile's ShellRunner when the helm binary is absent.
|
|
return nil, fmt.Errorf("unexpected error: exec: %q: executable file not found in $PATH", cmd)
|
|
}
|
|
if len(args) >= 2 && args[0] == pluginCmd && args[1] == installCmd {
|
|
installCalled = true
|
|
}
|
|
return []byte{}, nil
|
|
},
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
logger := NewLogger(&buffer, "debug")
|
|
helm := &execer{
|
|
helmBinary: "helm",
|
|
version: semver.MustParse("3.16.4"),
|
|
logger: logger,
|
|
runner: runner,
|
|
}
|
|
|
|
err := helm.UpdatePlugin("diff", "https://github.com/databus23/helm-diff", "v3.15.10")
|
|
require.Error(t, err, "a missing-binary uninstall error must be returned, not swallowed")
|
|
assert.Contains(t, err.Error(), "executable file not found")
|
|
assert.False(t, installCalled, "install must not be attempted when the helm binary is missing")
|
|
}
|
|
|
|
func Test_dedupeWroteLines(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
name: "empty",
|
|
in: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "no wrote lines untouched",
|
|
in: "Found 25 resources in manifest for release system\n",
|
|
want: "Found 25 resources in manifest for release system\n",
|
|
},
|
|
{
|
|
name: "collapses duplicate wrote lines preserving first-seen order",
|
|
in: "wrote /tmp/a/service_account.yaml\n" +
|
|
"wrote /tmp/a/service_account.yaml\n" +
|
|
"wrote /tmp/a/secrets.yaml\n" +
|
|
"wrote /tmp/a/service_account.yaml\n" +
|
|
"wrote /tmp/a/secrets.yaml\n" +
|
|
"Found 25 resources in manifest for release system",
|
|
want: "wrote /tmp/a/service_account.yaml\n" +
|
|
"wrote /tmp/a/secrets.yaml\n" +
|
|
"Found 25 resources in manifest for release system",
|
|
},
|
|
{
|
|
name: "non-wrote duplicates kept",
|
|
in: "hello\nhello\n",
|
|
want: "hello\nhello\n",
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got := string(dedupeWroteLines([]byte(c.in)))
|
|
if got != c.want {
|
|
t.Errorf("dedupeWroteLines() = %q, want %q", got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|