diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index c2e5e295d..d1f0fec58 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -54,7 +54,7 @@ func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) { cmd.Flags().Var( - newWaitValue(wait), + newWaitValue(kube.HookOnlyStrategy, wait), "wait", "if set, will wait until all resources are in the expected state before marking the operation as successful. It will wait for as long as --timeout. Options are (true, false, watcher, and legacy)", ) @@ -64,7 +64,8 @@ func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) { type waitValue kube.WaitStrategy -func newWaitValue(ws *kube.WaitStrategy) *waitValue { +func newWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy) *waitValue { + *ws = defaultValue return (*waitValue)(ws) } @@ -77,7 +78,7 @@ func (ws *waitValue) String() string { func (ws *waitValue) Set(s string) error { switch s { - case string(kube.StatusWatcherStrategy), string(kube.LegacyWaiterStrategy): + case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy): *ws = waitValue(s) return nil case "true": @@ -87,7 +88,7 @@ func (ws *waitValue) Set(s string) error { *ws = "" return nil default: - return fmt.Errorf("invalid wait input %q. Valid inputs are true, false, %s, and %s", s, kube.StatusWatcherStrategy, kube.LegacyWaiterStrategy) + return fmt.Errorf("invalid wait input %q. Valid inputs are true, false, %s, and %s", s, kube.StatusWatcherStrategy, kube.LegacyStrategy) } } diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 16545b6ae..649c5c8b8 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -198,7 +198,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema") - f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used") + f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically to \"watcher\" if --atomic is used") f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present") f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") f.BoolVar(&client.SkipSchemaValidation, "skip-schema-validation", false, "if set, disables JSON schema validation") diff --git a/cmd/helm/uninstall.go b/cmd/helm/uninstall.go index 9c5e25c87..3504fd322 100644 --- a/cmd/helm/uninstall.go +++ b/cmd/helm/uninstall.go @@ -76,10 +76,10 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") f.BoolVar(&client.IgnoreNotFound, "ignore-not-found", false, `Treat "release not found" as a successful uninstall`) f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") - f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout") f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background.") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.StringVar(&client.Description, "description", "", "add a custom description") + AddWaitFlag(cmd, &client.Wait) return cmd } diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index e5e485eae..092f6bdcc 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -279,7 +279,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored") f.BoolVar(&client.ResetThenReuseValues, "reset-then-reuse-values", false, "when upgrading, reset the values to the ones built into the chart, apply the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' or '--reuse-values' is specified, this is ignored") f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") - f.BoolVar(&client.Atomic, "atomic", false, "if set, upgrade process rolls back changes made in case of failed upgrade. The --wait flag will be set automatically if --atomic is used") + f.BoolVar(&client.Atomic, "atomic", false, "if set, upgrade process rolls back changes made in case of failed upgrade. The --wait flag will be set automatically to \"watcher\" if --atomic is used") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails") f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") diff --git a/pkg/action/install.go b/pkg/action/install.go index 61b5ebd33..a12dee11d 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -293,9 +293,9 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma // Make sure if Atomic is set, that wait is set as well. This makes it so // the user doesn't have to specify both - if !i.shouldWait() { + if i.Wait == kube.HookOnlyStrategy { if i.Atomic { - i.Wait = "watcher" + i.Wait = kube.StatusWatcherStrategy } } @@ -473,15 +473,13 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource return rel, err } - if i.shouldWait() { - if i.WaitForJobs { - err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout) - } else { - err = i.cfg.KubeClient.Wait(resources, i.Timeout) - } - if err != nil { - return rel, err - } + if i.WaitForJobs { + err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout) + } else { + err = i.cfg.KubeClient.Wait(resources, i.Timeout) + } + if err != nil { + return rel, err } if !i.DisableHooks { diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index 8ec134832..8cb8b4ed4 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -228,21 +228,19 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas } } - if r.shouldWait() { - if r.WaitForJobs { - if err := r.cfg.KubeClient.WaitWithJobs(target, r.Timeout); err != nil { - targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) - r.cfg.recordRelease(currentRelease) - r.cfg.recordRelease(targetRelease) - return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name) - } - } else { - if err := r.cfg.KubeClient.Wait(target, r.Timeout); err != nil { - targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) - r.cfg.recordRelease(currentRelease) - r.cfg.recordRelease(targetRelease) - return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name) - } + if r.WaitForJobs { + if err := r.cfg.KubeClient.WaitWithJobs(target, r.Timeout); err != nil { + targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) + r.cfg.recordRelease(currentRelease) + r.cfg.recordRelease(targetRelease) + return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name) + } + } else { + if err := r.cfg.KubeClient.Wait(target, r.Timeout); err != nil { + targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) + r.cfg.recordRelease(currentRelease) + r.cfg.recordRelease(targetRelease) + return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name) } } diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 75d999976..0a03f2180 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -41,7 +41,7 @@ type Uninstall struct { DryRun bool IgnoreNotFound bool KeepHistory bool - Wait bool + Wait kube.WaitStrategy DeletionPropagation string Timeout time.Duration Description string @@ -130,10 +130,8 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) } res.Info = kept - if u.Wait { - if err := u.cfg.KubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil { - errs = append(errs, err) - } + if err := u.cfg.KubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil { + errs = append(errs, err) } if !u.DisableHooks { diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index eca9e6ad8..1c67cab7f 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/release" ) @@ -82,7 +83,7 @@ func TestUninstallRelease_Wait(t *testing.T) { unAction := uninstallAction(t) unAction.DisableHooks = true unAction.DryRun = false - unAction.Wait = true + unAction.Wait = kube.StatusWatcherStrategy rel := releaseStub() rel.Name = "come-fail-away" @@ -113,7 +114,7 @@ func TestUninstallRelease_Cascade(t *testing.T) { unAction := uninstallAction(t) unAction.DisableHooks = true unAction.DryRun = false - unAction.Wait = false + unAction.Wait = kube.HookOnlyStrategy unAction.DeletionPropagation = "foreground" rel := releaseStub() diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 8d103ab6b..671426a27 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -155,7 +155,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart. // Make sure if Atomic is set, that wait is set as well. This makes it so // the user doesn't have to specify both - if !u.shouldWait() { + if u.Wait == kube.HookOnlyStrategy { if u.Atomic { u.Wait = kube.StatusWatcherStrategy } @@ -190,10 +190,6 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart. return res, nil } -func (u *Upgrade) shouldWait() bool { - return u.Wait != "" -} - // isDryRun returns true if Upgrade is set to run as a DryRun func (u *Upgrade) isDryRun() bool { if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" { @@ -451,22 +447,17 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele } } - if u.shouldWait() { - u.cfg.Log( - "waiting for release %s resources (created: %d updated: %d deleted: %d)", - upgradedRelease.Name, len(results.Created), len(results.Updated), len(results.Deleted)) - if u.WaitForJobs { - if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { - u.cfg.recordRelease(originalRelease) - u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) - return - } - } else { - if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { - u.cfg.recordRelease(originalRelease) - u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) - return - } + if u.WaitForJobs { + if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { + u.cfg.recordRelease(originalRelease) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return + } + } else { + if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { + u.cfg.recordRelease(originalRelease) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return } } @@ -534,7 +525,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e rollin := NewRollback(u.cfg) rollin.Version = filteredHistory[0].Version - if !u.shouldWait() { + if u.Wait == kube.HookOnlyStrategy { rollin.Wait = kube.StatusWatcherStrategy } rollin.WaitForJobs = u.WaitForJobs diff --git a/pkg/kube/client.go b/pkg/kube/client.go index ba7794ac4..de28c3421 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -84,7 +84,8 @@ type WaitStrategy string const ( StatusWatcherStrategy WaitStrategy = "watcher" - LegacyWaiterStrategy WaitStrategy = "legacy" + LegacyStrategy WaitStrategy = "legacy" + HookOnlyStrategy WaitStrategy = "noop" ) func init() { @@ -98,36 +99,46 @@ func init() { } } +func (c *Client) newStatusWatcher() (*statusWaiter, error) { + cfg, err := c.Factory.ToRESTConfig() + if err != nil { + return nil, err + } + dynamicClient, err := c.Factory.DynamicClient() + if err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(cfg) + if err != nil { + return nil, err + } + restMapper, err := apiutil.NewDynamicRESTMapper(cfg, httpClient) + if err != nil { + return nil, err + } + return &statusWaiter{ + restMapper: restMapper, + client: dynamicClient, + log: c.Log, + }, nil +} + func (c *Client) newWaiter(strategy WaitStrategy) (Waiter, error) { switch strategy { - case LegacyWaiterStrategy: + case LegacyStrategy: kc, err := c.Factory.KubernetesClientSet() if err != nil { return nil, err } return &HelmWaiter{kubeClient: kc, log: c.Log}, nil case StatusWatcherStrategy: - cfg, err := c.Factory.ToRESTConfig() - if err != nil { - return nil, err - } - dynamicClient, err := c.Factory.DynamicClient() - if err != nil { - return nil, err - } - httpClient, err := rest.HTTPClientFor(cfg) - if err != nil { - return nil, err - } - restMapper, err := apiutil.NewDynamicRESTMapper(cfg, httpClient) + return c.newStatusWatcher() + case HookOnlyStrategy: + sw, err := c.newStatusWatcher() if err != nil { return nil, err } - return &statusWaiter{ - restMapper: restMapper, - client: dynamicClient, - log: c.Log, - }, nil + return &hookOnlyWaiter{sw: sw}, nil default: return nil, errors.New("unknown wait strategy") } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 4c8719f98..8c8f89cdb 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -513,7 +513,7 @@ func TestWait(t *testing.T) { }), } var err error - c.Waiter, err = c.newWaiter(LegacyWaiterStrategy) + c.Waiter, err = c.newWaiter(LegacyStrategy) if err != nil { t.Fatal(err) } @@ -570,7 +570,7 @@ func TestWaitJob(t *testing.T) { }), } var err error - c.Waiter, err = c.newWaiter(LegacyWaiterStrategy) + c.Waiter, err = c.newWaiter(LegacyStrategy) if err != nil { t.Fatal(err) } @@ -629,7 +629,7 @@ func TestWaitDelete(t *testing.T) { }), } var err error - c.Waiter, err = c.newWaiter(LegacyWaiterStrategy) + c.Waiter, err = c.newWaiter(LegacyStrategy) if err != nil { t.Fatal(err) } diff --git a/pkg/kube/statuswait.go b/pkg/kube/statuswait.go index 0729d0d1b..4a0dcd0d2 100644 --- a/pkg/kube/statuswait.go +++ b/pkg/kube/statuswait.go @@ -209,3 +209,23 @@ func statusObserver(cancel context.CancelFunc, desired status.Status, logFn func } } } + +type hookOnlyWaiter struct { + sw *statusWaiter +} + +func (w *hookOnlyWaiter) WatchUntilReady(resourceList ResourceList, timeout time.Duration) error { + return w.sw.WatchUntilReady(resourceList, timeout) +} + +func (w *hookOnlyWaiter) Wait(resourceList ResourceList, timeout time.Duration) error { + return nil +} + +func (w *hookOnlyWaiter) WaitWithJobs(resourceList ResourceList, timeout time.Duration) error { + return nil +} + +func (w *hookOnlyWaiter) WaitForDelete(resourceList ResourceList, timeout time.Duration) error { + return nil +}