From 1e41788adc41820b17ec67274db6b21991ff5e0c Mon Sep 17 00:00:00 2001 From: caretak3r <50377477+caretak3r@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:41:58 -0500 Subject: [PATCH] =?UTF-8?q?feat(spec):=20Task=205=20=E2=80=94=20OrderedWai?= =?UTF-8?q?tStrategy,=20--wait=3Dordered,=20--readiness-timeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds OrderedWaitStrategy ("ordered") to the WaitStrategy enum in pkg/kube. Updates --wait flag to accept "ordered" and flags.go validation. Adds --readiness-timeout flag (default 1m) to install and upgrade commands. Adds ReadinessTimeout field to Install and Upgrade action structs. Validates ReadinessTimeout must not exceed Timeout at run time. --- pkg/action/install.go | 8 ++++++++ pkg/action/upgrade.go | 8 ++++++++ pkg/cmd/flags.go | 6 +++--- pkg/cmd/flags_test.go | 22 ++++++++++++++++++++++ pkg/cmd/install.go | 1 + pkg/cmd/upgrade.go | 1 + pkg/kube/client.go | 15 +++++++++++++-- 7 files changed, 56 insertions(+), 5 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 0fe1f1a6e..6b318fdf3 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -100,6 +100,10 @@ type Install struct { Devel bool DependencyUpdate bool Timeout time.Duration + // ReadinessTimeout is the per-batch timeout when --wait=ordered is used. + // Each batch waits at most this long for resources to become ready. + // Must not exceed Timeout. Defaults to 1 minute when zero. + ReadinessTimeout time.Duration Namespace string ReleaseName string GenerateName bool @@ -290,6 +294,10 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, fmt.Errorf("release name check failed: %w", err) } + if i.ReadinessTimeout > 0 && i.Timeout > 0 && i.ReadinessTimeout > i.Timeout { + return nil, fmt.Errorf("--readiness-timeout (%s) must not exceed --timeout (%s)", i.ReadinessTimeout, i.Timeout) + } + if err := chartutil.ProcessDependencies(chrt, vals); err != nil { i.cfg.Logger().Error("chart dependencies processing failed", slog.Any("error", err)) return nil, fmt.Errorf("chart dependencies processing failed: %w", err) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 4c93855b1..8ebf9a24e 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -70,6 +70,10 @@ type Upgrade struct { SkipCRDs bool // Timeout is the timeout for this operation Timeout time.Duration + // ReadinessTimeout is the per-batch timeout when --wait=ordered is used. + // Each batch waits at most this long for resources to become ready. + // Must not exceed Timeout. Defaults to 1 minute when zero. + ReadinessTimeout time.Duration // WaitStrategy determines what type of waiting should be done WaitStrategy kube.WaitStrategy // WaitOptions are additional options for waiting on resources @@ -187,6 +191,10 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char return nil, fmt.Errorf("release name is invalid: %s", name) } + if u.ReadinessTimeout > 0 && u.Timeout > 0 && u.ReadinessTimeout > u.Timeout { + return nil, fmt.Errorf("--readiness-timeout (%s) must not exceed --timeout (%s)", u.ReadinessTimeout, u.Timeout) + } + u.cfg.Logger().Debug("preparing upgrade", "name", name) currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals) if err != nil { diff --git a/pkg/cmd/flags.go b/pkg/cmd/flags.go index 6d9d117f8..00db2ea6f 100644 --- a/pkg/cmd/flags.go +++ b/pkg/cmd/flags.go @@ -59,7 +59,7 @@ func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) { cmd.Flags().Var( newWaitValue(kube.HookOnlyStrategy, wait), "wait", - "wait until resources are ready (up to --timeout). Use '--wait' alone for 'watcher' strategy, or specify one of: 'watcher', 'hookOnly', 'legacy'. Default when flag is omitted: 'hookOnly'.", + "wait until resources are ready (up to --timeout). Use '--wait' alone for 'watcher' strategy, or specify one of: 'watcher', 'hookOnly', 'legacy', 'ordered'. Default when flag is omitted: 'hookOnly'.", ) cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy) } @@ -80,7 +80,7 @@ func (ws *waitValue) String() string { func (ws *waitValue) Set(s string) error { switch s { - case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy): + case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy), string(kube.OrderedWaitStrategy): *ws = waitValue(s) return nil case "true": @@ -92,7 +92,7 @@ func (ws *waitValue) Set(s string) error { *ws = waitValue(kube.HookOnlyStrategy) return nil default: - return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy) + return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy, kube.OrderedWaitStrategy) } } diff --git a/pkg/cmd/flags_test.go b/pkg/cmd/flags_test.go index 614970252..02f28c038 100644 --- a/pkg/cmd/flags_test.go +++ b/pkg/cmd/flags_test.go @@ -25,6 +25,7 @@ import ( "helm.sh/helm/v4/pkg/action" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -99,6 +100,27 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) { runTestCmd(t, tests) } +func TestWaitValueOrdered(t *testing.T) { + // --wait=ordered should be accepted and set the strategy correctly. + var ws kube.WaitStrategy + wv := newWaitValue(kube.HookOnlyStrategy, &ws) + + if err := wv.Set("ordered"); err != nil { + t.Fatalf("expected --wait=ordered to be accepted, got error: %v", err) + } + if ws != kube.OrderedWaitStrategy { + t.Errorf("expected OrderedWaitStrategy, got %q", ws) + } +} + +func TestWaitValueInvalid(t *testing.T) { + var ws kube.WaitStrategy + wv := newWaitValue(kube.HookOnlyStrategy, &ws) + if err := wv.Set("invalid"); err == nil { + t.Error("expected error for invalid wait strategy, got nil") + } +} + func TestPostRendererFlagSetOnce(t *testing.T) { cfg := action.Configuration{} client := action.NewInstall(&cfg) diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index d36cd9e34..81f18bcc4 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -212,6 +212,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal addValueOptionsFlags(f, valueOpts) addChartPathOptionsFlags(f, &client.ChartPathOptions) AddWaitFlag(cmd, &client.WaitStrategy) + f.DurationVar(&client.ReadinessTimeout, "readiness-timeout", time.Minute, "per-batch timeout when --wait=ordered is used; each resource batch must become ready within this duration (must not exceed --timeout)") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 918d6f5b8..07047482b 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -305,6 +305,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { bindOutputFlag(cmd, &outfmt) bindPostRenderFlag(cmd, &client.PostRenderer, settings) AddWaitFlag(cmd, &client.WaitStrategy) + f.DurationVar(&client.ReadinessTimeout, "readiness-timeout", time.Minute, "per-batch timeout when --wait=ordered is used; each resource batch must become ready within this duration (must not exceed --timeout)") cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") diff --git a/pkg/kube/client.go b/pkg/kube/client.go index fc706496d..d61c1d97d 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -117,6 +117,13 @@ const ( // HookOnlyStrategy: wait only for hook Pods/Jobs to complete; does not wait for general chart resources. HookOnlyStrategy WaitStrategy = "hookOnly" + + // OrderedWaitStrategy: deploy resources in DAG-ordered batches, waiting for each batch to become ready + // before proceeding to the next. Subchart and resource-group sequencing is driven by + // helm.sh/depends-on/subcharts, helm.sh/resource-group, and helm.sh/depends-on/resource-groups annotations. + // Custom readiness conditions via helm.sh/readiness-success and helm.sh/readiness-failure are evaluated + // per-resource; otherwise kstatus is used. + OrderedWaitStrategy WaitStrategy = "ordered" ) type FieldValidationDirective string @@ -200,10 +207,14 @@ func (c *Client) GetWaiterWithOptions(strategy WaitStrategy, opts ...WaitOption) return nil, err } return &hookOnlyWaiter{sw: sw}, nil + case OrderedWaitStrategy: + // Sequencing logic lives in the action layer; at the kube level, ordered + // strategy uses the status watcher for per-batch readiness checks. + return c.newStatusWatcher(opts...) case "": - return nil, errors.New("wait strategy not set. Choose one of: " + string(StatusWatcherStrategy) + ", " + string(HookOnlyStrategy) + ", " + string(LegacyStrategy)) + return nil, errors.New("wait strategy not set. Choose one of: " + string(StatusWatcherStrategy) + ", " + string(HookOnlyStrategy) + ", " + string(LegacyStrategy) + ", " + string(OrderedWaitStrategy)) default: - return nil, errors.New("unknown wait strategy (s" + string(strategy) + "). Valid values are: " + string(StatusWatcherStrategy) + ", " + string(HookOnlyStrategy) + ", " + string(LegacyStrategy)) + return nil, errors.New("unknown wait strategy (" + string(strategy) + "). Valid values are: " + string(StatusWatcherStrategy) + ", " + string(HookOnlyStrategy) + ", " + string(LegacyStrategy) + ", " + string(OrderedWaitStrategy)) } }