diff --git a/cmd/helm.go b/cmd/helm.go index 2d7f36bf..4281aa51 100644 --- a/cmd/helm.go +++ b/cmd/helm.go @@ -217,8 +217,10 @@ func (d *diffCmd) template(isUpgrade bool) ([]byte, error) { // Helm automatically enable --reuse-values when there's no --set, --set-string, --set-json, --set-values, --set-file present. // Let's simulate that in helm-diff. // See https://medium.com/@kcatstack/understand-helm-upgrade-flags-reset-values-reuse-values-6e58ac8f127e - shouldDefaultReusingValues := isUpgrade && len(d.values) == 0 && len(d.stringValues) == 0 && len(d.stringLiteralValues) == 0 && len(d.jsonValues) == 0 && len(d.valueFiles) == 0 && len(d.fileValues) == 0 - if (d.reuseValues || d.resetThenReuseValues || shouldDefaultReusingValues) && !d.resetValues && d.clusterAccessAllowed() { + shouldDefaultReusingValues := len(d.values) == 0 && len(d.stringValues) == 0 && len(d.stringLiteralValues) == 0 && len(d.jsonValues) == 0 && len(d.valueFiles) == 0 && len(d.fileValues) == 0 + // A release that is not installed yet has no values to reuse. Like `helm upgrade --install`, + // ignore --reuse-values and --reset-then-reuse-values in that case. + if isUpgrade && (d.reuseValues || d.resetThenReuseValues || shouldDefaultReusingValues) && !d.resetValues && d.clusterAccessAllowed() { tmpfile, err := os.CreateTemp("", "existing-values") if err != nil { return nil, err diff --git a/cmd/main_test.go b/cmd/main_test.go index c49bc6b9..3b4fc58e 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -73,7 +73,7 @@ func TestMain(m *testing.M) { } else { fmt.Print(os.Getenv("HELM_DIFF_FAKE_OUTPUT_2")) } - case "capture_args": + case "capture_args", "unreleased": argsFile := os.Getenv("HELM_DIFF_FAKE_ARGS_FILE") if argsFile != "" { f, err := os.OpenFile(argsFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) @@ -82,6 +82,12 @@ func TestMain(m *testing.M) { _ = f.Close() } } + // "unreleased" captures the args like "capture_args", but every + // `helm get` fails the way helm does when the release does not exist. + if mode == "unreleased" && len(os.Args) > 1 && os.Args[1] == "get" { + fmt.Fprintln(os.Stderr, "Error: release: not found") + os.Exit(1) + } printFakeHelmOutput() default: printFakeHelmOutput() diff --git a/cmd/upgrade_test.go b/cmd/upgrade_test.go index bab777f9..e7a79151 100644 --- a/cmd/upgrade_test.go +++ b/cmd/upgrade_test.go @@ -380,6 +380,129 @@ data: }) } +// A release that is not installed yet has no values to reuse, so the flags that +// reuse them must not make the diff of a new install fail. +// See https://github.com/databus23/helm-diff/issues/481 +func TestUpgradeCommand_Execution_ReuseValues(t *testing.T) { + manifestYAML := `--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config + namespace: prod-apps +data: + key: value +` + + cases := []struct { + name string + // fakeHelmMode is "capture_args" for an existing release and + // "unreleased" for a release that does not exist. + fakeHelmMode string + args []string + useUpgradeDryRun bool + // wantGetValues is the expected `helm get values` call. + // Empty means that the values must not be fetched at all. + wantGetValues string + }{ + { + name: "existing release with --reuse-values", + fakeHelmMode: "capture_args", + args: []string{"--reuse-values"}, + wantGetValues: "get values my-release --output yaml --all --namespace prod-apps\n", + }, + { + name: "existing release with --reset-then-reuse-values", + fakeHelmMode: "capture_args", + args: []string{"--reset-then-reuse-values"}, + wantGetValues: "get values my-release --output yaml --namespace prod-apps\n", + }, + { + name: "existing release with --install --reuse-values", + fakeHelmMode: "capture_args", + args: []string{"--install", "--reuse-values"}, + wantGetValues: "get values my-release --output yaml --all --namespace prod-apps\n", + }, + { + // Helm enables --reuse-values by default when there is no --set, --set-string, + // --set-literal, --set-json, --values or --set-file flag, so the values of an + // existing release are fetched even without any of the reuse flags. + name: "existing release without value flags", + fakeHelmMode: "capture_args", + wantGetValues: "get values my-release --output yaml --all --namespace prod-apps\n", + }, + { + name: "unreleased with --install", + fakeHelmMode: "unreleased", + args: []string{"--install"}, + }, + { + name: "unreleased with --allow-unreleased --reuse-values", + fakeHelmMode: "unreleased", + args: []string{"--allow-unreleased", "--reuse-values"}, + }, + { + name: "unreleased with --install --reuse-values", + fakeHelmMode: "unreleased", + args: []string{"--install", "--reuse-values"}, + }, + { + name: "unreleased with --allow-unreleased --reset-then-reuse-values", + fakeHelmMode: "unreleased", + args: []string{"--allow-unreleased", "--reset-then-reuse-values"}, + }, + { + name: "unreleased with --install --reset-then-reuse-values", + fakeHelmMode: "unreleased", + args: []string{"--install", "--reset-then-reuse-values"}, + }, + { + name: "unreleased with --install --reuse-values and HELM_DIFF_USE_UPGRADE_DRY_RUN", + fakeHelmMode: "unreleased", + args: []string{"--install", "--reuse-values"}, + useUpgradeDryRun: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + argsFile := t.TempDir() + "/args" + setupFakeHelm(t, tc.fakeHelmMode, manifestYAML, argsFile, "") + renderSubcmd := "template" + if tc.useUpgradeDryRun { + t.Setenv("HELM_DIFF_USE_UPGRADE_DRY_RUN", "true") + renderSubcmd = "upgrade" + } + + chartDir := t.TempDir() + cmd := newChartCommand() + cmd.SetArgs(append([]string{"my-release", chartDir, "-n", "prod-apps"}, tc.args...)) + + err := cmd.Execute() + if err != nil { + t.Fatalf("unexpected error executing upgrade command: %v", err) + } + + data, err := os.ReadFile(argsFile) + if err != nil { + t.Fatalf("failed to read fake helm args: %v", err) + } + argsContent := string(data) + + if !strings.Contains(argsContent, renderSubcmd+" my-release "+chartDir) { + t.Errorf("expected the chart to be rendered with 'helm %s', got:\n%s", renderSubcmd, argsContent) + } + if tc.wantGetValues == "" { + if strings.Contains(argsContent, "get values") { + t.Errorf("expected no 'helm get values' call, got:\n%s", argsContent) + } + } else if !strings.Contains(argsContent, tc.wantGetValues) { + t.Errorf("expected 'helm %s', got:\n%s", strings.TrimSpace(tc.wantGetValues), argsContent) + } + }) + } +} + func TestThreeWayMergeModeFlag(t *testing.T) { if f := newChartCommand().Flags().Lookup("three-way-merge-mode"); f == nil { t.Fatal("expected flag --three-way-merge-mode to be registered")