diff --git a/api/krusty/generatormergeandreplace_test.go b/api/krusty/generatormergeandreplace_test.go index 0dff5ae9b..677bfb260 100644 --- a/api/krusty/generatormergeandreplace_test.go +++ b/api/krusty/generatormergeandreplace_test.go @@ -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) { th := kusttest_test.MakeHarness(t) th.WriteK("app", ` diff --git a/api/resmap/reswrangler.go b/api/resmap/reswrangler.go index 33578a0bb..4883cac49 100644 --- a/api/resmap/reswrangler.go +++ b/api/resmap/reswrangler.go @@ -588,8 +588,15 @@ func (m *resWrangler) appendReplaceOrMerge(res *resource.Resource) error { if err != nil { 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.MergeDataMapFrom(old) + res.MergeDataMapFrom(oldForData) res.MergeBinaryDataMapFrom(old) if orig != nil { 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 func (m *resWrangler) AnnotateAll(key string, value string) error { return m.ApplyFilter(annotations.Filter{ diff --git a/api/resmap/reswrangler_test.go b/api/resmap/reswrangler_test.go index 73c7d7142..59df4f60f 100644 --- a/api/resmap/reswrangler_test.go +++ b/api/resmap/reswrangler_test.go @@ -990,6 +990,144 @@ func TestAbsorbAll(t *testing.T) { 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) { input := `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole