fix: preserve Secret stringData during generator merge (#6237)

* test: reproduce secretGenerator stringData merge failure

* fix: preserve Secret stringData during generator merge
This commit is contained in:
Yugo Kobayashi
2026-09-05 04:12:20 +09:00
committed by GitHub
parent 3f00f0aa94
commit 7a3bab80fb
3 changed files with 214 additions and 1 deletions

View File

@@ -453,6 +453,42 @@ metadata:
`) `)
} }
// Regression test for https://github.com/kubernetes-sigs/kustomize/issues/5955
func TestSecretGeneratorMergeStringData(t *testing.T) {
th := kusttest_test.MakeHarness(t)
th.WriteK(".", `
resources:
- secret.yaml
secretGenerator:
- name: test
behavior: merge
literals:
- property2=value2
options:
disableNameSuffixHash: false
`)
th.WriteF("secret.yaml", `
apiVersion: v1
kind: Secret
metadata:
name: test
type: Opaque
stringData:
property1: value1
`)
m := th.Run(".", th.MakeDefaultOptions())
th.AssertActualEqualsExpected(m, `
apiVersion: v1
data:
property1: dmFsdWUx
property2: dmFsdWUy
kind: Secret
metadata:
name: test
type: Opaque
`)
}
func TestMergeAndReplaceDisableNameSuffixHashGenerators(t *testing.T) { func TestMergeAndReplaceDisableNameSuffixHashGenerators(t *testing.T) {
th := kusttest_test.MakeHarness(t) th := kusttest_test.MakeHarness(t)
th.WriteK("app", ` th.WriteK("app", `

View File

@@ -588,8 +588,15 @@ func (m *resWrangler) appendReplaceOrMerge(res *resource.Resource) error {
if err != nil { if err != nil {
return err return err
} }
oldForData := old
if res.GetApiVersion() == "v1" && res.GetKind() == "Secret" {
oldForData, err = normalizeSecretStringData(old)
if err != nil {
return fmt.Errorf("failed to normalize stringData for %s: %w", id, err)
}
}
res.CopyMergeMetaDataFieldsFrom(old) res.CopyMergeMetaDataFieldsFrom(old)
res.MergeDataMapFrom(old) res.MergeDataMapFrom(oldForData)
res.MergeBinaryDataMapFrom(old) res.MergeBinaryDataMapFrom(old)
if orig != nil { if orig != nil {
res.SetOrigin(orig) res.SetOrigin(orig)
@@ -614,6 +621,38 @@ func (m *resWrangler) appendReplaceOrMerge(res *resource.Resource) error {
} }
} }
// normalizeSecretStringData returns a copy of res with stringData encoded into
// data, matching the normalization performed by the Kubernetes API server.
// Values in stringData take precedence over values with the same key in data.
func normalizeSecretStringData(res *resource.Resource) (*resource.Resource, error) {
normalized := res.DeepCopy()
stringData, err := normalized.Pipe(kyaml.Lookup("stringData"))
if err != nil {
return nil, fmt.Errorf("lookup stringData: %w", err)
}
if !kyaml.IsMissingOrNull(stringData) {
values := map[string]string{}
if err := stringData.VisitFields(func(node *kyaml.MapNode) error {
key := kyaml.GetValue(node.Key)
value := node.Value
if value == nil || value.YNode() == nil || value.YNode().Kind != kyaml.ScalarNode {
return fmt.Errorf("stringData value for key %q must be a scalar", key)
}
values[key] = kyaml.GetValue(value)
return nil
}); err != nil {
return nil, fmt.Errorf("read stringData: %w", err)
}
if err := normalized.LoadMapIntoSecretData(values); err != nil {
return nil, fmt.Errorf("encode stringData: %w", err)
}
}
if err := normalized.PipeE(kyaml.Clear("stringData")); err != nil {
return nil, fmt.Errorf("clear stringData: %w", err)
}
return normalized, nil
}
// AnnotateAll implements ResMap // AnnotateAll implements ResMap
func (m *resWrangler) AnnotateAll(key string, value string) error { func (m *resWrangler) AnnotateAll(key string, value string) error {
return m.ApplyFilter(annotations.Filter{ return m.ApplyFilter(annotations.Filter{

View File

@@ -990,6 +990,144 @@ func TestAbsorbAll(t *testing.T) {
t, strings.Contains(err.Error(), "behavior must be merge or replace")) t, strings.Contains(err.Error(), "behavior must be merge or replace"))
} }
func TestAbsorbAllMergeSecretStringData(t *testing.T) {
tests := []struct {
name string
existing string
incoming string
expected string
}{
{
name: "core v1 Secret normalizes stringData",
existing: `
apiVersion: v1
data:
dataOnly: ZGF0YQ==
generatorWins: b2xkLWRhdGE=
stringWins: b2xkLWRhdGE=
kind: Secret
metadata:
name: test
stringData:
generatorWins: string
long: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
multiline: |
line one
line two
stringOnly: string
stringWins: string
type: Opaque
`,
incoming: `
apiVersion: v1
data:
generatedOnly: Z2VuZXJhdGVk
generatorWins: Z2VuZXJhdGVk
kind: Secret
metadata:
name: test
type: Opaque
`,
expected: `
apiVersion: v1
data:
dataOnly: ZGF0YQ==
generatedOnly: Z2VuZXJhdGVk
generatorWins: Z2VuZXJhdGVk
long: |
YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWj
AxMjM0NTY3ODk=
multiline: bGluZSBvbmUKbGluZSB0d28K
stringOnly: c3RyaW5n
stringWins: c3RyaW5n
kind: Secret
metadata:
name: test
type: Opaque
`,
},
{
name: "custom resource does not normalize stringData",
existing: `
apiVersion: example.com/v1
data:
oldOnly: b2xk
kind: Secret
metadata:
name: test
stringData:
customOnly: plain
`,
incoming: `
apiVersion: example.com/v1
data:
generatedOnly: Z2VuZXJhdGVk
kind: Secret
metadata:
name: test
`,
expected: `
apiVersion: example.com/v1
data:
generatedOnly: Z2VuZXJhdGVk
oldOnly: b2xk
kind: Secret
metadata:
name: test
`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
existing, err := rmF.NewResMapFromBytes([]byte(tc.existing))
require.NoError(t, err)
incoming, err := rmF.NewResMapFromBytes([]byte(tc.incoming))
require.NoError(t, err)
incoming.Resources()[0].SetBehavior(types.BehaviorMerge)
expected, err := rmF.NewResMapFromBytes([]byte(tc.expected))
require.NoError(t, err)
require.NoError(t, existing.AbsorbAll(incoming))
existing.RemoveBuildAnnotations()
require.NoError(t, expected.ErrorIfNotEqualLists(existing))
})
}
}
func TestAbsorbAllRejectsNonScalarSecretStringData(t *testing.T) {
existing, err := rmF.NewResMapFromBytes([]byte(`
apiVersion: v1
kind: Secret
metadata:
name: test
stringData:
invalid:
nested: value
type: Opaque
`))
require.NoError(t, err)
incoming, err := rmF.NewResMapFromBytes([]byte(`
apiVersion: v1
data:
generated: Z2VuZXJhdGVk
kind: Secret
metadata:
name: test
type: Opaque
`))
require.NoError(t, err)
incoming.Resources()[0].SetBehavior(types.BehaviorMerge)
before, err := existing.AsYaml()
require.NoError(t, err)
err = existing.AbsorbAll(incoming)
require.ErrorContains(t, err, `stringData value for key "invalid" must be a scalar`)
after, yamlErr := existing.AsYaml()
require.NoError(t, yamlErr)
assert.Equal(t, before, after)
}
func TestToRNodeSlice(t *testing.T) { func TestToRNodeSlice(t *testing.T) {
input := `apiVersion: rbac.authorization.k8s.io/v1 input := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: ClusterRole