Documentation
¶
Overview ¶
cel.go contains CEL runtime integration: expression compilation/evaluation and error classification for distinguishing retryable "data pending" errors from expression bugs.
Custom CEL extension functions (plural, .ready(), simpleSchema.toOpenAPI(), .updated(), .dependencies()) are defined in celfuncs.go.
Performance model: CEL environments and programs are compiled eagerly when a Graph spec is first seen (or when it changes). The reconcile loop only evaluates pre-compiled programs — no compilation happens during the resource walk.
Per-instance mutable state (scope, forEach state) is tracked separately in instanceState, keyed by namespace/revision-name.
celfuncs.go defines the custom CEL extension functions registered into the compilation environment: plural(), .ready(), simpleSchema.toOpenAPI(), .updated(), and .dependencies(). These are pure function factories — they produce cel.EnvOption values consumed by CompileGraphSpec and compileDeferredExpressions.
deferred.go validates deferred ($${...}) expressions at compile time. When a node's template produces a child Graph CR, the compiler extracts the child's scope (node IDs + forEach variables) and validates deferred expressions against it. Parse errors and undeclared references are caught at the parent's compile time instead of deferring to the child controller.
Per 004-compilation.md § Recursive Compilation: "The compiler handles arbitrary depth by recursing. $${expr} is depth 1... The mechanism is general; the current patterns are not."
fieldcompat.go validates that CEL expression return types match schema field types.
fieldpath.go extracts referenced field paths from compiled CEL expressions.
The design (005-reconciliation.md) commits to field-path-level hashing: "At graph compilation, the controller walks each compiled expression's AST to extract reference chains — sequences of select operations rooted at a scope variable." This file implements that extraction.
The FieldPath type itself lives in graph/ (it's a pure data type). This file contains the CEL AST walking that produces FieldPaths — compiler logic that depends on cel-go/common/ast.
infertype.go infers DeclTypes from template structure without schema information.
readyrewrite.go implements CEL AST rewrites that substitute member-function calls on scope variables with map lookups on reserved scope variables.
Two rewrites use the same mechanism:
`<wk_id>.ready()` → `__kroNodeReady["<wk_id>"]` for Watch node IDs. This lets `.ready()` on a Watch reflect the node's own readyWhen verdict — including when the collection is empty. Per 001-graph.md § readyWhen.
`<ident>.dependencies()` → `__kroDeps["<ident>"]` for all node IDs. Per 001-graph.md § propagateWhen.
Both use rewriteMemberCallToMapLookup — a generic AST walker parameterized by the function name to match, the ID set to match against, and the reserved variable to redirect to.
timesolver.go implements compile-time detection of solvable time comparison expressions and runtime threshold evaluation for precise requeue scheduling.
When a CEL expression like `time.now() - timestamp(X) >= duration('2h')` evaluates to false, the solver computes the exact moment it will become true (X + 2h) and returns the duration until that moment as a requeue hint.
Detection happens at compile time via AST pattern matching. Evaluation of the threshold sub-expression happens at runtime against the current scope.
typecache.go tracks the global schema epoch for compilation staleness. Per 004-compilation.md § Type Cache: "The cache tracks a single global generation counter." Any schema change (CRD installed, updated, removed) advances it. Staleness is one integer comparison: current generation exceeds the artifact's recorded generation.
typerefine.go narrows definition and iterator types using compiled expression return types.
typesource.go resolves OpenAPI schemas and builds CEL type declarations.
Index ¶
- Constants
- Variables
- func ExtractLiteralGVK(tmpl map[string]any) *runtimeschema.GroupVersionKind
- func InferFieldType(path string, value any) *apiservercel.DeclType
- func InferObjectType(typeName string, tmpl map[string]any) *apiservercel.DeclType
- func InferStringType(s string) *apiservercel.DeclType
- func StripDeferralLevel(v any) any
- type ChildScope
- type CompiledGraph
- type SchemaGeneration
- type TimeComparison
- type TypeSource
Constants ¶
const ReservedDepsMapVar = "__kroDeps"
ReservedDepsMapVar is the CEL scope variable carrying per-node dependency lists for the .dependencies() member function. Maps node ID → list of dependency scope values. Populated before propagateWhen evaluation. Per 001-graph.md § propagateWhen.
const ReservedNodeReadyVar = "__kroNodeReady"
ReservedNodeReadyVar is the CEL scope variable carrying per-Watch readiness verdicts. Declared as map(string, bool) in the env; populated before each prg.Eval from instanceState. The leading underscore distinguishes it from user-visible identifiers.
Variables ¶
var ErrDependencyError = dagpkg.ErrCircularDependency
ErrDependencyError is an alias for dag.ErrCircularDependency. Kept for backward compatibility with status.go's error classification.
var ErrInvalidExpression = errors.New("invalid expression")
ErrInvalidExpression indicates that one or more CEL expressions in the Graph spec failed to compile. This is a permanent error until the spec is fixed.
Functions ¶
func ExtractLiteralGVK ¶
func ExtractLiteralGVK(tmpl map[string]any) *runtimeschema.GroupVersionKind
ExtractLiteralGVK extracts a GVK from a template if apiVersion and kind are literal strings (not CEL expressions). Returns nil if either is missing or contains an expression.
func InferFieldType ¶
func InferFieldType(path string, value any) *apiservercel.DeclType
inferFieldType determines the CEL type of a template value.
func InferObjectType ¶
func InferObjectType(typeName string, tmpl map[string]any) *apiservercel.DeclType
inferObjectType builds a DeclType from a template map. Each key becomes a typed field. Nested maps produce nested ObjectTypes with path-based naming.
func InferStringType ¶
func InferStringType(s string) *apiservercel.DeclType
inferStringType classifies a string value for type inference:
- Pure literal (no ${...}): string
- Standalone expression (${expr} is the entire string): dyn
- Embedded expression (text around ${expr}): string (interpolation always produces string)
func StripDeferralLevel ¶
stripDeferralLevel walks a value tree and strips one $ from every $${...} pattern in string values. Used to produce the child Graph's spec from the parent's template: $${expr} → ${expr}, $$${expr} → $${expr}, ${expr} → ${expr} (unchanged).
Types ¶
type ChildScope ¶
type ChildScope struct {
NodeIDs []string // child node IDs (from spec.nodes[].id)
ForEachVars []string // child forEach variable names
}
ChildScope holds the identifiers visible in a child Graph's CEL environment.
func ExtractChildScopeFromBody ¶
func ExtractChildScopeFromBody(body map[string]any) ChildScope
extractChildScopeFromBody extracts the child Graph's scope (node IDs + forEach variables) from a template body that produces a Graph CR. Returns an empty scope if the body is not a Graph CR or if the child's node list can't be determined statically.
type CompiledGraph ¶
type CompiledGraph struct {
Programs map[string]cel.Program // expression string → compiled program
Topology *dagpkg.Topology // shared DAG structure (immutable after BuildDAG)
UnresolvedGVKs []schema.GroupVersionKind // GVKs that fell back to dyn (triggers recompilation on CRD install)
// collectionIDs captures the set of Watch node IDs in this spec.
// Used by the dynamic-compile fallback to apply the same
// `<wk_id>.ready()` AST rewrite that the eager-compile path does —
// expressions that reach the dynamic path (cross-revision
// finalization, forEach-finalizer synthesis, ad-hoc test evals)
// must honor the same rewrite, otherwise empty-Watch `.ready()`
// reverts to vacuously-true on that path.
CollectionIDs map[string]bool
// resourceSchemas maps node ID → resolved OpenAPI schema. Used at Eval
// time to wrap scope entries via UnstructuredToVal so schema-typed
// fields (e.g. Secret data values declared format:"byte") arrive in
// CEL as their declared runtime types (types.Bytes, not types.String).
// Mirrors upstream pkg/runtime/node_context.go buildContext.
ResourceSchemas map[string]*spec.Schema
// TimeComparisons maps expression strings to their time-solving metadata.
// Only populated for expressions matching solvable patterns (e.g.,
// `time.now() - timestamp(X) >= duration('2h')`). Used at eval time to
// compute precise requeue durations when such expressions return false.
TimeComparisons map[string]*TimeComparison
// contains filtered or unexported fields
}
CompiledGraph holds the immutable compilation artifacts for a Graph spec. All fields are derived from the spec. The DAG topology, CEL programs, and CEL environment are all immutable after construction — cel.Program is thread-safe by the CEL spec, and BuildDAG produces a read-only topology. Per-instance concrete node bodies are assembled separately via assembleDAG.
func CompileGraphSpec ¶
func CompileGraphSpec(spec *graph.GraphSpec, typeInfo *TypeSource) (*CompiledGraph, error)
CompileGraphSpec builds a typed CEL environment, eagerly compiles every expression, and builds the dependency graph. Returns a CompiledGraph ready for sharing across multiple instances.
The typeInfo parameter carries resolved types from ResolveNodeTypes. When nil, all nodes fall back to dyn.
func (*CompiledGraph) Env ¶
func (c *CompiledGraph) Env() *cel.Env
Env returns the CEL environment for use in tests and downstream compilation.
func (*CompiledGraph) SolveTimeComparison ¶
SolveTimeComparison evaluates a time comparison's threshold against the current scope and returns how long until the comparison becomes true. Returns 0 if the threshold is already past or evaluation fails.
func (*CompiledGraph) WrapScope ¶
func (c *CompiledGraph) WrapScope(scope map[string]any) map[string]any
wrapScope returns an activation map where scope entries for nodes with resolved OpenAPI schemas are wrapped via UnstructuredToVal. Without this, a Secret's data.key (declared format:"byte" in the OpenAPI spec) enters CEL as a raw base64 string, so string(secret.data.key) is an identity op rather than a decode. After wrapping, the runtime value conveys its declared type — schema-aware type conversion happens at field-access time.
Wrapping is shallow and per-call: the original scope map is never mutated, so hash inputs and serialization paths still see plain map[string]any values.
Entries without a schema (definitions, unresolved CRDs, forEach iterators) pass through unchanged — CEL's default type adapter handles them as before.
Mirrors upstream's pkg/runtime/node_context.go buildContext behavior.
type SchemaGeneration ¶
type SchemaGeneration struct {
// contains filtered or unexported fields
}
SchemaGeneration tracks the global schema epoch for compilation staleness. Per 004-compilation.md § Type Cache: "The cache tracks a single global generation counter." Any schema change (CRD installed, updated, removed) advances it. Staleness is one integer comparison: current generation exceeds the artifact's recorded generation.
Thread-safe: the generation counter is atomic. No locking required for the hot-path staleness check (one Load per reconcile per graph).
func NewSchemaGeneration ¶
func NewSchemaGeneration() *SchemaGeneration
NewSchemaGeneration creates a schema generation tracker.
func (*SchemaGeneration) AdvanceGeneration ¶
func (sg *SchemaGeneration) AdvanceGeneration()
AdvanceGeneration increments the generation counter. Called when a schema change is detected (CRD install, update, or removal). All artifacts with a recorded generation less than the new value are stale.
func (*SchemaGeneration) Generation ¶
func (sg *SchemaGeneration) Generation() int64
Generation returns the current schema generation. Artifacts compare their recorded generation against this to determine staleness.
type TimeComparison ¶
type TimeComparison struct {
// ThresholdProgram evaluates to the timestamp at which the comparison
// becomes true. For `time.now() - E >= D`, this computes `E + D`.
ThresholdProgram cel.Program
}
TimeComparison holds compile-time metadata for a solvable time expression. The threshold program evaluates to the timestamp at which the comparison flips.
type TypeSource ¶
type TypeSource struct {
// ResourceSchemas maps node ID → OpenAPI schema for nodes with resolved GVKs.
// Does NOT include forEach nodes (those stay dyn in outer scope).
ResourceSchemas map[string]*spec.Schema
// DefinitionTypes maps node ID → inferred DeclType for definition nodes.
DefinitionTypes map[string]*apiservercel.DeclType
// ForEachDefinitions tracks definition nodes that have forEach (scope is list, not object).
ForEachDefinitions map[string]bool
// UntypedIDs are node/variable identifiers declared as dyn.
UntypedIDs []string
// unresolvedGVKs are GVKs that had literal apiVersion/kind but whose schema
// could not be resolved (CRD not yet installed). Used by the CRD watch to
// detect when recompilation is needed.
UnresolvedGVKs []runtimeschema.GroupVersionKind
// DynamicGVKNodes lists node IDs whose apiVersion or kind contains a CEL
// expression. Per 004-compilation.md § Deferred Types: the type is
// unknowable until runtime.
DynamicGVKNodes []string
// contains filtered or unexported fields
}
TypeSource holds all resolved type information for building the CEL environment. Populated during compilation phases 1 (schema resolution) and 2 (definition inference).
func ResolveNodeTypes ¶
func ResolveNodeTypes(nodes []graph.Node, schemaResolver resolver.SchemaResolver) *TypeSource
ResolveNodeTypes resolves types for all nodes in the spec. It resolves OpenAPI schemas for resource nodes and infers types for definition nodes. The resolver may be nil — all resource nodes fall back to dyn.
func (*TypeSource) PrePopulateSchema ¶
func (ts *TypeSource) PrePopulateSchema(nodeID string, gvk runtimeschema.GroupVersionKind, schemaResolver resolver.SchemaResolver)
PrePopulateSchema injects a resolved schema for a dynamic GVK node. Called by the compilation caller (not the compiler) when the node's GVK was resolved on a previous reconcile. The compiler sees the pre-populated schema and types the node like any static resource node.
Per 004-compilation.md § Deferred Types: "The caller resolves the schema for the recorded GVK and pre-populates the type source before calling the compiler."