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}