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