use test_util::prelude::sim_assert_eq;
use super::*;
#[test]
fn guard_only_path_does_not_treat_the_declared_default_as_input_typing() {
let src = indoc! {r"
apiVersion: v1
kind: Secret
metadata:
name: example
{{- if .Values.existingSecret }}
stringData:
password: ignored
{{- end }}
"};
let values_yaml = indoc! {"
existingSecret: \"\"
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
sim_assert_eq!(
have: schema,
want: serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"existingSecret": {},
},
"type": "object",
})
);
}
#[test]
fn block_scalar_parts_do_not_project_input_shapes() {
let src = indoc! {r"
apiVersion: v1
kind: ConfigMap
metadata:
name: example
data:
config.yaml: |
enabled: {{ .Values.enabled }}
items:
{{ .Values.items | toYaml | indent 4 }}
"};
let values_yaml = indoc! {"
enabled: true
items:
- name: default
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
sim_assert_eq!(
have: schema,
want: serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"enabled": {},
"items": {},
},
"type": "object",
})
);
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "the complete fixture scenario is clearest as one contiguous test"
)]
fn helper_yaml_rendered_inside_block_scalar_does_not_project_payload_shape() {
let helpers = indoc! {r#"
{{- define "collector.config" -}}
receivers:
k8s_cluster:
collection_interval: {{ .Values.presets.clusterMetrics.collectionInterval }}
allocatable_types_to_report:
{{- toYaml .Values.presets.clusterMetrics.allocatableTypesToReport | nindent 10 }}
{{- end -}}
"#};
let src = indoc! {r#"
apiVersion: v1
kind: ConfigMap
metadata:
name: collector
data:
collector.yaml: |-
{{- include "collector.config" . | nindent 4 }}
"#};
let values_yaml = indoc! {"
presets:
clusterMetrics:
collectionInterval: 30s
allocatableTypesToReport:
- cpu
- memory
"};
let schema = schema_for_values_yaml(parse_ir_with_helpers(src, helpers), Some(values_yaml));
for (instance, want, label) in [
(
serde_json::json!({}),
false,
"the outer navigated host is missing",
),
(
serde_json::json!({ "presets": {} }),
false,
"the inner navigated host is missing",
),
(
serde_json::json!({ "presets": { "clusterMetrics": null } }),
false,
"the inner navigated host is null",
),
(
serde_json::json!({
"presets": {
"clusterMetrics": {
"collectionInterval": "30s",
"allocatableTypesToReport": ["cpu"]
}
}
}),
true,
"both navigated hosts are present",
),
(
serde_json::json!({
"presets": {
"clusterMetrics": {
"collectionInterval": { "custom": 7 },
"allocatableTypesToReport": 7
}
}
}),
true,
"the helper serializes both leaves into block text",
),
] {
assert!(
schema_accepts_instance(&schema, &instance) == want,
"{label}: instance={instance}; schema={schema}"
);
}
let expected = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"allOf": [
navigated_host_clause(&["presets"]),
navigated_host_missing_ancestor_clause(&["presets", "clusterMetrics"]),
],
"properties": {
"presets": {
"type": "object",
"additionalProperties": {},
"allOf": [
navigated_host_clause(&["clusterMetrics"]),
],
"properties": {
"clusterMetrics": {
"type": "object",
"additionalProperties": {},
"properties": {
"allocatableTypesToReport": {},
"collectionInterval": {}
}
}
}
}
}
});
sim_assert_eq!(have: schema, want: expected);
}
#[test]
fn shallow_helper_output_inside_block_scalar_stays_pathless() {
let helpers = indoc! {r#"
{{- define "collector.config" -}}
collector.yaml: |-
{{- if .Values.payload }}
payload:
{{ .Values.payload | toYaml | indent 4 }}
{{- end }}
{{- end -}}
"#};
let src = indoc! {r#"
{{- if .Values.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: collector
annotations:
checksum/config: {{ include "collector.config" . | sha256sum }}
data:
{{ include "collector.config" . | nindent 2 }}
{{- end }}
"#};
let values_yaml = indoc! {"
enabled: true
payload:
- default
"};
let schema = schema_for_values_yaml(parse_ir_with_helpers(src, helpers), Some(values_yaml));
sim_assert_eq!(
have: schema,
want: serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"enabled": {},
"payload": {},
},
"type": "object",
})
);
}
#[test]
fn shallow_control_after_block_scalar_remains_a_mapping_sibling() {
let src = indoc! {r"
apiVersion: v1
kind: ConfigMap
metadata:
name: notifications
data:
context: |
enabled: true
{{- with .Values.notifiers }}
{{- toYaml . | nindent 2 }}
{{- end }}
"};
let values_yaml = indoc! {"
notifiers:
service.test: enabled
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
for (notifiers, want) in [
(serde_json::json!({ "service.test": "enabled" }), true),
(serde_json::json!({ "service.test": 7 }), false),
(serde_json::json!({ "other": 7 }), false),
(serde_json::json!({ "other": null }), true),
(serde_json::json!(7), false),
] {
let instance = serde_json::json!({ "notifiers": notifiers });
assert!(
schema_accepts_instance(&schema, &instance) == want,
"the control body renders beside the block scalar: instance={instance}; schema={schema}"
);
}
sim_assert_eq!(
have: schema,
want: serde_json::json!({
"$defs": {
"t": {
"anyOf": [
{ "const": true },
{ "not": { "const": 0 }, "type": "number" },
{ "minLength": 1, "type": "string" },
{ "minItems": 1, "type": "array" },
{ "minProperties": 1, "type": "object" },
],
},
},
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"notifiers": {
"anyOf": [
{
"additionalProperties": {
"type": ["string", "null"],
},
"description": "Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.",
"properties": {
"service.test": {
"type": "string",
},
},
"type": "object",
},
{
"not": {
"$ref": "#/$defs/t",
},
},
{
"type": "null",
},
],
},
},
"type": "object",
})
);
}
#[test]
fn helper_local_yaml_merge_inside_block_scalar_does_not_project_payload_shape() {
let helpers = indoc! {r#"
{{- define "collector.config" -}}
{{- $config := include "collector.baseConfig" . | fromYaml }}
{{- if .Values.presets.clusterMetrics.enabled }}
{{- $config = (include "collector.applyClusterMetricsConfig" (dict "Values" . "config" $config) | fromYaml) }}
{{- end }}
{{- tpl (toYaml $config) . }}
{{- end -}}
{{- define "collector.baseConfig" -}}
service:
pipelines:
metrics:
receivers: []
exporters: []
{{- end -}}
{{- define "collector.applyClusterMetricsConfig" -}}
{{- $config := mustMergeOverwrite (include "collector.clusterMetricsConfig" .Values | fromYaml) .config }}
{{- $config | toYaml }}
{{- end -}}
{{- define "collector.clusterMetricsConfig" -}}
receivers:
k8s_cluster:
collection_interval: {{ .Values.presets.clusterMetrics.collectionInterval }}
allocatable_types_to_report:
{{- toYaml .Values.presets.clusterMetrics.allocatableTypesToReport | nindent 10 }}
service:
pipelines:
metrics:
receivers:
- k8s_cluster
{{- end -}}
"#};
let src = indoc! {r#"
apiVersion: v1
kind: ConfigMap
metadata:
name: collector
data:
collector.yaml: |-
{{- include "collector.config" . | nindent 4 }}
"#};
let values_yaml = indoc! {"
presets:
clusterMetrics:
enabled: true
collectionInterval: 30s
allocatableTypesToReport:
- cpu
- memory
"};
let schema = schema_for_values_yaml(parse_ir_with_helpers(src, helpers), Some(values_yaml));
let expected = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"allOf": [
navigated_host_clause(&["presets"]),
navigated_host_missing_ancestor_clause(&["presets", "clusterMetrics"]),
],
"properties": {
"presets": {
"type": "object",
"additionalProperties": {},
"allOf": [
navigated_host_clause(&["clusterMetrics"]),
],
"properties": {
"clusterMetrics": {
"type": "object",
"additionalProperties": {},
"properties": {
"allocatableTypesToReport": {},
"collectionInterval": {},
"enabled": {}
}
}
}
}
}
});
sim_assert_eq!(have: schema, want: expected);
}
#[test]
fn local_default_alias_render_applies_provider_schema_to_fallback_path() {
let src = indoc! {r#"
apiVersion: example.com/v1
kind: Widget
spec:
{{- $storageClass := default .Values.persistence.storageClass .Values.global.storageClass -}}
{{- if $storageClass }}
{{- if (eq "-" $storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ $storageClass }}
{{- end }}
{{- end }}
"#};
let values_yaml = indoc! {"
global:
storageClass:
persistence:
storageClass:
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
for (instance, want, label) in [
(
serde_json::json!({
"global": { "storageClass": null },
"persistence": { "storageClass": "fast" }
}),
true,
"string fallback",
),
(
serde_json::json!({
"global": { "storageClass": null },
"persistence": { "storageClass": 7 }
}),
false,
"invalid selected fallback",
),
(
serde_json::json!({
"global": { "storageClass": "fast" },
"persistence": { "storageClass": 7 }
}),
true,
"shadowed invalid fallback",
),
(
serde_json::json!({
"global": { "storageClass": 7 },
"persistence": { "storageClass": "fast" }
}),
false,
"invalid selected primary",
),
(
serde_json::json!({
"global": { "storageClass": "-" },
"persistence": { "storageClass": null }
}),
true,
"special unset marker",
),
(
serde_json::json!({
"global": { "storageClass": null },
"persistence": { "storageClass": null }
}),
true,
"empty effective value",
),
] {
assert!(
schema_accepts_instance(&schema, &instance) == want,
"{label}: instance={instance}; schema={schema}"
);
}
}
#[test]
fn unconstrained_object_fragment_keeps_nested_maps_open() {
let src = indoc! {r"
apiVersion: example.com/v1
kind: Widget
spec:
resources: {{ toYaml .Values.resources | nindent 4 }}
"};
let values_yaml = indoc! {"
resources:
requests:
cpu: 100m
memory: 200Mi
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
let expected = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"resources": {
"type": "object",
"additionalProperties": {},
"properties": {
"requests": {
"type": "object",
"additionalProperties": {},
"properties": {
"cpu": {
"type": "string"
},
"memory": {
"type": "string"
}
}
}
}
}
}
});
sim_assert_eq!(have: schema, want: expected);
}
#[test]
fn with_bound_range_dot_annotations_stay_string_map() {
let src = indoc! {r"
apiVersion: v1
kind: Pod
metadata:
name: test
{{- with .Values.annotations }}
annotations:
{{- range $key, $value := . }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
"};
let values_yaml = indoc! {"
annotations:
foo: bar
"};
let ir = parse_ir(src);
let schema = schema_for_values_yaml(&ir, Some(values_yaml));
let annotations = schema
.pointer("/properties/annotations")
.expect("annotations present");
let map_arm = ranged_arm_of_type(annotations, "object")
.unwrap_or_else(|| panic!("annotations object arm missing, got {annotations}"));
sim_assert_eq!(
have: map_arm
.pointer("/additionalProperties/type")
.and_then(Value::as_str),
want: Some("string"),
"annotations should stay an open string map, got {annotations}"
);
}
#[test]
fn with_defaulted_object_body_rebinds_dot_to_fallback_path() {
let src = indoc! {r#"
{{- range $db, $cfg := .Values.jobs }}
apiVersion: v1
kind: Pod
spec:
containers:
- name: runner
{{- with (.image | default $.Values.globalImage) }}
image: "{{ .repository }}:{{ .tag }}"
{{- end }}
{{- end }}
"#};
let values_yaml = indoc! {"
globalImage:
repository: repo/app
jobs:
first: {}
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
assert!(
schema_accepts_instance(
&schema,
&serde_json::json!({
"globalImage": {
"repository": "repo/app",
"tag": 1.25
},
"jobs": { "first": {} }
})
),
"the fallback image accepts scalar tag interpolation: {schema}"
);
assert!(
schema_accepts_instance(
&schema,
&serde_json::json!({
"globalImage": {
"repository": "repo/app",
"tag": { "bad": true }
},
"jobs": { "first": {} }
})
),
"the range-member-dependent fallback cannot be lowered safely, so its tag contract must abstain: {schema}"
);
}
#[test]
fn ranged_with_defaulted_object_body_abstains_on_cross_range_fallback() {
let src = indoc! {r#"
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- range $db, $cfg := .Values.migrations.databases }}
apiVersion: batch/v1
kind: Job
spec:
template:
spec:
containers:
- name: runner
{{- with (.image | default $.Values.migrations.image) }}
image: "{{ .repository }}:{{ .tag | default $tag }}"
imagePullPolicy: {{ .pullPolicy | default "Always" }}
{{- end }}
{{- end }}
"#};
let values_yaml = indoc! {"
image:
tag: app-version
migrations:
image:
repository: repo/app
pullPolicy: Always
databases:
first: {}
"};
let ir = parse_ir(src);
let schema = schema_for_values_yaml(&ir, Some(values_yaml));
for instance in [
serde_json::json!({
"image": { "tag": "app-version" },
"migrations": {
"databases": { "first": {} },
"image": {
"repository": "repo/app",
"pullPolicy": "Always",
"tag": 7
}
}
}),
serde_json::json!({
"image": { "tag": "app-version" },
"migrations": {
"databases": { "first": { "image": {
"repository": "member/app",
"tag": "member",
"pullPolicy": "Always"
} } },
"image": { "tag": 7 }
}
}),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"an unlowerable member/fallback choice must abstain: instance={instance}; schema={schema}; ir={ir:?}"
);
}
}