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
34 changes: 33 additions & 1 deletion api/filters/replacement/replacement.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,23 @@ func setFieldValue(options *types.FieldOptions, targetField *yaml.RNode, value *
}

if targetField.YNode().Kind == yaml.ScalarNode {
// For scalar, only copy the value (leave any type intact to auto-convert int->string or string->int)
// For an existing scalar, only copy the value and leave its tag intact,
// so it auto-converts to the target's pre-existing type (int->string or
// string->int).
targetField.YNode().Value = value.YNode().Value
if targetField.YNode().Tag == "" {
// Unless the field has no resolved type, meaning it was just created
// (e.g. via `options.create: true`) rather than copied from source
// YAML. Resolve and set its implicit YAML 1.2 tag now, matching what
// would be inferred if this value were serialized directly. Without
// this, the field would keep rendering correctly until some later step
// (e.g. a strategic merge patch) walks the document: FieldSetter's
// YAML-1.1-compatibility check treats an untagged scalar as a string
// and force-quotes it if its value looks like a bool/int/null keyword,
// silently turning e.g. a replacement-sourced "true" into the string
// "true" instead of the boolean true.
targetField.YNode().Tag = resolveImplicitTag(targetField.YNode().Value)
}
} else {
targetField.SetYNode(value.YNode())
}
Expand Down Expand Up @@ -403,3 +418,20 @@ func serializeAsYAML(structuredData *yaml.RNode) (string, error) {

return strings.TrimSpace(modifiedData), nil
}

// resolveImplicitTag returns the YAML 1.2 tag that value would resolve to if
// written out as a plain (unquoted, untagged) scalar, e.g. "true" -> !!bool,
// "3" -> !!int, "hello" -> !!str.
func resolveImplicitTag(value string) string {
if value == "" {
return yaml.NodeTagString
}
var n yaml.Node
if err := yaml.Unmarshal([]byte(value), &n); err != nil || len(n.Content) != 1 {
return yaml.NodeTagString
}
if c := n.Content[0]; c.Kind == yaml.ScalarNode {
return c.Tag
}
return yaml.NodeTagString
}
190 changes: 190 additions & 0 deletions api/krusty/replacementtransformer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1002,3 +1002,193 @@ metadata:
name: pre-app-config-dev-7266b7f2m9
`)
}

func TestReplacementsCreateBoolFieldSurvivesUnrelatedStrategicMergePatch(t *testing.T) {
th := kusttest_test.MakeEnhancedHarness(t)
defer th.Reset()

th.WriteK(".", `
resources:
- deployment.yaml
components:
- components/add-volume
patches:
- path: patch.yaml
`)
th.WriteF("deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: myapp
image: myimage
`)
th.WriteF("patch.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
metadata:
annotations:
foo: bar
`)
th.WriteC("components/add-volume", `
configMapGenerator:
- name: volume-config
literals:
- readOnly=true
- mountPath=/mnt/myvolume
replacements:
- source:
name: volume-config
kind: ConfigMap
fieldPath: data.readOnly
targets:
- select:
kind: Deployment
fieldPaths:
- spec.template.spec.containers.0.volumeMounts.[name=myvolume].readOnly
options:
create: true
- source:
name: volume-config
kind: ConfigMap
fieldPath: data.mountPath
targets:
- select:
kind: Deployment
fieldPaths:
- spec.template.spec.containers.0.volumeMounts.[name=myvolume].mountPath
options:
create: true
`)
m := th.Run(".", th.MakeDefaultOptions())
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
metadata:
annotations:
foo: bar
spec:
containers:
- image: myimage
name: myapp
volumeMounts:
- mountPath: /mnt/myvolume
name: myvolume
readOnly: true
---
apiVersion: v1
data:
mountPath: /mnt/myvolume
readOnly: "true"
kind: ConfigMap
metadata:
name: volume-config-kbkmhkchcd
`)
}

func TestReplacementsCreateBoolFieldFromLocalConfigSurvivesUnrelatedStrategicMergePatch(t *testing.T) {
th := kusttest_test.MakeEnhancedHarness(t)
defer th.Reset()

th.WriteK(".", `
resources:
- deployment.yaml
components:
- components/add-volume
patches:
- path: patch.yaml
`)
th.WriteF("deployment.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: myapp
image: myimage
`)
th.WriteF("patch.yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
metadata:
annotations:
foo: bar
`)
th.WriteC("components/add-volume", `
resources:
- local-config.yaml
replacements:
- source:
kind: VolumeConfig
name: volume-config
fieldPath: spec.readOnly
targets:
- select:
kind: Deployment
fieldPaths:
- spec.template.spec.containers.0.volumeMounts.[name=myvolume].readOnly
options:
create: true
- source:
kind: VolumeConfig
name: volume-config
fieldPath: spec.mountPath
targets:
- select:
kind: Deployment
fieldPaths:
- spec.template.spec.containers.0.volumeMounts.[name=myvolume].mountPath
options:
create: true
`)
th.WriteF("components/add-volume/local-config.yaml", `
apiVersion: example.com/v1
kind: VolumeConfig
metadata:
name: volume-config
annotations:
config.kubernetes.io/local-config: "true"
spec:
readOnly: true
mountPath: /mnt/myvolume
`)
m := th.Run(".", th.MakeDefaultOptions())
th.AssertActualEqualsExpected(m, `
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
metadata:
annotations:
foo: bar
spec:
containers:
- image: myimage
name: myapp
volumeMounts:
- mountPath: /mnt/myvolume
name: myvolume
readOnly: true
`)
}