From fbd7cb48cea34c2f03e51d5e545d4654265e1bc7 Mon Sep 17 00:00:00 2001 From: Jeff Rescignano Date: Wed, 10 Jun 2026 23:42:22 -0400 Subject: [PATCH 1/2] fix: resolve YAML aliases before flattening kind: List in post-render Signed-off-by: Jeff Rescignano --- pkg/action/action.go | 65 +++++++++++++++++++++++++++++++++++++++ pkg/action/action_test.go | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/pkg/action/action.go b/pkg/action/action.go index 4ed718371..a36774033 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -33,6 +33,7 @@ import ( "text/template" "time" + "go.yaml.in/yaml/v3" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/discovery" @@ -203,6 +204,12 @@ func annotateAndMerge(files map[string]string) (string, error) { if strings.TrimSpace(doc) == "" { continue } + // kio.ParseAll flattens `kind: List` into one document per item; resolve aliases + // first so an anchor and its alias aren't split across documents (anchors are + // document-scoped), which would leave the alias dangling. + if resolved, rerr := resolveYAMLAliases(doc); rerr == nil { + doc = resolved + } manifests, err := kio.ParseAll(doc) if err != nil { return "", fmt.Errorf("parsing %s: %w", fname, err) @@ -223,6 +230,64 @@ func annotateAndMerge(files map[string]string) (string, error) { return merged, nil } +// resolveYAMLAliases materializes YAML aliases within a single document, leaving +// documents that contain none unchanged. +func resolveYAMLAliases(doc string) (string, error) { + var node yaml.Node + if err := yaml.Unmarshal([]byte(doc), &node); err != nil { + return "", err + } + if !resolveAliasNodes(&node) { + return doc, nil + } + stripAnchors(&node) + var buf bytes.Buffer + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + if err := enc.Encode(&node); err != nil { + return "", err + } + _ = enc.Close() + return buf.String(), nil +} + +// resolveAliasNodes replaces alias nodes with a deep copy of their anchor target, +// returning true if any replacement was made. +func resolveAliasNodes(n *yaml.Node) bool { + changed := false + for i, c := range n.Content { + if c.Kind == yaml.AliasNode && c.Alias != nil { + n.Content[i] = deepCopyClearAnchor(c.Alias) + changed = true + continue + } + if resolveAliasNodes(c) { + changed = true + } + } + return changed +} + +// deepCopyClearAnchor returns a deep copy of n with anchors removed. +func deepCopyClearAnchor(n *yaml.Node) *yaml.Node { + cp := *n + cp.Anchor = "" + cp.Alias = nil + cp.Content = make([]*yaml.Node, len(n.Content)) + for i, c := range n.Content { + cp.Content[i] = deepCopyClearAnchor(c) + } + return &cp +} + +// stripAnchors clears any remaining anchor names in the tree. +func stripAnchors(n *yaml.Node) { + n.Anchor = "" + for _, c := range n.Content { + stripAnchors(c) + } +} + // splitAndDeannotate reconstructs individual files from a merged YAML stream, // removing filename annotations and grouping documents by their original filenames. // Documents without a filename annotation are assigned a synthesized name of the diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 54b07273b..93393a96e 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -508,6 +508,70 @@ metadata: postrenderer.helm.sh/postrender-filename: 'templates/multi.yaml' data: key: value2 +`, + }, + { + name: "kind List with anchor shared across items", + files: map[string]string{ + "templates/list.yaml": ` +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-a + data: &shared + key: value +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm-b + data: *shared`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm-a + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/list.yaml' +data: + key: value +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm-b + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/list.yaml' +data: + key: value +`, + }, + { + name: "anchor and alias within a single manifest", + files: map[string]string{ + "templates/cm.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + defaults: &d + a: "1" + prod: *d`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: cm + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +data: + defaults: + a: "1" + prod: + a: "1" `, }, { From 4900776683dc4943329b74edaca4a37e24d66a41 Mon Sep 17 00:00:00 2001 From: Jeff Rescignano Date: Thu, 11 Jun 2026 00:11:56 -0400 Subject: [PATCH 2/2] fix: propagate alias resolution and encoder close errors Signed-off-by: Jeff Rescignano --- pkg/action/action.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index a36774033..2d70732a0 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -207,8 +207,9 @@ func annotateAndMerge(files map[string]string) (string, error) { // kio.ParseAll flattens `kind: List` into one document per item; resolve aliases // first so an anchor and its alias aren't split across documents (anchors are // document-scoped), which would leave the alias dangling. - if resolved, rerr := resolveYAMLAliases(doc); rerr == nil { - doc = resolved + doc, err := resolveYAMLAliases(doc) + if err != nil { + return "", fmt.Errorf("resolving aliases in %s: %w", fname, err) } manifests, err := kio.ParseAll(doc) if err != nil { @@ -235,7 +236,9 @@ func annotateAndMerge(files map[string]string) (string, error) { func resolveYAMLAliases(doc string) (string, error) { var node yaml.Node if err := yaml.Unmarshal([]byte(doc), &node); err != nil { - return "", err + // Preserve the surrounding code's leniency for badly-formed docs; + // kio.ParseAll reports them downstream. + return doc, nil } if !resolveAliasNodes(&node) { return doc, nil @@ -247,7 +250,9 @@ func resolveYAMLAliases(doc string) (string, error) { if err := enc.Encode(&node); err != nil { return "", err } - _ = enc.Close() + if err := enc.Close(); err != nil { + return "", err + } return buf.String(), nil }