kube_core/
cel.rs

1//! CEL validation for CRDs
2
3use std::str::FromStr;
4
5#[cfg(feature = "schema")] use schemars::schema::Schema;
6use serde::{Deserialize, Serialize};
7
8/// Rule is a CEL validation rule for the CRD field
9#[derive(Default, Serialize, Deserialize, Clone, Debug)]
10#[serde(rename_all = "camelCase")]
11pub struct Rule {
12    /// rule represents the expression which will be evaluated by CEL.
13    /// The `self` variable in the CEL expression is bound to the scoped value.
14    pub rule: String,
15    /// message represents CEL validation message for the provided type
16    /// If unset, the message is "failed rule: {Rule}".
17    #[serde(flatten)]
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub message: Option<Message>,
20    /// fieldPath represents the field path returned when the validation fails.
21    /// It must be a relative JSON path, scoped to the location of the field in the schema
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub field_path: Option<String>,
24    /// reason is a machine-readable value providing more detail about why a field failed the validation.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub reason: Option<Reason>,
27}
28
29impl Rule {
30    /// Initialize the rule
31    ///
32    /// ```rust
33    /// use kube_core::Rule;
34    /// let r = Rule::new("self == oldSelf");
35    ///
36    /// assert_eq!(r.rule, "self == oldSelf".to_string())
37    /// ```
38    pub fn new(rule: impl Into<String>) -> Self {
39        Self {
40            rule: rule.into(),
41            ..Default::default()
42        }
43    }
44
45    /// Set the rule message.
46    ///
47    /// use kube_core::Rule;
48    /// ```rust
49    /// use kube_core::{Rule, Message};
50    ///
51    /// let r = Rule::new("self == oldSelf").message("is immutable");
52    /// assert_eq!(r.rule, "self == oldSelf".to_string());
53    /// assert_eq!(r.message, Some(Message::Message("is immutable".to_string())));
54    /// ```
55    pub fn message(mut self, message: impl Into<Message>) -> Self {
56        self.message = Some(message.into());
57        self
58    }
59
60    /// Set the failure reason.
61    ///
62    /// use kube_core::Rule;
63    /// ```rust
64    /// use kube_core::{Rule, Reason};
65    ///
66    /// let r = Rule::new("self == oldSelf").reason(Reason::default());
67    /// assert_eq!(r.rule, "self == oldSelf".to_string());
68    /// assert_eq!(r.reason, Some(Reason::FieldValueInvalid));
69    /// ```
70    pub fn reason(mut self, reason: impl Into<Reason>) -> Self {
71        self.reason = Some(reason.into());
72        self
73    }
74
75    /// Set the failure field_path.
76    ///
77    /// use kube_core::Rule;
78    /// ```rust
79    /// use kube_core::Rule;
80    ///
81    /// let r = Rule::new("self == oldSelf").field_path("obj.field");
82    /// assert_eq!(r.rule, "self == oldSelf".to_string());
83    /// assert_eq!(r.field_path, Some("obj.field".to_string()));
84    /// ```
85    pub fn field_path(mut self, field_path: impl Into<String>) -> Self {
86        self.field_path = Some(field_path.into());
87        self
88    }
89}
90
91impl From<&str> for Rule {
92    fn from(value: &str) -> Self {
93        Self {
94            rule: value.into(),
95            ..Default::default()
96        }
97    }
98}
99
100impl From<(&str, &str)> for Rule {
101    fn from((rule, msg): (&str, &str)) -> Self {
102        Self {
103            rule: rule.into(),
104            message: Some(msg.into()),
105            ..Default::default()
106        }
107    }
108}
109/// Message represents CEL validation message for the provided type
110#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
111#[serde(rename_all = "lowercase")]
112pub enum Message {
113    /// Message represents the message displayed when validation fails. The message is required if the Rule contains
114    /// line breaks. The message must not contain line breaks.
115    /// Example:
116    /// "must be a URL with the host matching spec.host"
117    Message(String),
118    /// Expression declares a CEL expression that evaluates to the validation failure message that is returned when this rule fails.
119    /// Since messageExpression is used as a failure message, it must evaluate to a string. If messageExpression results in a runtime error, the runtime error is logged, and the validation failure message is produced
120    /// as if the messageExpression field were unset. If messageExpression evaluates to an empty string, a string with only spaces, or a string
121    /// that contains line breaks, then the validation failure message will also be produced as if the messageExpression field were unset, and
122    /// the fact that messageExpression produced an empty string/string with only spaces/string with line breaks will be logged.
123    /// messageExpression has access to all the same variables as the rule; the only difference is the return type.
124    /// Example:
125    /// "x must be less than max ("+string(self.max)+")"
126    #[serde(rename = "messageExpression")]
127    Expression(String),
128}
129
130impl From<&str> for Message {
131    fn from(value: &str) -> Self {
132        Message::Message(value.to_string())
133    }
134}
135
136/// Reason is a machine-readable value providing more detail about why a field failed the validation.
137///
138/// More in [docs](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-reason)
139#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
140pub enum Reason {
141    /// FieldValueInvalid is used to report malformed values (e.g. failed regex
142    /// match, too long, out of bounds).
143    #[default]
144    FieldValueInvalid,
145    /// FieldValueForbidden is used to report valid (as per formatting rules)
146    /// values which would be accepted under some conditions, but which are not
147    /// permitted by the current conditions (such as security policy).
148    FieldValueForbidden,
149    /// FieldValueRequired is used to report required values that are not
150    /// provided (e.g. empty strings, null values, or empty arrays).
151    FieldValueRequired,
152    /// FieldValueDuplicate is used to report collisions of values that must be
153    /// unique (e.g. unique IDs).
154    FieldValueDuplicate,
155}
156
157impl FromStr for Reason {
158    type Err = serde_json::Error;
159
160    fn from_str(s: &str) -> Result<Self, Self::Err> {
161        serde_json::from_str(s)
162    }
163}
164
165/// Validate takes schema and applies a set of validation rules to it. The rules are stored
166/// on the top level under the "x-kubernetes-validations".
167///
168/// ```rust
169/// use schemars::schema::Schema;
170/// use kube::core::{Rule, Reason, Message, validate};
171///
172/// let mut schema = Schema::Object(Default::default());
173/// let rules = &[Rule{
174///     rule: "self.spec.host == self.url.host".into(),
175///     message: Some("must be a URL with the host matching spec.host".into()),
176///     field_path: Some("spec.host".into()),
177///     ..Default::default()
178/// }];
179/// validate(&mut schema, rules)?;
180/// assert_eq!(
181///     serde_json::to_string(&schema).unwrap(),
182///     r#"{"x-kubernetes-validations":[{"fieldPath":"spec.host","message":"must be a URL with the host matching spec.host","rule":"self.spec.host == self.url.host"}]}"#,
183/// );
184/// # Ok::<(), serde_json::Error>(())
185///```
186#[cfg(feature = "schema")]
187#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
188pub fn validate(s: &mut Schema, rules: &[Rule]) -> Result<(), serde_json::Error> {
189    match s {
190        Schema::Bool(_) => (),
191        Schema::Object(schema_object) => {
192            schema_object
193                .extensions
194                .insert("x-kubernetes-validations".into(), serde_json::to_value(rules)?);
195        }
196    };
197    Ok(())
198}
199
200/// Validate property mutates property under property_index of the schema
201/// with the provided set of validation rules.
202///
203/// ```rust
204/// use schemars::JsonSchema;
205/// use kube::core::{Rule, validate_property};
206///
207/// #[derive(JsonSchema)]
208/// struct MyStruct {
209///     field: Option<String>,
210/// }
211///
212/// let gen = &mut schemars::gen::SchemaSettings::openapi3().into_generator();
213/// let mut schema = MyStruct::json_schema(gen);
214/// let rules = &[Rule::new("self != oldSelf")];
215/// validate_property(&mut schema, 0, rules)?;
216/// assert_eq!(
217///     serde_json::to_string(&schema).unwrap(),
218///     r#"{"type":"object","properties":{"field":{"type":"string","nullable":true,"x-kubernetes-validations":[{"rule":"self != oldSelf"}]}}}"#
219/// );
220/// # Ok::<(), serde_json::Error>(())
221///```
222#[cfg(feature = "schema")]
223#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
224pub fn validate_property(
225    s: &mut Schema,
226    property_index: usize,
227    rules: &[Rule],
228) -> Result<(), serde_json::Error> {
229    match s {
230        Schema::Bool(_) => (),
231        Schema::Object(schema_object) => {
232            let obj = schema_object.object();
233            for (n, (_, schema)) in obj.properties.iter_mut().enumerate() {
234                if n == property_index {
235                    return validate(schema, rules);
236                }
237            }
238        }
239    };
240
241    Ok(())
242}
243
244/// Merge schema properties in order to pass overrides or extension properties from the other schema.
245///
246/// ```rust
247/// use schemars::JsonSchema;
248/// use kube::core::{Rule, merge_properties};
249///
250/// #[derive(JsonSchema)]
251/// struct MyStruct {
252///     a: Option<bool>,
253/// }
254///
255/// #[derive(JsonSchema)]
256/// struct MySecondStruct {
257///     a: bool,
258///     b: Option<bool>,
259/// }
260/// let gen = &mut schemars::gen::SchemaSettings::openapi3().into_generator();
261/// let mut first = MyStruct::json_schema(gen);
262/// let mut second = MySecondStruct::json_schema(gen);
263/// merge_properties(&mut first, &mut second);
264///
265/// assert_eq!(
266///     serde_json::to_string(&first).unwrap(),
267///     r#"{"type":"object","properties":{"a":{"type":"boolean"},"b":{"type":"boolean","nullable":true}}}"#
268/// );
269/// # Ok::<(), serde_json::Error>(())
270#[cfg(feature = "schema")]
271#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
272pub fn merge_properties(s: &mut Schema, merge: &mut Schema) {
273    match s {
274        schemars::schema::Schema::Bool(_) => (),
275        schemars::schema::Schema::Object(schema_object) => {
276            let obj = schema_object.object();
277            for (k, v) in &merge.clone().into_object().object().properties {
278                obj.properties.insert(k.clone(), v.clone());
279            }
280        }
281    }
282}