feat: implement HIP-0025 resource creation sequencing#32038
feat: implement HIP-0025 resource creation sequencing#32038
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Implements HIP-0025 ordered resource creation sequencing in Helm v4 by introducing DAG-based ordering for subcharts and resource groups, along with optional custom readiness evaluation and CLI support.
Changes:
- Added generic DAG utilities plus subchart/resource-group DAG builders and parsers.
- Implemented
--wait=orderedexecution across install/upgrade/rollback/uninstall and added--readiness-timeout. - Added ordered delimiter output for
helm template, new lint rules, and extensive unit/integration test coverage.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/release/v1/util/resource_group.go | Parses resource-group annotations and builds a DAG for ordered batching. |
| pkg/release/v1/util/resource_group_test.go | Unit tests for resource-group parsing, warnings, and DAG behavior. |
| pkg/release/v1/release.go | Adds SequencingInfo metadata to releases for rollback/uninstall behavior. |
| pkg/release/v1/release_test.go | Tests JSON backward compatibility and round-tripping for SequencingInfo. |
| pkg/kube/readiness.go | Adds custom readiness expression parsing and evaluation via JSONPath. |
| pkg/kube/readiness_test.go | Unit tests for custom readiness evaluation and expression parsing. |
| pkg/kube/custom_readiness_status_reader.go | Integrates custom readiness into kstatus waiting via a StatusReader. |
| pkg/kube/custom_readiness_status_reader_test.go | Verifies waiter integration and status reader behavior. |
| pkg/kube/options.go | Adds waiter option to enable the custom readiness status reader. |
| pkg/kube/client.go | Adds OrderedWaitStrategy and wires in optional custom readiness readers. |
| pkg/kube/client_wait_strategy_test.go | Tests waiter selection and error messaging for ordered strategy. |
| pkg/cmd/flags.go | Adds --wait=ordered parsing support (where applicable) and --readiness-timeout. |
| pkg/cmd/flags_test.go | Tests CLI flag acceptance/rejection and readiness-timeout validation. |
| pkg/cmd/install.go | Wires --readiness-timeout and ordered wait handling into install command. |
| pkg/cmd/upgrade.go | Wires --wait=ordered and --readiness-timeout into upgrade command. |
| pkg/cmd/template.go | Adds ordered template rendering with resource-group delimiters and subchart ordering. |
| pkg/cmd/template_test.go | Adds golden tests for ordered delimiter output and absence without ordered wait. |
| pkg/cmd/testdata/testcharts/sequenced-chart/Chart.yaml | Test fixture chart for ordered template output. |
| pkg/cmd/testdata/testcharts/sequenced-chart/templates/aa-databases-configmap.yaml | Test fixture manifest with resource-group annotation. |
| pkg/cmd/testdata/testcharts/sequenced-chart/templates/bb-app-configmap.yaml | Test fixture manifest with depends-on resource-group annotation. |
| pkg/cmd/testdata/testcharts/sequenced-chart/templates/cc-unsequenced-configmap.yaml | Test fixture manifest without sequencing annotations. |
| pkg/cmd/testdata/testcharts/sequenced-chart/charts/worker/Chart.yaml | Test subchart fixture for ordered template output. |
| pkg/cmd/testdata/testcharts/sequenced-chart/charts/worker/templates/aa-worker-configmap.yaml | Test subchart fixture manifest with resource-group annotation. |
| pkg/cmd/testdata/output/template-ordered-delimiters.txt | Golden output verifying START/END delimiters and ordering. |
| pkg/chart/v2/util/dag.go | Introduces generic DAG with deterministic batching and cycle detection. |
| pkg/chart/v2/util/dag_test.go | Unit tests for DAG batching, cycles, and helper methods. |
| pkg/chart/v2/util/subchart_dag.go | Builds subchart dependency DAG from Chart.yaml fields and annotation. |
| pkg/chart/v2/util/subchart_dag_test.go | Unit tests for subchart DAG ordering, aliases, disabled deps, and errors. |
| pkg/chart/v2/dependency.go | Adds depends-on field to chart dependencies for sequencing declarations. |
| pkg/chart/v2/dependency_json_test.go | Tests JSON/YAML tags and backward compatibility for the new dependency field. |
| pkg/chart/v2/lint/lint.go | Registers new sequencing lint rule. |
| pkg/chart/v2/lint/lint_test.go | Validates sequencing rules are registered and produce expected errors. |
| pkg/chart/v2/lint/rules/sequencing.go | Lint rules for subchart/resource-group cycles and readiness annotation completeness. |
| pkg/chart/v2/lint/rules/sequencing_test.go | Tests sequencing lint behavior across error/warning scenarios. |
| pkg/chart/common/util/jsonschema.go | Minor change to schema validation error formatting implementation. |
| pkg/action/action.go | Refactors render pipeline to optionally return sorted manifests for sequencing-aware actions. |
| pkg/action/sequencing.go | Adds sequenced deployment engine (two-level DAG, per-batch waits, annotation stripping). |
| pkg/action/sequencing_test.go | Comprehensive tests for ordered installs, batching, timeouts, and hooks behavior. |
| pkg/action/install.go | Adds ordered install execution path with per-batch readiness timeout. |
| pkg/action/upgrade.go | Adds ordered upgrade execution path and sequencing metadata recording. |
| pkg/action/upgrade_sequenced_test.go | Tests ordered upgrade behaviors including rollback-on-failure/cleanup and transitions. |
| pkg/action/rollback.go | Adds ordered rollback execution path based on stored sequencing metadata. |
| pkg/action/rollback_sequenced_test.go | Tests ordered rollback behavior, cleanup, and sequencing info preservation. |
| pkg/action/uninstall.go | Adds ordered uninstall path using reverse DAG order when sequencing metadata is present. |
| pkg/action/uninstall_sequenced_test.go | Tests sequenced uninstall reverse ordering, hooks, keep policy/history, and dry-run. |
| pkg/action/warning_system_test.go | Tests slog warnings for partial readiness, isolated groups, and orphan dependencies. |
| pkg/action/backward_compat_test.go | Verifies backward-compat behavior for ordered wait without annotations and legacy releases. |
Comments suppressed due to low confidence (1)
pkg/kube/client.go:1
- Because
newCustomReadinessStatusReader()is prepended and itsSupports()returns true for all kinds, enabling it changes which StatusReader is used for all resources in that wait (including those without custom readiness annotations). If the intent is to only affect resources that actually define both readiness annotations, consider tightening the enablement condition (e.g., only enable the reader when at least one resource in the batch has both annotations) to reduce behavior drift and minimize surprises in mixed batches.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func batchHasCustomReadiness(manifests []releaseutil.Manifest) bool { | ||
| for _, manifest := range manifests { | ||
| if manifest.Head == nil || manifest.Head.Metadata == nil { | ||
| continue | ||
| } | ||
| annotations := manifest.Head.Metadata.Annotations | ||
| if annotations == nil { | ||
| continue | ||
| } | ||
| if annotations[kube.AnnotationReadinessSuccess] != "" || annotations[kube.AnnotationReadinessFailure] != "" { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
This enables the custom readiness status reader for a whole batch even when only one of the readiness annotations is present. That triggers redundant warnings (you already warn in warnIfPartialReadinessAnnotations, and EvaluateCustomReadiness logs too) and needlessly changes the waiter reader stack for resources that will fall back to kstatus anyway. Consider requiring both helm.sh/readiness-success and helm.sh/readiness-failure to be non-empty before returning true, and rely on the existing warning path for partial annotation configs.
There was a problem hiding this comment.
This is intentional since a single annotation triggers a warning via
warnIfPartialReadinessAnnotations, and the reader gracefully falls back to kstatus for resources missing the pair.
Tightening the gate here would suppress that warning path. If a user sets only 1 annotation (readiness-success) they get zero feedback. Linting catches issues pre-deployment, whereas this would be caught during runtime (for charts that don't lint).
41f231a to
e5409e8
Compare
Document the new resource sequencing feature in Helm v4, including: - Resource group annotations and dependencies - Subchart sequencing via Chart.yaml - Custom readiness conditions with JSONPath - CLI flags (--wait=ordered, --readiness-timeout) - Lint rules for sequencing issues - Execution order for install, upgrade, rollback, and uninstall Refs: HIP-0025, helm/helm#32038 Signed-off-by: promptless[bot] <promptless[bot]@users.noreply.github.com>
|
Promptless prepared a documentation update related to this change. Triggered by helm/helm#32038 Created comprehensive documentation for HIP-0025 resource sequencing, covering the new Review at helm/helm-www#2068 |
Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
Includes the readiness rollback flag fix for the integration readiness script. Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
e5409e8 to
da559ec
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 47 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
pkg/kube/readiness.go:1
evaluateExpressionparses JSONPath (jsonpath.New().Parse(...)) on every evaluation. DuringstatusWaiterpolling this can become hot (expressions × resources × poll iterations). To reduce overhead, consider compiling/parsing expressions once (e.g., parse the annotation JSON into a slice of pre-parsed{template, op, expected}structs and reuse compiled JSONPath objects) and reusing across polling iterations for the same resource/batch.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| byName := make(map[string]subchartInfo, len(c.Metadata.Dependencies)) | ||
| for _, dep := range c.Metadata.Dependencies { | ||
| name := effectiveDependencyName(dep) | ||
| disabled := !dep.Enabled && dep.Condition == "" && len(dep.Tags) == 0 | ||
| byName[name] = subchartInfo{disabled: disabled} | ||
| if !disabled { | ||
| dag.AddNode(name) | ||
| } |
There was a problem hiding this comment.
The disabled heuristic ignores dependencies that were disabled via condition/tags processing (i.e., dep.Enabled == false but dep.Condition != "" or len(dep.Tags) > 0), which contradicts the function doc (“Disabled subcharts are excluded from the DAG”). A concrete fix is to treat any !dep.Enabled as disabled (and ensure callers run dependency processing so Enabled is meaningful), or alternatively make the contract explicit (e.g., require ProcessDependencies and document it) to avoid misclassifying disabled subcharts.
There was a problem hiding this comment.
The concern here is gating this on !dep.Enabled which would silently produce wrong DAGs when callers haven't processed dependencies.
If I have the callers correct then we have:
| caller | ProcessDependencyEnabled run before? |
|---|---|
| sequencedDeployment | yes - action pipeline processes the deps |
| sequencedUninstall | yes - uses stored release chart |
| helm template --wait=ordered | yes - checkDependencies |
| sequencingRule(lint) | not guaranteed (based on linting rules) |
The Enabled field is only reliable after ProcessDependencyEnabled resolves conditions/tags against values, and BuildSubchartDAG is called from contexts where that processing hasn't necessarily run yet (e.g., lint).
| func (u *Uninstall) deleteManifestBatch(manifests []releaseutil.Manifest, waiter kube.Waiter) (kube.ResourceList, []error) { | ||
| resources, err := u.buildDeleteResources(manifests) | ||
| if err != nil || len(resources) == 0 { | ||
| if err != nil { | ||
| return nil, []error{err} | ||
| } | ||
| return nil, nil | ||
| } | ||
|
|
||
| _, errs := u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation)) | ||
| if len(errs) > 0 { | ||
| return resources, errs | ||
| } | ||
| if err := waiter.WaitForDelete(resources, u.Timeout); err != nil { | ||
| errs = append(errs, err) | ||
| } | ||
| return resources, errs | ||
| } |
There was a problem hiding this comment.
In sequenced uninstall, each batch uses u.Timeout for WaitForDelete, so total uninstall runtime can grow to O(numBatches * Timeout), which is a behavioral change from the non-sequenced single-wait path and can exceed operators’ expectations. Consider tracking an overall deadline (similar to sequencedDeployment) and using min(u.Timeout, remainingToDeadline) per batch, or introducing a per-batch delete timeout to cap total wall time.
| readinessTimeout := r.Timeout / 2 | ||
| if readinessTimeout <= 0 { | ||
| readinessTimeout = time.Minute | ||
| } |
There was a problem hiding this comment.
The per-batch readiness timeout defaulting to r.Timeout / 2 is a non-obvious magic rule and differs from install/upgrade (which default to 1 minute unless set). This makes troubleshooting and tuning harder because rollback behaves differently without an explicit user-facing control. Consider using a consistent default (e.g., 1 minute) and/or extracting a shared helper/constant with a documented rationale.
correct thing to do here is return `cerr` otherwise decode failure silently vanish Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: rohit <50377477+caretak3r@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 47 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
pkg/kube/readiness.go:1
- All JSONPath execution errors are currently swallowed and treated as 'condition not met'. This can hide real authoring errors (e.g., incorrect JSONPath that parses but fails at execution time due to type/indexing issues) and lead to resources waiting indefinitely with no actionable signal. Prefer returning an error for execution failures, or at least only suppress a narrow 'field not found / empty result' case while surfacing other failures with context (expression + JSONPath template).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var head releaseutil.SimpleHead | ||
| if err := yaml.Unmarshal([]byte(content), &head); err != nil { | ||
| return nil, fmt.Errorf("YAML parse error on %s: %w", name, err) | ||
| } |
There was a problem hiding this comment.
helm template --wait=ordered now hard-fails if any rendered document cannot be unmarshaled into SimpleHead. This is a behavior change vs the previous template path (which printed manifests without requiring YAML parsing) and can break charts that render non-standard/invalid YAML (especially in scenarios where users rely on --debug to inspect partial output). Consider falling back to the non-ordered/flat output (with a warning) when parsing fails, rather than returning an error.
| templates: map[string]string{ | ||
| "templates/configmap.yaml": manifestYAML("ConfigMap", "partial-readiness", map[string]string{ | ||
| releaseutil.AnnotationResourceGroup: "bootstrap", | ||
| "helm.sh/readiness-success": `'["{.status.phase} == \"Ready\""]'`, |
There was a problem hiding this comment.
These test fixtures use {.status.phase} in readiness expressions, but EvaluateCustomReadiness evaluates expressions against the .status object and prepends .status internally (so {.phase} == ... is the expected form). Even though this lint test only checks presence (so it won't fail), keeping examples aligned with the implementation helps prevent copy/paste confusion; consider updating these fixtures to use {.phase} (and similarly elsewhere) to match intended usage.
| "helm.sh/readiness-success": `'["{.status.phase} == \"Ready\""]'`, | |
| "helm.sh/readiness-success": `'["{.phase} == \"Ready\""]'`, |
avoid mis-matching charts nested in deeper paths Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: rohit <50377477+caretak3r@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 47 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
pkg/kube/readiness.go:1
- readinessJSONPath()
always prefixes the provided path with.status, which means an expression like{.status.phase} == "Ready"turns into{.status.status.phase}and will never match. This is easy to hit because several fixtures/tests in this PR use{.status.*}style. Consider either (a) explicitly documenting/enforcing that paths are relative to.status(so{.phase}), and updating fixtures accordingly, or (b) supporting both by stripping a leading.status/.status.prefix before prefixing, so{.status.phase}and{.phase}` behave the same.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if orderedTemplateOutput { | ||
| templateChart, err := loadTemplateChart(args, client) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := renderOrderedTemplate(templateChart, strings.TrimSpace(rel.Manifest), out); err != nil { | ||
| return err | ||
| } | ||
| if !client.DisableHooks { |
There was a problem hiding this comment.
The ordered template output path hard-fails if ordered rendering can’t parse/structure the manifests. This conflicts with the surrounding behavior (and comment) that Helm should still print the YAML even when it isn't valid (notably with --debug or partial rendering failures). A more robust approach is to treat ordered rendering as best-effort: if parseTemplateManifests()/renderOrderedTemplate() fails, fall back to printing strings.TrimSpace(rel.Manifest) (and hooks) without delimiters, rather than returning early.
| rawManifests := releaseutil.SplitManifests(targetRelease.Manifest) | ||
| _, sortedManifests, err := releaseutil.SortManifests(rawManifests, nil, releaseutil.InstallOrder) | ||
| if err != nil { | ||
| return fail(nil, fmt.Errorf("parsing target release manifest for sequenced rollback: %w", err)) | ||
| } |
There was a problem hiding this comment.
Sequenced rollback relies on deployChartLevel, which groups manifests by subchart using Manifest.Name path prefixes (e.g., "/charts//..."). The manifests coming from SortManifests are not guaranteed to have those source-path names (often they’re keyed by split keys or resource names), so subchart sequencing can silently degrade into a flat chart-level deployment order. To make rollback sequencing consistent with install/upgrade/uninstall, consider recovering source paths for rollback manifests as well (e.g., re-render via renderResourcesWithFiles using the stored chart+values for that revision, or parse "# Source:" lines and populate Manifest.Name accordingly).
| if err := sd.deployChartLevel(ctx, targetRelease.Chart, sortedManifests); err != nil { | ||
| return fail(sd.createdResources, err) | ||
| } |
There was a problem hiding this comment.
Sequenced rollback relies on deployChartLevel, which groups manifests by subchart using Manifest.Name path prefixes (e.g., "/charts//..."). The manifests coming from SortManifests are not guaranteed to have those source-path names (often they’re keyed by split keys or resource names), so subchart sequencing can silently degrade into a flat chart-level deployment order. To make rollback sequencing consistent with install/upgrade/uninstall, consider recovering source paths for rollback manifests as well (e.g., re-render via renderResourcesWithFiles using the stored chart+values for that revision, or parse "# Source:" lines and populate Manifest.Name accordingly).
| slog.Warn("subchart not found in chart dependencies; deploying without subchart sequencing", | ||
| "subchart", subchartName, | ||
| "batch", batchIdx, | ||
| ) |
There was a problem hiding this comment.
These new warnings use the global slog logger directly, while the rest of the action layer commonly uses cfg.Logger() (which is configurable per Helm invocation). Using the global logger can bypass Helm’s configured log handler/level, making warnings harder to route/disable consistently in different embedding contexts. Prefer emitting warnings via s.cfg.Logger().Warn(...) (or threading a logger into this component) for consistent operational logging behavior.
| return fmt.Errorf("parsing resource-group annotations: %w", err) | ||
| } | ||
| for _, w := range warnings { | ||
| slog.Warn("resource-group annotation warning", "warning", w) |
There was a problem hiding this comment.
These new warnings use the global slog logger directly, while the rest of the action layer commonly uses cfg.Logger() (which is configurable per Helm invocation). Using the global logger can bypass Helm’s configured log handler/level, making warnings harder to route/disable consistently in different embedding contexts. Prefer emitting warnings via s.cfg.Logger().Warn(...) (or threading a logger into this component) for consistent operational logging behavior.
| _, hasSuccess := annotations[kube.AnnotationReadinessSuccess] | ||
| _, hasFailure := annotations[kube.AnnotationReadinessFailure] |
There was a problem hiding this comment.
This lint rule treats an annotation key as “present” even if its value is the empty string, because it only checks map presence. Elsewhere (e.g., parseReadinessExpressions/EvaluateCustomReadiness) empty values are effectively treated as absent. This mismatch can produce confusing lint errors (or miss misconfigs) depending on how templates render annotations. Consider checking for non-empty, trimmed values (e.g., strings.TrimSpace(annotations[key]) != "") to align lint behavior with runtime behavior.
| _, hasSuccess := annotations[kube.AnnotationReadinessSuccess] | |
| _, hasFailure := annotations[kube.AnnotationReadinessFailure] | |
| hasSuccess := strings.TrimSpace(annotations[kube.AnnotationReadinessSuccess]) != "" | |
| hasFailure := strings.TrimSpace(annotations[kube.AnnotationReadinessFailure]) != "" |
Reverting original change, in favor or non-standard YAML conventions. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: rohit <50377477+caretak3r@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 47 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
pkg/release/v1/util/resource_group.go:1
- resourceGroupResourceID does not include namespace. If a chart renders same Kind/Name into multiple namespaces (possible via templating), ParseResourceGroups can falsely report “assigned to multiple resource groups” even though the resources are distinct. Include namespace in the identity when available (e.g., apiVersion/kind/namespace/name) to match Kubernetes object identity semantics.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Deploy each subchart batch in order | ||
| for batchIdx, batch := range batches { | ||
| for _, subchartName := range batch { | ||
| subManifests := grouped[subchartName] | ||
| if len(subManifests) == 0 { | ||
| continue | ||
| } | ||
|
|
||
| // Find the subchart chart object for recursive nested sequencing | ||
| subChart := findSubchart(chrt, subchartName) | ||
| if subChart == nil { | ||
| // Subchart not found in chart object (may have been disabled or aliased differently) | ||
| // Fall back to flat resource-group deployment for these manifests | ||
| slog.Warn("subchart not found in chart dependencies; deploying without subchart sequencing", | ||
| "subchart", subchartName, | ||
| "batch", batchIdx, | ||
| ) | ||
| if err := s.deployResourceGroupBatches(ctx, subManifests); err != nil { | ||
| return fmt.Errorf("deploying subchart %s resources: %w", subchartName, err) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| // Recursively deploy the subchart (handles its own nested subcharts and resource-groups) | ||
| if err := s.deployChartLevel(ctx, subChart, subManifests); err != nil { | ||
| return fmt.Errorf("deploying subchart %s: %w", subchartName, err) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The PR description states “parallel within batch” for subchart batches, but the implementation processes each subchart in a batch sequentially. If parallelism is intended, consider executing each subchart in a batch concurrently (and joining errors) before proceeding to the next batch. Tradeoff: concurrency will improve rollout time for independent subcharts but requires careful aggregation of per-subchart errors and avoiding interleaved logging/output confusion.
| chartsPrefix := chartManifestPrefix(chartPath) + "/charts/" | ||
|
|
||
| for _, manifest := range manifests { | ||
| if !strings.HasPrefix(manifest.Name, chartsPrefix) { | ||
| result[""] = append(result[""], manifest) | ||
| continue | ||
| } | ||
|
|
||
| rest := manifest.Name[len(chartsPrefix):] | ||
| idx := strings.Index(rest, "/") | ||
| if idx < 0 { | ||
| result[""] = append(result[""], manifest) | ||
| continue | ||
| } | ||
|
|
||
| subchartName := rest[:idx] | ||
| result[subchartName] = append(result[subchartName], manifest) | ||
| } | ||
|
|
||
| return result | ||
| } | ||
|
|
There was a problem hiding this comment.
There are now two different implementations of “group manifests by subchart” logic: GroupManifestsByDirectSubchart (action) and groupManifestsByChartPath/chartManifestPrefix (cmd/template). This duplication is likely to drift (especially around nested subcharts and alias handling). Consider extracting a shared helper (e.g., in pkg/release/v1/util or pkg/chart/v2/util) that works off a chart-path concept and can be reused by action, uninstall logic, and template rendering.
| chartsPrefix := chartManifestPrefix(chartPath) + "/charts/" | |
| for _, manifest := range manifests { | |
| if !strings.HasPrefix(manifest.Name, chartsPrefix) { | |
| result[""] = append(result[""], manifest) | |
| continue | |
| } | |
| rest := manifest.Name[len(chartsPrefix):] | |
| idx := strings.Index(rest, "/") | |
| if idx < 0 { | |
| result[""] = append(result[""], manifest) | |
| continue | |
| } | |
| subchartName := rest[:idx] | |
| result[subchartName] = append(result[subchartName], manifest) | |
| } | |
| return result | |
| } | |
| for _, manifest := range manifests { | |
| subchartName, ok := directSubchartForManifest(chartPath, manifest.Name) | |
| if !ok { | |
| result[""] = append(result[""], manifest) | |
| continue | |
| } | |
| result[subchartName] = append(result[subchartName], manifest) | |
| } | |
| return result | |
| } | |
| func directSubchartForManifest(chartPath, manifestName string) (string, bool) { | |
| chartsPrefix := chartManifestPrefix(chartPath) + "/charts/" | |
| rest, ok := strings.CutPrefix(manifestName, chartsPrefix) | |
| if !ok { | |
| return "", false | |
| } | |
| subchartName, _, ok := strings.Cut(rest, "/") | |
| if !ok || subchartName == "" { | |
| return "", false | |
| } | |
| return subchartName, true | |
| } |
| for _, batch := range batches { | ||
| for _, groupName := range batch { | ||
| fmt.Fprintf(out, "## START resource-group: %s %s\n", chartPath, groupName) | ||
| for _, manifest := range result.Groups[groupName] { | ||
| fmt.Fprintf(out, "---\n%s\n", manifest.Content) | ||
| } | ||
| fmt.Fprintf(out, "## END resource-group: %s %s\n", chartPath, groupName) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for _, manifest := range result.Unsequenced { | ||
| fmt.Fprintf(out, "---\n%s\n", manifest.Content) | ||
| } |
There was a problem hiding this comment.
In ordered template mode, the output includes Helm-internal sequencing annotations (e.g., "helm.sh/depends-on/resource-groups") that are not valid Kubernetes annotation keys due to multiple slashes, and Helm only strips them during apply/upgrade flows. This makes helm template --wait=ordered | kubectl apply -f - fail. Consider emitting a warning on stderr when ordered delimiters are enabled (or documenting clearly in the --wait=ordered/template help text) that ordered template output is not meant to be applied directly without stripping these internal annotations.
What this PR does / why we need it
This PR implements HIP-0025: Better Support for Resource Creation Sequencing, adding native DAG-based ordering for resource groups and subcharts to Helm v4.
Today, Helm applies all rendered manifests to the cluster simultaneously. Application distributors who need ordered deployment must resort to hooks (tedious to maintain) or bake sequencing into the application itself (unnecessary complexity). HIP-0025 gives chart authors a first-class mechanism to define deployment order using annotations and
Chart.yamlfields, and gives operators a--wait=orderedflag to enable sequenced execution.Overview
When
--wait=orderedis specified, Helm builds two levels of dependency graphs:depends-onfields inChart.yamldependencies and/or thehelm.sh/depends-on/subchartsannotation.helm.sh/resource-groupandhelm.sh/depends-on/resource-groupsannotations.Resources are deployed in topological order (Kahn's algorithm). Helm waits for each batch to reach readiness before proceeding to the next. Readiness defaults to kstatus evaluation, but chart authors can override it with custom JSONPath expressions via
helm.sh/readiness-successandhelm.sh/readiness-failureannotations supporting 6 operators (==,!=,<,<=,>,>=).Execution Flow
Commit Organization
This PR is organized into 7 logical commits to aid review. Each builds on the previous:
feat(hip-0025): add sequencing DAG foundationsAddNode,AddEdge,GetBatches, cycle detection, deterministic batching (pkg/chart/v2/util/dag.go)feat(hip-0025): add resource group parsing and readinesspkg/release/v1/util/resource_group.go), subchart DAG builder fromChart.yamlDependsOn+ annotation (pkg/chart/v2/util/subchart_dag.go), custom readiness eval via JSONPath (pkg/kube/readiness.go)feat(hip-0025): add sequencing schema metadataSequencingInfoonRelease,OrderedWaitStrategyconstant,DependsOn []stringonchart.Dependencyfeat(hip-0025): sequence install upgrade rollback and uninstallpkg/action/sequencing.go, modifications toinstall.go,upgrade.go,rollback.go,uninstall.go)feat(hip-0025): add ordered wait flags and template output--wait=orderedand--readiness-timeoutCLI flags,helm templateresource-group delimiters (## START/END resource-group:)feat(hip-0025): add sequencing lint rules and warningstest(hip-0025): add sequencing tests and integration fixturesNew Annotations
helm.sh/resource-grouphelm.sh/depends-on/resource-groupshelm.sh/depends-on/subchartshelm.sh/readiness-successhelm.sh/readiness-failureNew Chart.yaml Fields
depends-onondependencies[]entries — declares subchart ordering dependencies by name or aliasNew CLI Flags
--wait=orderedinstall,upgrade--readiness-timeoutinstall,upgrade--timeout)Schema Changes
DependsOn []stringadded tochart.DependencySequencingInfostruct added torelease.Release(Enabled bool,Strategy string)OrderedWaitStrategyconstant addedKey Design Decisions
--wait=orderedare upgraded/rolled back/uninstalled using the traditional single-batch flow.helm.sh/hook-weightas before.How to Test
Unit tests:
Integration tests (requires kind cluster):
Manual verification checklist:
helm templatewith--wait=orderedshows resource-group delimitershelm lintdetects circular dependencies, partial readiness, and orphan refs--readiness-timeoutis honored per resource groupSequencingInfowhen--wait=orderedis usedReference Documents
Special notes for reviewers:
make test-unit,make lint,make vet). There is a known pre-existing DATA RACE inpkg/repo/v1.TestConcurrencyDownloadIndexthat is not related to this PR.If applicable:
Signed-off-by: Rohit Gudi 50377477+caretak3r@users.noreply.github.com
refs https://github.com/helm/community/blob/main/hips/hip-0025.md