Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apis/meta/reference_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ type ValuesReference struct {
// transient error will still result in a reconciliation failure.
// +optional
Optional bool `json:"optional,omitempty"`

// Literal marks this ValuesReference as a literal value. When set in
// combination with TargetPath, the referenced value is merged at the target
// path without interpreting Helm's `--set` syntax (commas, brackets, dots,
// equal signs, etc.), mirroring the behavior of `helm --set-literal`. This
// is the only safe way to inject arbitrary file content (config files, JSON
// blobs, multi-line strings containing special characters) through
// `valuesFrom`. Has no effect when TargetPath is empty: in that mode the
// referenced value is always YAML-merged at the root.
// +optional
Literal bool `json:"literal,omitempty"`
}

// GetValuesKey returns the defined ValuesKey, or the default ('values.yaml').
Expand Down
32 changes: 31 additions & 1 deletion chartutil/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,11 @@ func ChartValuesFromReferences(ctx context.Context, log logr.Logger, client kube
// TODO(hidde): this is a bit of hack, as it mimics the way the option string is passed
// to Helm from a CLI perspective. Given the parser is however not publicly accessible
// while it contains all logic around parsing the target path, it is a fair trade-off.
if err := ReplacePathValue(result, ref.TargetPath, string(valuesData)); err != nil {
merger := ReplacePathValue
if ref.Literal {
merger = ReplacePathLiteralValue
}
if err := merger(result, ref.TargetPath, string(valuesData)); err != nil {
return nil, NewErrValuesReference(namespacedName, ref, ErrValueMerge, err)
}
continue
Expand Down Expand Up @@ -269,3 +273,29 @@ func ReplacePathValue(values common.Values, path string, value string) error {
value = path + "=" + value
return strvals.ParseInto(value, values)
}

// ReplacePathLiteralValue replaces the value at the dot notation path with the
// given value, treating the value as a literal string. The value is consumed
// verbatim: commas, brackets, braces, equal signs and backslashes that `--set`
// would interpret as syntax are preserved as part of the value. This is the
// only safe way to inject arbitrary file content (config files, JSON blobs,
// multi-line strings containing special characters) at a target path.
//
// Mirrors the behavior of `helm --set-literal` for the value, while keeping
// `\.` escape support in the path (which `helm --set-literal` itself does
// not). Implemented by pre-escaping strvals metacharacters in the value and
// then delegating to strvals.ParseIntoString — that combination yields a
// verbatim value AND escape-aware path parsing.
func ReplacePathLiteralValue(values common.Values, path string, value string) error {
// Order matters: backslash must be escaped first so subsequent escapes
// don't get re-escaped.
escaper := strings.NewReplacer(
`\`, `\\`,
`,`, `\,`,
`[`, `\[`,
`]`, `\]`,
`{`, `\{`,
`}`, `\}`,
)
return strvals.ParseIntoString(path+"="+escaper.Replace(value), values)
}
208 changes: 208 additions & 0 deletions chartutil/values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,94 @@ invalid`,
},
wantErr: true,
},
{
// Documents the bug that Literal exists to work around: a value
// containing helm-set metacharacters (here: a comma inside a YAML
// flow sequence) is interpreted by strvals.ParseInto and corrupts
// the merged value. Same input passes through cleanly in the
// "literal mode" test cases below.
name: "non-literal target path mangles value with commas",
resources: []runtime.Object{
mockConfigMap("values", map[string]string{
"application.yml": `endpoints: [a,b,c]`,
}),
},
references: []meta.ValuesReference{
{
Kind: kindConfigMap,
Name: "values",
ValuesKey: "application.yml",
TargetPath: `externalConfig.application\.yml.content`,
},
},
wantErr: true,
},
{
name: "literal target path preserves helm-set metacharacters",
resources: []runtime.Object{
mockConfigMap("values", map[string]string{
"application.yml": "server:\n port: 8080\nmanagement:\n endpoints:\n web:\n exposure:\n include: [ \"prometheus\", \"health\" ]\n",
}),
},
references: []meta.ValuesReference{
{
Kind: kindConfigMap,
Name: "values",
ValuesKey: "application.yml",
TargetPath: `externalConfig.application\.yml.content`,
Literal: true,
},
},
want: common.Values{
"externalConfig": map[string]interface{}{
"application.yml": map[string]interface{}{
"content": "server:\n port: 8080\nmanagement:\n endpoints:\n web:\n exposure:\n include: [ \"prometheus\", \"health\" ]\n",
},
},
},
},
{
name: "literal target path preserves multi-line HOCON with equals and braces",
resources: []runtime.Object{
mockSecret("values", map[string][]byte{
"application.conf": []byte("kafka {\n bootstrap = \"b-1:9098,b-2:9098\"\n group = \"my.service\"\n}\n"),
}),
},
references: []meta.ValuesReference{
{
Kind: kindSecret,
Name: "values",
ValuesKey: "application.conf",
TargetPath: `externalConfig.application\.conf.content`,
Literal: true,
},
},
want: common.Values{
"externalConfig": map[string]interface{}{
"application.conf": map[string]interface{}{
"content": "kafka {\n bootstrap = \"b-1:9098,b-2:9098\"\n group = \"my.service\"\n}\n",
},
},
},
},
{
name: "literal flag without targetPath is ignored (root YAML merge)",
resources: []runtime.Object{
mockConfigMap("values", map[string]string{
"values.yaml": "flat: value\n",
}),
},
references: []meta.ValuesReference{
{
Kind: kindConfigMap,
Name: "values",
Literal: true,
},
},
want: common.Values{
"flat": "value",
},
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -406,6 +494,126 @@ func TestReplacePathValue(t *testing.T) {
}
}

// TestReplacePathLiteralValue covers the helm `--set-literal` equivalent:
// the value is consumed verbatim, with metacharacters preserved.
func TestReplacePathLiteralValue(t *testing.T) {
tests := []struct {
name string
value []byte
path string
want map[string]interface{}
wantErr bool
}{
{
name: "simple string",
value: []byte("value"),
path: "outer.inner",
want: map[string]interface{}{
"outer": map[string]interface{}{
"inner": "value",
},
},
},
{
name: "value with commas is preserved",
value: []byte("a,b,c"),
path: "name",
want: map[string]interface{}{
"name": "a,b,c",
},
},
{
name: "value with braces is preserved (not parsed as inline list)",
value: []byte("{a,b,c}"),
path: "name",
want: map[string]interface{}{
"name": "{a,b,c}",
},
},
{
name: "value with equals signs is preserved",
value: []byte("foo=bar=baz"),
path: "name",
want: map[string]interface{}{
"name": "foo=bar=baz",
},
},
{
name: "value with brackets is preserved (not parsed as array index)",
value: []byte("endpoints: [a, b, c]"),
path: "config",
want: map[string]interface{}{
"config": "endpoints: [a, b, c]",
},
},
{
name: "multi-line YAML content is preserved verbatim",
value: []byte("server:\n port: 8080\nmanagement:\n endpoints:\n web:\n exposure:\n include: [ \"prometheus\", \"health\" ]\n"),
path: `externalConfig.application\.yml.content`,
want: map[string]interface{}{
"externalConfig": map[string]interface{}{
"application.yml": map[string]interface{}{
"content": "server:\n port: 8080\nmanagement:\n endpoints:\n web:\n exposure:\n include: [ \"prometheus\", \"health\" ]\n",
},
},
},
},
{
name: "HOCON content with equals and quoted strings is preserved",
value: []byte("kafka {\n bootstrap = \"b-1:9098,b-2:9098\"\n group = \"svc.consumer\"\n}\n"),
path: `externalConfig.application\.conf.content`,
want: map[string]interface{}{
"externalConfig": map[string]interface{}{
"application.conf": map[string]interface{}{
"content": "kafka {\n bootstrap = \"b-1:9098,b-2:9098\"\n group = \"svc.consumer\"\n}\n",
},
},
},
},
{
name: "JSON string is preserved verbatim (no escape needed)",
value: []byte(`["a","b","c"]`),
path: "subnet_ids",
want: map[string]interface{}{
"subnet_ids": `["a","b","c"]`,
},
},
{
name: "boolean-like string stays a string",
value: []byte("true"),
path: "feature.enabled",
want: map[string]interface{}{
"feature": map[string]interface{}{
"enabled": "true",
},
},
},
{
name: "dot escape in path still works",
value: []byte("master"),
path: `nodeSelector.kubernetes\.io/role`,
want: map[string]interface{}{
"nodeSelector": map[string]interface{}{
"kubernetes.io/role": "master",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
values := map[string]interface{}{}
err := ReplacePathLiteralValue(values, tt.path, string(tt.value))
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
return
}
g.Expect(err).ToNot(HaveOccurred())
g.Expect(values).To(Equal(tt.want))
})
}
}

func mockSecret(name string, data map[string][]byte) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Expand Down