use std::collections::{BTreeMap, BTreeSet};
use indoc::indoc;
use serde_json::Value;
use crate::{
PreparedValuesDocuments, ValuesSchemaInput, generate_values_schema,
resolve_policy::{
ResolvePolicy, ValuePathSchemaFacts, ValuePathSchemaInputs,
open_objects_rejecting_declared_members, preserve_declared_default_in_schema,
},
values_yaml::ValuesYamlPathFacts,
};
use helm_schema_ast::DefineIndex;
use helm_schema_core::{ProviderSchemaFragment, ResourceSchemaOracle};
use helm_schema_ir::{
ContractIr, ContractSchemaSignals, ContractUse, ContractValuePathFacts, Guard, GuardValue,
ProviderSchemaUse, ResourceRef, SymbolicIrContext, SymbolicPolicy, ValueKind, YamlPath,
};
use helm_schema_k8s::{Chain, CrdsCatalogSchemaProvider, KubernetesJsonSchemaProvider};
mod block_scalar_projection;
mod bound_helpers;
mod canonical_emission;
mod chart_local_crd_contracts;
mod completed_token_contracts;
mod default_hint_extraction;
mod emission_profiles;
mod empty_collections;
mod fail_validators;
mod fallback_selection;
mod file_template_contracts;
mod fragment_projection;
mod fragment_seeds;
mod guard_lowering;
mod helper_projection;
mod int_cast_preimages;
mod iterable_lanes;
mod kind_partition_matrix;
mod member_access_contracts;
mod member_carriers;
mod member_serialized_shapes;
mod merge_shadowing;
mod nullability_defaults;
mod operand_kind_contracts;
mod pattern_dialect;
mod program_wrappers;
mod provider_evidence;
mod provider_requirement_synthesis;
mod range_collections;
mod range_contracts;
mod range_key_contracts;
mod resolve_policy;
mod schema_node;
mod shape_alternatives;
mod string_transform_contracts;
mod validator_reachability;
pub(crate) fn bundle_cache_dir() -> std::path::PathBuf {
test_util::workspace_testdata().join("provider-bundle/kubernetes-json-schema-cache")
}
fn provider() -> Chain {
Chain::new(vec![Box::new(
KubernetesJsonSchemaProvider::new("v1.35.0")
.with_cache_dir(bundle_cache_dir())
.with_allow_download(false),
)])
}
fn production_chain_provider() -> Chain {
let k8s_provider = KubernetesJsonSchemaProvider::new("v1.35.0")
.with_cache_dir(bundle_cache_dir())
.with_allow_download(false)
.with_api_version_guess(true);
Chain::new(vec![Box::new(k8s_provider)]).with_inference_enabled(true)
}
fn parse_ir(src: &str) -> ContractIr {
let idx = DefineIndex::new();
SymbolicIrContext::new(&idx).generate_contract_ir(src)
}
fn parse_ir_with_helpers(src: &str, helpers: &str) -> ContractIr {
parse_ir_with_helpers_and_kubernetes_version(src, helpers, None)
}
fn parse_ir_with_files(src: &str, files: &[(&str, &str)]) -> ContractIr {
let mut index = DefineIndex::new();
for (path, source) in files {
index.add_file_source(path, source);
}
SymbolicIrContext::new(&index).generate_contract_ir(src)
}
fn parse_ir_with_helpers_and_kubernetes_version(
src: &str,
helpers: &str,
kubernetes_version: Option<&str>,
) -> ContractIr {
let mut idx = DefineIndex::new();
if !helpers.trim().is_empty() {
idx.add_file_source("helpers.tpl", helpers);
}
SymbolicIrContext::with_policy(
&idx,
SymbolicPolicy {
kubernetes_version: kubernetes_version.map(str::to_string),
..SymbolicPolicy::default()
},
)
.generate_contract_ir(src)
}
fn parse_ir_with_kubernetes_version(src: &str, kubernetes_version: &str) -> ContractIr {
parse_ir_with_helpers_and_kubernetes_version(src, "", Some(kubernetes_version))
}
fn with_type_hints(mut contract: ContractIr, hints: &[(&str, &str)]) -> ContractIr {
for (path, schema_type) in hints {
contract.add_type_hint(*path, *schema_type);
}
contract
}
trait SchemaSignalSource {
fn into_schema_signals(self) -> ContractSchemaSignals;
}
impl SchemaSignalSource for Vec<ContractUse> {
fn into_schema_signals(self) -> ContractSchemaSignals {
ContractIr::from_contract_uses(self).into_schema_signals()
}
}
impl SchemaSignalSource for &[ContractUse] {
fn into_schema_signals(self) -> ContractSchemaSignals {
ContractIr::from_contract_uses(self.to_vec()).into_schema_signals()
}
}
impl SchemaSignalSource for &Vec<ContractUse> {
fn into_schema_signals(self) -> ContractSchemaSignals {
self.as_slice().into_schema_signals()
}
}
impl SchemaSignalSource for &ContractIr {
fn into_schema_signals(self) -> ContractSchemaSignals {
self.clone().into_schema_signals()
}
}
impl SchemaSignalSource for ContractIr {
fn into_schema_signals(self) -> ContractSchemaSignals {
self.finalize().into_schema_signals()
}
}
impl SchemaSignalSource for ContractSchemaSignals {
fn into_schema_signals(self) -> ContractSchemaSignals {
self
}
}
fn schema_signals_for(source: impl SchemaSignalSource) -> ContractSchemaSignals {
source.into_schema_signals()
}
fn schema_for(source: impl SchemaSignalSource) -> Value {
let schema_signals = source.into_schema_signals();
generate_values_schema(ValuesSchemaInput::new(&schema_signals, &provider()))
}
pub(crate) fn prepared_values_documents(values_yaml: Option<&str>) -> PreparedValuesDocuments {
let composed = values_yaml
.and_then(|source| serde_yaml::from_str(source).ok())
.unwrap_or(serde_yaml::Value::Null);
PreparedValuesDocuments::new(composed, serde_yaml::Value::Null, serde_yaml::Value::Null)
}
fn schema_for_values_yaml(source: impl SchemaSignalSource, values_yaml: Option<&str>) -> Value {
let schema_signals = source.into_schema_signals();
generate_values_schema(
ValuesSchemaInput::new(&schema_signals, &provider())
.with_values_documents(&prepared_values_documents(values_yaml)),
)
}
fn schema_for_dependency_values_yaml(
source: impl SchemaSignalSource,
values_yaml: &str,
deeper_stage_yaml: &str,
refill_yaml: &str,
) -> Value {
let schema_signals = source.into_schema_signals();
let documents = PreparedValuesDocuments::new(
serde_yaml::from_str(values_yaml).unwrap_or(serde_yaml::Value::Null),
serde_yaml::from_str(deeper_stage_yaml).unwrap_or(serde_yaml::Value::Null),
serde_yaml::from_str(refill_yaml).unwrap_or(serde_yaml::Value::Null),
);
generate_values_schema(
ValuesSchemaInput::new(&schema_signals, &provider()).with_values_documents(&documents),
)
}
fn plain_token_exclusions(token_initial: bool) -> Vec<Value> {
crate::resolve_policy::plain_scalar_structural_exclusions(token_initial)
}
fn expected_values_schema(
properties: serde_json::Map<String, Value>,
all_of: Vec<Value>,
uses_helm_truthy: bool,
) -> Value {
let mut schema = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"type": "object",
});
schema["properties"] = Value::Object(properties);
if !all_of.is_empty() {
schema["allOf"] = Value::Array(all_of);
}
if uses_helm_truthy {
schema["$defs"] = serde_json::json!({
"t": {
"anyOf": [
{ "const": true },
{ "not": { "const": 0 }, "type": "number" },
{ "minLength": 1, "type": "string" },
{ "minItems": 1, "type": "array" },
{ "minProperties": 1, "type": "object" },
]
}
});
}
schema
}
fn root_property_schema(path: &str, schema: Value) -> Value {
let mut properties = serde_json::Map::new();
properties.insert(path.to_string(), schema);
serde_json::json!({ "additionalProperties": {}, "properties": properties })
}
fn helm_truthy_guard(path: &str) -> Value {
let mut properties = serde_json::Map::new();
properties.insert(path.to_string(), serde_json::json!({ "$ref": "#/$defs/t" }));
serde_json::json!({
"properties": properties,
"required": [path],
"type": "object",
})
}
fn expected_range_key_string_schema(path: &str, includes_declared_map: bool) -> Value {
let mut variants = vec![serde_json::json!({ "type": "array" })];
if includes_declared_map {
variants.insert(
0,
serde_json::json!({ "additionalProperties": {}, "type": "object" }),
);
variants.extend([
serde_json::json!({ "type": "null" }),
serde_json::json!({ "type": "object" }),
]);
} else {
variants.extend([
serde_json::json!({ "type": "object" }),
serde_json::json!({ "type": "null" }),
]);
}
let iterable = serde_json::json!({ "type": ["array", "null", "object"] });
let collection = if includes_declared_map {
serde_json::json!({ "anyOf": variants })
} else {
iterable.clone()
};
let string_key_domain = serde_json::json!({
"anyOf": [
{ "type": "object" },
{ "maxItems": 0, "type": "array" },
{ "type": "null" },
]
});
let mut properties = serde_json::Map::new();
properties.insert(path.to_string(), collection);
expected_values_schema(
properties,
vec![
root_property_schema(path, string_key_domain),
root_property_schema(path, iterable),
],
false,
)
}
fn navigated_host_clause(segments: &[&str]) -> Value {
serde_json::json!({
"if": navigated_host_condition(segments),
"then": false,
})
}
fn navigated_host_condition(segments: &[&str]) -> Value {
let chain = |leaf: Value| {
let mut node = leaf;
for segment in segments.iter().rev() {
node = serde_json::json!({
"type": "object",
"required": [*segment],
"properties": { *segment: node },
});
}
node
};
serde_json::json!({ "anyOf": [
{ "not": chain(serde_json::json!({})) },
chain(serde_json::json!({ "enum": [null] })),
] })
}
fn navigated_host_missing_ancestor_clause(segments: &[&str]) -> Value {
let ancestor = segments
.get(..segments.len().saturating_sub(1))
.unwrap_or_default();
serde_json::json!({
"if": { "allOf": [
navigated_host_condition(segments),
navigated_host_condition(ancestor),
] },
"then": false,
})
}
fn composed_instance(values_yaml: &str, overrides: Value) -> Value {
let defaults = serde_yaml::from_str::<serde_yaml::Value>(values_yaml)
.ok()
.and_then(|doc| serde_json::to_value(doc).ok())
.unwrap_or_else(|| serde_json::json!({}));
let mut instance = defaults;
merge_composed_override(&mut instance, overrides);
instance
}
fn merge_composed_override(base: &mut Value, overrides: Value) {
let (Some(base), Value::Object(overrides)) = (base.as_object_mut(), overrides) else {
return;
};
for (key, value) in overrides {
if value.is_null() {
base.remove(&key);
} else if base.get(&key).is_some_and(Value::is_object) && value.is_object() {
if let Some(existing) = base.get_mut(&key) {
merge_composed_override(existing, value);
}
} else {
base.insert(key, value);
}
}
}
fn schema_accepts_instance(schema: &Value, instance: &Value) -> bool {
let needs_truthy = crate::condition_encoding::value_references_helm_truthy(schema)
&& schema
.get("$defs")
.and_then(|defs| defs.get(crate::condition_encoding::HELM_TRUTHY_DEFINITION_NAME))
.is_none();
let document = needs_truthy.then(|| {
let mut defs = schema
.get("$defs")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
defs[crate::condition_encoding::HELM_TRUTHY_DEFINITION_NAME] =
crate::condition_encoding::helm_truthy_definition_schema();
serde_json::json!({ "$defs": defs, "allOf": [schema] })
});
jsonschema::validator_for(document.as_ref().unwrap_or(schema))
.expect("schema validator")
.is_valid(instance)
}
fn type_hints_for(source: impl SchemaSignalSource) -> BTreeMap<String, BTreeSet<String>> {
source
.into_schema_signals()
.schema_evidence_by_value_path()
.iter()
.map(|(path, evidence)| {
let mut hints = evidence.type_hints.clone();
hints.extend(evidence.fallback_type_hints.iter().cloned());
for overlay in &evidence.conditional_overlays {
hints.extend(overlay.evidence.type_hints.iter().cloned());
}
(path.clone(), hints)
})
.filter(|(_, hints)| !hints.is_empty())
.collect()
}
fn schema_contains_open_string_map(schema: &Value) -> bool {
if schema
.pointer("/additionalProperties/type")
.and_then(Value::as_str)
== Some("string")
{
return true;
}
["anyOf", "allOf", "oneOf"]
.into_iter()
.filter_map(|key| schema.get(key).and_then(Value::as_array))
.flatten()
.any(schema_contains_open_string_map)
}
fn schema_contains_type(schema: &Value, schema_type: &str) -> bool {
if schema.get("const").is_some_and(|value| match schema_type {
"null" => value.is_null(),
"boolean" => value.is_boolean(),
"number" => value.is_number(),
"string" => value.is_string(),
"array" => value.is_array(),
"object" => value.is_object(),
_ => false,
}) {
return true;
}
if schema.get("type").and_then(Value::as_str) == Some(schema_type) {
return true;
}
if schema
.get("type")
.and_then(Value::as_array)
.is_some_and(|types| {
types
.iter()
.any(|value| value.as_str() == Some(schema_type))
})
{
return true;
}
["anyOf", "allOf", "oneOf"]
.into_iter()
.filter_map(|key| schema.get(key).and_then(Value::as_array))
.flatten()
.any(|variant| schema_contains_type(variant, schema_type))
}
fn schema_property_contains_type(schema: &Value, property: &str, schema_type: &str) -> bool {
if let Some(property_schema) = schema
.get("properties")
.and_then(Value::as_object)
.and_then(|properties| properties.get(property))
&& schema_contains_type(property_schema, schema_type)
{
return true;
}
["anyOf", "oneOf"]
.into_iter()
.filter_map(|key| schema.get(key).and_then(Value::as_array))
.flatten()
.chain(
schema
.get("allOf")
.and_then(Value::as_array)
.into_iter()
.flatten(),
)
.any(|variant| schema_property_contains_type(variant, property, schema_type))
|| ["then", "else"]
.into_iter()
.filter_map(|key| schema.get(key))
.any(|child| schema_property_contains_type(child, property, schema_type))
}
fn schema_contains_property(schema: &Value, property: &str) -> bool {
if schema
.get("properties")
.and_then(Value::as_object)
.is_some_and(|properties| properties.contains_key(property))
{
return true;
}
["anyOf", "oneOf", "allOf"]
.into_iter()
.filter_map(|key| schema.get(key).and_then(Value::as_array))
.flatten()
.any(|child| schema_contains_property(child, property))
|| ["then", "else"]
.into_iter()
.filter_map(|key| schema.get(key))
.any(|child| schema_contains_property(child, property))
}
fn property_schema_with_type_exists(schema: &Value, property: &str, schema_type: &str) -> bool {
if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
if let Some(property_schema) = properties.get(property)
&& permits_type(property_schema, schema_type)
{
return true;
}
if properties.values().any(|property_schema| {
property_schema_with_type_exists(property_schema, property, schema_type)
}) {
return true;
}
}
if let Some(array) = schema.get("allOf").and_then(Value::as_array)
&& array
.iter()
.any(|entry| property_schema_with_type_exists(entry, property, schema_type))
{
return true;
}
for key in ["anyOf", "oneOf"] {
if let Some(array) = schema.get(key).and_then(Value::as_array)
&& array
.iter()
.any(|entry| property_schema_with_type_exists(entry, property, schema_type))
{
return true;
}
}
for key in ["items", "additionalProperties"] {
if let Some(child) = schema.get(key)
&& property_schema_with_type_exists(child, property, schema_type)
{
return true;
}
}
for key in ["then", "else"] {
if let Some(child) = schema.get(key)
&& property_schema_with_type_exists(child, property, schema_type)
{
return true;
}
}
false
}
fn property_schema_contains_open_string_map(schema: &Value, property: &str) -> bool {
if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
if let Some(property_schema) = properties.get(property)
&& schema_contains_open_string_map(property_schema)
{
return true;
}
if properties.values().any(|property_schema| {
property_schema_contains_open_string_map(property_schema, property)
}) {
return true;
}
}
if let Some(array) = schema.get("allOf").and_then(Value::as_array)
&& array
.iter()
.any(|entry| property_schema_contains_open_string_map(entry, property))
{
return true;
}
for key in ["anyOf", "oneOf"] {
if let Some(array) = schema.get(key).and_then(Value::as_array)
&& array
.iter()
.any(|entry| property_schema_contains_open_string_map(entry, property))
{
return true;
}
}
for key in ["items", "additionalProperties"] {
if let Some(child) = schema.get(key)
&& property_schema_contains_open_string_map(child, property)
{
return true;
}
}
for key in ["then", "else"] {
if let Some(child) = schema.get(key)
&& property_schema_contains_open_string_map(child, property)
{
return true;
}
}
false
}
fn assert_open_string_map_or_templated_string(schema: &Value, label: &str) {
assert!(
schema_contains_open_string_map(schema),
"{label} should include an open string-map branch, got {schema}"
);
assert!(
schema_contains_type(schema, "string"),
"{label} should include a templated string branch, got {schema}"
);
}
#[derive(Debug)]
struct SharedObjectProvider;
impl ResourceSchemaOracle for SharedObjectProvider {
fn schema_fragment_for_use(&self, _use_: &ProviderSchemaUse) -> Option<ProviderSchemaFragment> {
Some(ProviderSchemaFragment::new(serde_json::json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" }
},
"additionalProperties": false
}
})))
}
}
#[derive(Debug)]
struct NoopProvider;
impl ResourceSchemaOracle for NoopProvider {
fn schema_fragment_for_use(&self, _use_: &ProviderSchemaUse) -> Option<ProviderSchemaFragment> {
None
}
}
fn bitnami_tplvalues_helpers() -> &'static str {
indoc! {r#"
{{- define "common.tplvalues.render" -}}
{{- $value := typeIs "string" .value | ternary .value (.value | toYaml) }}
{{- if contains "{{" (toJson .value) }}
{{- if .scope }}
{{- tpl (cat "{{- with $.RelativeScope -}}" $value "{{- end }}") (merge (dict "RelativeScope" .scope) .context) }}
{{- else }}
{{- tpl $value .context }}
{{- end }}
{{- else }}
{{- $value }}
{{- end }}
{{- end -}}
{{- define "common.tplvalues.merge" -}}
{{- $dst := dict -}}
{{- range .values -}}
{{- $dst = include "common.tplvalues.render" (dict "value" . "context" $.context "scope" $.scope) | fromYaml | merge $dst -}}
{{- end -}}
{{ $dst | toYaml }}
{{- end -}}
"#}
}
fn bitnami_labels_helpers() -> String {
format!(
"{}\n{}",
bitnami_tplvalues_helpers(),
indoc! {r#"
{{- define "common.names.name" -}}minio{{- end -}}
{{- define "common.names.chart" -}}minio{{- end -}}
{{- define "common.labels.standard" -}}
{{- if and (hasKey . "customLabels") (hasKey . "context") -}}
{{- $default := dict "app.kubernetes.io/name" (include "common.names.name" .context) "helm.sh/chart" (include "common.names.chart" .context) "app.kubernetes.io/instance" .context.Release.Name "app.kubernetes.io/managed-by" .context.Release.Service -}}
{{- with .context.Chart.AppVersion -}}
{{- $_ := set $default "app.kubernetes.io/version" . -}}
{{- end -}}
{{ template "common.tplvalues.merge" (dict "values" (list .customLabels $default) "context" .context) }}
{{- else -}}
app.kubernetes.io/name: {{ include "common.names.name" . }}
helm.sh/chart: {{ include "common.names.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Chart.AppVersion }}
app.kubernetes.io/version: {{ . | quote }}
{{- end -}}
{{- end -}}
{{- end -}}
"#}
)
}
fn permits_null(schema: &Value) -> bool {
schema_accepts_instance(schema, &Value::Null)
}
fn schema_variant_matching<'a, F: Fn(&'a Value) -> bool>(
schema: &'a Value,
predicate: F,
) -> Option<&'a Value> {
fn find<'a>(schema: &'a Value, predicate: &impl Fn(&'a Value) -> bool) -> Option<&'a Value> {
if predicate(schema) {
return Some(schema);
}
for keyword in ["anyOf", "allOf", "oneOf"] {
if let Some(found) = schema
.get(keyword)
.and_then(Value::as_array)
.and_then(|variants| variants.iter().find_map(|variant| find(variant, predicate)))
{
return Some(found);
}
}
schema.get("then").and_then(|then| find(then, predicate))
}
find(schema, &predicate)
}
fn ranged_arm_of_type<'a>(schema: &'a Value, ty: &str) -> Option<&'a Value> {
if schema.get("type").and_then(Value::as_str) == Some(ty)
|| schema
.get("type")
.and_then(Value::as_array)
.is_some_and(|types| {
types
.iter()
.any(|schema_type| schema_type.as_str() == Some(ty))
})
{
return Some(schema);
}
for keyword in ["anyOf", "oneOf"] {
if let Some(arm) = schema
.get(keyword)
.and_then(Value::as_array)
.and_then(|arms| arms.iter().find_map(|arm| ranged_arm_of_type(arm, ty)))
{
return Some(arm);
}
}
if let Some(arm) = schema
.get("then")
.and_then(|then| ranged_arm_of_type(then, ty))
{
return Some(arm);
}
schema
.get("allOf")
.and_then(Value::as_array)
.and_then(|arms| arms.iter().find_map(|arm| ranged_arm_of_type(arm, ty)))
}
fn object_variant_with_property<'a>(schema: &'a Value, property: &str) -> Option<&'a Value> {
if schema.pointer(&format!("/properties/{property}")).is_some() {
return Some(schema);
}
schema
.get("anyOf")
.and_then(Value::as_array)
.and_then(|variants| {
variants
.iter()
.find_map(|variant| object_variant_with_property(variant, property))
})
}
fn permits_type(schema: &Value, ty: &str) -> bool {
if schema.get("type").and_then(Value::as_str) == Some(ty) {
return true;
}
if schema
.get("type")
.and_then(Value::as_array)
.is_some_and(|types| types.iter().any(|value| value.as_str() == Some(ty)))
{
return true;
}
schema
.get("anyOf")
.and_then(Value::as_array)
.is_some_and(|variants| variants.iter().any(|variant| permits_type(variant, ty)))
}
fn permits_empty_string(schema: &Value) -> bool {
if let Some(variants) = schema.get("anyOf").and_then(Value::as_array) {
return variants.iter().any(permits_empty_string);
}
if let Some(variants) = schema.get("oneOf").and_then(Value::as_array) {
return variants.iter().any(permits_empty_string);
}
if !permits_type(schema, "string") {
return false;
}
if let Some(values) = schema.get("enum").and_then(Value::as_array) {
return values.iter().any(|value| value.as_str() == Some(""));
}
schema
.get("minLength")
.and_then(Value::as_u64)
.is_none_or(|min_length| min_length == 0)
}