Skip to content

Commit

Permalink
Naive CEL validation example
Browse files Browse the repository at this point in the history
for #1367 and kube-rs/website#53

Signed-off-by: clux <[email protected]>
  • Loading branch information
clux committed Dec 14, 2023
1 parent 3a4f724 commit d693433
Showing 1 changed file with 72 additions and 24 deletions.
96 changes: 72 additions & 24 deletions examples/crd_derive_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,11 @@ pub struct FooSpec {
#[serde(default)]
#[schemars(schema_with = "set_listable_schema")]
set_listable: Vec<u32>,
// Field with CEL validation
#[serde(default)]
#[schemars(schema_with = "cel_validations")]
cel_validated: Option<String>,
}

// 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!({
Expand All @@ -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()
}
Expand All @@ -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");
Expand Down Expand Up @@ -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!(),
}
Expand All @@ -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(())
}

Expand Down

0 comments on commit d693433

Please sign in to comment.