diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index ff8a3de08..727611cad 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -158,7 +158,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.DryRunStrategy = dryRunStrategy - printWaitMessage(out, outfmt, client.WaitStrategy, client.DryRunStrategy, client.Timeout) + printWaitMessage(out, outfmt, client.WaitStrategy, client.RollbackOnFailure, client.DryRunStrategy, client.Timeout) rel, err := runInstall(args, client, valueOpts, out) if err != nil { @@ -373,7 +373,12 @@ func checkIfInstallable(ch chart.Accessor) error { return fmt.Errorf("%s charts are not installable", meta["Type"]) } -func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, dryRun action.DryRunStrategy, timeout time.Duration) { +func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, rollbackOnFailure bool, dryRun action.DryRunStrategy, timeout time.Duration) { + // Rollback-on-failure implicitly enables watcher waits in the action layer. + // Account for that here, before the action mutates its WaitStrategy. + if strategy == kube.HookOnlyStrategy && rollbackOnFailure { + strategy = kube.StatusWatcherStrategy + } if outfmt != output.Table || strategy == kube.HookOnlyStrategy || dryRun != action.DryRunNone { return } diff --git a/pkg/cmd/install_test.go b/pkg/cmd/install_test.go index d1e081e5c..59092efa9 100644 --- a/pkg/cmd/install_test.go +++ b/pkg/cmd/install_test.go @@ -17,14 +17,19 @@ limitations under the License. package cmd import ( + "bytes" "fmt" "net/http" "net/http/httptest" "path/filepath" "testing" + "time" "github.com/stretchr/testify/require" + "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/cli/output" + "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/repo/v1/repotest" ) @@ -323,3 +328,56 @@ func TestInstallFileCompletion(t *testing.T) { checkFileCompletion(t, "install myname", true) checkFileCompletion(t, "install myname mychart", false) } + +func TestPrintWaitMessage(t *testing.T) { + const message = "Waiting for resources to become ready (timeout: 42s)\n" + tests := []struct { + name string + format output.Format + strategy kube.WaitStrategy + rollbackOnFailure bool + dryRun action.DryRunStrategy + want string + }{ + {name: "explicit watcher wait", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunNone, want: message}, + {name: "implicit watcher wait", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunNone, want: message}, + {name: "hook-only wait", format: output.Table, strategy: kube.HookOnlyStrategy, dryRun: action.DryRunNone}, + {name: "structured output", format: output.JSON, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunNone}, + {name: "client dry run", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunClient}, + {name: "server dry run", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunServer}, + {name: "implicit wait client dry run", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunClient}, + {name: "implicit wait server dry run", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunServer}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var out bytes.Buffer + printWaitMessage(&out, tt.format, tt.strategy, tt.rollbackOnFailure, tt.dryRun, 42*time.Second) + require.Equal(t, tt.want, out.String()) + }) + } +} + +func TestInstallImplicitWaitProgress(t *testing.T) { + for _, flag := range []string{"--rollback-on-failure", "--atomic"} { + t.Run(flag, func(t *testing.T) { + defer resetEnv()() + + _, out, err := executeActionCommand("install implicit-wait testdata/testcharts/empty " + flag) + require.NoError(t, err) + require.Contains(t, out, "Waiting for resources to become ready (timeout: 5m0s)\n") + }) + } +} + +func TestInstallDryRunSuppressesWaitProgress(t *testing.T) { + for _, dryRun := range []string{"client", "server"} { + t.Run(dryRun, func(t *testing.T) { + defer resetEnv()() + + _, out, err := executeActionCommand("install dry-run-wait testdata/testcharts/empty --rollback-on-failure --dry-run=" + dryRun) + require.NoError(t, err) + require.NotContains(t, out, "Waiting for resources to become ready") + }) + } +} diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index e8bc834ac..6be0bab45 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -163,7 +163,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Replace = true } - printWaitMessage(out, outfmt, instClient.WaitStrategy, instClient.DryRunStrategy, instClient.Timeout) + printWaitMessage(out, outfmt, instClient.WaitStrategy, instClient.RollbackOnFailure, instClient.DryRunStrategy, instClient.Timeout) rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { @@ -259,7 +259,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cancel() }() - printWaitMessage(out, outfmt, client.WaitStrategy, client.DryRunStrategy, client.Timeout) + printWaitMessage(out, outfmt, client.WaitStrategy, client.RollbackOnFailure, client.DryRunStrategy, client.Timeout) rel, err := client.RunWithContext(ctx, args[0], ch, vals) if err != nil { diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 5aba7b11b..3a16d8323 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -587,3 +587,34 @@ func TestUpgradeInstallServerSideApply(t *testing.T) { }) } } + +func TestUpgradeImplicitWaitProgress(t *testing.T) { + tests := []struct { + name string + install bool + }{ + {name: "upgrade"}, + {name: "upgrade install", install: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + releaseName := "implicit-wait" + relMock, ch, chartPath := prepareMockRelease(t, releaseName) + store := storageFixture() + installFlag := "" + if tt.install { + installFlag = "--install" + } else { + require.NoError(t, store.Create(relMock(releaseName, 1, ch))) + } + + cmd := fmt.Sprintf("upgrade %s %s --rollback-on-failure '%s'", releaseName, installFlag, chartPath) + _, out, err := executeActionCommandC(store, cmd) + require.NoError(t, err) + assert.Contains(t, out, "Waiting for resources to become ready (timeout: 5m0s)\n") + }) + } +}