1//! Utilities for managing [`CustomResourceDefinition`] schemas
2//!
3//! [`CustomResourceDefinition`]: `k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition`
45// Used in docs
6#[allow(unused_imports)] use schemars::gen::SchemaSettings;
78use schemars::{
9 schema::{InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec},
10 visit::Visitor,
11 MapEntry,
12};
1314/// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules
15///
16/// The following two transformations are applied
17/// * Rewrite enums from `oneOf` to `object`s with multiple variants ([schemars#84](https://github.com/GREsau/schemars/issues/84))
18/// * Rewrite untagged enums from `anyOf` to `object`s with multiple variants ([kube#1028](https://github.com/kube-rs/kube/pull/1028))
19/// * Rewrite `additionalProperties` from `#[serde(flatten)]` to `x-kubernetes-preserve-unknown-fields` ([kube#844](https://github.com/kube-rs/kube/issues/844))
20///
21/// This is used automatically by `kube::derive`'s `#[derive(CustomResource)]`,
22/// but it can also be used manually with [`SchemaSettings::with_visitor`].
23///
24/// # Panics
25///
26/// The [`Visitor`] functions may panic if the transform could not be applied. For example,
27/// there must not be any overlapping properties between `oneOf` branches.
28#[derive(Debug, Clone)]
29pub struct StructuralSchemaRewriter;
3031impl Visitor for StructuralSchemaRewriter {
32fn visit_schema_object(&mut self, schema: &mut schemars::schema::SchemaObject) {
33 schemars::visit::visit_schema_object(self, schema);
3435if let Some(subschemas) = &mut schema.subschemas {
36if let Some(one_of) = subschemas.one_of.as_mut() {
37// Tagged enums are serialized using `one_of`
38hoist_subschema_properties(one_of, &mut schema.object, &mut schema.instance_type);
3940// "Plain" enums are serialized using `one_of` if they have doc tags
41hoist_subschema_enum_values(one_of, &mut schema.enum_values, &mut schema.instance_type);
4243if one_of.is_empty() {
44 subschemas.one_of = None;
45 }
46 }
4748if let Some(any_of) = &mut subschemas.any_of {
49// Untagged enums are serialized using `any_of`
50hoist_subschema_properties(any_of, &mut schema.object, &mut schema.instance_type);
51 }
52 }
5354// check for maps without with properties (i.e. flattened maps)
55 // and allow these to persist dynamically
56if let Some(object) = &mut schema.object {
57if !object.properties.is_empty()
58 && object.additional_properties.as_deref() == Some(&Schema::Bool(true))
59 {
60 object.additional_properties = None;
61 schema
62 .extensions
63 .insert("x-kubernetes-preserve-unknown-fields".into(), true.into());
64 }
65 }
6667// As of version 1.30 Kubernetes does not support setting `uniqueItems` to `true`,
68 // so we need to remove this fields.
69 // Users can still set `x-kubernetes-list-type=set` in case they want the apiserver
70 // to do validation, but we can't make an assumption about the Set contents here.
71 // See https://kubernetes.io/docs/reference/using-api/server-side-apply/ for details.
72if let Some(array) = &mut schema.array {
73 array.unique_items = None;
74 }
75 }
76}
7778/// Bring all plain enum values up to the root schema,
79/// since Kubernetes doesn't allow subschemas to define enum options.
80///
81/// (Enum here means a list of hard-coded values, not a tagged union.)
82fn hoist_subschema_enum_values(
83 subschemas: &mut Vec<Schema>,
84 common_enum_values: &mut Option<Vec<serde_json::Value>>,
85 instance_type: &mut Option<SingleOrVec<InstanceType>>,
86) {
87 subschemas.retain(|variant| {
88if let Schema::Object(SchemaObject {
89 instance_type: variant_type,
90 enum_values: Some(variant_enum_values),
91 ..
92 }) = variant
93 {
94if let Some(variant_type) = variant_type {
95match instance_type {
96None => *instance_type = Some(variant_type.clone()),
97Some(tpe) => {
98if tpe != variant_type {
99panic!("Enum variant set {variant_enum_values:?} has type {variant_type:?} but was already defined as {instance_type:?}. The instance type must be equal for all subschema variants.")
100 }
101 }
102 }
103 }
104 common_enum_values
105 .get_or_insert_with(Vec::new)
106 .extend(variant_enum_values.iter().cloned());
107false
108} else {
109true
110}
111 })
112}
113114/// Bring all property definitions from subschemas up to the root schema,
115/// since Kubernetes doesn't allow subschemas to define properties.
116fn hoist_subschema_properties(
117 subschemas: &mut Vec<Schema>,
118 common_obj: &mut Option<Box<ObjectValidation>>,
119 instance_type: &mut Option<SingleOrVec<InstanceType>>,
120) {
121for variant in subschemas {
122if let Schema::Object(SchemaObject {
123 instance_type: variant_type,
124 object: Some(variant_obj),
125 metadata: variant_metadata,
126 ..
127 }) = variant
128 {
129let common_obj = common_obj.get_or_insert_with(Box::<ObjectValidation>::default);
130131if let Some(variant_metadata) = variant_metadata {
132// Move enum variant description from oneOf clause to its corresponding property
133if let Some(description) = std::mem::take(&mut variant_metadata.description) {
134if let Some(Schema::Object(variant_object)) =
135 only_item(variant_obj.properties.values_mut())
136 {
137let metadata = variant_object
138 .metadata
139 .get_or_insert_with(Box::<Metadata>::default);
140 metadata.description = Some(description);
141 }
142 }
143 }
144145// Move all properties
146let variant_properties = std::mem::take(&mut variant_obj.properties);
147for (property_name, property) in variant_properties {
148match common_obj.properties.entry(property_name) {
149 MapEntry::Vacant(entry) => {
150 entry.insert(property);
151 }
152 MapEntry::Occupied(entry) => {
153if &property != entry.get() {
154panic!("Property {:?} has the schema {:?} but was already defined as {:?} in another subschema. The schemas for a property used in multiple subschemas must be identical",
155 entry.key(),
156&property,
157 entry.get());
158 }
159 }
160 }
161 }
162163// Kubernetes doesn't allow variants to set additionalProperties
164variant_obj.additional_properties = None;
165166 merge_metadata(instance_type, variant_type.take());
167 }
168 }
169}
170171fn only_item<I: Iterator>(mut i: I) -> Option<I::Item> {
172let item = i.next()?;
173if i.next().is_some() {
174return None;
175 }
176Some(item)
177}
178179fn merge_metadata(
180 instance_type: &mut Option<SingleOrVec<InstanceType>>,
181 variant_type: Option<SingleOrVec<InstanceType>>,
182) {
183match (instance_type, variant_type) {
184 (_, None) => {}
185 (common_type @ None, variant_type) => {
186*common_type = variant_type;
187 }
188 (Some(common_type), Some(variant_type)) => {
189if *common_type != variant_type {
190panic!(
191"variant defined type {variant_type:?}, conflicting with existing type {common_type:?}"
192);
193 }
194 }
195 }
196}