Преглед изворни кода

fix(templating): preserve mixed-case in generic target paths (#6459)

Templating into a generic custom resource via spec.target.manifest with a
mixed-case templateFrom[].target dotted path (e.g.
spec.headers.customRequestHeaders) lowercased the whole path before splitting
it, so the rendered field was written under an unknown lowercase key and
dropped by the API server.

The lowercasing exists to match the well-known top-level targets (annotations,
labels, data, spec) case-insensitively. Switch on a lowercased copy for that
dispatch but keep the original target for the nested-path branch, so arbitrary
custom-resource paths retain their case. Add regression tests for both the
KeysAndValues and Values scopes.

Fixes: external-secrets/external-secrets#6458

Signed-off-by: Alexander Chernov <alexander@chernov.it>
Alexander Chernov пре 1 недеља
родитељ
комит
a788bad066
2 измењених фајлова са 101 додато и 4 уклоњено
  1. 7 4
      runtime/template/v2/template.go
  2. 94 0
      runtime/template/v2/template_test.go

+ 7 - 4
runtime/template/v2/template.go

@@ -94,8 +94,10 @@ func init() {
 }
 
 func applyToTarget(k string, val []byte, target string, obj client.Object) error {
-	target = strings.ToLower(target)
-	switch target {
+	// Match the well-known top-level targets case-insensitively, but keep the
+	// original case of target so nested custom-resource paths preserve
+	// mixed-case segments (e.g. spec.headers.customRequestHeaders). Issue #6458.
+	switch strings.ToLower(target) {
 	case "annotations":
 		annotations := obj.GetAnnotations()
 		if annotations == nil {
@@ -184,8 +186,9 @@ func mapScopeApply(tpl string, data map[string][]byte, target string, secret cli
 		return fmt.Errorf(errExecute, tpl, err)
 	}
 
-	target = strings.ToLower(target)
-	switch target {
+	// See applyToTarget: switch on the lowercased target but keep the original
+	// case so nested paths are not lowercased (issue #6458).
+	switch strings.ToLower(target) {
 	case "annotations", "labels", "data":
 		// normal route
 		src := make(map[string]string)

+ 94 - 0
runtime/template/v2/template_test.go

@@ -1382,6 +1382,100 @@ channel: {{ .new_channel }}
 				assert.Equal(t, "test-value", slackMap["other_field"], "existing other_field should be preserved")
 			},
 		},
+		{
+			name:   "nested path preserves mixed-case segments (issue #6458)",
+			target: "spec.headers.customRequestHeaders",
+			scope:  esapi.TemplateScopeKeysAndValues,
+			tpl: map[string][]byte{
+				"header": []byte(`foo: {{ .token }}`),
+			},
+			data: map[string][]byte{
+				"token": []byte("Bearer"),
+			},
+			verify: func(t *testing.T, obj map[string]any) {
+				specMap := obj["spec"].(map[string]any)
+				headersMap := specMap["headers"].(map[string]any)
+
+				// The mixed-case segment must be preserved, not lowercased.
+				crh, ok := headersMap["customRequestHeaders"].(map[string]any)
+				require.True(t, ok, "customRequestHeaders should keep its mixed case")
+				assert.Equal(t, "Bearer", crh["foo"], "foo should be set under the mixed-case path")
+
+				// The lowercased variant must NOT exist.
+				_, lowered := headersMap["customrequestheaders"]
+				assert.False(t, lowered, "path must not be forcibly lowercased")
+			},
+		},
+		{
+			name:   "values scope nested path preserves mixed-case (issue #6458)",
+			target: "spec.headers.customRequestHeaders",
+			scope:  esapi.TemplateScopeValues,
+			tpl: map[string][]byte{
+				"foo": []byte(`{{ .token }}`),
+			},
+			data: map[string][]byte{
+				"token": []byte("Bearer"),
+			},
+			verify: func(t *testing.T, obj map[string]any) {
+				specMap := obj["spec"].(map[string]any)
+				headersMap := specMap["headers"].(map[string]any)
+
+				// applyToTarget sets the value at the final mixed-case segment.
+				assert.Equal(t, "Bearer", headersMap["customRequestHeaders"], "value should land under the mixed-case key")
+
+				// The lowercased variant must NOT exist.
+				_, lowered := headersMap["customrequestheaders"]
+				assert.False(t, lowered, "path must not be forcibly lowercased")
+			},
+		},
+		{
+			name:   "nested path preserves intermediate mixed-case segment (issue #6458)",
+			target: "spec.tlsConfig.certResolver",
+			scope:  esapi.TemplateScopeKeysAndValues,
+			tpl: map[string][]byte{
+				"resolver": []byte(`name: {{ .val }}`),
+			},
+			data: map[string][]byte{
+				"val": []byte("myResolver"),
+			},
+			verify: func(t *testing.T, obj map[string]any) {
+				specMap := obj["spec"].(map[string]any)
+
+				// Both the intermediate and the leaf segment keep their case.
+				tlsConfig, ok := specMap["tlsConfig"].(map[string]any)
+				require.True(t, ok, "intermediate tlsConfig segment should keep its case")
+				certResolver, ok := tlsConfig["certResolver"].(map[string]any)
+				require.True(t, ok, "leaf certResolver segment should keep its case")
+				assert.Equal(t, "myResolver", certResolver["name"])
+
+				// No lowercased intermediate segment should be created.
+				_, lowered := specMap["tlsconfig"]
+				assert.False(t, lowered, "intermediate segment must not be lowercased")
+			},
+		},
+		{
+			name:   "values scope preserves intermediate mixed-case segment (issue #6458)",
+			target: "spec.tlsConfig.certResolver",
+			scope:  esapi.TemplateScopeValues,
+			tpl: map[string][]byte{
+				"x": []byte(`{{ .val }}`),
+			},
+			data: map[string][]byte{
+				"val": []byte("myResolver"),
+			},
+			verify: func(t *testing.T, obj map[string]any) {
+				specMap := obj["spec"].(map[string]any)
+
+				// The intermediate segment keeps its case and the value lands on it.
+				tlsConfig, ok := specMap["tlsConfig"].(map[string]any)
+				require.True(t, ok, "intermediate tlsConfig segment should keep its case")
+				assert.Equal(t, "myResolver", tlsConfig["certResolver"])
+
+				// No lowercased intermediate segment should be created.
+				_, lowered := specMap["tlsconfig"]
+				assert.False(t, lowered, "intermediate segment must not be lowercased")
+			},
+		},
 	}
 
 	for _, tt := range tests {