Skip to main content

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}