use test_util::prelude::sim_assert_eq;
use super::*;
#[test]
fn self_guarded_fragment_object_keeps_exact_empty_object_placeholder() {
let src = indoc! {r"
apiVersion: v1
kind: PersistentVolumeClaim
spec:
{{- with .Values.dataSource }}
dataSource: {{- toYaml . | nindent 4 }}
{{- end }}
"};
let values_yaml = indoc! {"
dataSource: {}
"};
let ir = parse_ir(src);
let schema = schema_for_values_yaml(&ir, Some(values_yaml));
for instance in [
serde_json::json!({ "dataSource": {} }),
serde_json::json!({ "dataSource": { "name": "snap", "kind": "VolumeSnapshot" } }),
serde_json::json!({}),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"the off-state and arbitrary fragment shapes render: instance={instance}; schema={schema}"
);
}
}
#[test]
fn self_guarded_tplvalues_render_object_union_keeps_exact_empty_object_placeholder() {
let src = indoc! {r#"
apiVersion: v1
kind: PersistentVolumeClaim
spec:
{{- if .Values.persistence.dataSource }}
dataSource: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.dataSource "context" .) | nindent 4 }}
{{- end }}
"#};
let values_yaml = indoc! {"
persistence:
dataSource: {}
"};
let helpers = bitnami_tplvalues_helpers();
let mut define_index = DefineIndex::new();
define_index.add_file_source("helpers.tpl", helpers);
let schema_signals = SymbolicIrContext::new(&define_index)
.generate_contract_ir(src)
.finalize()
.into_schema_signals();
let schema = generate_values_schema(
ValuesSchemaInput::new(&schema_signals, &provider())
.with_values_documents(&prepared_values_documents(Some(values_yaml))),
);
let data_source = schema
.pointer("/properties/persistence/properties/dataSource")
.expect("persistence.dataSource present");
schema_variant_matching(data_source, |variant| {
variant.get("type").and_then(Value::as_str) == Some("object")
&& variant.get("maxProperties").and_then(Value::as_u64) == Some(0)
})
.unwrap_or_else(|| {
panic!(
"exact empty object placeholder variant missing from helper-rendered object union: {data_source}",
)
});
}
#[test]
fn self_guarded_range_collection_keeps_exact_empty_object_placeholder() {
let src = indoc! {r"
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: busybox
env:
{{- range .Values.env }}
- name: {{ .name }}
{{- if .valueFrom }}
valueFrom: {{- toYaml .valueFrom | nindent 20 }}
{{- else }}
value: {{ .value | quote }}
{{- end }}
{{- end }}
"};
let values_yaml = indoc! {"
env: {}
"};
let ir = parse_ir(src);
let schema = schema_for_values_yaml(&ir, Some(values_yaml));
let env = schema.pointer("/properties/env").expect("env present");
schema_variant_matching(env, |variant| {
variant.get("type").and_then(Value::as_str) == Some("object")
&& variant.get("maxProperties").and_then(Value::as_u64) == Some(0)
})
.unwrap_or_else(|| panic!("exact empty object off-state missing: {env}; ir={ir:?}"));
schema_variant_matching(env, |variant| {
variant.get("type").and_then(Value::as_str) == Some("array")
})
.unwrap_or_else(|| panic!("non-empty array form missing: {env}"));
}
#[test]
fn guard_only_empty_map_default_does_not_constrain_the_input_kind() {
let src = indoc! {r#"
apiVersion: v1
kind: Pod
metadata:
name: test
{{- if .Values.config }}
annotations:
config-enabled: "true"
{{- end }}
spec:
containers:
- name: app
image: busybox
"#};
let values_yaml = indoc! {"
config: {}
"};
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": {
"config": {},
},
"type": "object",
})
);
}
#[test]
fn with_guarded_whole_splice_accepts_empty_map_default_and_list_form() {
let src = indoc! {r"
apiVersion: apps/v1
kind: Deployment
metadata:
name: repro
spec:
template:
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: app
image: busybox
"};
let values_yaml = indoc! {"
imagePullSecrets: {}
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
assert!(
schema_accepts_instance(&schema, &serde_json::json!({ "imagePullSecrets": {} })),
"the declared empty-map off-state must stay accepted: {schema}"
);
assert!(
schema_accepts_instance(
&schema,
&serde_json::json!({ "imagePullSecrets": [{ "name": "regcred" }] })
),
"the rendered list form must stay accepted: {schema}"
);
}
#[test]
fn undeclared_self_ranged_map_stays_open() {
let src = indoc! {r#"
apiVersion: v1
kind: ConfigMap
metadata:
name: repro
data:
{{- if not .Values.env.FORCE }}
forced: "no"
{{- end }}
{{- range $key, $val := .Values.env }}
{{ $key }}: {{ $val | quote }}
{{- end }}
"#};
let schema = schema_for_values_yaml(parse_ir(src), None);
assert!(
schema_accepts_instance(&schema, &serde_json::json!({ "env": { "ANY_KEY": "x" } })),
"user-populated entries of an undeclared ranged map must stay accepted: {schema}"
);
}
#[test]
fn serialized_empty_map_union_keeps_open_arm_for_members() {
let src = indoc! {r"
{{- if .Values.config.apiVersion }}
apiVersion: v1
kind: ConfigMap
metadata:
name: versioned
data:
v: {{ .Values.config.apiVersion | quote }}
{{- end }}
---
{{- with .Values.config }}
apiVersion: v1
kind: ConfigMap
metadata:
name: repro
data:
config.yaml: |
{{ toYaml .Values.config | indent 4 }}
{{- end }}
"};
let values_yaml = indoc! {"
config: {}
"};
let schema = schema_for_values_yaml(parse_ir(src), Some(values_yaml));
for instance in [
serde_json::json!({ "config": {} }),
serde_json::json!({ "config": { "userField": true } }),
serde_json::json!({ "config": { "apiVersion": "controller.config/v1alpha1" } }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"serialized user-populated map must accept {instance}: {schema}"
);
}
}
#[test]
fn member_probe_keeps_helper_ranged_empty_map_open() {
let helpers = indoc! {r#"
{{- define "repro.entries" -}}
{{- range $key, $value := . }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end -}}
"#};
let src = indoc! {r#"
{{- if not .Values.envDict.FORCE }}
apiVersion: v1
kind: ConfigMap
metadata:
name: guarded
data:
on: "true"
{{- end }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: repro
data:
entries: |
{{- include "repro.entries" .Values.envDict | indent 4 }}
"#};
let values_yaml = indoc! {"
envDict: {}
"};
let schema = schema_for_values_yaml(parse_ir_with_helpers(src, helpers), Some(values_yaml));
let env_dict = schema
.pointer("/properties/envDict")
.expect("envDict present");
assert!(
env_dict.get("additionalProperties") != Some(&Value::Bool(false)),
"member probe must not close the user-populated map, got {env_dict}"
);
assert!(
schema_accepts_instance(
&schema,
&serde_json::json!({ "envDict": { "USER_KEY": "value" } })
),
"user-populated entries must stay accepted, got {env_dict}"
);
}