feat(spec): Task 9 - helm template resource-group delimiters

When --wait=ordered is used with helm template, output manifests in
resource-group DAG order with START/END delimiters per HIP-0025 spec.
Unsequenced resources are output last without delimiters.
pull/31992/head
caretak3r 8 months ago
parent 1a821763a7
commit 00542abacf

@ -22,6 +22,7 @@ import (
"fmt"
"io"
"io/fs"
"log/slog"
"os"
"path/filepath"
"regexp"
@ -37,6 +38,7 @@ import (
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/kube"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
)
@ -117,84 +119,100 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
// We ignore a potential error here because, when the --debug flag was specified,
// we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
if rel != nil {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks {
fileWritten := make(map[string]bool)
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
// When --wait=ordered is set and no file filters or output-dir are active,
// render manifests in resource-group DAG order with delimiters.
if client.WaitStrategy == kube.OrderedWaitStrategy && len(showFiles) == 0 && client.OutputDir == "" {
if err2 := renderOrderedTemplate(strings.TrimSpace(rel.Manifest), out); err2 != nil {
return err2
}
if !client.DisableHooks {
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
}
fmt.Fprintf(out, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
}
if client.OutputDir == "" {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
} else {
newDir := client.OutputDir
if client.UseReleaseName {
newDir = filepath.Join(client.OutputDir, client.ReleaseName)
}
} else {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks {
fileWritten := make(map[string]bool)
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
}
_, err := os.Stat(filepath.Join(newDir, m.Path))
if err == nil {
fileWritten[m.Path] = true
if client.OutputDir == "" {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
} else {
newDir := client.OutputDir
if client.UseReleaseName {
newDir = filepath.Join(client.OutputDir, client.ReleaseName)
}
_, err := os.Stat(filepath.Join(newDir, m.Path))
if err == nil {
fileWritten[m.Path] = true
}
err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
if err != nil {
return err
}
}
err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
if err != nil {
return err
}
}
}
}
// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
// This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names.
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestsKeys := make([]string, 0, len(splitManifests))
for k := range splitManifests {
manifestsKeys = append(manifestsKeys, k)
}
sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))
manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
// Use linux-style filepath separators to unify user's input path
f = filepath.ToSlash(f)
for _, manifestKey := range manifestsKeys {
manifest := splitManifests[manifestKey]
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
// This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names.
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestsKeys := make([]string, 0, len(splitManifests))
for k := range splitManifests {
manifestsKeys = append(manifestsKeys, k)
}
sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))
manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
// Use linux-style filepath separators to unify user's input path
f = filepath.ToSlash(f)
for _, manifestKey := range manifestsKeys {
manifest := splitManifests[manifestKey]
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
// manifest.Path is connected using linux-style filepath separators on Windows as
// well as macOS/linux
manifestPath := strings.Join(manifestPathSplit, "/")
// if the filepath provided matches a manifest path in the
// chart, render that manifest
if matched, _ := filepath.Match(f, manifestPath); !matched {
continue
}
manifestsToRender = append(manifestsToRender, manifest)
missing = false
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
// manifest.Path is connected using linux-style filepath separators on Windows as
// well as macOS/linux
manifestPath := strings.Join(manifestPathSplit, "/")
// if the filepath provided matches a manifest path in the
// chart, render that manifest
if matched, _ := filepath.Match(f, manifestPath); !matched {
continue
if missing {
return fmt.Errorf("could not find template %s in chart", f)
}
manifestsToRender = append(manifestsToRender, manifest)
missing = false
}
if missing {
return fmt.Errorf("could not find template %s in chart", f)
for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m)
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m)
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
}
@ -225,6 +243,62 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return cmd
}
// renderOrderedTemplate outputs manifests in resource-group DAG order with
// ## START resource-group / ## END resource-group delimiters per the HIP-0025 spec.
// Resources without a resource-group annotation are output after all sequenced groups.
func renderOrderedTemplate(manifest string, out io.Writer) error {
if manifest == "" {
return nil
}
rawManifests := releaseutil.SplitManifests(manifest)
_, sortedManifests, err := releaseutil.SortManifests(rawManifests, nil, releaseutil.InstallOrder)
if err != nil {
return fmt.Errorf("sorting manifests for ordered output: %w", err)
}
result, warnings := releaseutil.ParseResourceGroups(sortedManifests)
for _, w := range warnings {
slog.Warn("resource-group annotation warning during helm template", "warning", w)
}
// If no resource-groups present, output in sorted install order without delimiters.
if len(result.Groups) == 0 {
for _, m := range sortedManifests {
fmt.Fprintf(out, "---\n%s\n", m.Content)
}
return nil
}
dag, err := releaseutil.BuildResourceGroupDAG(result)
if err != nil {
return fmt.Errorf("building resource-group DAG for ordered output: %w", err)
}
batches, err := dag.GetBatches()
if err != nil {
return fmt.Errorf("computing resource-group batches for ordered output: %w", err)
}
// Output each batch's groups with START/END delimiters.
// m.Content already contains the "# Source:" comment from rel.Manifest.
for _, batch := range batches {
for _, groupName := range batch {
fmt.Fprintf(out, "## START resource-group: %s\n", groupName)
for _, m := range result.Groups[groupName] {
fmt.Fprintf(out, "---\n%s\n", m.Content)
}
fmt.Fprintf(out, "## END resource-group: %s\n", groupName)
}
}
// Output unsequenced resources without delimiters.
for _, m := range result.Unsequenced {
fmt.Fprintf(out, "---\n%s\n", m.Content)
}
return nil
}
func isTestHook(h *release.Hook) bool {
return slices.Contains(h.Events, release.HookTest)
}

@ -166,6 +166,11 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath),
golden: "output/template-subchart-cm-set-file.txt",
},
{
name: "template with ordered wait strategy shows resource group delimiters",
cmd: "template --wait=ordered 'testdata/testcharts/sequenced-chart'",
golden: "output/template-ordered-delimiters.txt",
},
}
runTestCmd(t, tests)
}

@ -0,0 +1,33 @@
## START resource-group: databases
---
# Source: sequenced-chart/templates/aa-databases-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: database-config
annotations:
helm.sh/resource-group: databases
data:
host: localhost
## END resource-group: databases
## START resource-group: app
---
# Source: sequenced-chart/templates/bb-app-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
annotations:
helm.sh/resource-group: app
helm.sh/depends-on/resource-groups: '["databases"]'
data:
db_host: localhost
## END resource-group: app
---
# Source: sequenced-chart/templates/cc-unsequenced-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: unsequenced-config
data:
key: value

@ -0,0 +1,5 @@
apiVersion: v2
name: sequenced-chart
description: A test chart with resource-group sequencing annotations
type: application
version: 0.1.0

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: database-config
annotations:
helm.sh/resource-group: databases
data:
host: localhost

@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
annotations:
helm.sh/resource-group: app
helm.sh/depends-on/resource-groups: '["databases"]'
data:
db_host: localhost

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: unsequenced-config
data:
key: value
Loading…
Cancel
Save