kube_core/schema.rs
1//! Utilities for managing [`CustomResourceDefinition`] schemas
2//!
3//! [`CustomResourceDefinition`]: `k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition`
4
5// Used in docs
6#[allow(unused_imports)] use schemars::generate::SchemaSettings;
7
8use schemars::{transform::Transform, JsonSchema};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
12
13/// schemars [`Visitor`] that rewrites a [`Schema`] to conform to Kubernetes' "structural schema" rules
14///
15/// The following two transformations are applied
16/// * Rewrite enums from `oneOf` to `object`s with multiple variants ([schemars#84](https://github.com/GREsau/schemars/issues/84))
17/// * Rewrite untagged enums from `anyOf` to `object`s with multiple variants ([kube#1028](https://github.com/kube-rs/kube/pull/1028))
18/// * Rewrite `additionalProperties` from `#[serde(flatten)]` to `x-kubernetes-preserve-unknown-fields` ([kube#844](https://github.com/kube-rs/kube/issues/844))
19///
20/// This is used automatically by `kube::derive`'s `#[derive(CustomResource)]`,
21/// but it can also be used manually with [`SchemaSettings::with_transform`].
22///
23/// # Panics
24///
25/// The [`Visitor`] functions may panic if the transform could not be applied. For example,
26/// there must not be any overlapping properties between `oneOf` branches.
27#[derive(Debug, Clone)]
28pub struct StructuralSchemaRewriter;
29
30/// A JSON Schema.
31#[allow(clippy::large_enum_variant)]
32#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
33#[serde(untagged)]
34enum Schema {
35 /// A trivial boolean JSON Schema.
36 ///
37 /// The schema `true` matches everything (always passes validation), whereas the schema `false`
38 /// matches nothing (always fails validation).
39 Bool(bool),
40 /// A JSON Schema object.
41 Object(SchemaObject),
42}
43
44/// A JSON Schema object.
45#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
46#[serde(rename_all = "camelCase", default)]
47struct SchemaObject {
48 /// Properties which annotate the [`SchemaObject`] which typically have no effect when an object is being validated against the schema.
49 #[serde(flatten, deserialize_with = "skip_if_default")]
50 metadata: Option<Box<Metadata>>,
51 /// The `type` keyword.
52 ///
53 /// See [JSON Schema Validation 6.1.1. "type"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1)
54 /// and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).
55 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
56 instance_type: Option<SingleOrVec<InstanceType>>,
57 /// The `format` keyword.
58 ///
59 /// See [JSON Schema Validation 7. A Vocabulary for Semantic Content With "format"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).
60 #[serde(skip_serializing_if = "Option::is_none")]
61 format: Option<String>,
62 /// The `enum` keyword.
63 ///
64 /// See [JSON Schema Validation 6.1.2. "enum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)
65 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
66 enum_values: Option<Vec<Value>>,
67 /// Properties of the [`SchemaObject`] which define validation assertions in terms of other schemas.
68 #[serde(flatten, deserialize_with = "skip_if_default")]
69 subschemas: Option<Box<SubschemaValidation>>,
70 /// Properties of the [`SchemaObject`] which define validation assertions for arrays.
71 #[serde(flatten, deserialize_with = "skip_if_default")]
72 array: Option<Box<ArrayValidation>>,
73 /// Properties of the [`SchemaObject`] which define validation assertions for objects.
74 #[serde(flatten, deserialize_with = "skip_if_default")]
75 object: Option<Box<ObjectValidation>>,
76 /// Arbitrary extra properties which are not part of the JSON Schema specification, or which `schemars` does not support.
77 #[serde(flatten)]
78 extensions: BTreeMap<String, Value>,
79 /// Arbitrary data.
80 #[serde(flatten)]
81 other: Value,
82}
83
84// Deserializing "null" to `Option<Value>` directly results in `None`,
85// this function instead makes it deserialize to `Some(Value::Null)`.
86fn allow_null<'de, D>(de: D) -> Result<Option<Value>, D::Error>
87where
88 D: serde::Deserializer<'de>,
89{
90 Value::deserialize(de).map(Option::Some)
91}
92
93fn skip_if_default<'de, D, T>(deserializer: D) -> Result<Option<Box<T>>, D::Error>
94where
95 D: serde::Deserializer<'de>,
96 T: Deserialize<'de> + Default + PartialEq,
97{
98 let value = T::deserialize(deserializer)?;
99 if value == T::default() {
100 Ok(None)
101 } else {
102 Ok(Some(Box::new(value)))
103 }
104}
105
106/// Properties which annotate a [`SchemaObject`] which typically have no effect when an object is being validated against the schema.
107#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
108#[serde(rename_all = "camelCase", default)]
109struct Metadata {
110 /// The `description` keyword.
111 ///
112 /// See [JSON Schema Validation 9.1. "title" and "description"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).
113 #[serde(skip_serializing_if = "Option::is_none")]
114 description: Option<String>,
115 /// The `default` keyword.
116 ///
117 /// See [JSON Schema Validation 9.2. "default"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2).
118 #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "allow_null")]
119 default: Option<Value>,
120}
121
122/// Properties of a [`SchemaObject`] which define validation assertions in terms of other schemas.
123#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
124#[serde(rename_all = "camelCase", default)]
125struct SubschemaValidation {
126 /// The `anyOf` keyword.
127 ///
128 /// See [JSON Schema 9.2.1.2. "anyOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).
129 #[serde(skip_serializing_if = "Option::is_none")]
130 any_of: Option<Vec<Schema>>,
131 /// The `oneOf` keyword.
132 ///
133 /// See [JSON Schema 9.2.1.3. "oneOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).
134 #[serde(skip_serializing_if = "Option::is_none")]
135 one_of: Option<Vec<Schema>>,
136}
137
138/// Properties of a [`SchemaObject`] which define validation assertions for arrays.
139#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
140#[serde(rename_all = "camelCase", default)]
141struct ArrayValidation {
142 /// The `items` keyword.
143 ///
144 /// See [JSON Schema 9.3.1.1. "items"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).
145 #[serde(skip_serializing_if = "Option::is_none")]
146 items: Option<SingleOrVec<Schema>>,
147 /// The `additionalItems` keyword.
148 ///
149 /// See [JSON Schema 9.3.1.2. "additionalItems"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).
150 #[serde(skip_serializing_if = "Option::is_none")]
151 additional_items: Option<Box<Schema>>,
152 /// The `maxItems` keyword.
153 ///
154 /// See [JSON Schema Validation 6.4.1. "maxItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).
155 #[serde(skip_serializing_if = "Option::is_none")]
156 max_items: Option<u32>,
157 /// The `minItems` keyword.
158 ///
159 /// See [JSON Schema Validation 6.4.2. "minItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).
160 #[serde(skip_serializing_if = "Option::is_none")]
161 min_items: Option<u32>,
162 /// The `uniqueItems` keyword.
163 ///
164 /// See [JSON Schema Validation 6.4.3. "uniqueItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).
165 #[serde(skip_serializing_if = "Option::is_none")]
166 unique_items: Option<bool>,
167 /// The `contains` keyword.
168 ///
169 /// See [JSON Schema 9.3.1.4. "contains"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).
170 #[serde(skip_serializing_if = "Option::is_none")]
171 contains: Option<Box<Schema>>,
172}
173
174/// Properties of a [`SchemaObject`] which define validation assertions for objects.
175#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
176#[serde(rename_all = "camelCase", default)]
177struct ObjectValidation {
178 /// The `maxProperties` keyword.
179 ///
180 /// See [JSON Schema Validation 6.5.1. "maxProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).
181 #[serde(skip_serializing_if = "Option::is_none")]
182 max_properties: Option<u32>,
183 /// The `minProperties` keyword.
184 ///
185 /// See [JSON Schema Validation 6.5.2. "minProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).
186 #[serde(skip_serializing_if = "Option::is_none")]
187 min_properties: Option<u32>,
188 /// The `required` keyword.
189 ///
190 /// See [JSON Schema Validation 6.5.3. "required"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).
191 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
192 required: BTreeSet<String>,
193 /// The `properties` keyword.
194 ///
195 /// See [JSON Schema 9.3.2.1. "properties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).
196 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
197 properties: BTreeMap<String, Schema>,
198 /// The `patternProperties` keyword.
199 ///
200 /// See [JSON Schema 9.3.2.2. "patternProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).
201 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
202 pattern_properties: BTreeMap<String, Schema>,
203 /// The `additionalProperties` keyword.
204 ///
205 /// See [JSON Schema 9.3.2.3. "additionalProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).
206 #[serde(skip_serializing_if = "Option::is_none")]
207 additional_properties: Option<Box<Schema>>,
208 /// The `propertyNames` keyword.
209 ///
210 /// See [JSON Schema 9.3.2.5. "propertyNames"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).
211 #[serde(skip_serializing_if = "Option::is_none")]
212 property_names: Option<Box<Schema>>,
213}
214
215/// The possible types of values in JSON Schema documents.
216///
217/// See [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).
218#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, JsonSchema)]
219#[serde(rename_all = "camelCase")]
220enum InstanceType {
221 /// Represents the JSON null type.
222 Null,
223 /// Represents the JSON boolean type.
224 Boolean,
225 /// Represents the JSON object type.
226 Object,
227 /// Represents the JSON array type.
228 Array,
229 /// Represents the JSON number type (floating point).
230 Number,
231 /// Represents the JSON string type.
232 String,
233 /// Represents the JSON integer type.
234 Integer,
235}
236
237/// A type which can be serialized as a single item, or multiple items.
238///
239/// In some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.
240#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, JsonSchema)]
241#[serde(untagged)]
242enum SingleOrVec<T> {
243 /// Represents a single item.
244 Single(Box<T>),
245 /// Represents a vector of items.
246 Vec(Vec<T>),
247}
248
249impl Transform for StructuralSchemaRewriter {
250 fn transform(&mut self, transform_schema: &mut schemars::Schema) {
251 schemars::transform::transform_subschemas(self, transform_schema);
252
253 let mut schema: SchemaObject = match serde_json::from_value(transform_schema.clone().to_value()).ok()
254 {
255 Some(schema) => schema,
256 None => return,
257 };
258
259 if let Some(subschemas) = &mut schema.subschemas {
260 if let Some(one_of) = subschemas.one_of.as_mut() {
261 // Tagged enums are serialized using `one_of`
262 hoist_subschema_properties(one_of, &mut schema.object, &mut schema.instance_type);
263
264 // "Plain" enums are serialized using `one_of` if they have doc tags
265 hoist_subschema_enum_values(one_of, &mut schema.enum_values, &mut schema.instance_type);
266
267 if one_of.is_empty() {
268 subschemas.one_of = None;
269 }
270 }
271
272 if let Some(any_of) = &mut subschemas.any_of {
273 // Untagged enums are serialized using `any_of`
274 hoist_subschema_properties(any_of, &mut schema.object, &mut schema.instance_type);
275 }
276 }
277
278 // check for maps without with properties (i.e. flattened maps)
279 // and allow these to persist dynamically
280 if let Some(object) = &mut schema.object {
281 if !object.properties.is_empty()
282 && object.additional_properties.as_deref() == Some(&Schema::Bool(true))
283 {
284 object.additional_properties = None;
285 schema
286 .extensions
287 .insert("x-kubernetes-preserve-unknown-fields".into(), true.into());
288 }
289 }
290
291 // As of version 1.30 Kubernetes does not support setting `uniqueItems` to `true`,
292 // so we need to remove this fields.
293 // Users can still set `x-kubernetes-list-type=set` in case they want the apiserver
294 // to do validation, but we can't make an assumption about the Set contents here.
295 // See https://kubernetes.io/docs/reference/using-api/server-side-apply/ for details.
296 if let Some(array) = &mut schema.array {
297 array.unique_items = None;
298 }
299
300 if let Ok(schema) = serde_json::to_value(schema) {
301 if let Ok(transformed) = serde_json::from_value(schema) {
302 *transform_schema = transformed;
303 }
304 }
305 }
306}
307
308/// Bring all plain enum values up to the root schema,
309/// since Kubernetes doesn't allow subschemas to define enum options.
310///
311/// (Enum here means a list of hard-coded values, not a tagged union.)
312fn hoist_subschema_enum_values(
313 subschemas: &mut Vec<Schema>,
314 common_enum_values: &mut Option<Vec<serde_json::Value>>,
315 instance_type: &mut Option<SingleOrVec<InstanceType>>,
316) {
317 subschemas.retain(|variant| {
318 if let Schema::Object(SchemaObject {
319 instance_type: variant_type,
320 enum_values: Some(variant_enum_values),
321 ..
322 }) = variant
323 {
324 if let Some(variant_type) = variant_type {
325 match instance_type {
326 None => *instance_type = Some(variant_type.clone()),
327 Some(tpe) => {
328 if tpe != variant_type {
329 panic!("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.")
330 }
331 }
332 }
333 }
334 common_enum_values
335 .get_or_insert_with(Vec::new)
336 .extend(variant_enum_values.iter().cloned());
337 false
338 } else {
339 true
340 }
341 })
342}
343
344/// Bring all property definitions from subschemas up to the root schema,
345/// since Kubernetes doesn't allow subschemas to define properties.
346fn hoist_subschema_properties(
347 subschemas: &mut Vec<Schema>,
348 common_obj: &mut Option<Box<ObjectValidation>>,
349 instance_type: &mut Option<SingleOrVec<InstanceType>>,
350) {
351 for variant in subschemas {
352 if let Schema::Object(SchemaObject {
353 instance_type: variant_type,
354 object: Some(variant_obj),
355 metadata: variant_metadata,
356 ..
357 }) = variant
358 {
359 let common_obj = common_obj.get_or_insert_with(Box::<ObjectValidation>::default);
360
361 if let Some(variant_metadata) = variant_metadata {
362 // Move enum variant description from oneOf clause to its corresponding property
363 if let Some(description) = std::mem::take(&mut variant_metadata.description) {
364 if let Some(Schema::Object(variant_object)) =
365 only_item(variant_obj.properties.values_mut())
366 {
367 let metadata = variant_object
368 .metadata
369 .get_or_insert_with(Box::<Metadata>::default);
370 metadata.description = Some(description);
371 }
372 }
373 }
374
375 // Move all properties
376 let variant_properties = std::mem::take(&mut variant_obj.properties);
377 for (property_name, property) in variant_properties {
378 match common_obj.properties.entry(property_name) {
379 Entry::Vacant(entry) => {
380 entry.insert(property);
381 }
382 Entry::Occupied(entry) => {
383 if &property != entry.get() {
384 panic!("Property {:?} has the schema {:?} but was already defined as {:?} in another subschema. The schemas for a property used in multiple subschemas must be identical",
385 entry.key(),
386 &property,
387 entry.get());
388 }
389 }
390 }
391 }
392
393 // Kubernetes doesn't allow variants to set additionalProperties
394 variant_obj.additional_properties = None;
395
396 merge_metadata(instance_type, variant_type.take());
397 }
398 }
399}
400
401fn only_item<I: Iterator>(mut i: I) -> Option<I::Item> {
402 let item = i.next()?;
403 if i.next().is_some() {
404 return None;
405 }
406 Some(item)
407}
408
409fn merge_metadata(
410 instance_type: &mut Option<SingleOrVec<InstanceType>>,
411 variant_type: Option<SingleOrVec<InstanceType>>,
412) {
413 match (instance_type, variant_type) {
414 (_, None) => {}
415 (common_type @ None, variant_type) => {
416 *common_type = variant_type;
417 }
418 (Some(common_type), Some(variant_type)) => {
419 if *common_type != variant_type {
420 panic!(
421 "variant defined type {variant_type:?}, conflicting with existing type {common_type:?}"
422 );
423 }
424 }
425 }
426}