From 024cd08d241163114d142ca0fec12ef0186a33da Mon Sep 17 00:00:00 2001 From: Danil-Grigorev Date: Mon, 28 Oct 2024 20:44:37 +0100 Subject: [PATCH] Extend with supported values from docs - https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules Signed-off-by: Danil-Grigorev --- examples/crd_derive_schema.rs | 6 +-- kube-derive/src/custom_resource.rs | 66 +++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/examples/crd_derive_schema.rs b/examples/crd_derive_schema.rs index 4a69d29d1..186a2c7b9 100644 --- a/examples/crd_derive_schema.rs +++ b/examples/crd_derive_schema.rs @@ -9,7 +9,7 @@ use kube::{ runtime::wait::{await_condition, conditions}, Client, CustomResource, CustomResourceExt, }; -use kube_derive::cel_validation; +use kube::cel_validation; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -89,7 +89,7 @@ pub struct FooSpec { set_listable: Vec, // Field with CEL validation #[serde(default)] - #[validated(rule = "self != 'illegal'", message = "string cannot be illegal")] + #[validated(rule = "self != 'illegal'", message_expression = "'string cannot be illegal'")] #[validated(rule = "self != 'not legal'")] cel_validated: Option, } @@ -124,7 +124,7 @@ async fn main() -> Result<()> { println!("Creating CRD v1"); let client = Client::try_default().await?; delete_crd(client.clone()).await?; - assert!(create_crd(client.clone()).await.is_ok()); + assert!(dbg!(create_crd(client.clone()).await).is_ok()); // Test creating Foo resource. let foos = Api::::default_namespaced(client.clone()); diff --git a/kube-derive/src/custom_resource.rs b/kube-derive/src/custom_resource.rs index b455e3c96..238206302 100644 --- a/kube-derive/src/custom_resource.rs +++ b/kube-derive/src/custom_resource.rs @@ -141,6 +141,21 @@ pub(crate) fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStrea .to_compile_error() } } + + if let Data::Struct(struct_data) = &derive_input.data { + if let syn::Fields::Named(fields) = &struct_data.fields { + for field in &fields.named { + if let Some(attr) = field.attrs.iter().find(|attr| attr.path().is_ident("validated")) { + return syn::Error::new_spanned( + attr, + r#"#[cel_validation] macro should be placed before the #[derive(JsonSchema)] macro to use with #[validated]"#, + ) + .to_compile_error(); + } + } + } + } + let kube_attrs = match KubeAttrs::from_derive_input(&derive_input) { Err(err) => return err.write_errors(), Ok(attrs) => attrs, @@ -544,6 +559,9 @@ fn generate_hasspec(spec_ident: &Ident, root_ident: &Ident, kube_core: &Path) -> struct CELAttr { rule: String, message: Option, + message_expression: Option, + field_path: Option, + reason: Option, } pub(crate) fn cel_validation(_: TokenStream, input: TokenStream) -> TokenStream { @@ -560,8 +578,9 @@ pub(crate) fn cel_validation(_: TokenStream, input: TokenStream) -> TokenStream .to_compile_error(); } + // Create a struct name for added validation rules, following the original struct name + "Validation" let struct_name = ast.ident.to_string() + "Validation"; - let anchor = Ident::new(&struct_name, Span::call_site()); + let validation_struct = Ident::new(&struct_name, Span::call_site()); let mut validations: Vec = vec![]; @@ -584,18 +603,48 @@ pub(crate) fn cel_validation(_: TokenStream, input: TokenStream) -> TokenStream .iter() .filter(|attr| attr.path().is_ident("validated")) { - let CELAttr { rule, message } = match CELAttr::from_attributes(&vec![attr.clone()]) { + let CELAttr { + rule, + message, + field_path, + message_expression, + reason, + } = match CELAttr::from_attributes(&vec![attr.clone()]) { Ok(cel) => cel, Err(e) => return e.with_span(&attr.meta).write_errors(), }; + if let (Some(_), Some(_)) = (&message, &message_expression) { + return syn::Error::new_spanned( + attr, + r#"Either message or message_expression should be specified at once"#, + ).to_compile_error() + } let message = if let Some(message) = message { - quote! { "message": #message } + quote! { "message": #message, } + } else { + quote! {} + }; + let field_path = if let Some(field_path) = field_path { + quote! { "fieldPath": #field_path, } + } else { + quote! {} + }; + let message_expression = if let Some(message_expression) = message_expression { + quote! { "messageExpression": #message_expression, } + } else { + quote! {} + }; + let reason = if let Some(reason) = reason { + quote! { "reason": #reason, } } else { quote! {} }; rules.push(quote! {{ "rule": #rule, #message + #field_path + #message_expression + #reason },}); } @@ -603,6 +652,13 @@ pub(crate) fn cel_validation(_: TokenStream, input: TokenStream) -> TokenStream continue; } + field.attrs = field + .attrs + .clone() + .into_iter() + .filter(|attr| !attr.path().is_ident("validated")) + .collect(); + let validation_method_name = field.ident.as_ref().map(|i| i.to_string()).unwrap_or_default(); let name = Ident::new(&validation_method_name, Span::call_site()); let field_type = &field.ty; @@ -638,9 +694,9 @@ pub(crate) fn cel_validation(_: TokenStream, input: TokenStream) -> TokenStream } quote! { - struct #anchor {} + struct #validation_struct {} - impl #anchor { + impl #validation_struct { #(#validations)* }