Feat: add a root-agnostic CEL engine for $( ) expressions
Part of kubevela/kubevela#7409
Description
Adds cel, an engine for CEL expressions written inside $( ) in a properties tree, and cel/template, which parses those fragments and walks the tree.
The engine knows no roots by name. A host declares the roots its expressions may read and passes a resolver for each one. EvalTree collects every read in the tree, calls each root's resolver once with all of its reads, and only then evaluates. Expressions never perform I/O, and every read is known before evaluation, so it can be type-checked and validated at admission.
engine, _ := cel.NewEngine(
cel.Root{Name: "source"},
cel.Root{Name: "context"},
)
out, err := engine.EvalTree(ctx, properties, map[string]cel.Resolver{
"source": cel.ResolverFunc(fetchSources), // gets every source.* read, with the property it feeds
"context": cel.Static(renderContext),
}, cel.TreeOptions{})
properties:
url: 'https://$(source.registry.host)/health'
replicas: '$(source.sizing.replicas)' # a lone expression keeps its type
owner: '$(has(context.labels.owner) ? context.labels.owner : "unassigned")'
What is in it
Engine | CEL environments per set of roots, compile and typed-environment caches, evaluation, reference extraction with has() / ternary guard analysis |
Plan | A tree parsed once, serving every stage: Reads(root) for dependencies and status, Expressions() for per-target typing, Check(permitted, checkers) for admission with a Checker per root (every fault at once), Eval(ctx, resolvers, opts) to resolve and evaluate; EvalTree is Plan then Eval |
Resolver, Read | A resolver answers all reads of one root; each Read carries the path and the property it feeds |
ErrNotReady | A resolver's "wait, not fail" signal, checked with errors.Is |
Unknown, TreeOptions.Unknown | A root with no value in this evaluation (a dry run); the host renders those reads |
Typed | Marks values a schema has already typed, so integral floats stay floats |
PlanJSON | Plans a JSON document once for its bytes: the same properties reach admission and every render, and a plan is read-only once built |
CheckTypes, Target, TypeMismatch, MayBeAbsent | Asserts each expression's result fits the schema where it lands: kind and element types, text embedding, dyn accepted, an unguarded possibly-missing read refused where the target is required. A bare read is judged by the kind its schema declares (TypeOptions.DeclaredKind), since CEL types CUE's number as double; KindFits exposes the rule |
Qualifier | Literal-argument member calls on a root, such as db.at("east"), answered from values the resolver delivered; refused on roots that do not declare them |
DeclType, OptionalPath, ElementsCompatible | CUE schema to CEL type, "can this path be absent under this schema", element-type compatibility |
template | $( ) parsing with $$( as an escape, tree Walk / Map, Reference rendering |
Only pure extension libraries are enabled (ext.Strings, ext.Lists). A test refuses any import of github.com/oam-dev/kubevela from this tree.
Why here
KubeVela already evaluates $( ) expressions in component, trait, policy and workflow step properties (pkg/definition/celexpr, pkg/sources). Step properties are substituted when the workflow is generated, because workflow cannot import kubevela. With the engine here, KubeVela can pass its resolvers to the workflow engine, and a step can be evaluated when it runs: fresh source values per step, and later roots native to the workflow such as step outputs.
Dependencies
github.com/google/cel-go v0.20.1 moves from indirect to direct, at the version already required. k8s.io/apiserver (for apiserver/pkg/cel) and cuelang.org/go are existing direct dependencies.
Testing
go test ./cel/...: engine, resolvers, guards, language behaviour, caches, typing, template parsing.make reviewablepasses.
Summary by cubic
Adds a root-agnostic CEL engine for $( ) expressions in a properties tree, so KubeVela can evaluate step properties when the step runs instead of when the workflow is generated. The engine knows no roots by name: a host declares the roots expressions may read and passes one Resolver per root, which answers every read before any evaluation, so expressions never perform I/O and every read is known statically.
Enginebuilds CEL environments per set of roots with caches, and extracts references withhas(), negation,&&/||guard, and ternary arm analysis.Planparses a tree once and serves every stage:Reads(root),Expressions(),Check(permitted, checkers), andEval(ctx, resolvers, opts);EvalTreeisPlanthenEval.CheckTypesverifies each expression's result fits the target CUE schema and refuses unguarded possibly-missing reads where the target is required;Qualifiersupports literal-argument member calls on roots that declare them.ErrNotReadysignals a wait;Unknownlets a host render reads of a root with no value in a dry run;TreeOptions.OnEvalErrorhands evaluation errors to the host.cel/templateparses$( )fragments with$$(as an escape and walks or rewrites the tree.github.com/google/cel-go v0.20.1becomes a direct dependency at the already-required version.