Skip to main content

kube_cel/
lib.rs

1//! Kubernetes CEL extension functions for the `cel` crate.
2//!
3//! This crate provides the Kubernetes-specific CEL (Common Expression Language) functions
4//! that are available in Kubernetes CRD validation rules, built on top of the `cel` crate.
5//!
6//! # Usage
7//!
8//! Register the compiled-in functions onto a [`cel::Context`] via the
9//! [`KubeCelExt`] extension trait:
10//!
11//! ```rust
12//! use kube_cel::{cel, KubeCelExt};
13//!
14//! let ctx = cel::Context::default().with_all();
15//! # let _ = ctx;
16//! ```
17//!
18//! See [`KubeCelExt`] for the borrowed-context form and the
19//! function-group → upstream-source table.
20//!
21//! # Version coherence
22//!
23//! This crate's public signatures use [`cel::Context`] and [`cel::Value`], so a
24//! `cel` version mismatch between your crate and `kube-cel` surfaces as a cryptic
25//! `Context` type mismatch. To avoid it, import `cel` **through** this crate
26//! rather than declaring a separate `cel` dependency:
27//!
28//! ```rust
29//! use kube_cel::cel; // re-export guaranteed to match kube-cel's `cel`
30//! # let _ = cel::Context::default();
31//! ```
32//!
33//! # Feature model
34//!
35//! Granularity is controlled at compile time through cargo features — there is
36//! no runtime per-library registration method. The `default` feature set enables
37//! every extension-function group. To narrow the surface you must disable the
38//! defaults explicitly, otherwise the listed features are simply added on top of
39//! the (already complete) default set and have no narrowing effect:
40//!
41//! ```toml
42//! # Only the string + list helpers:
43//! kube-cel = { version = "0.6", default-features = false, features = ["strings", "lists"] }
44//! ```
45//!
46//! The validation pipeline (CRD `x-kubernetes-validations`, VAP, static analysis)
47//! lives behind the `validation` feature (see below when it is enabled); it is
48//! **not** part of `default`.
49//!
50//! The `full` umbrella feature enables everything — all extension-function
51//! groups *and* the `validation` engine. Use it to restore the whole surface
52//! after narrowing, or to opt into validation alongside the default functions:
53//!
54//! ```toml
55//! kube-cel = { version = "0.6", features = ["full"] }
56//! ```
57//!
58//! # Versioning and stability
59//!
60//! kube-cel is pre-1.0 and **cannot reach 1.0 until the `cel` crate does** — its
61//! public surface exposes [`cel::Context`]/[`cel::Value`], and a crate cannot be
62//! stable while its public dependencies are not (Rust API Guidelines C-STABLE).
63//! After `cel` 1.0, kube-cel 1.x tracks `cel` 1.y; a `cel` major forces a
64//! kube-cel major. Two stability tiers: **Tier 1** (committed) is the
65//! registration surface — [`KubeCelExt`] and the `cel` re-export; **Tier 2**
66//! (evolving, `validation` feature) is the validation engine, whose surface may
67//! still change across pre-1.0 minors. See the README for details.
68#![cfg_attr(docsrs, feature(doc_cfg))]
69#![deny(missing_docs)]
70// The validation-pipeline section links to feature-gated items, so it is only
71// emitted when `validation` is enabled. Keeping it out of the always-compiled
72// `//!` block is what keeps `cargo doc --no-deps` (default features) free of
73// broken intra-doc links.
74#![cfg_attr(
75    feature = "validation",
76    doc = r#"
77# CRD Validation Pipeline (feature = `validation`)
78
79Compile and evaluate `x-kubernetes-validations` CEL rules client-side,
80without an API server.
81
82```toml
83kube-cel = { version = "0.6", features = ["validation"] }
84```
85
86```rust
87use kube_cel::Validator;
88use serde_json::json;
89
90let schema = json!({
91    "type": "object",
92    "x-kubernetes-validations": [
93        {"rule": "self.replicas >= 0", "message": "must be non-negative"}
94    ],
95    "properties": { "replicas": {"type": "integer"} }
96});
97
98let object = json!({"replicas": -1});
99let errors = Validator::new().validate(&schema, &object, None);
100assert_eq!(errors.len(), 1);
101```
102
103For repeated validation against the same schema, pre-compile with
104[`compile_schema`] and use [`Validator::validate_compiled`].
105"#
106)]
107
108/// Re-export of the [`cel`] crate, for version coherence (see the crate-level
109/// docs). Importing `cel` types via `kube_cel::cel` guarantees they match the
110/// `cel` version this crate was built against.
111pub use cel;
112
113// Source is grouped by the crate's two tiers (see the "Versioning and
114// stability" docs). Both are internal module trees; the public API is the flat
115// set of crate-root re-exports below, not the file layout.
116//
117// `functions/` — Tier 1, the Kubernetes CEL extension-function port (always on,
118// each library behind its own feature). Public entry point: `KubeCelExt`.
119mod ext;
120mod functions;
121
122// `validation/` — Tier 2, the client-side validation engine (`validation`
123// feature). Its public items are re-exported flatly below.
124#[cfg(feature = "validation")] mod validation;
125
126pub use ext::KubeCelExt;
127
128#[cfg(feature = "validation")]
129pub use crate::validation::{
130    ErrorKind, RootContext, ValidationError, Validator,
131    analysis::{
132        AnalysisWarning, ScopeContext, WarningKind, analyze_rule, check_rule_scope, estimate_rule_cost,
133    },
134    compilation::{CompilationError, CompilationResult, CompiledSchema, Rule, compile_schema},
135    defaults::apply_defaults,
136    validate, validate_compiled,
137    values::SchemaFormat,
138    vap::{
139        AdmissionRequest, CompiledVapExpression, GroupVersionKind, GroupVersionResource, VapError,
140        VapEvaluator, VapEvaluatorBuilder, VapExpression, VapResult,
141    },
142};
143
144/// Registers all compiled-in Kubernetes CEL extension functions into `ctx`.
145///
146/// Re-exported from [`functions`] so in-crate callers can keep writing
147/// `crate::register_all`.
148pub(crate) use functions::register_all;
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[allow(unused_imports)] use std::sync::Arc;
155
156    use cel::{Context, Program, Value};
157
158    #[allow(dead_code)]
159    fn eval(expr: &str) -> Value {
160        let ctx = Context::default().with_all();
161        Program::compile(expr).unwrap().execute(&ctx).unwrap()
162    }
163
164    #[test]
165    #[cfg(feature = "strings")]
166    fn test_integration_strings() {
167        assert_eq!(eval("'hello'.charAt(1)"), Value::String(Arc::new("e".into())));
168        assert_eq!(
169            eval("'HELLO'.lowerAscii()"),
170            Value::String(Arc::new("hello".into()))
171        );
172        assert_eq!(
173            eval("'  hello  '.trim()"),
174            Value::String(Arc::new("hello".into()))
175        );
176    }
177
178    #[test]
179    #[cfg(feature = "lists")]
180    fn test_integration_lists() {
181        assert_eq!(eval("[1, 2, 3].isSorted()"), Value::Bool(true));
182        assert_eq!(eval("[3, 1, 2].isSorted()"), Value::Bool(false));
183        assert_eq!(eval("[1, 2, 3].sum()"), Value::Int(6));
184    }
185
186    #[test]
187    #[cfg(feature = "sets")]
188    fn test_integration_sets() {
189        assert_eq!(eval("sets.contains([1, 2, 3], [1, 2])"), Value::Bool(true));
190        assert_eq!(eval("sets.intersects([1, 2], [2, 3])"), Value::Bool(true));
191    }
192
193    #[test]
194    #[cfg(feature = "regex_funcs")]
195    fn test_integration_regex() {
196        assert_eq!(
197            eval("'hello world'.find('[a-z]+')"),
198            Value::String(Arc::new("hello".into()))
199        );
200    }
201
202    #[test]
203    #[cfg(feature = "strings")]
204    fn test_dispatch_index_of_string() {
205        assert_eq!(eval("'hello world'.indexOf('world')"), Value::Int(6));
206        assert_eq!(eval("'hello'.indexOf('x')"), Value::Int(-1));
207    }
208
209    #[test]
210    #[cfg(feature = "lists")]
211    fn test_dispatch_index_of_list() {
212        assert_eq!(eval("[1, 2, 3].indexOf(2)"), Value::Int(1));
213        assert_eq!(eval("[1, 2, 3].indexOf(4)"), Value::Int(-1));
214    }
215
216    #[test]
217    #[cfg(feature = "strings")]
218    fn test_dispatch_last_index_of_string() {
219        assert_eq!(eval("'abcabc'.lastIndexOf('abc')"), Value::Int(3));
220    }
221
222    #[test]
223    #[cfg(feature = "lists")]
224    fn test_dispatch_last_index_of_list() {
225        assert_eq!(eval("[1, 2, 3, 2].lastIndexOf(2)"), Value::Int(3));
226    }
227
228    #[test]
229    #[cfg(feature = "format")]
230    fn test_integration_format() {
231        assert_eq!(
232            eval("'hello %s'.format(['world'])"),
233            Value::String(Arc::new("hello world".into()))
234        );
235        assert_eq!(
236            eval("'%d items'.format([5])"),
237            Value::String(Arc::new("5 items".into()))
238        );
239    }
240
241    #[test]
242    #[cfg(feature = "semver_funcs")]
243    fn test_integration_semver() {
244        assert_eq!(eval("isSemver('1.2.3')"), Value::Bool(true));
245        assert_eq!(eval("semver('1.2.3').major()"), Value::Int(1));
246        assert_eq!(
247            eval("semver('2.0.0').isGreaterThan(semver('1.0.0'))"),
248            Value::Bool(true)
249        );
250    }
251}