kube_core/cel.rs
1//! CEL validation for CRDs
2
3use std::{collections::BTreeMap, str::FromStr};
4
5use derive_more::From;
6#[cfg(feature = "schema")] use schemars::Schema;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Rule is a CEL validation rule for the CRD field
11#[derive(Default, Serialize, Deserialize, Clone, Debug)]
12#[serde(rename_all = "camelCase")]
13pub struct Rule {
14 /// rule represents the expression which will be evaluated by CEL.
15 /// The `self` variable in the CEL expression is bound to the scoped value.
16 pub rule: String,
17 /// message represents CEL validation message for the provided type
18 /// If unset, the message is "failed rule: {Rule}".
19 #[serde(flatten)]
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub message: Option<Message>,
22 /// fieldPath represents the field path returned when the validation fails.
23 /// It must be a relative JSON path, scoped to the location of the field in the schema
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub field_path: Option<String>,
26 /// reason is a machine-readable value providing more detail about why a field failed the validation.
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub reason: Option<Reason>,
29}
30
31impl Rule {
32 /// Initialize the rule
33 ///
34 /// ```rust
35 /// use kube_core::Rule;
36 /// let r = Rule::new("self == oldSelf");
37 ///
38 /// assert_eq!(r.rule, "self == oldSelf".to_string())
39 /// ```
40 pub fn new(rule: impl Into<String>) -> Self {
41 Self {
42 rule: rule.into(),
43 ..Default::default()
44 }
45 }
46
47 /// Set the rule message.
48 ///
49 /// use kube_core::Rule;
50 /// ```rust
51 /// use kube_core::{Rule, Message};
52 ///
53 /// let r = Rule::new("self == oldSelf").message("is immutable");
54 /// assert_eq!(r.rule, "self == oldSelf".to_string());
55 /// assert_eq!(r.message, Some(Message::Message("is immutable".to_string())));
56 /// ```
57 pub fn message(mut self, message: impl Into<Message>) -> Self {
58 self.message = Some(message.into());
59 self
60 }
61
62 /// Set the failure reason.
63 ///
64 /// use kube_core::Rule;
65 /// ```rust
66 /// use kube_core::{Rule, Reason};
67 ///
68 /// let r = Rule::new("self == oldSelf").reason(Reason::default());
69 /// assert_eq!(r.rule, "self == oldSelf".to_string());
70 /// assert_eq!(r.reason, Some(Reason::FieldValueInvalid));
71 /// ```
72 pub fn reason(mut self, reason: impl Into<Reason>) -> Self {
73 self.reason = Some(reason.into());
74 self
75 }
76
77 /// Set the failure field_path.
78 ///
79 /// use kube_core::Rule;
80 /// ```rust
81 /// use kube_core::Rule;
82 ///
83 /// let r = Rule::new("self == oldSelf").field_path("obj.field");
84 /// assert_eq!(r.rule, "self == oldSelf".to_string());
85 /// assert_eq!(r.field_path, Some("obj.field".to_string()));
86 /// ```
87 pub fn field_path(mut self, field_path: impl Into<String>) -> Self {
88 self.field_path = Some(field_path.into());
89 self
90 }
91}
92
93impl From<&str> for Rule {
94 fn from(value: &str) -> Self {
95 Self {
96 rule: value.into(),
97 ..Default::default()
98 }
99 }
100}
101
102impl From<(&str, &str)> for Rule {
103 fn from((rule, msg): (&str, &str)) -> Self {
104 Self {
105 rule: rule.into(),
106 message: Some(msg.into()),
107 ..Default::default()
108 }
109 }
110}
111/// Message represents CEL validation message for the provided type
112#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
113#[serde(rename_all = "lowercase")]
114pub enum Message {
115 /// Message represents the message displayed when validation fails. The message is required if the Rule contains
116 /// line breaks. The message must not contain line breaks.
117 /// Example:
118 /// "must be a URL with the host matching spec.host"
119 Message(String),
120 /// Expression declares a CEL expression that evaluates to the validation failure message that is returned when this rule fails.
121 /// 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
122 /// as if the messageExpression field were unset. If messageExpression evaluates to an empty string, a string with only spaces, or a string
123 /// that contains line breaks, then the validation failure message will also be produced as if the messageExpression field were unset, and
124 /// the fact that messageExpression produced an empty string/string with only spaces/string with line breaks will be logged.
125 /// messageExpression has access to all the same variables as the rule; the only difference is the return type.
126 /// Example:
127 /// "x must be less than max ("+string(self.max)+")"
128 #[serde(rename = "messageExpression")]
129 Expression(String),
130}
131
132impl From<&str> for Message {
133 fn from(value: &str) -> Self {
134 Message::Message(value.to_string())
135 }
136}
137
138/// Reason is a machine-readable value providing more detail about why a field failed the validation.
139///
140/// More in [docs](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-reason)
141#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
142pub enum Reason {
143 /// FieldValueInvalid is used to report malformed values (e.g. failed regex
144 /// match, too long, out of bounds).
145 #[default]
146 FieldValueInvalid,
147 /// FieldValueForbidden is used to report valid (as per formatting rules)
148 /// values which would be accepted under some conditions, but which are not
149 /// permitted by the current conditions (such as security policy).
150 FieldValueForbidden,
151 /// FieldValueRequired is used to report required values that are not
152 /// provided (e.g. empty strings, null values, or empty arrays).
153 FieldValueRequired,
154 /// FieldValueDuplicate is used to report collisions of values that must be
155 /// unique (e.g. unique IDs).
156 FieldValueDuplicate,
157}
158
159impl FromStr for Reason {
160 type Err = serde_json::Error;
161
162 fn from_str(s: &str) -> Result<Self, Self::Err> {
163 serde_json::from_str(s)
164 }
165}
166
167/// Validate takes schema and applies a set of validation rules to it. The rules are stored
168/// on the top level under the "x-kubernetes-validations".
169///
170/// ```rust
171/// use schemars::Schema;
172/// use kube::core::{Rule, Reason, Message, validate};
173///
174/// let mut schema = Schema::default();
175/// let rule = Rule{
176/// rule: "self.spec.host == self.url.host".into(),
177/// message: Some("must be a URL with the host matching spec.host".into()),
178/// field_path: Some("spec.host".into()),
179/// ..Default::default()
180/// };
181/// validate(&mut schema, rule)?;
182/// assert_eq!(
183/// serde_json::to_string(&schema).unwrap(),
184/// 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"}]}"#,
185/// );
186/// # Ok::<(), serde_json::Error>(())
187///```
188#[cfg(feature = "schema")]
189#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
190pub fn validate(s: &mut Schema, rule: impl Into<Rule>) -> Result<(), serde_json::Error> {
191 let rule: Rule = rule.into();
192 let rule = serde_json::to_value(rule)?;
193 s.ensure_object()
194 .entry("x-kubernetes-validations")
195 .and_modify(|rules| {
196 if let Value::Array(rules) = rules {
197 rules.push(rule.clone());
198 }
199 })
200 .or_insert(serde_json::to_value(&[rule])?);
201 Ok(())
202}
203
204/// Validate property mutates property under property_index of the schema
205/// with the provided set of validation rules.
206///
207/// ```rust
208/// use schemars::JsonSchema;
209/// use kube::core::{Rule, validate_property};
210///
211/// #[derive(JsonSchema)]
212/// struct MyStruct {
213/// field: Option<String>,
214/// }
215///
216/// let generate = &mut schemars::generate::SchemaSettings::openapi3().into_generator();
217/// let mut schema = MyStruct::json_schema(generate);
218/// let rule = Rule::new("self != oldSelf");
219/// validate_property(&mut schema, 0, rule)?;
220/// assert_eq!(
221/// serde_json::to_string(&schema).unwrap(),
222/// r#"{"type":"object","properties":{"field":{"type":["string","null"],"x-kubernetes-validations":[{"rule":"self != oldSelf"}]}}}"#
223/// );
224/// # Ok::<(), serde_json::Error>(())
225///```
226#[cfg(feature = "schema")]
227#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
228pub fn validate_property(
229 s: &mut Schema,
230 property_index: usize,
231 rule: impl Into<Rule> + Clone,
232) -> Result<(), serde_json::Error> {
233 let obj = s.ensure_object();
234 if let Some(properties) = obj
235 .entry("properties")
236 .or_insert(serde_json::Value::Object(Default::default()))
237 .as_object_mut()
238 {
239 for (n, (_, schema)) in properties.iter_mut().enumerate() {
240 if n == property_index {
241 let mut prop = Schema::try_from(schema.clone())?;
242 validate(&mut prop, rule.clone())?;
243 *schema = prop.to_value();
244 }
245 }
246 }
247 Ok(())
248}
249
250/// Merge schema properties in order to pass overrides or extension properties from the other schema.
251///
252/// ```rust
253/// use schemars::JsonSchema;
254/// use kube::core::{Rule, merge_properties};
255///
256/// #[derive(JsonSchema)]
257/// struct MyStruct {
258/// a: Option<bool>,
259/// }
260///
261/// #[derive(JsonSchema)]
262/// struct MySecondStruct {
263/// a: bool,
264/// b: Option<bool>,
265/// }
266/// let generate = &mut schemars::generate::SchemaSettings::openapi3().into_generator();
267/// let mut first = MyStruct::json_schema(generate);
268/// let mut second = MySecondStruct::json_schema(generate);
269/// merge_properties(&mut first, &mut second);
270///
271/// assert_eq!(
272/// serde_json::to_string(&first).unwrap(),
273/// r#"{"type":"object","properties":{"a":{"type":"boolean"},"b":{"type":["boolean","null"]}}}"#
274/// );
275/// # Ok::<(), serde_json::Error>(())
276#[cfg(feature = "schema")]
277#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
278pub fn merge_properties(s: &mut Schema, merge: &mut Schema) {
279 if let Some(properties) = s
280 .ensure_object()
281 .entry("properties")
282 .or_insert(serde_json::Value::Object(Default::default()))
283 .as_object_mut()
284 {
285 if let Some(merge_properties) = merge
286 .ensure_object()
287 .entry("properties")
288 .or_insert(serde_json::Value::Object(Default::default()))
289 .as_object_mut()
290 {
291 for (k, v) in merge_properties {
292 properties.insert(k.clone(), v.clone());
293 }
294 }
295 }
296}
297
298/// ListType represents x-kubernetes merge strategy for list.
299#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
300#[serde(rename_all = "lowercase")]
301pub enum ListMerge {
302 /// Atomic represents a list, where entire list is replaced during merge. At any point in time, a single manager owns the list.
303 Atomic,
304 /// Set applies to lists that include only scalar elements. These elements must be unique.
305 Set,
306 /// Map applies to lists of nested types only. The key values must be unique in the list.
307 Map(Vec<String>),
308}
309
310/// MapMerge represents x-kubernetes merge strategy for map.
311#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
312#[serde(rename_all = "lowercase")]
313pub enum MapMerge {
314 /// Atomic represents a map, which can only be entirely replaced by a single manager.
315 Atomic,
316 /// Granular represents a map, which supports separate managers updating individual fields.
317 Granular,
318}
319
320/// StructMerge represents x-kubernetes merge strategy for struct.
321#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
322#[serde(rename_all = "lowercase")]
323pub enum StructMerge {
324 /// Atomic represents a struct, which can only be entirely replaced by a single manager.
325 Atomic,
326 /// Granular represents a struct, which supports separate managers updating individual fields.
327 Granular,
328}
329
330/// MergeStrategy represents set of options for a server-side merge strategy applied to a field.
331///
332/// See upstream documentation of values at https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy
333#[derive(From, Serialize, Deserialize, Clone, Debug, PartialEq)]
334pub enum MergeStrategy {
335 /// ListType represents x-kubernetes merge strategy for list.
336 #[serde(rename = "x-kubernetes-list-type")]
337 ListType(ListMerge),
338 /// MapType represents x-kubernetes merge strategy for map.
339 #[serde(rename = "x-kubernetes-map-type")]
340 MapType(MapMerge),
341 /// StructType represents x-kubernetes merge strategy for struct.
342 #[serde(rename = "x-kubernetes-struct-type")]
343 StructType(StructMerge),
344}
345
346impl MergeStrategy {
347 fn keys(self) -> Result<BTreeMap<String, Value>, serde_json::Error> {
348 if let Self::ListType(ListMerge::Map(keys)) = self {
349 let mut data = BTreeMap::new();
350 data.insert("x-kubernetes-list-type".into(), "map".into());
351 data.insert("x-kubernetes-list-map-keys".into(), serde_json::to_value(&keys)?);
352
353 return Ok(data);
354 }
355
356 let value = serde_json::to_value(self)?;
357 serde_json::from_value(value)
358 }
359}
360
361/// Merge strategy property mutates property under property_index of the schema
362/// with the provided set of merge strategy rules.
363///
364/// ```rust
365/// use schemars::JsonSchema;
366/// use kube::core::{MapMerge, merge_strategy_property};
367///
368/// #[derive(JsonSchema)]
369/// struct MyStruct {
370/// field: Option<String>,
371/// }
372///
373/// let generate = &mut schemars::generate::SchemaSettings::openapi3().into_generator();
374/// let mut schema = MyStruct::json_schema(generate);
375/// merge_strategy_property(&mut schema, 0, MapMerge::Atomic)?;
376/// assert_eq!(
377/// serde_json::to_string(&schema).unwrap(),
378/// r#"{"type":"object","properties":{"field":{"type":["string","null"],"x-kubernetes-map-type":"atomic"}}}"#
379/// );
380///
381/// # Ok::<(), serde_json::Error>(())
382///```
383#[cfg(feature = "schema")]
384#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
385pub fn merge_strategy_property(
386 s: &mut Schema,
387 property_index: usize,
388 strategy: impl Into<MergeStrategy>,
389) -> Result<(), serde_json::Error> {
390 if let Some(properties) = s
391 .ensure_object()
392 .entry("properties")
393 .or_insert(serde_json::Value::Object(Default::default()))
394 .as_object_mut()
395 {
396 for (n, (_, schema)) in properties.iter_mut().enumerate() {
397 if n == property_index {
398 return merge_strategy(schema, strategy.into());
399 }
400 }
401 }
402
403 Ok(())
404}
405
406/// Merge strategy takes schema and applies a set of merge strategy x-kubernetes rules to it,
407/// such as "x-kubernetes-list-type" and "x-kubernetes-list-map-keys".
408///
409/// ```rust
410/// use kube::core::{ListMerge, Reason, Message, merge_strategy};
411///
412/// let mut schema = serde_json::Value::Object(Default::default());
413/// merge_strategy(&mut schema, ListMerge::Map(vec!["key".into(),"another".into()]).into())?;
414/// assert_eq!(
415/// serde_json::to_string(&schema).unwrap(),
416/// r#"{"x-kubernetes-list-map-keys":["key","another"],"x-kubernetes-list-type":"map"}"#,
417/// );
418///
419/// # Ok::<(), serde_json::Error>(())
420///```
421#[cfg(feature = "schema")]
422#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
423pub fn merge_strategy(s: &mut Value, strategy: MergeStrategy) -> Result<(), serde_json::Error> {
424 for (key, value) in strategy.keys()? {
425 if let Some(s) = s.as_object_mut() {
426 s.insert(key, value);
427 }
428 }
429 Ok(())
430}