From d693433e7468cf62aa8699eb2138dec29e855ec2 Mon Sep 17 00:00:00 2001 From: clux Date: Thu, 14 Dec 2023 03:58:46 +0000 Subject: [PATCH 1/2] Naive CEL validation example for #1367 and https://github.com/kube-rs/website/pull/53 Signed-off-by: clux --- examples/crd_derive_schema.rs | 96 ++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/examples/crd_derive_schema.rs b/examples/crd_derive_schema.rs index 25efb32ff..824f5cac4 100644 --- a/examples/crd_derive_schema.rs +++ b/examples/crd_derive_schema.rs @@ -85,8 +85,11 @@ pub struct FooSpec { #[serde(default)] #[schemars(schema_with = "set_listable_schema")] set_listable: Vec, + // Field with CEL validation + #[serde(default)] + #[schemars(schema_with = "cel_validations")] + cel_validated: Option, } - // https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy fn set_listable_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { serde_json::from_value(serde_json::json!({ @@ -101,6 +104,18 @@ fn set_listable_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::sche .unwrap() } +// https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation-rules +fn cel_validations(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + serde_json::from_value(serde_json::json!({ + "type": "string", + "x-kubernetes-validations": [{ + "rule": "self != 'illegal'", + "message": "string cannot be illegal" + }] + })) + .unwrap() +} + fn default_value() -> String { "default_value".into() } @@ -127,24 +142,28 @@ async fn main() -> Result<()> { // Nullables defaults to `None` and only sent if it's not configured to skip. let bar = Foo::new("bar", FooSpec { ..FooSpec::default() }); let bar = foos.create(&PostParams::default(), &bar).await?; - assert_eq!(bar.spec, FooSpec { - // Nonnullable without default is required. - non_nullable: String::default(), - // Defaulting didn't happen because an empty string was sent. - non_nullable_with_default: String::default(), - // `nullable_skipped` field does not exist in the object (see below). - nullable_skipped: None, - // `nullable` field exists in the object (see below). - nullable: None, - // Defaulting happened because serialization was skipped. - nullable_skipped_with_default: default_nullable(), - // Defaulting did not happen because `null` was sent. - // Deserialization does not apply the default either. - nullable_with_default: None, - // Empty listables to be patched in later - default_listable: Default::default(), - set_listable: Default::default(), - }); + assert_eq!( + bar.spec, + FooSpec { + // Nonnullable without default is required. + non_nullable: String::default(), + // Defaulting didn't happen because an empty string was sent. + non_nullable_with_default: String::default(), + // `nullable_skipped` field does not exist in the object (see below). + nullable_skipped: None, + // `nullable` field exists in the object (see below). + nullable: None, + // Defaulting happened because serialization was skipped. + nullable_skipped_with_default: default_nullable(), + // Defaulting did not happen because `null` was sent. + // Deserialization does not apply the default either. + nullable_with_default: None, + // Empty listables to be patched in later + default_listable: Default::default(), + set_listable: Default::default(), + cel_validated: Default::default(), + } + ); // Set up dynamic resource to test using raw values. let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo"); @@ -190,10 +209,8 @@ async fn main() -> Result<()> { assert_eq!(err.code, 422); assert_eq!(err.reason, "Invalid"); assert_eq!(err.status, "Failure"); - assert_eq!( - err.message, - "Foo.clux.dev \"qux\" is invalid: spec.non_nullable: Required value" - ); + assert!(err.message.contains("clux.dev \"qux\" is invalid")); + assert!(err.message.contains("spec.non_nullable: Required value")); } _ => panic!(), } @@ -213,8 +230,39 @@ async fn main() -> Result<()> { assert_eq!(pres.spec.set_listable, vec![2, 3]); println!("{:?}", serde_json::to_value(pres.spec)); - delete_crd(client.clone()).await?; + // cel validation triggers: + let cel_patch = serde_json::json!({ + "apiVersion": "clux.dev/v1", + "kind": "Foo", + "spec": { + "cel_validated": Some("illegal") + } + }); + let cel_res = foos.patch("baz", &ssapply, &Patch::Apply(cel_patch)).await; + assert!(cel_res.is_err()); + match cel_res.err() { + Some(kube::Error::Api(err)) => { + assert_eq!(err.code, 422); + assert_eq!(err.reason, "Invalid"); + assert_eq!(err.status, "Failure"); + assert!(err.message.contains("Foo.clux.dev \"baz\" is invalid")); + assert!(err.message.contains("spec.cel_validated: Invalid value")); + assert!(err.message.contains("string cannot be illegal")); + } + _ => panic!(), + } + // cel validation happy: + let cel_patch_ok = serde_json::json!({ + "apiVersion": "clux.dev/v1", + "kind": "Foo", + "spec": { + "cel_validated": Some("legal") + } + }); + foos.patch("baz", &ssapply, &Patch::Apply(cel_patch_ok)).await?; + // all done + delete_crd(client.clone()).await?; Ok(()) } From 35fc48119a63e6b65a5b37cfba66bd82a17836e5 Mon Sep 17 00:00:00 2001 From: clux Date: Thu, 14 Dec 2023 04:01:24 +0000 Subject: [PATCH 2/2] fmt Signed-off-by: clux --- examples/crd_derive_schema.rs | 41 ++++++++++++++++------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/examples/crd_derive_schema.rs b/examples/crd_derive_schema.rs index 824f5cac4..8c58afacb 100644 --- a/examples/crd_derive_schema.rs +++ b/examples/crd_derive_schema.rs @@ -142,28 +142,25 @@ async fn main() -> Result<()> { // Nullables defaults to `None` and only sent if it's not configured to skip. let bar = Foo::new("bar", FooSpec { ..FooSpec::default() }); let bar = foos.create(&PostParams::default(), &bar).await?; - assert_eq!( - bar.spec, - FooSpec { - // Nonnullable without default is required. - non_nullable: String::default(), - // Defaulting didn't happen because an empty string was sent. - non_nullable_with_default: String::default(), - // `nullable_skipped` field does not exist in the object (see below). - nullable_skipped: None, - // `nullable` field exists in the object (see below). - nullable: None, - // Defaulting happened because serialization was skipped. - nullable_skipped_with_default: default_nullable(), - // Defaulting did not happen because `null` was sent. - // Deserialization does not apply the default either. - nullable_with_default: None, - // Empty listables to be patched in later - default_listable: Default::default(), - set_listable: Default::default(), - cel_validated: Default::default(), - } - ); + assert_eq!(bar.spec, FooSpec { + // Nonnullable without default is required. + non_nullable: String::default(), + // Defaulting didn't happen because an empty string was sent. + non_nullable_with_default: String::default(), + // `nullable_skipped` field does not exist in the object (see below). + nullable_skipped: None, + // `nullable` field exists in the object (see below). + nullable: None, + // Defaulting happened because serialization was skipped. + nullable_skipped_with_default: default_nullable(), + // Defaulting did not happen because `null` was sent. + // Deserialization does not apply the default either. + nullable_with_default: None, + // Empty listables to be patched in later + default_listable: Default::default(), + set_listable: Default::default(), + cel_validated: Default::default(), + }); // Set up dynamic resource to test using raw values. let gvk = GroupVersionKind::gvk("clux.dev", "v1", "Foo");