use super::*;
use color_eyre::eyre::{self, OptionExt as _};
use helm_schema_core::ConditionalOverlayFlavor;
use test_util::prelude::sim_assert_eq;
#[test]
fn contract_builder_emits_typed_kind_branch_evidence() -> eyre::Result<()> {
let source = indoc! {r#"
apiVersion: apps/v1
kind: {{ .Values.workload.kind }}
metadata:
name: test
spec:
{{- if eq .Values.workload.kind "Deployment" }}
strategy: {{- toYaml .Values.workload.strategy | nindent 4 }}
{{- else if eq .Values.workload.kind "StatefulSet" }}
updateStrategy: {{- toYaml .Values.workload.strategy | nindent 4 }}
{{- end }}
"#};
let signals = schema_signals_for(parse_ir(source));
let evidence = signals
.evidence_for("workload.strategy")
.ok_or_eyre("workload strategy evidence missing")?;
eyre::ensure!(!evidence.conditional_overlays.is_empty());
for overlay in &evidence.conditional_overlays {
sim_assert_eq!(have: overlay.flavor, want: ConditionalOverlayFlavor::KindBranch);
eyre::ensure!(!overlay.evidence.provider_schema_uses.is_empty());
for use_ in &overlay.evidence.provider_schema_uses {
eyre::ensure!(use_.resource.kind_branches.is_empty());
eyre::ensure!(use_.resource.kind_candidates.is_empty());
}
}
Ok(())
}
#[test]
fn contract_builder_retains_literal_control_kind_branches() -> eyre::Result<()> {
let source = indoc! {r#"
{{- if eq .Values.local.kind "ConfigMap" }}
apiVersion: v1
kind: ConfigMap
metadata:
name: test
immutable: {{ .Values.local.setting }}
{{- else if eq .Values.local.kind "Service" }}
apiVersion: v1
kind: Service
metadata:
name: test
spec:
type: {{ .Values.local.setting }}
{{- end }}
"#};
let signals = schema_signals_for(parse_ir(source));
let evidence = signals
.evidence_for("local.setting")
.ok_or_eyre("local setting evidence missing")?;
eyre::ensure!(
evidence
.conditional_overlays
.iter()
.all(|overlay| overlay.flavor == ConditionalOverlayFlavor::KindBranch),
"literal kind branches were not retained: {evidence:#?}"
);
Ok(())
}
fn strict_provider() -> Chain {
Chain::new(vec![Box::new(
KubernetesJsonSchemaProvider::new("v1.29.0-standalone-strict")
.with_cache_dir(super::bundle_cache_dir())
.with_allow_download(false),
)])
}
#[test]
fn values_selected_kind_partitions_strategy_provider_projection() {
let helpers = indoc! {r#"
{{- define "common.capabilities.statefulset.apiVersion" -}}
{{- print "apps/v1" -}}
{{- end -}}
"#};
let src = indoc! {r#"
apiVersion: {{ include "common.capabilities.statefulset.apiVersion" . }}
kind: {{ .Values.master.kind }}
metadata:
name: test
spec:
{{- if not (eq .Values.master.kind "DaemonSet") }}
replicas: {{ .Values.master.count }}
{{- end }}
{{- if (eq .Values.master.kind "StatefulSet") }}
serviceName: test-headless
{{- end }}
{{- if .Values.master.updateStrategy }}
{{- if (eq .Values.master.kind "Deployment") }}
strategy: {{- toYaml .Values.master.updateStrategy | nindent 4 }}
{{- else }}
updateStrategy: {{- toYaml .Values.master.updateStrategy | nindent 4 }}
{{- end }}
{{- end }}
"#};
let values_yaml = indoc! {"
master:
kind: StatefulSet
count: 1
updateStrategy:
type: RollingUpdate
"};
let signals = schema_signals_for(parse_ir_with_helpers(src, helpers));
let schema = generate_values_schema(
ValuesSchemaInput::new(&signals, &strict_provider())
.with_values_documents(&prepared_values_documents(Some(values_yaml))),
);
for instance in [
serde_json::json!({ "master": { "kind": "Deployment", "updateStrategy": { "rollingUpdate": { "maxSurge": "25%" } } } }),
serde_json::json!({ "master": { "kind": "StatefulSet", "updateStrategy": { "rollingUpdate": { "partition": 1 } } } }),
serde_json::json!({ "master": { "kind": "StatefulSet", "updateStrategy": { "type": "RollingUpdate" } } }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"the strategy field set matching the selected kind renders and validates: \
instance={instance}; schema={schema}"
);
}
for instance in [
serde_json::json!({ "master": { "kind": "Deployment", "updateStrategy": { "rollingUpdate": { "partition": 1 } } } }),
serde_json::json!({ "master": { "kind": "StatefulSet", "updateStrategy": { "rollingUpdate": { "maxSurge": "25%" } } } }),
] {
assert!(
!schema_accepts_instance(&schema, &instance),
"a strategy field from the OTHER kind's schema is rejected on this partition: \
instance={instance}; schema={schema}"
);
}
}
#[test]
fn inline_local_kind_partition_projects_per_arm_provider_schemas() {
let src = indoc! {r#"
{{- $stateful := and (contains "Local" .Values.executor) .Values.persistence.enabled }}
apiVersion: apps/v1
kind: {{ if $stateful }}StatefulSet{{ else }}Deployment{{ end }}
metadata:
name: test
spec:
replicas: {{ .Values.replicas }}
{{- if and $stateful .Values.updateStrategy }}
updateStrategy: {{- toYaml .Values.updateStrategy | nindent 4 }}
{{- end }}
{{- if and (not $stateful) .Values.strategy }}
strategy: {{- toYaml .Values.strategy | nindent 4 }}
{{- end }}
"#};
let values_yaml = indoc! {"
executor: CeleryExecutor
persistence:
enabled: false
replicas: 1
updateStrategy: ~
strategy: ~
"};
let signals = schema_signals_for(parse_ir(src));
let schema = generate_values_schema(
ValuesSchemaInput::new(&signals, &strict_provider())
.with_values_documents(&prepared_values_documents(Some(values_yaml))),
);
for overrides in [
serde_json::json!({ "strategy": { "rollingUpdate": { "maxSurge": "25%" } } }),
serde_json::json!({ "strategy": { "type": "RollingUpdate" } }),
serde_json::json!({
"executor": "LocalExecutor",
"persistence": { "enabled": true },
"updateStrategy": { "rollingUpdate": { "partition": 1 } },
}),
serde_json::json!({
"executor": "LocalExecutor",
"persistence": { "enabled": true },
"strategy": 7,
}),
] {
let instance = composed_instance(values_yaml, overrides);
assert!(
schema_accepts_instance(&schema, &instance),
"the arm-matching strategy shape renders and validates: \
instance={instance}; schema={schema}"
);
}
for instance in [
serde_json::json!({ "strategy": 7 }),
serde_json::json!({ "strategy": { "rollingUpdate": { "partition": 1 } } }),
serde_json::json!({
"executor": "LocalExecutor",
"persistence": { "enabled": true },
"updateStrategy": { "rollingUpdate": { "maxSurge": "25%" } },
}),
] {
assert!(
!schema_accepts_instance(&schema, &instance),
"a strategy shape outside the LIVE arm's kind is rejected: \
instance={instance}; schema={schema}"
);
}
}
#[test]
fn shared_slot_kind_arms_resolve_through_selecting_predicates() {
let src = indoc! {r#"
{{- $stateful := and (contains "Local" .Values.executor) .Values.persistence.enabled }}
apiVersion: apps/v1
kind: {{ if $stateful }}StatefulSet{{ else }}DaemonSet{{ end }}
metadata:
name: test
spec:
{{- if and $stateful .Values.updateStrategy }}
updateStrategy: {{- toYaml .Values.updateStrategy | nindent 4 }}
{{- end }}
{{- if and (not $stateful) .Values.daemonUpdateStrategy }}
updateStrategy: {{- toYaml .Values.daemonUpdateStrategy | nindent 4 }}
{{- end }}
"#};
let values_yaml = indoc! {"
executor: CeleryExecutor
persistence:
enabled: false
updateStrategy: ~
daemonUpdateStrategy: ~
"};
let signals = schema_signals_for(parse_ir(src));
let schema = generate_values_schema(
ValuesSchemaInput::new(&signals, &strict_provider())
.with_values_documents(&prepared_values_documents(Some(values_yaml))),
);
for overrides in [
serde_json::json!({ "daemonUpdateStrategy": { "rollingUpdate": { "maxSurge": 1 } } }),
serde_json::json!({
"executor": "LocalExecutor",
"persistence": { "enabled": true },
"updateStrategy": { "rollingUpdate": { "partition": 1 } },
}),
] {
let instance = composed_instance(values_yaml, overrides);
assert!(
schema_accepts_instance(&schema, &instance),
"each arm's row resolves to its OWN kind's slot schema: \
instance={instance}; schema={schema}"
);
}
for instance in [
serde_json::json!({ "daemonUpdateStrategy": { "rollingUpdate": { "partition": 1 } } }),
serde_json::json!({
"executor": "LocalExecutor",
"persistence": { "enabled": true },
"updateStrategy": { "rollingUpdate": { "maxSurge": 1 } },
}),
] {
assert!(
!schema_accepts_instance(&schema, &instance),
"a member from the OTHER kind's slot schema is rejected: \
instance={instance}; schema={schema}"
);
}
}
#[test]
fn pdb_int_or_string_survives_declared_integer_default() {
let helpers = indoc! {r#"
{{- define "pdb.apiVersion" -}}
{{- if semverCompare ">=1.21-0" .Capabilities.KubeVersion.GitVersion -}}
{{- print "policy/v1" -}}
{{- else -}}
{{- print "policy/v1beta1" -}}
{{- end -}}
{{- end -}}
"#};
let src = indoc! {r#"
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: {{ template "pdb.apiVersion" . }}
kind: PodDisruptionBudget
metadata:
name: test
spec:
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable | default 1 }}
{{- end }}
"#};
let values_yaml = indoc! {"
podDisruptionBudget:
enabled: false
maxUnavailable: 1
"};
let signals = schema_signals_for(parse_ir_with_helpers(src, helpers));
let schema = generate_values_schema(
ValuesSchemaInput::new(&signals, &strict_provider())
.with_values_documents(&prepared_values_documents(Some(values_yaml))),
);
for instance in [
serde_json::json!({ "podDisruptionBudget": { "enabled": true, "maxUnavailable": "50%" } }),
serde_json::json!({ "podDisruptionBudget": { "enabled": true, "maxUnavailable": 1 } }),
serde_json::json!({ "podDisruptionBudget": { "enabled": true } }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"the provider slot accepts int-or-string and the default covers absence: \
instance={instance}; schema={schema}"
);
}
assert!(
schema_accepts_instance(
&schema,
&serde_json::json!({
"podDisruptionBudget": { "enabled": true, "maxUnavailable": { "a": 1 } }
})
),
"Go's plain formatting turns a safe mapping into a string accepted by IntOrString: {schema}"
);
}
#[test]
fn self_kind_dispatch_keeps_complement_kinds_open() {
let helpers = indoc! {r#"
{{- define "test.kubeVersion" -}}
{{- .Capabilities.KubeVersion.Version -}}
{{- end -}}
"#};
let src = indoc! {r#"
apiVersion: v1
kind: Pod
metadata:
name: test
spec:
{{- if and (semverCompare ">=1.33-0" (include "test.kubeVersion" .)) (kindIs "bool" .Values.hostUsers) }}
hostUsers: {{ .Values.hostUsers }}
{{- end }}
containers:
- name: main
"#};
let signals = schema_signals_for(parse_ir_with_helpers(src, helpers));
let schema = generate_values_schema(
ValuesSchemaInput::new(&signals, &strict_provider())
.with_values_documents(&prepared_values_documents(Some("hostUsers: nil\n"))),
);
assert!(
schema
.get("properties")
.and_then(|properties| properties.get("hostUsers"))
.is_some(),
"the dispatched path stays a referenced property: {schema}"
);
for instance in [
serde_json::json!({ "hostUsers": { "a": 1 } }),
serde_json::json!({ "hostUsers": true }),
serde_json::json!({ "hostUsers": "nil" }),
serde_json::json!({ "hostUsers": 7 }),
] {
assert!(
schema_accepts_instance(&schema, &instance),
"a kind outside the dispatch is omitted, not rejected: \
instance={instance}; schema={schema}"
);
}
}
#[test]
fn type_of_dispatch_keeps_serialized_arm_structured() {
let helpers = indoc! {r#"
{{- define "test.affinity" -}}
{{- if .Values.affinity }}
affinity:
{{ $tp := typeOf .Values.affinity }}
{{- if eq $tp "string" }}
{{- tpl .Values.affinity . | nindent 8 | trim }}
{{- else }}
{{- toYaml .Values.affinity | nindent 8 }}
{{- end }}
{{- end }}
{{- end -}}
"#};
let src = indoc! {r#"
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
template:
spec:
{{ template "test.affinity" . }}
containers:
- name: main
"#};
let signals = schema_signals_for(parse_ir_with_helpers(src, helpers));
assert!(
signals
.evidence_for("affinity")
.is_some_and(
|evidence| evidence.conditional_overlays.iter().any(|overlay| {
overlay.guards.iter().any(|guard| {
matches!(
guard,
helm_schema_core::ConditionalGuard::Not(inner)
if matches!(
inner.as_ref(),
helm_schema_core::ConditionalGuard::TypeIs {
path,
schema_type,
} if path == "affinity" && schema_type == "string"
)
)
}) && !overlay.evidence.facts.used_as_serialized
})
),
"the structure-preserving complement must keep provider evidence ahead of the scalar declared default"
);
let schema = generate_values_schema(
ValuesSchemaInput::new(&signals, &strict_provider()).with_values_documents(
&prepared_values_documents(Some(indoc! {"
affinity: |
nodeAffinity: {}
"})),
),
);
for (instance, want) in [
(
serde_json::json!({ "affinity": { "nodeAffinity": {
"requiredDuringSchedulingIgnoredDuringExecution": { "nodeSelectorTerms": [] }
} } }),
true,
),
(serde_json::json!({ "affinity": "nodeAffinity: {}" }), true),
(
serde_json::json!({ "affinity": { "nodeAffinity": 7 } }),
false,
),
] {
assert!(
schema_accepts_instance(&schema, &instance) == want,
"the toYaml arm keeps provider typing for structured values: \
instance={instance}; schema={schema}"
);
}
}