Merge branch 'helm:main' into feature/rollback-description-flag

pull/31580/head
MrJack 3 weeks ago committed by GitHub
commit f1cfe95286
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -47,7 +47,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # pinv4.35.1
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -58,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # pinv4.35.1
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -72,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # pinv4.35.1
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # pinv4.35.2

@ -64,6 +64,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
sarif_file: results.sarif

1
.gitignore vendored

@ -3,6 +3,7 @@
.DS_Store
.coverage/
.idea
.claude
.vimrc
.vscode/
.devcontainer/

@ -15,7 +15,7 @@ chance to try to fix the issue before it is exploited in the wild.
Helm v4 development takes place on the `main` branch while Helm v3 is on the `dev-v3` branch.
Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancement will still be applied until November 11th 2026. See the blog <https://helm.sh/blog/helm-4-released#helm-v3-support> for more details.
Helm v3 will continue to receive bug fixes and updates for new Kubernetes releases until July 8th 2026. Security enhancements will still be applied until November 11th 2026. See the blog <https://helm.sh/blog/helm-4-released#helm-v3-support> for more details.
Bugs should first be fixed on Helm v4 and then backported to Helm v3. Helm v3 (and the `dev-v3` branch) is no longer accepting new features.
@ -162,9 +162,9 @@ There are 5 types of issues (each with their own corresponding [label](#labels))
for future reference. Generally these are questions that are too complex or large to store in the
Slack channel or have particular interest to the community as a whole. Depending on the
discussion, these can turn into `feature` or `bug` issues.
- `proposal`: Used for items (like this one) that propose a new ideas or functionality that require
- `proposal`: Used for items (like this one) that propose new ideas or functionality that require
a larger community discussion. This allows for feedback from others in the community before a
feature is actually developed. This is not needed for small additions. Final word on whether
feature is actually developed. This is not needed for small additions. Final word on whether
a feature needs a proposal is up to the core maintainers. All issues that are proposals should
both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become
a `feature` and does not require a milestone.

@ -24,7 +24,7 @@ require (
github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3
github.com/mattn/go-shellwords v1.0.12
github.com/mattn/go-shellwords v1.0.13
github.com/moby/term v0.5.2
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
@ -39,14 +39,14 @@ require (
golang.org/x/term v0.42.0
golang.org/x/text v0.36.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.35.3
k8s.io/apiextensions-apiserver v0.35.3
k8s.io/apimachinery v0.35.3
k8s.io/apiserver v0.35.3
k8s.io/cli-runtime v0.35.3
k8s.io/client-go v0.35.3
k8s.io/api v0.35.4
k8s.io/apiextensions-apiserver v0.35.4
k8s.io/apimachinery v0.35.4
k8s.io/apiserver v0.35.4
k8s.io/cli-runtime v0.35.4
k8s.io/client-go v0.35.4
k8s.io/klog/v2 v2.130.1
k8s.io/kubectl v0.35.3
k8s.io/kubectl v0.35.4
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/kustomize/kyaml v0.21.1
@ -172,7 +172,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.35.3 // indirect
k8s.io/component-base v0.35.4 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect

@ -206,8 +206,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4=
github.com/mattn/go-shellwords v1.0.13/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -488,26 +488,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU=
k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8=
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.3 h1:D2eIcfJ05hEAEewoSDg+05e0aSRwx8Y4Agvd/wiomUI=
k8s.io/apiserver v0.35.3/go.mod h1:JI0n9bHYzSgIxgIrfe21dbduJ9NHzKJ6RchcsmIKWKY=
k8s.io/cli-runtime v0.35.3 h1:UZq4ipNimtzBmhN7PPKbfAdqo8quK0H0UdGl6qAQnqI=
k8s.io/cli-runtime v0.35.3/go.mod h1:O7MUmCqcKSd5xI+O5X7/pRkB5l0O2NIhOdUVwbHLXu4=
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
k8s.io/component-base v0.35.3 h1:mbKbzoIMy7JDWS/wqZobYW1JDVRn/RKRaoMQHP9c4P0=
k8s.io/component-base v0.35.3/go.mod h1:IZ8LEG30kPN4Et5NeC7vjNv5aU73ku5MS15iZyvyMYk=
k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988=
k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU=
k8s.io/apiextensions-apiserver v0.35.4 h1:HeP+Upp7ItdvnyGmub0yoix+2z5+ev4M5cE5TCgtOUU=
k8s.io/apiextensions-apiserver v0.35.4/go.mod h1:ogQlk+stIE8mnoRthSYCwlOS12fVqgWFiErMwPaXA7c=
k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds=
k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc=
k8s.io/apiserver v0.35.4 h1:vtuFqNFmF9bPRdHDL2lpK6qCTPWDreZJL4LRPwVM6ho=
k8s.io/apiserver v0.35.4/go.mod h1:JnBcb+J8kFXKpZkgcbcUnPBBHi4qgBii1I7dLxFY/oo=
k8s.io/cli-runtime v0.35.4 h1:8QRCXSDvopflFNM65Vkkdv42BljPdRSiqf6HFyI1iik=
k8s.io/cli-runtime v0.35.4/go.mod h1:MKLFuZxiJpm87UxjVeQRNy3sCaczHrSOPKN9pinlrM0=
k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8=
k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY=
k8s.io/component-base v0.35.4 h1:6n1tNJ87johN0Hif0Fs8K2GMthsaUwMqCebUDLYyv7U=
k8s.io/component-base v0.35.4/go.mod h1:qaDJgz5c1KYKla9occFmlJEfPpkuA55s90G509R+PeY=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/kubectl v0.35.3 h1:1KqSYXk/sodU7VeDvK6atX2kAGUZd2QTeR5K7Hb9r9w=
k8s.io/kubectl v0.35.3/go.mod h1:GPHxZqRe+u/i3gTBoVQHeIyq2NilfNPj9hDWeuN3x5s=
k8s.io/kubectl v0.35.4 h1:IHitney6OUeH29rBQnt6Cas6az8HpFeSAohormITNMc=
k8s.io/kubectl v0.35.4/go.mod h1:CGWAaof9ae4vGDAyhnSf1bSQN/U7jiWQHLVbMbLMjRI=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=

@ -21,6 +21,7 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
)
// SimpleHead defines what the structure of the head of a manifest file
@ -35,7 +36,16 @@ type SimpleHead struct {
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
// SplitManifests takes a string of manifest and returns a map contains individual manifests
// SplitManifests takes a manifest string and returns a map containing individual manifests.
//
// **Note for Chart API v3**: This function (due to the regex above) has allowed _WRONG_
// Go templates to be defined inside charts across the years. The generated text from Go
// templates may contain `---apiVersion: v1`, and this function magically splits this back
// to `---\napiVersion: v1`. This has caused issues recently after Helm 4 introduced
// kio.ParseAll to inject annotations when post-renderers are used. In Chart API v3,
// we should kill this regex with fire (or change it) and expose charts doing the wrong
// thing Go template-wise. Helm should say a big _NO_ to charts doing the wrong thing,
// with or without post-renderers.
func SplitManifests(bigFile string) map[string]string {
// Basically, we're quickly splitting a stream of YAML documents into an
// array of YAML docs. The file name is just a place holder, but should be
@ -44,15 +54,15 @@ func SplitManifests(bigFile string) map[string]string {
tpl := "manifest-%d"
res := map[string]string{}
// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
bigFileTmp := strings.TrimSpace(bigFile)
bigFileTmp := strings.TrimLeftFunc(bigFile, unicode.IsSpace)
docs := sep.Split(bigFileTmp, -1)
var count int
for _, d := range docs {
if d == "" {
if strings.TrimSpace(d) == "" {
continue
}
d = strings.TrimSpace(d)
d = strings.TrimLeftFunc(d, unicode.IsSpace)
res[fmt.Sprintf(tpl, count)] = d
count = count + 1
}

@ -21,7 +21,15 @@ import (
"testing"
)
const mockManifestFile = `
func TestSplitManifests(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]string
}{
{
name: "single doc with leading separator and whitespace",
input: `
---
apiVersion: v1
@ -35,9 +43,9 @@ spec:
- name: nemo-test
image: fake-image
cmd: fake-command
`
const expectedManifest = `apiVersion: v1
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: Pod
metadata:
name: finding-nemo,
@ -47,15 +55,463 @@ spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command`
cmd: fake-command
`,
},
},
{
name: "empty input",
input: "",
expected: map[string]string{},
},
{
name: "whitespace only",
input: " \n\n \n",
expected: map[string]string{},
},
{
name: "whitespace-only doc after separator is skipped",
input: "---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1\n---\n \n",
expected: map[string]string{
"manifest-0": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1",
},
},
{
name: "single doc no separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
`,
},
},
{
name: "two docs with proper separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
},
},
func TestSplitManifest(t *testing.T) {
manifests := SplitManifests(mockManifestFile)
if len(manifests) != 1 {
t.Errorf("Expected 1 manifest, got %v", len(manifests))
// Block scalar chomping indicator tests using | (clip), |- (strip), and |+ (keep)
// inputs with 0, 1, and 2 trailing newlines after the block content.
// Note: the emitter may normalize the output chomping indicator when the
// trailing newline count makes another indicator equivalent for the result.
// | (clip) input — clips trailing newlines to exactly one, though with
// 0 trailing newlines the emitted output may normalize to |-.
{
name: "block scalar clip (|) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
},
},
{
name: "block scalar clip (|) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
},
{
name: "block scalar clip (|) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
},
// |- (strip)
{
name: "block scalar strip (|-) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello`,
},
},
{
name: "block scalar strip (|-) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
},
{
name: "block scalar strip (|-) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
},
// |+ (keep)
{
name: "block scalar keep (|+) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
},
},
{
name: "block scalar keep (|+) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
},
{
name: "block scalar keep (|+) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
},
// Multi-doc with block scalars: the regex consumes \s*\n before ---,
// so trailing newlines from non-last docs are stripped.
{
name: "multi-doc block scalar clip (|) before separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
},
},
{
name: "multi-doc block scalar keep (|+) with 2 trailing newlines before separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
},
},
// **Note for Chart API v3**: The following tests exercise the lenient
// regex that splits `---apiVersion` back into separate documents.
// In Chart API v3, these inputs should return an _ERROR_ instead.
// See the comment on the SplitManifests function for more details.
{
name: "leading glued separator (---apiVersion)",
input: `
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
`,
},
},
{
name: "mid-content glued separator (---apiVersion)",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
},
},
{
name: "multiple glued separators",
input: `
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
},
},
{
name: "mixed glued and proper separators",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
},
},
}
expected := map[string]string{"manifest-0": expectedManifest}
if !reflect.DeepEqual(manifests, expected) {
t.Errorf("Expected %v, got %v", expected, manifests)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SplitManifests(tt.input)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected)
}
})
}
}

@ -418,7 +418,8 @@ func TestAnnotateAndMerge(t *testing.T) {
{
name: "single file with single manifest",
files: map[string]string{
"templates/configmap.yaml": `apiVersion: v1
"templates/configmap.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
@ -438,13 +439,15 @@ data:
{
name: "multiple files with multiple manifests",
files: map[string]string{
"templates/configmap.yaml": `apiVersion: v1
"templates/configmap.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
data:
key: value`,
"templates/secret.yaml": `apiVersion: v1
"templates/secret.yaml": `
apiVersion: v1
kind: Secret
metadata:
name: test-secret
@ -473,7 +476,8 @@ data:
{
name: "file with multiple manifests",
files: map[string]string{
"templates/multi.yaml": `apiVersion: v1
"templates/multi.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm1
@ -509,7 +513,8 @@ data:
{
name: "partials and empty files are removed",
files: map[string]string{
"templates/cm.yaml": `apiVersion: v1
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm1
@ -531,14 +536,16 @@ metadata:
{
name: "empty file",
files: map[string]string{
"templates/empty.yaml": "",
"templates/empty.yaml": `
`,
},
expected: ``,
},
{
name: "invalid yaml",
files: map[string]string{
"templates/invalid.yaml": `invalid: yaml: content:
"templates/invalid.yaml": `
invalid: yaml: content:
- malformed`,
},
expectedError: "parsing templates/invalid.yaml",
@ -546,7 +553,12 @@ metadata:
{
name: "leading doc separator glued to content by template whitespace trimming",
files: map[string]string{
"templates/service.yaml": "---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
"templates/service.yaml": `
---apiVersion: v1
kind: Service
metadata:
name: test-svc
`,
},
expected: `apiVersion: v1
kind: Service
@ -559,7 +571,13 @@ metadata:
{
name: "leading doc separator on its own line",
files: map[string]string{
"templates/service.yaml": "---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
"templates/service.yaml": `
---
apiVersion: v1
kind: Service
metadata:
name: test-svc
`,
},
expected: `apiVersion: v1
kind: Service
@ -572,7 +590,14 @@ metadata:
{
name: "multiple leading doc separators",
files: map[string]string{
"templates/service.yaml": "---\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
"templates/service.yaml": `
---
---
apiVersion: v1
kind: Service
metadata:
name: test-svc
`,
},
expected: `apiVersion: v1
kind: Service
@ -585,7 +610,16 @@ metadata:
{
name: "mid-content doc separator glued to content by template whitespace trimming",
files: map[string]string{
"templates/all.yaml": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-cm\n---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n",
"templates/all.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
---apiVersion: v1
kind: Service
metadata:
name: test-svc
`,
},
expected: `apiVersion: v1
kind: ConfigMap
@ -631,7 +665,7 @@ metadata:
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/configmap.yaml'
data:
ca.crt: |-
ca.crt: |
------BEGIN CERTIFICATE------
MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zAKBggqhkjOPQQDAzASMRAw
DgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYwMDAw
@ -667,7 +701,7 @@ metadata:
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/configmap.yaml'
data:
config: |-
config: |
# ---------------------------------------------------------------------------
[section]
key = value
@ -694,7 +728,7 @@ metadata:
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/dashboard.yaml'
data:
dashboard.json: |-
dashboard.json: |
{"options":{"---------":{"color":"#292929","text":"N/A"}}}
`,
},
@ -933,6 +967,605 @@ metadata:
name: cm-12
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/many.yaml'
`,
},
// Block scalar chomping indicator tests using | (clip), |- (strip), and |+ (keep)
// inputs with 0, 1, and 2 trailing newlines after the block content.
// Note: the emitter may normalize the output chomping indicator when the
// trailing newline count makes another indicator equivalent for the result.
// | (clip) input — clips trailing newlines to exactly one, though with
// 0 trailing newlines the emitted output may normalize to |-.
{
name: "block scalar clip (|) with 0 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
`,
},
{
name: "block scalar clip (|) with 1 trailing newline",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |
hello
`,
},
{
name: "block scalar clip (|) with 2 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |
hello
`,
},
// |- (strip) — strips all trailing newlines
{
name: "block scalar strip (|-) with 0 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
`,
},
{
name: "block scalar strip (|-) with 1 trailing newline",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
`,
},
{
name: "block scalar strip (|-) with 2 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
`,
},
// |+ (keep) — preserves all trailing newlines
{
name: "block scalar keep (|+) with 0 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
`,
},
{
name: "block scalar keep (|+) with 1 trailing newline",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |
hello
`,
},
{
name: "block scalar keep (|+) with 2 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |+
hello
`,
},
// Multi-doc tests: block scalar doc is NOT the last document.
// SplitManifests' regex consumes \s*\n before ---, so trailing
// newlines from non-last docs are always stripped.
// | (clip) in multi-doc (first doc)
{
name: "multi-doc block scalar clip (|) with 0 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
{
name: "multi-doc block scalar clip (|) with 1 trailing newline",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
{
name: "multi-doc block scalar clip (|) with 2 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
// |- (strip) in multi-doc (first doc)
{
name: "multi-doc block scalar strip (|-) with 0 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
{
name: "multi-doc block scalar strip (|-) with 1 trailing newline",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
{
name: "multi-doc block scalar strip (|-) with 2 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
// |+ (keep) in multi-doc (first doc)
{
name: "multi-doc block scalar keep (|+) with 0 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
{
name: "multi-doc block scalar keep (|+) with 1 trailing newline",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
{
name: "multi-doc block scalar keep (|+) with 2 trailing newlines",
files: map[string]string{
"templates/cm.yaml": `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
data:
val: simple`,
},
expected: `apiVersion: v1
kind: ConfigMap
metadata:
name: test
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
key: |-
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
annotations:
postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml'
data:
val: simple
`,
},
}
@ -1201,12 +1834,15 @@ func TestRenderResources_PostRenderer_Success(t *testing.T) {
expectedBuf := `---
# Source: yellow/templates/foodpie
foodpie: world
---
# Source: yellow/templates/with-partials
yellow: Earth
---
# Source: yellow/templates/yellow
yellow: world
`
expectedHook := `kind: ConfigMap
metadata:
@ -1214,7 +1850,8 @@ metadata:
annotations:
"helm.sh/hook": post-install,pre-delete,post-upgrade
data:
name: value`
name: value
`
assert.Equal(t, expectedBuf, buf.String())
assert.Len(t, hooks, 1)
@ -1319,14 +1956,17 @@ func TestRenderResources_PostRenderer_Integration(t *testing.T) {
# Source: hello/templates/goodbye
goodbye: world
color: blue
---
# Source: hello/templates/hello
hello: world
color: blue
---
# Source: hello/templates/with-partials
hello: Earth
color: blue
`
assert.Contains(t, output, "color: blue")
assert.Equal(t, 3, strings.Count(output, "color: blue"))

@ -23,3 +23,4 @@ subjects:
- kind: ServiceAccount
name: schedule-agents
namespace: spaced

@ -253,7 +253,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
var cerr error
currentRelease, cerr = releaserToV1Release(currentReleasei)
if cerr != nil {
return nil, nil, false, err
return nil, nil, false, cerr
}
if err != nil {
if errors.Is(err, driver.ErrNoDeployedReleases) &&

@ -251,6 +251,12 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st
// If the key is a child chart, coalesce tables with Merge set to true
merge := childChartMergeTrue(c, key, merge)
// When coalescing, clean nils from chart defaults before merging
// so they don't leak into the result.
if !merge {
cleanNilValues(src)
}
// Because v has higher precedence than nv, dest values override src
// values.
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
@ -258,6 +264,16 @@ func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix st
}
} else {
// If the key is not in v, copy it from nv.
// When coalescing, skip chart default nils and clean nils from
// nested maps so they don't shadow globals or produce %!s(<nil>).
if !merge {
if val == nil {
continue
}
if sub, ok := val.(map[string]any); ok {
cleanNilValues(sub)
}
}
v[key] = val
}
}
@ -326,7 +342,6 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix strin
// But if src also has nil (or key not in src), preserve the nil
delete(dst, key)
} else if !ok {
// key not in user values, preserve src value (including nil)
dst[key] = val
} else if istable(val) {
if istable(dv) {
@ -341,6 +356,18 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix strin
return dst
}
// cleanNilValues recursively removes nil entries in-place from a map so that chart
// default nils don't leak into the coalesced result.
func cleanNilValues(m map[string]any) {
for key, val := range m {
if val == nil {
delete(m, key)
} else if sub, ok := val.(map[string]any); ok {
cleanNilValues(sub)
}
}
}
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
func istable(v any) bool {
_, ok := v.(map[string]any)

@ -765,3 +765,166 @@ func TestCoalesceValuesEmptyMapWithNils(t *testing.T) {
is.True(ok, "Expected data.baz key to be present but it was removed")
is.Nil(data["baz"], "Expected data.baz key to be nil but it is not")
}
// TestCoalesceValuesSubchartDefaultNilsCleaned tests that nil values in subchart defaults
// are cleaned up during coalescing when the parent doesn't set those keys.
// Regression test for issue #31919.
func TestCoalesceValuesSubchartDefaultNilsCleaned(t *testing.T) {
is := assert.New(t)
// Subchart has a default with nil values (e.g. keyMapping: {password: null})
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"keyMapping": map[string]any{
"password": nil,
},
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// Parent user values don't mention keyMapping at all
vals := map[string]any{}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
keyMapping, ok := childVals["keyMapping"].(map[string]any)
is.True(ok, "keyMapping should be a map")
// The nil "password" key from chart defaults should be cleaned up
_, ok = keyMapping["password"]
is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed, but it is still present")
}
// TestCoalesceValuesUserNullErasesSubchartDefault tests that a user-supplied null
// value erases a subchart's default value during coalescing.
// Regression test for issue #31919.
func TestCoalesceValuesUserNullErasesSubchartDefault(t *testing.T) {
is := assert.New(t)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"someKey": "default",
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// User explicitly nullifies the subchart key via parent values
vals := map[string]any{
"child": map[string]any{
"someKey": nil,
},
}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
// someKey should be erased — user null overrides subchart default
_, ok = childVals["someKey"]
is.False(ok, "Expected someKey to be removed by user null override, but it is still present")
}
// TestCoalesceValuesSubchartNilDoesNotShadowGlobal tests that a nil value in
// subchart defaults doesn't shadow a global value accessible via pluck-like access.
// Regression test for issue #31971.
func TestCoalesceValuesSubchartNilDoesNotShadowGlobal(t *testing.T) {
is := assert.New(t)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"ingress": map[string]any{
"feature": nil, // nil in subchart defaults
},
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// Parent sets the global value
vals := map[string]any{
"global": map[string]any{
"ingress": map[string]any{
"feature": true,
},
},
}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
ingress, ok := childVals["ingress"].(map[string]any)
is.True(ok, "ingress should be a map")
// The nil "feature" from subchart defaults should be cleaned up,
// so that pluck can fall through to the global value
_, ok = ingress["feature"]
is.False(ok, "Expected ingress.feature (nil from chart defaults) to be removed so global can be used via pluck, but it is still present")
}
// TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides tests that nil
// values in subchart defaults are cleaned even when the user partially overrides
// the same map. Regression test for the coalesceTablesFullKey merge path.
func TestCoalesceValuesSubchartNilCleanedWhenUserPartiallyOverrides(t *testing.T) {
is := assert.New(t)
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Values: map[string]any{
"keyMapping": map[string]any{
"password": nil,
"format": "bcrypt",
},
},
}
parent := withDeps(&chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}, subchart)
// User overrides format but doesn't mention password
vals := map[string]any{
"child": map[string]any{
"keyMapping": map[string]any{
"format": "sha256",
},
},
}
v, err := CoalesceValues(parent, vals)
is.NoError(err)
childVals, ok := v["child"].(map[string]any)
is.True(ok, "child values should be a map")
keyMapping, ok := childVals["keyMapping"].(map[string]any)
is.True(ok, "keyMapping should be a map")
is.Equal("sha256", keyMapping["format"], "User override should be preserved")
_, ok = keyMapping["password"]
is.False(ok, "Expected keyMapping.password (nil from chart defaults) to be removed even when user partially overrides the map")
}

@ -302,7 +302,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
return nil, fmt.Errorf("failed reloading chart after repo update: %w", err)
}
} else {
return nil, fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err)
return nil, fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run 'helm dependency build' to fetch missing dependencies: %w", err)
}
}
}

@ -15,6 +15,7 @@ metadata:
name: test-secret
stringData:
foo: bar
---
# Source: chart-with-secret/templates/configmap.yaml
apiVersion: v1
@ -24,3 +25,4 @@ metadata:
data:
foo: bar

@ -2,30 +2,37 @@
# Source: issue-9027/charts/subchart/templates/values.yaml
global:
hash:
key1: 1
key2: 2
key3: 13
key4: 4
key5: 5
key6: 6
hash:
key1: 1
key2: 2
key3: 13
key4: 4
key5: 5
key6: 6
---
# Source: issue-9027/templates/values.yaml
global:
hash:
key1: null
key2: null
key3: 13
subchart:
global:
hash:
key1: 1
key2: 2
key3: 13
key4: 4
key5: 5
key6: 6
hash:
key1: 1
key2: 2
key3: 13
key4: 4
key5: 5

@ -155,6 +155,7 @@ spec:
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/01-a.yml
# 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order)

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -14,6 +15,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -28,6 +30,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -45,6 +48,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -62,6 +66,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -93,6 +98,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -112,3 +118,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -14,6 +15,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -28,6 +30,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -45,6 +48,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -62,6 +66,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -93,6 +98,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -112,3 +118,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -14,6 +15,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -28,6 +30,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -45,6 +48,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -62,6 +66,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/configmap.yaml
apiVersion: v1
@ -22,6 +23,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -36,6 +38,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -53,6 +56,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -70,6 +74,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -101,6 +106,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -120,3 +126,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/configmap.yaml
apiVersion: v1
@ -22,6 +23,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -36,6 +38,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -53,6 +56,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -70,6 +74,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -101,6 +106,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -120,3 +126,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/configmap.yaml
apiVersion: v1
@ -22,6 +23,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -36,6 +38,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -53,6 +56,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -70,6 +74,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -101,6 +106,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -120,3 +126,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -14,6 +15,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -28,6 +30,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -45,6 +48,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -62,6 +66,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -93,6 +98,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -112,3 +118,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -14,6 +15,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -28,6 +30,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -45,6 +48,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -62,6 +66,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -95,6 +100,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -114,3 +120,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -21,6 +21,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -31,6 +32,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -45,6 +47,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -62,6 +65,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -79,6 +83,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -110,6 +115,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -129,3 +135,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -14,6 +15,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -28,6 +30,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -45,6 +48,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -62,6 +66,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -93,6 +98,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -112,3 +118,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -4,6 +4,7 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: subchart-sa
---
# Source: subchart/templates/subdir/role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -14,6 +15,7 @@ rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","list","watch"]
---
# Source: subchart/templates/subdir/rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@ -28,6 +30,7 @@ subjects:
- kind: ServiceAccount
name: subchart-sa
namespace: default
---
# Source: subchart/charts/subcharta/templates/service.yaml
apiVersion: v1
@ -45,6 +48,7 @@ spec:
name: apache
selector:
app.kubernetes.io/name: subcharta
---
# Source: subchart/charts/subchartb/templates/service.yaml
apiVersion: v1
@ -62,6 +66,7 @@ spec:
name: nginx
selector:
app.kubernetes.io/name: subchartb
---
# Source: subchart/templates/service.yaml
apiVersion: v1
@ -93,6 +98,7 @@ metadata:
"helm.sh/hook": test
data:
message: Hello World
---
# Source: subchart/templates/tests/test-nothing.yaml
apiVersion: v1
@ -112,3 +118,4 @@ spec:
- echo
- "$message"
restartPolicy: Never

@ -1 +1 @@
Error: an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: reqsubchart2
Error: an error occurred while checking for chart dependencies. You may need to run 'helm dependency build' to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: reqsubchart2

@ -205,7 +205,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
if req := ac.MetaDependencies(); len(req) > 0 {
if err := action.CheckDependencies(ch, req); err != nil {
err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err)
err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run 'helm dependency build' to fetch missing dependencies: %w", err)
if client.DependencyUpdate {
man := &downloader.Manager{
Out: out,

@ -127,7 +127,7 @@ func (m *Manager) Build() error {
return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies")
}
} else {
return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies")
return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies with 'helm dependency update'")
}
}

@ -1505,3 +1505,65 @@ func TestTraceableError_NoTemplateForm(t *testing.T) {
}
}
}
// TestRenderSubchartDefaultNilNoStringify tests the full pipeline: subchart default
// nil values should not produce "%!s(<nil>)" in rendered template output.
// Regression test for the Bitnami common.secrets.key issue.
func TestRenderSubchartDefaultNilNoStringify(t *testing.T) {
modTime := time.Now()
// Subchart has a default with nil values
subchart := &chart.Chart{
Metadata: &chart.Metadata{Name: "child"},
Templates: []*common.File{
{
Name: "templates/test.yaml",
ModTime: modTime,
Data: []byte(`{{- if hasKey .Values.keyMapping "password" -}}{{- printf "subPath: %s" (index .Values.keyMapping "password") -}}{{- else -}}subPath: fallback{{- end -}}`),
},
},
Values: map[string]any{
"keyMapping": map[string]any{
"password": nil, // nil in chart defaults
},
},
}
parent := &chart.Chart{
Metadata: &chart.Metadata{Name: "parent"},
Values: map[string]any{},
}
parent.AddDependency(subchart)
// Parent user values don't set keyMapping
injValues := map[string]any{}
tmp, err := util.CoalesceValues(parent, injValues)
if err != nil {
t.Fatalf("Failed to coalesce values: %s", err)
}
inject := common.Values{
"Values": tmp,
"Chart": parent.Metadata,
"Release": common.Values{
"Name": "test-release",
},
}
out, err := Render(parent, inject)
if err != nil {
t.Fatalf("Failed to render templates: %s", err)
}
rendered := out["parent/charts/child/templates/test.yaml"]
if strings.Contains(rendered, "%!s(<nil>)") {
t.Errorf("Rendered output contains %%!s(<nil>), got: %q", rendered)
}
expected := "subPath: fallback"
if rendered != expected {
t.Errorf("Expected %q, got %q", expected, rendered)
}
}

@ -1241,7 +1241,7 @@ func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts
return fmt.Errorf("conflict occurred while applying object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err)
}
return err
return fmt.Errorf("server-side apply failed for object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err)
}
return target.Refresh(obj, true)

@ -1802,6 +1802,23 @@ func TestPatchResourceServerSide(t *testing.T) {
},
ExpectedErrorContains: "the server reported a conflict",
},
"generic server-side apply error": {
Pods: newPodList("whale"),
DryRun: false,
ForceConflicts: false,
FieldValidationDirective: FieldValidationDirectiveStrict,
Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, _ *http.Request) (*http.Response, error) {
t.Helper()
return newResponse(http.StatusBadRequest, &metav1.Status{
Status: metav1.StatusFailure,
Message: `failed to create typed patch object: .spec.template.spec.containers[name="test"].env: duplicate entries for key [name="SERVER_CONTEXT_PATH"]`,
Reason: metav1.StatusReasonBadRequest,
Code: http.StatusBadRequest,
})
},
ExpectedErrorContains: "server-side apply failed for object default/whale /v1, Kind=Pod: failed to create typed patch object",
},
}
for name, tc := range testCases {

@ -21,6 +21,7 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
)
// SimpleHead defines what the structure of the head of a manifest file
@ -35,7 +36,7 @@ type SimpleHead struct {
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
// SplitManifests takes a string of manifest and returns a map contains individual manifests
// SplitManifests takes a manifest string and returns a map containing individual manifests.
//
// **Note for Chart API v3**: This function (due to the regex above) has allowed _WRONG_
// Go templates to be defined inside charts across the years. The generated text from Go
@ -53,15 +54,15 @@ func SplitManifests(bigFile string) map[string]string {
tpl := "manifest-%d"
res := map[string]string{}
// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
bigFileTmp := strings.TrimSpace(bigFile)
bigFileTmp := strings.TrimLeftFunc(bigFile, unicode.IsSpace)
docs := sep.Split(bigFileTmp, -1)
var count int
for _, d := range docs {
if d == "" {
if strings.TrimSpace(d) == "" {
continue
}
d = strings.TrimSpace(d)
d = strings.TrimLeftFunc(d, unicode.IsSpace)
res[fmt.Sprintf(tpl, count)] = d
count = count + 1
}

@ -21,7 +21,15 @@ import (
"testing"
)
const mockManifestFile = `
func TestSplitManifests(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]string
}{
{
name: "single doc with leading separator and whitespace",
input: `
---
apiVersion: v1
@ -35,9 +43,9 @@ spec:
- name: nemo-test
image: fake-image
cmd: fake-command
`
const expectedManifest = `apiVersion: v1
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: Pod
metadata:
name: finding-nemo,
@ -47,15 +55,463 @@ spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command`
cmd: fake-command
`,
},
},
{
name: "empty input",
input: "",
expected: map[string]string{},
},
{
name: "whitespace only",
input: " \n\n \n",
expected: map[string]string{},
},
{
name: "whitespace-only doc after separator is skipped",
input: "---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1\n---\n \n",
expected: map[string]string{
"manifest-0": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm1",
},
},
{
name: "single doc no separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
`,
},
},
{
name: "two docs with proper separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
},
},
func TestSplitManifest(t *testing.T) {
manifests := SplitManifests(mockManifestFile)
if len(manifests) != 1 {
t.Errorf("Expected 1 manifest, got %v", len(manifests))
// Block scalar chomping indicator tests using | (clip), |- (strip), and |+ (keep)
// inputs with 0, 1, and 2 trailing newlines after the block content.
// Note: the emitter may normalize the output chomping indicator when the
// trailing newline count makes another indicator equivalent for the result.
// | (clip) input — clips trailing newlines to exactly one, though with
// 0 trailing newlines the emitted output may normalize to |-.
{
name: "block scalar clip (|) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
},
},
{
name: "block scalar clip (|) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
},
{
name: "block scalar clip (|) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
`,
},
},
// |- (strip)
{
name: "block scalar strip (|-) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello`,
},
},
{
name: "block scalar strip (|-) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
},
{
name: "block scalar strip (|-) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |-
hello
`,
},
},
// |+ (keep)
{
name: "block scalar keep (|+) with 0 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
},
},
{
name: "block scalar keep (|+) with 1 trailing newline",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
},
{
name: "block scalar keep (|+) with 2 trailing newlines",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
`,
},
},
// Multi-doc with block scalars: the regex consumes \s*\n before ---,
// so trailing newlines from non-last docs are stripped.
{
name: "multi-doc block scalar clip (|) before separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |
hello`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
},
},
{
name: "multi-doc block scalar keep (|+) with 2 trailing newlines before separator",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello
---
apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: test
data:
key: |+
hello`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: test2
`,
},
},
// **Note for Chart API v3**: The following tests exercise the lenient
// regex that splits `---apiVersion` back into separate documents.
// In Chart API v3, these inputs should return an _ERROR_ instead.
// See the comment on the SplitManifests function for more details.
{
name: "leading glued separator (---apiVersion)",
input: `
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
`,
},
},
{
name: "mid-content glued separator (---apiVersion)",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`,
},
},
{
name: "multiple glued separators",
input: `
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
},
},
{
name: "mixed glued and proper separators",
input: `
apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
---apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
expected: map[string]string{
"manifest-0": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm1`,
"manifest-1": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm2`,
"manifest-2": `apiVersion: v1
kind: ConfigMap
metadata:
name: cm3
`,
},
},
}
expected := map[string]string{"manifest-0": expectedManifest}
if !reflect.DeepEqual(manifests, expected) {
t.Errorf("Expected %v, got %v", expected, manifests)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SplitManifests(tt.input)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("SplitManifests() =\n%v\nwant:\n%v", result, tt.expected)
}
})
}
}

Loading…
Cancel
Save