kube_workflow/kube_workflow.rs
1//! The headline use case: client-side CRD validation inside a kube-rs workflow.
2//!
3//! A controller (or an admission webhook) often builds an object and is about to
4//! `apply`/`patch` it to the cluster. Running the CRD's `x-kubernetes-validations`
5//! rules *before* the round-trip catches invalid objects locally, surfaces a clear
6//! reason on the resource status, and avoids a guaranteed apiserver rejection.
7//!
8//! This example is intentionally dependency-light: it mocks the two things a real
9//! controller would *fetch* — the CRD's OpenAPI schema and the object under
10//! reconciliation — with `serde_json`, and marks each spot where a `kube::Api`
11//! call would slot in with a comment. That keeps `cargo run` fast and free of the
12//! heavy `kube`/`k8s-openapi` build, while showing exactly where validation fits.
13//!
14//! Run with: `cargo run --example kube_workflow --features validation`
15
16use kube_cel::Validator;
17use serde_json::{Value, json};
18
19fn main() {
20 // In a real controller this comes from the cluster, e.g.:
21 // use kube::{Api, Client};
22 // use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
23 // let crds: Api<CustomResourceDefinition> = Api::all(client);
24 // let crd = crds.get("widgets.example.com").await?;
25 // let schema = crd.spec.versions[0].schema.unwrap().open_api_v3_schema.unwrap();
26 // Here we mock that fetched OpenAPI v3 schema directly.
27 let schema = fetch_crd_schema();
28
29 let validator = Validator::new();
30
31 // A controller reconcile loop typically builds a *desired* object and applies
32 // it. We show two desired states: one valid, one that violates the CRD rules.
33 let desired_ok = json!({
34 "spec": { "replicas": 3, "image": "registry.example.com/widget:1.4.2" }
35 });
36 let desired_bad = json!({
37 "spec": { "replicas": 0, "image": "widget:latest" }
38 });
39
40 for (label, desired) in [("valid desired", &desired_ok), ("invalid desired", &desired_bad)] {
41 println!("=== reconcile: {label} ===");
42
43 // Client-side gate, run BEFORE touching the apiserver.
44 let errors = validator.validate(&schema, desired, None);
45
46 if errors.is_empty() {
47 println!(" validation passed -> apply to cluster");
48 // The real apply happens only on the happy path:
49 // let widgets: Api<Widget> = Api::namespaced(client, "default");
50 // widgets.patch("my-widget", &PatchParams::apply("my-controller"),
51 // &Patch::Apply(desired)).await?;
52 } else {
53 println!(" validation failed -> skip apply, record on status:");
54 for err in &errors {
55 println!(" [{}] {}", err.field_path, err.message);
56 }
57 // Instead of a doomed apply, the controller would surface the reason:
58 // widgets.patch_status("my-widget", &pp,
59 // &Patch::Merge(json!({"status": {"validationError": err.message}}))).await?;
60 }
61 println!();
62 }
63}
64
65/// Stand-in for the CRD schema a controller fetches from the apiserver.
66fn fetch_crd_schema() -> Value {
67 json!({
68 "type": "object",
69 "properties": {
70 "spec": {
71 "type": "object",
72 "x-kubernetes-validations": [
73 {"rule": "self.replicas >= 1", "message": "replicas must be at least 1"},
74 {
75 "rule": "!self.image.endsWith(':latest')",
76 "message": "pin the image to a concrete tag, not ':latest'"
77 }
78 ],
79 "properties": {
80 "replicas": {"type": "integer"},
81 "image": {"type": "string"}
82 }
83 }
84 }
85 })
86}