Static Analysis
One of kro's most powerful features is static analysis. When you create or update a ResourceGraphDefinition, kro performs extensive validation before accepting it. This analysis happens during RGD reconciliation - before any instances of your custom resource are created.
Static analysis catches errors early: invalid CEL syntax, references to non-existent resources or fields, type mismatches, circular dependencies, and schema violations. Without this upfront validation, these errors would only surface during reconciliation when kro attempts to create resources in your cluster. This means immediate feedback during development instead of runtime failures.
kro achieves this by integrating directly with Kubernetes OpenAPI schemas. For every resource in your graph - whether built-in types like Deployments or custom CRDs - kro fetches the schema and validates your templates against it. This ensures CEL expressions reference actual fields, output types match target field expectations, and your entire resource graph is structurally sound.
The Validation Process
When you create or update a ResourceGraphDefinition, kro performs validation in multiple stages:
Stage 1: Schema Validation
kro starts by validating your custom API schema:
- Parses your SimpleSchema definition - Reads the schema from
spec.schema - Converts to OpenAPI schema - Transforms SimpleSchema to standard OpenAPI format
- Validates the CRD spec - Ensures the generated CRD specification is valid
spec:
schema:
spec:
# kro validates this schema definition
replicas: integer | default=3
ports: array
labels: object
Stage 2: Status Schema Inference
For your custom API's status field, kro automatically infers the schema from CEL expressions:
status:
endpoint: ${service.status.loadBalancer.ingress[0].hostname}
replicas: ${deployment.status.availableReplicas}
kro inspects these expressions, determines their output types, and generates the OpenAPI schema for your status field automatically. This means you don't need to manually define status field types - kro figures them out from your CEL expressions.
Stage 3: Resource Naming Validation
kro validates that all resource IDs are valid CEL identifiers. Resource IDs must be valid variable names in CEL - no hyphens, special characters, or starting with numbers.
Why? CEL uses resource IDs as variables (like ${deployment.spec.replicas}). Invalid identifiers would cause CEL syntax errors.
# ✓ Valid IDs
resources:
- id: deployment
- id: configMap
- id: servicePrimary
# ✗ Invalid IDs
resources:
- id: my-deployment # Hyphens not allowed (CEL subtraction operator)
- id: 1st-service # Can't start with number
Stage 4: Resource Template Validation
For each resource template, kro:
- Validates basic Kubernetes object structure - Ensures the template has required fields like
apiVersion,kind, andmetadata - Resolves the OpenAPI schema - Gets the schema from the API server for the resource type. This works for both built-in Kubernetes resources (like Deployments and Services) and Custom Resource Definitions installed in your cluster.
- Extracts CEL expressions and determines expected types - For each field in the template:
- If the field contains a CEL expression: kro extracts the expression and determines what type the target field expects based on the OpenAPI schema
- If the field is a literal value: kro performs standard OpenAPI validation, just like the kube-api-server does
resources:
- id: deployment
template:
apiVersion: apps/v1 # kro fetches Deployment schema
kind: Deployment
# ... validates template against Deployment schema
Stage 5: AST Analysis and Dependency Graph Building
kro analyzes the Abstract Syntax Tree (AST) of all CEL expressions to understand how they reference each other:
- Parses CEL expressions into ASTs - Converts each expression into its abstract syntax tree representation
- Analyzes references - Identifies what each expression references (schema fields, other resources, functions)
- Validates references - Checks that referenced resources exist in the DAG and functions are declared
- Builds the dependency graph - Creates a directed acyclic graph showing which resources depend on which
- Detects circular dependencies - Identifies any cycles in the dependency graph
At this stage, kro already knows if you're referencing something that doesn't exist or using undeclared functions, and has detected any circular dependencies. See CEL AST Parsing and Dependency Detection for technical details.
Stage 6: Expression Type Checking
With expressions extracted and the dependency graph built, kro now performs comprehensive type checking on each CEL expression:
-
Type-checks the expression - kro uses CEL's type checker to validate the expression against the typed environment containing all resource schemas. This verifies that all field accesses, function calls, and operations are valid and type-safe.
-
Infers the expression's output type - Based on the type checking results, kro determines what type the expression will produce. For
${schema.spec.replicas}, kro infers an integer. For${schema.spec.name + "-deployment"}, kro infers a string. -
Validates type compatibility - kro compares the inferred output type against the expected type (determined earlier from the target field's OpenAPI schema). First, it tries CEL's built-in type assignability check. If that fails, it performs deep structural compatibility checking, which handles complex cases like map/struct conversions and subset validation. See Type Compatibility Deep Dive for technical details.
Example:
# Expression: ${schema.spec.replicas}
# Inferred output type: integer (from schema.spec definition)
# Expected type: integer (from Deployment.spec.replicas schema)
# Result: ✓ Compatible
Stage 7: Condition Expression Validation
kro validates special condition expressions used in resource lifecycle control:
- Validates
readyWhenexpressions - Ensures readiness conditions are valid CEL expressions that return boolean values - Validates
includeWhenexpressions - Ensures conditional inclusion expressions are valid CEL expressions that return boolean values
These conditions control when resources are considered ready and whether they should be created at all, so they must always return true or false.
Stage 8: RGD Activation
If all validation stages pass, kro activates the ResourceGraphDefinition:
- Infers topological order - Computes the order in which resources will be created based on the dependency graph
- Registers the CRD - Creates the Custom Resource Definition in the cluster for your new API
- Starts the microcontroller - Registers the controller that will reconcile instances of your custom resource
- Begins serving instances - Your ResourceGraphDefinition is now ready to accept instance creation requests
At this point, the RGD is fully validated and operational. When users create instances of your custom API, kro will orchestrate the resources according to the validated graph.
CEL AST Parsing and Dependency Detection
During Stage 5, kro parses every CEL expression into an Abstract Syntax Tree (AST) and analyzes how expressions reference each other. This enables kro to build a complete dependency graph and detect issues before any resources are created.
How AST Parsing Works
When kro encounters a CEL expression like ${deployment.spec.replicas}, it:
-
Parses the expression into an AST - The CEL parser breaks down the expression into its component parts: an identifier (
deployment), a field access (.spec), and another field access (.replicas). -
Walks the AST to find references - kro traverses the tree to identify what the expression references:
- Root identifiers (
schema,deployment,configmap, etc.) - Field accesses (
.spec,.data,.status) - Function calls (
size(),string(), etc.) - Operators (
+,*,?, etc.)
- Root identifiers (
-
Validates references exist - For each identifier found:
- If it's
schema, kro validates the field path exists in your custom schema - If it's a resource ID, kro checks that resource is defined in the DAG
- If it's a function, kro validates it's a declared CEL function
- If it's
-
Builds dependency edges - When resource B references resource A, kro adds an edge A → B in the dependency graph
Dependency Graph Construction
kro builds a directed acyclic graph (DAG) showing which resources depend on which:
resources:
- id: configmap
template:
data:
key: ${schema.spec.value} # depends on: schema
- id: deployment
template:
spec:
replicas: ${schema.spec.replicas} # depends on: schema
env:
- value: ${configmap.data.key} # depends on: configmap
kro builds this dependency graph:
schema
├─→ configmap
└─→ deployment
└─→ configmap
This graph determines:
- Creation order: configmap before deployment
- Evaluation dependencies: deployment expressions can only be evaluated after configmap exists
- Circular dependency detection: kro validates there are no cycles
Reference Validation
kro validates all references during AST analysis:
Resource references:
# ✓ Valid: deployment exists
value: ${deployment.spec.replicas}
# ✗ Invalid: typo in resource ID
value: ${deployent.spec.replicas} # Error: resource 'deployent' not found
Function references:
# ✓ Valid: size() is a CEL builtin
condition: ${schema.spec.items.size() > 0}
# ✗ Invalid: undefined function
condition: ${schema.spec.items.length()} # Error: function 'length' not declared
Circular Dependency Detection
kro detects circular dependencies by checking for cycles in the DAG:
# ✗ This fails validation
resources:
- id: serviceA
template:
spec:
port: ${serviceB.spec.targetPort} # A → B
- id: serviceB
template:
spec:
targetPort: ${serviceA.spec.port} # B → A (cycle!)
Error: circular dependency detected: serviceA → serviceB → serviceA
Type Compatibility Deep Dive
During Stage 6, kro performs deep structural type compatibility checking. This goes beyond simple type matching to handle complex Kubernetes schemas through recursive comparison of type structures.
Structural Compatibility
kro doesn't just check if types have the same name - it performs deep structural comparison:
For primitives: Checks kind equality (int, string, bool, etc.)
For lists: Recursively checks element type compatibility
# Expression returns: list<int>
# Field expects: list<int>
# Result: ✓ Compatible
# Expression returns: list<string>
# Field expects: list<int>
# Result: ✗ Incompatible - element types don't match
For maps: Recursively checks key and value type compatibility
# Expression returns: map<string, int>
# Field expects: map<string, int>
# Result: ✓ Compatible
# Expression returns: map<string, string>
# Field expects: map<string, int>
# Result: ✗ Incompatible - value types don't match
For structs: Validates that the output struct is a subset of the expected struct (subset semantics)
# Expression returns: {name: string, replicas: int}
# Field expects: {name: string, replicas: int}
# Result: ✓ Compatible (exact match)
# Expression returns: {name: string}
# Field expects: {name: string, replicas: int}
# Result: ✓ Compatible (output is subset - missing fields are OK)
# Expression returns: {name: string, replicas: int, extra: string}
# Field expects: {name: string, replicas: int}
# Result: ✗ Incompatible (output has extra field 'extra' not in expected)
Map/Struct Compatibility
Kubernetes often treats maps and structs interchangeably (like labels, annotations, data fields). kro handles this intelligently:
Map → Struct assignment:
# Expression: ${schema.spec.labels} (type: map<string, string>)
# Target field: labels (type: struct with string fields)
# kro validates: map keys are strings, map values match struct field types
# Result: ✓ Compatible if all struct fields accept strings
Struct → Map assignment:
# Expression: ${configmap.data} (type: struct with dynamic fields)
# Target field: data (type: map<string, string>)
# kro validates: all struct fields are string-compatible
# Result: ✓ Compatible if struct → map conversion is valid
Nested Field Type Checking
kro validates types at any depth by recursively walking the type structure:
spec:
template:
spec:
containers:
- env:
- name: PORT
# Expression returns: int
# Field path: spec.template.spec.containers[].env[].value
# Field expects: string
# kro resolves the full nested path and checks compatibility
value: ${schema.spec.port} # ✗ Type error: int → string
PreserveUnknownFields Handling
For fields with x-kubernetes-preserve-unknown-fields: true, kro uses permissive validation:
# ConfigMap.data has PreserveUnknownFields
configmap:
data:
# kro cannot validate structure at build time
# Any expression type is accepted
DATABASE_URL: ${schema.spec.dbUrl}
PORT: ${string(schema.spec.port)}
kro still validates:
- Expression syntax is correct
- Referenced resources exist
- Types are internally consistent
But it cannot validate:
- Field names are correct
- Field types match (since schema is unknown)
Next Steps
- Resource Basics - See how templates are validated
- CEL Expressions - Learn CEL type system
- SimpleSchema - Define typed schemas