Introduces KEP proposal for defkit, a Go SDK that enables platform engineers to author X-Definitions using native Go code instead of CUE. Key proposed features: - Fluent builder API for Component, Trait, Policy, and WorkflowStep definitions - Transparent Go-to-CUE compilation - IDE support with autocomplete and type checking - Schema-agnostic resource construction - Collection operations (map, filter, dedupe) - Composable health and status expressions - Addon integration with godef/ folder support - Module dependencies for definition sharing via go get Signed-off-by: Anoop Gopalakrishnan <agopalakrishnan@guidewire.com>
74 KiB
KEP: Definition Kit (defkit) - Go SDK for X-Definition Authoring
Summary
Definition Kit (defkit) is a Go SDK that enables platform engineers to author KubeVela X-Definitions using native Go code instead of CUE. The SDK compiles Go to CUE transparently, providing full IDE support while maintaining compatibility with the KubeVela controller.
Status: This document is a design proposal. While the core architecture and goals are established, specific API names, method signatures, and implementation details may evolve as we proceed with implementation and incorporate community feedback.
Motivation
Goals
- Author X-Definitions in Go - Write ComponentDefinition, TraitDefinition, PolicyDefinition, and WorkflowStepDefinition using fluent Go APIs
- Transparent CUE compilation - Go code compiles to CUE automatically; developers never see or write CUE
- Full IDE support - Autocomplete, type checking, and inline documentation
- Testable definitions - Unit test definitions using standard Go testing frameworks
- Schema-agnostic resource construction - Support any Kubernetes resource without coupling to K8s versions
- Easy distribution via Go modules - Share and version X-Definitions as standard Go packages, enabling
go getfor platform capabilities
Non-Goals
- Multi-language support in initial release (Go only)
- Replacing CUE as the controller's internal engine
- Runtime CUE evaluation in the SDK
Proposal
Why Go for X-Definitions?
1. Familiar Tooling - Platform engineers already use Go for controllers, operators, and CLI tools. Writing definitions in the same language eliminates context switching and leverages existing skills.
2. IDE Experience - Full autocomplete, type checking, go-to-definition, and inline documentation. No special CUE plugins required.
3. Standard Distribution - Definitions become Go packages that can be versioned, shared via go get, and composed like any other library. No custom registries or tooling needed.
4. Testability - Use standard Go testing frameworks (go test, testify, gomega) to unit test definitions before deployment. Mock contexts enable testing without a cluster.
5. Compile-Time Safety - Catch errors at compile time rather than at deployment time. Invalid field names, type mismatches, and missing required parameters are caught immediately.
Fluent API Design
defkit provides a fluent builder API where parameters are defined inline and bound to local variables. All types are in a single defkit package for simplicity:
func WebserviceDefinition() *defkit.ComponentDefinition {
// Parameters defined as local variables within the function
image := defkit.String("image").Required()
replicas := defkit.Int("replicas").Default(3).Min(1).Max(100)
cpu := defkit.String("cpu")
return defkit.NewComponent("webservice").
Description("A production-ready web service").
Workload("apps/v1", "Deployment").
Params(image, replicas, cpu).
Template(func(tpl *defkit.Template) {
vela := defkit.VelaCtx()
deploy := defkit.NewResource("apps/v1", "Deployment").
Set("spec.replicas", replicas).
Set("spec.selector.matchLabels[app.oam.dev/component]", vela.Name()).
Set("spec.template.spec.containers[0].name", vela.Name()).
Set("spec.template.spec.containers[0].image", image).
// Optional parameters use fluent conditional
SetIf(cpu.IsSet(), "spec.template.spec.containers[0].resources.limits.cpu", cpu)
tpl.Output(deploy)
})
}
Key design insight: Parameters are Go variables that can be used directly in both definition and template. No string lookups, no dual references. The variable image carries both its schema definition AND serves as the accessor.
This reads naturally: "Define image, replicas, and cpu as parameters. Create a webservice component using them."
Note: Parameters are defined as local variables within the function (not package-level) to ensure proper encapsulation and test isolation. The template function receives a Template context (named tpl to avoid confusion with Go's context package). Runtime context like component name and namespace is accessed via defkit.VelaCtx().
Common Patterns
The SDK provides typed helpers for common definition patterns:
| Pattern | Go API | Example |
|---|---|---|
| Required parameter | defkit.String("image").Required() |
image must be provided |
| Default value | defkit.Int("replicas").Default(3) |
replicas defaults to 3 |
| Validation | defkit.Int("replicas").Min(1).Max(100) |
replicas between 1-100 |
| Enums | defkit.Enum("policy", "Always", "Never") |
policy with allowed values |
| Optional object | defkit.Object("persistence", ...).Optional() |
persistence block |
| Lists | defkit.List("args", defkit.String()) |
args list |
Runtime Context Access
CUE templates access runtime values (like deployment status) through well-known paths. The Go SDK provides fluent methods via defkit.VelaCtx() that generate these paths:
vela := defkit.VelaCtx()
vela.Name() // → context.name
vela.Namespace() // → context.namespace
vela.AppName() // → context.appName
vela.AppRevision() // → context.appRevision
vela.ClusterVersion().Minor() // → context.clusterVersion.minor
Runtime context is accessed separately from template context, which focuses on output generation. Health policies and custom status use the built-in helpers:
// Health check using deployment helper
def.HealthPolicy(defkit.DeploymentHealth().Build())
// Custom status using deployment helper
def.CustomStatus(defkit.DeploymentStatus().Build())
When to Use What
┌─────────────────────────────────────────────────────────────────┐
│ DECISION FRAMEWORK │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Can I express it with defkit Go API? │
│ │
│ YES ──────────► Use Go (95% of cases) │
│ • Static values: Set("image", "nginx") │
│ • Parameters: use the variable directly │
│ • Custom status: defkit.Status().Field() │
│ • Cluster info: vela.ClusterVersion() │
│ │
│ NO ───────────► Use RawCUE() (rare escape hatch) │
│ • True unification semantics (a & b) │
│ • Complex nested comprehensions (3+ levels)│
│ │
└─────────────────────────────────────────────────────────────────┘
Target Developer Experience
package myplatform
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
func init() {
defkit.Register(WebserviceComponent())
}
func WebserviceComponent() *defkit.ComponentDefinition {
// Parameters scoped to definition function
image := defkit.String("image").Required()
replicas := defkit.Int("replicas").Default(3).Min(1).Max(100)
cpu := defkit.String("cpu").Optional()
return defkit.NewComponent("webservice").
Description("A production-ready web service").
Workload("apps/v1", "Deployment").
Params(image, replicas, cpu).
Template(func(tpl *defkit.Template) {
vela := defkit.VelaCtx()
deploy := defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", vela.Name()).
Set("spec.replicas", replicas).
Set("spec.selector.matchLabels[app.oam.dev/component]", vela.Name()).
Set("spec.template.spec.containers", []map[string]any{{
"name": vela.Name(),
"image": image,
}}).
SetIf(cpu.IsSet(), "spec.template.spec.containers[0].resources.limits.cpu", cpu)
tpl.Output(deploy)
}).
// Health policy using deployment helper
HealthPolicy(defkit.DeploymentHealth().Build()).
// Custom status using deployment helper
CustomStatus(defkit.DeploymentStatus().Build())
}
VelaContext API Reference
Runtime Context Accessors
The defkit.VelaCtx() function provides access to runtime context values:
| Go Method | Generated CUE | Description |
|---|---|---|
vela.Name() |
context.name |
Component name |
vela.Namespace() |
context.namespace |
Target namespace |
vela.AppName() |
context.appName |
Application name |
vela.AppRevision() |
context.appRevision |
Application revision |
vela.AppLabels() |
context.appLabels |
Application labels |
vela.AppAnnotations() |
context.appAnnotations |
Application annotations |
vela.Revision() |
context.revision |
Component revision |
vela.ClusterVersion().Minor() |
context.clusterVersion.minor |
K8s minor version |
vela.ClusterVersion().Major() |
context.clusterVersion.major |
K8s major version |
Parameter Variables
Parameters are typed variables that generate CUE paths automatically:
| Go Usage | Generated CUE | Description |
|---|---|---|
image (in template) |
parameter.image |
Parameter value (from variable name) |
cpu.IsSet() |
parameter.cpu != _|_ |
Check if optional parameter is set |
persistence.Field("size") |
parameter.persistence.size |
Nested field access |
How it works: Each parameter variable knows its name and type. When used in a template expression, it compiles to the corresponding parameter.X CUE path.
Comparison and Conditional Helpers
| Go Method | Generated CUE | Description |
|---|---|---|
defkit.Eq(a, b) |
a == b |
Equality comparison |
defkit.Ne(a, b) |
a != b |
Inequality comparison |
defkit.Lt(a, b) |
a < b |
Less than |
defkit.Le(a, b) |
a <= b |
Less than or equal |
defkit.Gt(a, b) |
a > b |
Greater than |
defkit.Ge(a, b) |
a >= b |
Greater than or equal |
defkit.And(a, b) |
a && b |
Logical AND |
defkit.Or(a, b) |
a || b |
Logical OR |
defkit.Not(cond) |
!cond |
Logical NOT |
defkit.If(cond) |
if cond {...} |
Conditional block |
defkit.Lit(value) |
value |
Literal value (for comparisons) |
param.IsSet() |
param != _|_ |
Check if optional param is set |
Examples
Health Policy
Composable Health Expressions
defkit provides a fully composable expression-based API for defining health policies. This enables health checks for any resource type, not just Kubernetes workloads.
Core Primitives:
All health expression methods are accessed via the Health() builder, providing a unified API:
| Primitive | Purpose | Example |
|---|---|---|
Health().Condition(type) |
Check status.conditions[] array |
Health().Condition("Ready").IsTrue() |
Health().Phase(phases...) |
Check status.phase field |
Health().Phase("Running", "Succeeded") |
Health().Field(path) |
Compare any status field | Health().Field("status.state").Eq("active") |
Health().Exists(path) |
Check field existence | Health().Exists("status.endpoint") |
Health().And(...) |
All expressions must be true | Health().And(expr1, expr2) |
Health().Or(...) |
Any expression must be true | Health().Or(expr1, expr2) |
Health().Not(...) |
Negate expression | Health().Not(h.Condition("Stalled").IsTrue()) |
Health().AllTrue(conds...) |
All conditions are True | Health().AllTrue("Ready", "Synced") |
Health().AnyTrue(conds...) |
Any condition is True | Health().AnyTrue("Ready", "Available") |
Health().Always() |
Always healthy (existence) | Health().Always() |
Condition Expressions:
h := defkit.Health()
// Single condition check (most CRDs, Crossplane, cert-manager, etc.)
HealthPolicyExpr(h.Condition("Ready").IsTrue())
// Multiple conditions - all must be true
HealthPolicyExpr(h.AllTrue("Ready", "Synced"))
// Multiple conditions - any is sufficient
HealthPolicyExpr(h.AnyTrue("Ready", "Available"))
// Check condition exists (regardless of status)
HealthPolicyExpr(h.Condition("Initialized").Exists())
// Check condition reason
HealthPolicyExpr(h.Condition("Ready").ReasonIs("Available"))
Generated CUE for h.Condition("Ready").IsTrue():
_readyCond: [ for c in context.output.status.conditions if c.type == "Ready" { c } ]
isHealth: len(_readyCond) > 0 && _readyCond[0].status == "True"
Generated CUE for h.AllTrue("Ready", "Synced"):
_readyCond: [ for c in context.output.status.conditions if c.type == "Ready" { c } ]
_syncedCond: [ for c in context.output.status.conditions if c.type == "Synced" { c } ]
isHealth: (len(_readyCond) > 0 && _readyCond[0].status == "True") &&
(len(_syncedCond) > 0 && _syncedCond[0].status == "True")
Phase Expressions:
h := defkit.Health()
// Pod-style phase check
HealthPolicyExpr(h.Phase("Running", "Succeeded"))
// Custom phase field path
HealthPolicyExpr(h.PhaseField("status.currentPhase", "Active", "Ready"))
Generated CUE for h.Phase("Running", "Succeeded"):
isHealth: context.output.status.phase == "Running" ||
context.output.status.phase == "Succeeded"
Field Expressions:
h := defkit.Health()
// Field equality
HealthPolicyExpr(h.Field("status.state").Eq("active"))
// Numeric comparisons
HealthPolicyExpr(h.Field("status.replicas").Gt(0))
HealthPolicyExpr(h.Field("status.availableReplicas").Gte(1))
// Field existence
HealthPolicyExpr(h.Exists("status.loadBalancer.ingress"))
// Field in set of values
HealthPolicyExpr(h.Field("status.phase").In("Running", "Succeeded", "Complete"))
// Compare field to another field
HealthPolicyExpr(h.Field("status.readyReplicas").Eq(h.FieldRef("spec.replicas")))
Generated CUE for h.Field("status.state").Eq("active"):
isHealth: context.output.status.state == "active"
Generated CUE for h.Field("status.readyReplicas").Eq(h.FieldRef("spec.replicas")):
isHealth: context.output.status.readyReplicas == context.output.spec.replicas
Composite Expressions:
h := defkit.Health()
// Complex composition with And/Or/Not
HealthPolicyExpr(h.And(
h.Condition("Ready").IsTrue(),
h.Not(h.Condition("Stalled").IsTrue()),
))
// Mixed patterns
HealthPolicyExpr(h.And(
h.Condition("Ready").IsTrue(),
h.Or(
h.Field("status.replicas").Gte(1),
h.Exists("status.endpoint"),
),
))
// Real-world: Custom CRD with arbitrary status structure
HealthPolicyExpr(h.And(
h.Field("status.state").Eq("active"),
h.Exists("status.connectionString"),
h.Field("status.lastSyncTime").Exists(),
))
Generated CUE for nested And/Or:
_readyCond: [ for c in context.output.status.conditions if c.type == "Ready" { c } ]
isHealth: (len(_readyCond) > 0 && _readyCond[0].status == "True") &&
((context.output.status.replicas >= 1) || (context.output.status.endpoint != _|_))
Field Expression Methods:
| Method | CUE Output | Description |
|---|---|---|
.Eq(value) |
field == value |
Equal to |
.Ne(value) |
field != value |
Not equal to |
.Gt(value) |
field > value |
Greater than |
.Gte(value) |
field >= value |
Greater than or equal |
.Lt(value) |
field < value |
Less than |
.Lte(value) |
field <= value |
Less than or equal |
.In(values...) |
field == v1 || field == v2 |
In set of values |
.Contains(substr) |
String contains | For string fields |
.Eq(h.FieldRef(path)) |
field == otherField |
Compare to another field |
Built-in Workload Helpers
For common Kubernetes workloads, defkit provides pre-configured helpers that implement the standard health patterns:
// For Deployments - use the built-in helper
HealthPolicy(defkit.DeploymentHealth().Build())
Generated CUE:
ready: {
updatedReplicas: *0 | int
readyReplicas: *0 | int
replicas: *0 | int
observedGeneration: *0 | int
} & {
if context.output.status.updatedReplicas != _|_ {
updatedReplicas: context.output.status.updatedReplicas
}
if context.output.status.readyReplicas != _|_ {
readyReplicas: context.output.status.readyReplicas
}
if context.output.status.replicas != _|_ {
replicas: context.output.status.replicas
}
if context.output.status.observedGeneration != _|_ {
observedGeneration: context.output.status.observedGeneration
}
}
_isHealth: (context.output.spec.replicas == ready.readyReplicas) &&
(context.output.spec.replicas == ready.updatedReplicas) &&
(context.output.spec.replicas == ready.replicas) &&
(ready.observedGeneration == context.output.metadata.generation ||
ready.observedGeneration > context.output.metadata.generation)
isHealth: *_isHealth | bool
if context.output.metadata.annotations != _|_ {
if context.output.metadata.annotations["app.oam.dev/disable-health-check"] != _|_ {
isHealth: true
}
}
Custom Status
Composable Status Expressions
defkit provides a fully composable expression-based API for defining custom status messages. This enables dynamic status messages for any resource type, not just Kubernetes workloads.
Core Primitives:
All status expression methods are accessed via the Status() builder, providing a unified API:
| Primitive | Purpose | Example |
|---|---|---|
Status().Condition(type) |
Check status.conditions[] array |
Status().Condition("Ready").StatusValue() |
Status().Field(path) |
Extract any status field value | Status().Field("status.replicas") |
Status().SpecField(path) |
Extract spec field value | Status().SpecField("spec.replicas") |
Status().Exists(path) |
Check field existence | Status().Exists("status.endpoint") |
Status().Format(template, ...) |
Build formatted message | Status().Format("Ready: %v/%v", readyExpr, desiredExpr) |
Status().Concat(...) |
Concatenate string parts | Status().Concat(part1, " - ", part2) |
Status().Switch(...) |
Conditional message selection | Status().Switch(case1, case2, default) |
Status().HealthAware(...) |
Message based on health status | Status().HealthAware(healthyMsg, unhealthyMsg) |
Status().WithDetails(...) |
Add structured details | Status().WithDetails(detail1, detail2) |
Field Expressions:
s := defkit.Status()
// Extract status fields with defaults
readyReplicas := s.Field("status.readyReplicas").Default(0)
phase := s.Field("status.phase").Default("Unknown")
endpoint := s.Field("status.endpoint").Default("pending")
// Extract spec fields
desiredReplicas := s.SpecField("spec.replicas")
// Build formatted message
CustomStatusExpr(s.Format("Ready: %v/%v", readyReplicas, desiredReplicas))
Generated CUE for field extraction:
_readyReplicas: *0 | int
if context.output.status.readyReplicas != _|_ {
_readyReplicas: context.output.status.readyReplicas
}
_desiredReplicas: context.output.spec.replicas
message: "Ready: \(_readyReplicas)/\(_desiredReplicas)"
Condition Expressions:
s := defkit.Status()
// Extract condition message
CustomStatusExpr(s.Condition("Ready").Message())
// Extract condition reason
CustomStatusExpr(s.Condition("Ready").Reason())
// Extract condition status
CustomStatusExpr(s.Condition("Ready").StatusValue())
// Combine condition info
CustomStatusExpr(s.Format("%v: %v",
s.Condition("Ready").StatusValue(),
s.Condition("Ready").Message(),
))
Generated CUE for condition extraction:
_readyCond: [ for c in context.output.status.conditions if c.type == "Ready" { c } ]
_readyStatus: *"Unknown" | string
_readyMessage: *"" | string
if len(_readyCond) > 0 {
_readyStatus: _readyCond[0].status
_readyMessage: _readyCond[0].message
}
message: "\(_readyStatus): \(_readyMessage)"
Conditional Status Messages:
s := defkit.Status()
// Switch based on field value
CustomStatusExpr(s.Switch(
s.Case(s.Field("status.phase").Eq("Running"), "Service is running"),
s.Case(s.Field("status.phase").Eq("Pending"), "Service is starting..."),
s.Case(s.Field("status.phase").Eq("Failed"), s.Concat("Failed: ", s.Field("status.reason"))),
s.Default("Unknown status"),
))
// Health-aware status (references context.status.healthy set by healthPolicy)
CustomStatusExpr(s.HealthAware(
"All systems operational", // when healthy
s.Concat("Degraded: ", s.Condition("Ready").Message()), // when unhealthy
))
Generated CUE for Switch:
message: *"Unknown status" | string
if context.output.status.phase == "Running" {
message: "Service is running"
}
if context.output.status.phase == "Pending" {
message: "Service is starting..."
}
if context.output.status.phase == "Failed" {
_failReason: *"" | string
if context.output.status.reason != _|_ {
_failReason: context.output.status.reason
}
message: "Failed: \(_failReason)"
}
Composite Status Messages:
s := defkit.Status()
// Build rich status message with multiple components
CustomStatusExpr(s.Concat(
"Replicas: ", s.Field("status.readyReplicas").Default(0),
"/", s.SpecField("spec.replicas"),
" | Phase: ", s.Field("status.phase").Default("Unknown"),
" | Generation: ", s.Field("status.observedGeneration").Default(0),
))
// Real-world: Custom CRD status
CustomStatusExpr(s.Concat(
"State: ", s.Field("status.state").Default("unknown"),
" | Endpoint: ", s.Field("status.endpoint").Default("pending"),
" | Connections: ", s.Field("status.activeConnections").Default(0),
))
Generated CUE for Concat:
_readyReplicas: *0 | int
if context.output.status.readyReplicas != _|_ {
_readyReplicas: context.output.status.readyReplicas
}
_phase: *"Unknown" | string
if context.output.status.phase != _|_ {
_phase: context.output.status.phase
}
_observedGen: *0 | int
if context.output.status.observedGeneration != _|_ {
_observedGen: context.output.status.observedGeneration
}
message: "Replicas: \(_readyReplicas)/\(context.output.spec.replicas) | Phase: \(_phase) | Generation: \(_observedGen)"
Status Details (Structured Data):
s := defkit.Status()
// Add structured details alongside message
CustomStatusExpr(s.
Message(s.Format("Ready: %v/%v",
s.Field("status.readyReplicas").Default(0),
s.SpecField("spec.replicas"),
)).
WithDetails(
s.Detail("endpoint", s.Field("status.endpoint")),
s.Detail("version", s.Field("status.version")),
s.Detail("lastSync", s.Field("status.lastSyncTime")),
))
Field Expression Methods:
| Method | CUE Output | Description |
|---|---|---|
.Field(path) |
Extract from context.output.path |
Status field value |
.SpecField(path) |
Extract from context.output.path |
Spec field value |
.Default(value) |
*value | type |
Default if field missing |
.Exists(path) |
path != _|_ |
Check field exists |
.Condition(type) |
Array filter | Access condition by type |
.StatusValue() |
Condition status | Get condition status |
.Message() |
Condition message | Get condition message |
.Reason() |
Condition reason | Get condition reason |
Message Building Methods:
| Method | Description | Example |
|---|---|---|
Format(template, args...) |
Printf-style formatting | Format("Ready: %v/%v", ready, total) |
Concat(parts...) |
String concatenation | Concat("State: ", state, " - ", msg) |
Switch(cases...) |
Conditional selection | Switch(case1, case2, default) |
Case(cond, msg) |
Switch case | Case(phase.Eq("Running"), "OK") |
Default(msg) |
Switch default | Default("Unknown") |
HealthAware(ok, fail) |
Health-based | HealthAware("OK", errMsg) |
Built-in Workload Helpers
For common Kubernetes workloads, defkit provides pre-configured helpers:
// For Deployments - use the built-in helper
CustomStatus(defkit.DeploymentStatus().Build())
Generated CUE:
ready: {
readyReplicas: *0 | int
} & {
if context.output.status.readyReplicas != _|_ {
readyReplicas: context.output.status.readyReplicas
}
}
message: "Ready:\(ready.readyReplicas)/\(context.output.spec.replicas)"
Creating Custom Status Helpers
Users can create their own helpers by composing the primitives:
// Example: Crossplane-style status
func CrossplaneStatus() defkit.StatusExpression {
s := defkit.Status()
return s.Switch(
s.Case(s.Condition("Ready").Is("True"),
s.Concat("Ready: ", s.Condition("Ready").Message())),
s.Case(s.Condition("Synced").Is("False"),
s.Concat("Syncing: ", s.Condition("Synced").Message())),
s.Default(s.Concat(
"Ready: ", s.Condition("Ready").StatusValue(),
" | Synced: ", s.Condition("Synced").StatusValue(),
)),
)
}
// Example: Database CRD status
func DatabaseStatus() defkit.StatusExpression {
s := defkit.Status()
return s.Concat(
"State: ", s.Field("status.state").Default("initializing"),
" | Connections: ", s.Field("status.connections").Default(0),
" | Endpoint: ", s.Field("status.endpoint").Default("pending"),
)
}
// Example: Certificate status
func CertificateStatus() defkit.StatusExpression {
s := defkit.Status()
return s.HealthAware(
s.Concat("Valid until ", s.Field("status.notAfter")),
s.Concat("Not ready: ", s.Condition("Ready").Message()),
)
}
// Usage - pick ONE status per component:
CustomStatusExpr(CrossplaneStatus())
CustomStatusExpr(DatabaseStatus())
Workload Helper Reference
defkit provides pre-configured helpers for common Kubernetes workloads:
| Workload | Status Helper | Health Helper |
|---|---|---|
| Deployment | DeploymentStatus() |
DeploymentHealth() |
| DaemonSet | DaemonSetStatus() |
DaemonSetHealth() |
| StatefulSet | StatefulSetStatus() |
StatefulSetHealth() |
| Job | - | JobHealth() |
| CronJob | - | CronJobHealth() |
Example for DaemonSet:
// DaemonSet health policy
HealthPolicy(defkit.DaemonSetHealth().Build())
Generated CUE:
ready: {
replicas: *0 | int
} & {
if context.output.status.numberReady != _|_ {
replicas: context.output.status.numberReady
}
}
desired: {
replicas: *0 | int
} & {
if context.output.status.desiredNumberScheduled != _|_ {
replicas: context.output.status.desiredNumberScheduled
}
}
// ... additional fields for current, updated, generation
isHealth: desired.replicas == ready.replicas &&
desired.replicas == updated.replicas &&
desired.replicas == current.replicas &&
(generation.observed == generation.metadata || generation.observed > generation.metadata)
Creating Custom Helpers
Users can create their own helpers by composing the primitives:
// Example: Crossplane-style health check
func CrossplaneHealth() defkit.HealthExpression {
h := defkit.Health()
return h.AllTrue("Ready", "Synced")
}
// Example: Certificate health check
func CertificateHealth() defkit.HealthExpression {
h := defkit.Health()
return h.Condition("Ready").IsTrue()
}
// Example: Custom database CRD
func DatabaseHealth() defkit.HealthExpression {
h := defkit.Health()
return h.And(
h.Condition("Ready").IsTrue(),
h.Field("status.phase").Eq("Running"),
h.Exists("status.connectionEndpoint"),
)
}
// Usage - pick ONE health policy per component:
// For a Crossplane managed resource:
HealthPolicyExpr(CrossplaneHealth())
// For a database CRD:
HealthPolicyExpr(DatabaseHealth())
Cluster Version Check
// Go - inside template function using conditional apiVersion
Template(func(tpl *defkit.Template) {
vela := defkit.VelaCtx()
// Use VersionIf for cluster-version-aware resources
cronjob := defkit.NewResourceWithConditionalVersion("batch/v1", "CronJob").
VersionIf(vela.ClusterVersion().Minor().Lt(defkit.Lit(25)), "batch/v1beta1").
Set("metadata.name", vela.Name()).
Set("spec.schedule", schedule)
tpl.Output(cronjob)
})
// Generated CUE
// if context.clusterVersion.minor < 25 {
// apiVersion: "batch/v1beta1"
// }
// if context.clusterVersion.minor >= 25 {
// apiVersion: "batch/v1"
// }
Conditional Fields
// Go - use SetIf for conditional field setting
Template(func(tpl *defkit.Template) {
vela := defkit.VelaCtx()
cpu := defkit.String("cpu") // optional parameter
deploy := defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", vela.Name()).
Set("spec.replicas", replicas).
SetIf(cpu.IsSet(), "spec.template.spec.containers[0].resources.limits.cpu", cpu)
tpl.Output(deploy)
})
// Generated CUE
// if parameter.cpu != _|_ {
// spec: template: spec: containers: [{resources: limits: cpu: parameter.cpu}]
// }
Conditional Patterns
Checking if an optional parameter is set
Use param.IsSet() to conditionally set fields when an optional parameter has a value:
// Single condition - set field only if parameter was provided
deploy.SetIf(cpu.IsSet(), "spec.resources.limits.cpu", cpu)
deploy.SetIf(labels.IsSet(), "spec.template.metadata.labels", labels)
Checking if a boolean parameter's value is true
For boolean parameters, use Eq(param, Lit(true)) to check if the value is true:
// Check if boolean parameter is true
addRevisionLabel := defkit.Bool("addRevisionLabel").Default(false)
deploy.SetIf(
defkit.Eq(addRevisionLabel, defkit.Lit(true)),
"spec.template.metadata.labels[app.oam.dev/revision]",
ctx.Revision(),
)
Why this pattern? Using param.IsSet() on a boolean only checks if it was provided, not its value. A boolean set to false would pass IsSet(). Use Eq(param, Lit(true)) when you need to check the actual value.
Compound Conditions
Use defkit.And() and defkit.Or() to compose conditions:
// Single condition
deploy.SetIf(cpu.IsSet(), "spec.resources.limits.cpu", cpu)
// AND - both must be true (vela := defkit.VelaCtx())
deploy.SetIf(
defkit.And(
vela.ClusterVersion().Minor().Gte(25),
enableNewAPI.IsSet(),
),
"apiVersion", "batch/v1",
)
// OR - either condition
deploy.SetIf(
defkit.Or(
vela.ClusterVersion().Minor().Lt(21),
legacyMode.IsSet(),
),
"apiVersion", "batch/v1beta1",
)
// Complex: (A && B) || C
deploy.SetIf(
defkit.Or(
defkit.And(isProduction, highAvailability),
forceHA.IsSet(),
),
"spec.replicas", 3,
)
Generated CUE:
if parameter.cpu != _|_ {
spec: resources: limits: cpu: parameter.cpu
}
if context.clusterVersion.minor >= 25 && parameter.enableNewAPI != _|_ {
apiVersion: "batch/v1"
}
if context.clusterVersion.minor < 21 || parameter.legacyMode != _|_ {
apiVersion: "batch/v1beta1"
}
if (parameter.isProduction && parameter.highAvailability) || parameter.forceHA != _|_ {
spec: replicas: 3
}
Multiple fields under the same condition:
// Use If().EndIf() block for multiple fields
deploy.
If(defkit.And(persistence.IsSet(), persistence.Field("enabled"))).
Set("spec.volumeClaimTemplates", []map[string]any{{...}}).
Set("spec.template.spec.volumes", []map[string]any{{...}}).
EndIf()
RawCUE() Escape Hatch
For the ~5% of patterns that cannot be expressed in Go, use RawCUE():
True Unification Semantics
// CUE's & operator has mathematical semantics Go cannot replicate
defkit.RawCUE(`
baseConfig: {
replicas: >=1
image: string
}
prodConfig: baseConfig & {
replicas: >=3 & <=10
}
`)
Complex Nested Comprehensions
// Triple-nested comprehension with complex conditions
defkit.RawCUE(`
result: [
for ns in namespaces
for svc in ns.services if svc.exposed
for port in svc.ports if port.protocol == "TCP" {
name: "\(ns.name)-\(svc.name)-\(port.port)"
}
]
`)
Parameter Types
| defkit Type | CUE Equivalent | Go Type |
|---|---|---|
defkit.String("name") |
name: string |
string |
defkit.Int("name") |
name: int |
int |
defkit.Bool("name") |
name: bool |
bool |
defkit.Float("name") |
name: float |
float64 |
defkit.List("name", T) |
name: [...T] |
[]T |
defkit.Map("name").Of(V) |
name: {[string]: V} |
map[string]V |
defkit.Object("name", ...) |
name: {field: type} |
struct |
defkit.Enum("name", "a", "b") |
name: "a" | "b" |
string with validation |
defkit.OneOf("name", ...) |
name: close({...}) | close({...}) |
discriminated union |
defkit.Struct("name") |
name: {...} |
struct with named fields |
defkit.StringList("name") |
name: [...string] |
convenience alias |
defkit.IntList("name") |
name: [...int] |
convenience alias |
defkit.StringKeyMap("name") |
name: {[string]: string} |
string→string map |
Parameter Modifiers
defkit.String("image").
Required(). // Must be provided
Default("nginx:latest"). // CUE: *"nginx:latest" | string
Optional(). // Can be omitted
Description("Container image") // Schema description
defkit.Int("replicas").
Min(1).Max(100). // Numeric constraints
Default(3)
defkit.String("name").
Pattern("^[a-z][a-z0-9-]*$") // Regex validation
Complex Parameter Types
Object parameters for nested configuration:
persistence := defkit.Object("persistence").WithFields(
defkit.Bool("enabled").Default(false),
defkit.String("storageClass").Required(),
defkit.String("size").Default("10Gi"),
).Optional()
// Usage in template - fluent conditional for optional object
deploy.
If(defkit.And(persistence.IsSet(), persistence.Field("enabled"))).
Set("spec.volumeClaimTemplates", []map[string]any{{
"spec": map[string]any{
"storageClassName": persistence.Field("storageClass"),
"resources": map[string]any{
"requests": map[string]any{
"storage": persistence.Field("size"),
},
},
},
}})
Discriminated unions for variant types:
volume := defkit.OneOf("volume",
defkit.Variant("emptyDir",
defkit.String("medium").Default(""),
),
defkit.Variant("pvc",
defkit.String("claimName").Required(),
),
defkit.Variant("configMap",
defkit.String("name").Required(),
defkit.Int("defaultMode").Default(420),
),
)
// Usage: volume.Discriminator() returns "emptyDir", "pvc", or "configMap"
Collection Operations
defkit provides comprehensive collection operations for transforming arrays and lists. These are commonly used for patterns like ports, volumeMounts, and environment variables.
Basic Collection Pipeline
// Each() creates a collection pipeline from a list parameter
ports := defkit.Array("ports")
// Filter, transform, and map fields
exposedPorts := defkit.Each(ports).
Filter(defkit.FieldEquals("expose", true)).
Map(defkit.FieldMap{
"containerPort": defkit.FieldRef("port"),
"name": defkit.FieldRef("name"),
"protocol": defkit.FieldRef("protocol"),
}).
DefaultField("name", defkit.Format("port-%v", defkit.FieldRef("port")))
Collection Operations Reference
| Operation | Description | Example |
|---|---|---|
Each(source) |
Start a collection pipeline | defkit.Each(ports) |
.Filter(pred) |
Keep items matching predicate | .Filter(FieldEquals("expose", true)) |
.Map(fieldMap) |
Transform item fields | .Map(FieldMap{"newKey": FieldRef("oldKey")}) |
.Pick(fields...) |
Select only specified fields | .Pick("name", "mountPath") |
.Rename(from, to) |
Rename a field | .Rename("port", "containerPort") |
.Wrap(key) |
Wrap item under a key | .Wrap("name") → {name: value} |
.DefaultField(f, v) |
Set default for missing field | .DefaultField("name", Lit("default")) |
.Flatten() |
Flatten nested arrays | .Flatten() |
Multi-Source Collections
For patterns like volumeMounts where items come from multiple sub-fields:
volumeMounts := defkit.Object("volumeMounts")
// Combine items from multiple sources (pvc, configMap, secret, etc.)
mounts := defkit.FromFields(volumeMounts, "pvc", "configMap", "secret", "emptyDir", "hostPath").
Pick("name", "mountPath").
Dedupe("name")
| Operation | Description | Example |
|---|---|---|
FromFields(src, fields...) |
Combine items from multiple object fields | FromFields(volumeMounts, "pvc", "configMap") |
.MapBySource(map) |
Apply different mappings per source type | .MapBySource(map[string]FieldMap{...}) |
.Dedupe(keyField) |
Remove duplicates by key | .Dedupe("name") |
Field Value Helpers
| Helper | Description | Example |
|---|---|---|
FieldRef("field") |
Reference item field | FieldRef("port") |
FieldRef("f").Or(fallback) |
Field with fallback | FieldRef("name").Or(LitField("default")) |
LitField(value) |
Literal value | LitField("TCP") |
Format(fmt, args...) |
Formatted string | Format("port-%v", FieldRef("port")) |
Nested(fieldMap) |
Nested object | Nested(FieldMap{"claimName": FieldRef("name")}) |
Optional("field") |
Include only if field exists | Optional("subPath") |
FieldEquals(f, v) |
Predicate: field equals value | FieldEquals("expose", true) |
FieldExists("field") |
Predicate: field is set | FieldExists("mountPath") |
Helper Builder Pattern
For complex collection transformations (like volumeMounts and volumes), defkit provides a helper builder pattern that creates named template-level helpers.
Basic Helper
Template(func(tpl *defkit.Template) {
ports := defkit.Array("ports")
// Create a named helper for port mapping
portsArray := tpl.Helper("portsArray").
From(ports).
Pick("port", "name", "protocol").
Rename("port", "containerPort").
DefaultField("name", defkit.Format("port-%v", defkit.FieldRef("port"))).
Build()
deploy := defkit.NewResource("apps/v1", "Deployment").
Set("spec.template.spec.containers[0].ports", portsArray)
tpl.Output(deploy)
})
Helper Builder Methods
| Method | Description |
|---|---|
tpl.Helper(name) |
Start building a named helper |
.From(source) |
Set single source |
.FromFields(src, fields...) |
Set multiple sources |
.FromHelper(helper) |
Reference another helper |
.Guard(cond) |
Outer condition for comprehension |
.Each(fn) |
Transform each item with function |
.Pick(fields...) |
Select fields |
.PickIf(cond, field) |
Conditionally include field |
.Map(fieldMap) |
Transform fields |
.MapBySource(map) |
Per-source transformations |
.Filter(cond) |
Filter by condition |
.FilterPred(pred) |
Filter by predicate |
.Wrap(key) |
Wrap items under key |
.Dedupe(keyField) |
Remove duplicates |
.DefaultField(f, v) |
Set default value |
.Rename(from, to) |
Rename field |
.AfterOutput() |
Place helper after output block |
.Build() |
Finalize and register helper |
Advanced Helpers
For complex patterns like volumeMounts (struct-based arrays):
// Struct array helper for mounting volumes
mountsArray := tpl.StructArrayHelper("mountsArray", volumeMounts).
Field("pvc", defkit.FieldMap{"name": defkit.FieldRef("name"), "mountPath": defkit.FieldRef("mountPath")}).
Field("configMap", defkit.FieldMap{"name": defkit.FieldRef("name"), "mountPath": defkit.FieldRef("mountPath")}).
Field("secret", defkit.FieldMap{"name": defkit.FieldRef("name"), "mountPath": defkit.FieldRef("mountPath")}).
Build()
// Volumes array with per-source mappings
volumesArray := tpl.StructArrayHelper("volumesArray", volumeMounts).
Field("pvc", defkit.FieldMap{
"name": defkit.FieldRef("name"),
"persistentVolumeClaim": defkit.Nested(defkit.FieldMap{"claimName": defkit.FieldRef("claimName")}),
}).
Field("configMap", defkit.FieldMap{
"name": defkit.FieldRef("name"),
"configMap": defkit.Nested(defkit.FieldMap{"name": defkit.FieldRef("cmName")}),
}).
Build()
// Concat helper combines struct arrays
volumesList := tpl.ConcatHelper("volumesList", volumesArray).
Fields("pvc", "configMap", "secret", "emptyDir", "hostPath").
Build()
// Dedupe helper removes duplicates
deDupVolumes := tpl.DedupeHelper("deDupVolumesArray", volumesList).
ByKey("name").
Build()
Helper Types
| Type | Purpose | Example |
|---|---|---|
*HelperVar |
Reference to basic helper | Returned by Helper(...).Build() |
*StructArrayHelper |
Struct with array fields | tpl.StructArrayHelper(name, src) |
*ConcatHelper |
Concatenate arrays | tpl.ConcatHelper(name, src) |
*DedupeHelper |
Deduplicate by key | tpl.DedupeHelper(name, src) |
Definition Types
ComponentDefinition
func init() {
defkit.Register(MyComponent())
}
func MyComponent() *defkit.ComponentDefinition {
image := defkit.String("image").Required()
return defkit.NewComponent("webservice").
Description("...").
Workload("apps/v1", "Deployment").
Params(image).
Template(func(tpl *defkit.Template) {
vela := defkit.VelaCtx()
deploy := defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", vela.Name()).
Set("spec.template.spec.containers[0].image", image)
tpl.Output(deploy)
}).
HealthPolicy(defkit.DeploymentHealth().Build()).
CustomStatus(defkit.DeploymentStatus().Build())
}
TraitDefinition
func init() {
defkit.Register(RateLimitTrait())
}
func RateLimitTrait() *defkit.TraitDefinition {
rps := defkit.Int("rps").Required()
return defkit.NewTrait("rate-limit").
Description("...").
AppliesTo("webservice", "microservice").
Params(rps).
PatchTemplate(func(tpl *defkit.Template) {
// Trait templates patch the workload
tpl.PatchOutput(defkit.NewResource("", "").
Set("metadata.annotations[ratelimit.example.com/rps]", rps))
})
}
PolicyDefinition
// Example: Topology policy for multi-cluster deployment
func Topology() *defkit.PolicyDefinition {
clusters := defkit.StringList("clusters").Description("Specify the names of the clusters to select.")
clusterLabelSelector := defkit.StringKeyMap("clusterLabelSelector").Description("Specify the label selector for clusters")
allowEmpty := defkit.Bool("allowEmpty").Description("Ignore empty cluster error")
namespace := defkit.String("namespace").Description("Specify the target namespace to deploy in the selected clusters")
return defkit.NewPolicy("topology").
Description("Describe the destination where components should be deployed to.").
Params(clusters, clusterLabelSelector, allowEmpty, namespace)
}
Example policies: topology, apply-once, garbage-collect, override, read-only, replication, resource-update, shared-resource, take-over
WorkflowStepDefinition
// Example: Deploy step with multi-cluster support
func Deploy() *defkit.WorkflowStepDefinition {
return defkit.NewWorkflowStep("deploy").
Description("A powerful and unified deploy step for components multi-cluster delivery with policies.").
Category("Application Delivery").
Scope("Application").
WithImports("vela/multicluster", "vela/builtin").
RawCUE(`...`) // Complex CUE patterns use RawCUE() escape hatch
}
Example workflow steps: deploy, suspend, apply-component, apply-deployment, apply-object, apply-terraform-config, apply-terraform-provider, build-push-image, check-metrics, clean-jobs, collect-service-endpoints, create-config, delete-config, depends-on-app, deploy-cloud-resource, export-data, export-service, export2config, export2secret, generate-jdbc-connection, list-config, notification, print-message-in-status, read-config, read-object, request, share-cloud-resource, step-group, webhook
Schema-Agnostic Resource Construction
defkit does NOT ship typed Kubernetes helpers. Instead, it provides a universal builder that works with any resource:
// Core Kubernetes
defkit.NewResource("apps/v1", "Deployment").
SetName("my-app").
Set("spec.replicas", 3)
// CRDs (Crossplane, KRO, etc.)
defkit.NewResource("database.aws.crossplane.io/v1beta1", "DBInstance").
SetName("my-db").
Set("spec.forProvider.engine", "postgres")
Optional typed adapter for users who want compile-time type safety:
import appsv1 "k8s.io/api/apps/v1"
deployment := &appsv1.Deployment{...}
return defkit.FromTyped(deployment)
CLI Commands
The following commands extend the existing vela def command group to support Go definitions:
# Apply Go definitions (extends existing `vela def apply`)
# CUE compilation is transparent - .go files are automatically detected
vela def apply ./definitions/webservice.go
# Apply all definitions in directory (supports mixed .cue and .go)
vela def apply ./definitions/
# Dry-run to see generated resources (existing flag)
vela def apply ./definitions/ --dry-run
# Validate Go definitions without applying (existing command, extended for .go)
vela def vet ./definitions/webservice.go
# Initialize a new Go definition module with scaffolding
vela def init-module ./my-definitions --name my-definitions
# Validate all definitions in a Go module without cluster connection
vela def validate-module ./my-definitions
vela def validate-module ./my-definitions --verbose
New Subcommands
| Command | Description |
|---|---|
vela def init-module |
Scaffold a complete Go definition module with example components |
vela def validate-module |
Validate all Go definitions in a module without requiring a cluster |
vela def gen-go |
Generate Go defkit code from existing CUE definitions (Phase 3 - migration) |
Extended Existing Commands
| Command | Extension |
|---|---|
vela def apply |
Accepts .go files, compiles to CUE transparently |
vela def vet |
Validates .go definitions including Go compilation |
vela def gen-api |
Already generates Go SDK from CUE (inverse direction) |
Module Scaffolding
The init-module command creates a complete Go module structure:
$ vela def init-module ./my-platform --name my-platform
Created Go definition module at ./my-platform:
go.mod - Go module definition
components/
webservice.go - Example webservice component
webservice_test.go - Example tests
main.go - Module entry point for validation
The scaffolded module can be validated immediately:
$ cd my-platform
$ vela def validate-module .
Validating Go definition module...
Found 1 definitions
✓ webservice (ComponentDefinition) - CUE validation passed
All definitions validated successfully
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Definition Authoring Pipeline │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ defkit │ │ IR │ │ Compiler │ │
│ │ Go API │───▶│ (JSON) │───▶│ Go → CUE │──▶ CR │
│ │ │ │ │ │ │ │
│ │ tpl.Output() │ │ • Schema │ │ • Validation │ │
│ │ param vars │ │ • Template │ │ • CUE Gen │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ (CUE compilation is transparent - developers never see it) │
│ │
└─────────────────────────────────────────────────────────────────┘
Compilation Approach
The SDK uses declarative capture rather than Go AST transformation:
- Template functions execute with a tracing context
- Each
Set(),If(),tpl.Output()call is recorded - Parameter variables track their usage and generate corresponding CUE paths
- Recorded operations form a declarative tree that maps directly to CUE
How Parameter Variables Work
Parameter variables like image := defkit.String("image").Required() are expression builders, not value holders:
// What you write:
image := defkit.String("image").Required()
deploy.Set("spec.image", image)
// What happens internally:
// 1. defkit.String("image") creates a Param{name: "image", type: "string"}
// 2. deploy.Set() receives this Param and records: SetOp{path: "spec.image", value: ParamRef("image")}
// 3. During compilation, ParamRef("image") becomes `parameter.image` in CUE
Conditional handling uses the fluent API rather than Go's if:
// For optional parameters, use SetIf or the If() fluent method:
deploy.SetIf(cpu.IsSet(), "spec.resources.limits.cpu", cpu)
// Or with fluent chaining:
deploy.If(cpu.IsSet()).Set("spec.resources.limits.cpu", cpu)
// Both compile to CUE:
// if parameter.cpu != _|_ {
// spec: resources: limits: cpu: parameter.cpu
// }
This approach is similar to how query builders (like GORM, Squirrel) work - the Go code describes operations declaratively without executing them at definition time.
Testing
defkit provides testing support using Ginkgo and Gomega with custom matchers, enabling BDD-style test-driven development without requiring a Kubernetes cluster.
Test Levels
| Test Level | Framework | Cluster Required | What It Tests |
|---|---|---|---|
| Unit tests | Ginkgo/Gomega | No | Parameter validation, template output, conditional logic |
| CUE compilation | Ginkgo/Gomega | No | Generated CUE is syntactically valid |
| Integration | envtest | No | Controller reconciliation with fake cluster |
| E2E | Full cluster | Yes | Complete deployment lifecycle |
Custom Matchers
defkit provides custom Gomega matchers for readable, expressive tests:
// Resource type matchers
BeDeployment(), BeService(), BeIngress(), BeConfigMap(), BeSecret()
BeResourceOfKind(kind string)
// Metadata matchers
HaveAPIVersion(version string), HaveName(name string), HaveNamespace(ns string)
HaveLabel(key, value string), HaveAnnotation(key, value string)
// Spec matchers
HaveReplicas(count int), HaveImage(image string), HaveContainerNamed(name string)
HavePort(port int), HaveEnvVar(name, value string)
HaveResourceLimit(resource, value string), HaveResourceRequest(resource, value string)
// Path-based matcher for any field
HaveFieldPath(path string, value any)
// Validation and health matchers
FailValidationWith(substring string), PassValidation()
BeHealthy(), BeUnhealthy(), HaveHealthMessage(msg string)
Example: Testing a ComponentDefinition
package webservice_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/definition/defkit"
. "github.com/oam-dev/kubevela/pkg/definition/defkit/testing/matchers"
)
var _ = Describe("Webservice ComponentDefinition", func() {
var def *defkit.ComponentDefinition
BeforeEach(func() {
def = webservice.Webservice()
})
Describe("Template Rendering", func() {
It("should render a deployment with defaults", func() {
ctx := defkit.TestContext().
WithName("my-app").
WithNamespace("production").
WithParam("image", "nginx:1.21")
output := def.Render(ctx)
Expect(output).To(BeDeployment())
Expect(output).To(HaveAPIVersion("apps/v1"))
Expect(output).To(HaveName("my-app"))
Expect(output).To(HaveReplicas(3)) // default value
Expect(output).To(HaveImage("nginx:1.21"))
})
It("should set resource limits when cpu is provided", func() {
ctx := defkit.TestContext().
WithName("my-app").
WithParam("image", "nginx:1.21").
WithParam("cpu", "500m")
Expect(def.Render(ctx)).To(HaveResourceLimit("cpu", "500m"))
})
})
Describe("Parameter Validation", func() {
It("should fail when required image is missing", func() {
ctx := defkit.TestContext().WithName("my-app")
Expect(def.Validate(ctx)).To(FailValidationWith("image is required"))
})
It("should fail when replicas exceeds maximum", func() {
ctx := defkit.TestContext().
WithName("my-app").
WithParam("image", "nginx:1.21").
WithParam("replicas", 200)
Expect(def.Validate(ctx)).To(FailValidationWith("replicas must be <= 100"))
})
})
Describe("Health Policy", func() {
It("should report healthy when all replicas are ready", func() {
ctx := defkit.TestContext().
WithName("my-app").
WithParam("image", "nginx:1.21").
WithParam("replicas", 3).
WithOutputStatus(map[string]any{"readyReplicas": 3})
Expect(def.EvaluateHealth(ctx)).To(BeHealthy())
Expect(def.EvaluateHealth(ctx)).To(HaveHealthMessage("Ready: 3/3"))
})
})
})
Table-Driven Tests
DescribeTable("parameter combinations",
func(params map[string]any, expectedField string, expectedValue any, shouldFail bool) {
ctx := defkit.TestContext().WithName("test")
for k, v := range params {
ctx = ctx.WithParam(k, v)
}
if shouldFail {
Expect(def.Validate(ctx)).NotTo(PassValidation())
return
}
Expect(def.Render(ctx)).To(HaveFieldPath(expectedField, expectedValue))
},
Entry("minimal config uses default replicas",
map[string]any{"image": "nginx:1.21"}, "spec.replicas", 3, false),
Entry("custom replicas are respected",
map[string]any{"image": "nginx:1.21", "replicas": 5}, "spec.replicas", 5, false),
Entry("missing image fails validation",
map[string]any{"replicas": 3}, "", nil, true),
)
Testing Traits
var _ = Describe("RateLimit Trait", func() {
It("should patch workload with annotation", func() {
workload := defkit.NewResource("apps/v1", "Deployment").SetName("my-app")
ctx := defkit.TestContext().
WithWorkload(workload).
WithParam("rps", 1000)
patches := ratelimit.New().Patch(ctx)
Expect(patches).To(HaveAnnotation("ratelimit.example.com/rps", "1000"))
})
})
Testing Cluster Version Conditionals
var _ = Describe("CronJob", func() {
It("should use v1beta1 on old clusters", func() {
ctx := defkit.TestContext().
WithName("my-job").
WithParam("schedule", "0 * * * *").
WithClusterVersion(1, 24)
Expect(cronjob.New().Render(ctx)).To(HaveAPIVersion("batch/v1beta1"))
})
It("should use v1 on new clusters", func() {
ctx := defkit.TestContext().
WithName("my-job").
WithParam("schedule", "0 * * * *").
WithClusterVersion(1, 25)
Expect(cronjob.New().Render(ctx)).To(HaveAPIVersion("batch/v1"))
})
})
What to Test vs What NOT to Test
DO Test
| What | Why | Example |
|---|---|---|
| Parameter validation | Catch invalid inputs early | Required fields, min/max constraints |
| Template output structure | Ensure correct K8s resources | Resource kind, API version, spec fields |
| Conditional logic | Verify branches work correctly | If cpu is set, limits are added |
| Default values | Ensure defaults are applied | replicas defaults to 3 |
| Health policy evaluation | Verify health checks work | Ready when replicas match |
| Auxiliary outputs | Verify all resources generated | Service, Ingress alongside Deployment |
| Edge cases | Handle unusual inputs | Empty arrays, nil values |
DO NOT Test
| What | Why |
|---|---|
| CUE language behavior | CUE is well-tested; trust it |
| Controller reconciliation logic | Out of defkit's scope |
| Kubernetes API behavior | Not your code |
| Network operations | Use mocks/fakes for integration tests |
Testing Anti-Patterns to Avoid
// ❌ BAD: Testing CUE internals
func TestBad(t *testing.T) {
cue, _ := def.ToCUE()
assert.Contains(t, cue, "replicas: parameter.replicas") // Too brittle
}
// ✅ GOOD: Test the behavior, not the implementation
func TestGood(t *testing.T) {
ctx := defkit.TestContext().WithParam("replicas", 5)
output := def.Render(ctx)
assert.Equal(t, 5, output.Get("spec.replicas"))
}
// ❌ BAD: Hardcoding exact output
func TestBad2(t *testing.T) {
output := def.Render(ctx)
expected := `{"apiVersion":"apps/v1",...}` // Brittle, hard to maintain
assert.Equal(t, expected, output.ToJSON())
}
// ✅ GOOD: Assert on specific fields you care about
func TestGood2(t *testing.T) {
output := def.Render(ctx)
assert.Equal(t, "apps/v1", output.APIVersion())
assert.Equal(t, 3, output.Get("spec.replicas"))
}
Integration Testing with envtest
For testing controller integration without a full cluster:
package integration_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"myplatform/definitions/webservice"
)
func TestWebserviceIntegration(t *testing.T) {
// Start envtest
testEnv := &envtest.Environment{}
cfg, err := testEnv.Start()
require.NoError(t, err)
defer testEnv.Stop()
// Create client
k8sClient, err := client.New(cfg, client.Options{})
require.NoError(t, err)
// Apply definition
def := webservice.New()
cr, err := def.BuildCR()
require.NoError(t, err)
err = k8sClient.Create(context.Background(), cr)
require.NoError(t, err)
// Verify definition was created
var retrieved v1beta1.ComponentDefinition
err = k8sClient.Get(context.Background(),
types.NamespacedName{Name: "webservice", Namespace: "vela-system"},
&retrieved)
require.NoError(t, err)
assert.Equal(t, "webservice", retrieved.Name)
}
Definition Deployment Workflow
Development Cycle
┌─────────────────────────────────────────────────────────────────┐
│ Definition Development Cycle │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. WRITE 2. TEST 3. VALIDATE 4. APPLY │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Go Code │────▶│go test │─────▶│vela def │─▶│vela def │ │
│ │ │ │ │ │vet │ │apply │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ │ │ │ │ │
│ └────────────────┴──────────────────┴─────────────┘ │
│ ITERATE │
│ │
└─────────────────────────────────────────────────────────────────┘
CI/CD Integration
# .github/workflows/definitions.yaml
name: Definition CI
on:
push:
paths:
- 'definitions/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Run Unit Tests
run: go test ./definitions/... -v
- name: Validate Definitions
run: vela def vet ./definitions/
- name: Generate CUE (dry-run)
run: vela def apply ./definitions/ --dry-run
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Apply Definitions
run: vela def apply ./definitions/
env:
KUBECONFIG: ${{ secrets.KUBECONFIG }}
Distribution
Go Modules
Go definitions are distributed as standard Go packages:
// go.mod for your definitions package
module github.com/myorg/platform-defs
require github.com/oam-dev/kubevela v1.10.0 // includes pkg/definition/defkit
# Consumers import your definitions
go get github.com/myorg/platform-defs@v1.0.0
// Use in application code
import "github.com/myorg/platform-defs/components/webservice"
app := application.New("my-app").
AddComponent(webservice.New("api").Image("myapp:v1"))
This leverages Go's existing ecosystem: semantic versioning, checksums, proxy caching, and private module support.
Module Dependencies
Defkit modules could import definitions from other Go modules, enabling composition and reuse across teams and organizations:
// go.mod
module github.com/myorg/platform-defs
require (
github.com/oam-dev/kubevela v1.10.0 // defkit SDK
github.com/myorg/base-components v1.2.0 // shared base definitions
github.com/anotherorg/monitoring-defs v2.0.0 // third-party definitions
)
// components/monitored_webservice.go
package components
import (
"github.com/oam-dev/kubevela/pkg/definition/defkit"
"github.com/myorg/base-components/components" // import base webservice
"github.com/anotherorg/monitoring-defs/traits" // import monitoring traits
)
// MonitoredWebservice composes a webservice with monitoring capabilities
func MonitoredWebservice() *defkit.ComponentDefinition {
// Extend or compose with imported definitions
base := components.WebserviceParams()
// Add monitoring-specific parameters
metricsPort := defkit.Int("metricsPort").Default(9090)
enableTracing := defkit.Bool("enableTracing").Default(true)
return defkit.NewComponent("monitored-webservice").
Description("Webservice with built-in monitoring").
Workload("apps/v1", "Deployment").
Params(append(base, metricsPort, enableTracing)...).
Template(monitoredWebserviceTemplate)
}
This would enable:
- Layered platforms: Base definitions from central team, extended by product teams
- Third-party ecosystems: Community-contributed definition libraries
- Version control: Semantic versioning for definition dependencies via Go modules
Addon Integration
KubeVela addons could include Go-based definitions alongside traditional CUE definitions. The proposed addon structure would support both:
my-addon/
├── metadata.yaml # Addon metadata
├── resources/ # Kubernetes resources
├── definitions/ # Traditional CUE definitions (existing)
│ ├── component-a.cue
│ └── trait-b.cue
├── godef/ # Go-based definitions (new)
│ ├── go.mod
│ ├── go.sum
│ ├── components/
│ │ ├── webservice.go
│ │ └── worker.go
│ └── traits/
│ ├── scaler.go
│ └── ingress.go
└── README.md
Key design points:
-
Coexistence: The
definitions/folder contains CUE definitions,godef/contains Go definitions. Both would be processed during addon installation. -
CLI detection: The
vela addoncommands would detect thegodef/folder and compile Go definitions to CUE before applying:# During addon enable, the CLI: # 1. Compiles godef/ to CUE using defkit # 2. Applies both definitions/ and compiled godef/ outputs vela addon enable my-addon -
Module dependencies in addons: The
godef/go.modcould import definitions from other Go modules:// godef/go.mod module github.com/my-addon/godef require ( github.com/oam-dev/kubevela v1.10.0 github.com/kubevela/catalog/addons/base-defs v1.0.0 // depend on another addon's definitions ) -
Build-time compilation: Addon maintainers could pre-compile Go definitions to CUE for distribution:
# Pre-compile for distribution (optional, for addons that want to avoid Go dependency at install time) cd my-addon/godef vela def render . --output ../definitions-compiled/ -
Validation: The addon validation process would include Go definition validation:
vela addon validate my-addon # validates both CUE and Go definitions
GitOps
# Render Go definitions to CUE/YAML for GitOps (extends `vela def render`)
vela def render ./definitions/ --output ./dist/ --format yaml
# Commit and sync with ArgoCD/Flux
git add ./dist/
git commit -m "Update definitions"
git push
Implementation Plan
Note
: This implementation plan represents the proposed design direction. API names, method signatures, and specific features may evolve during implementation as we discover edge cases, gather community feedback, or identify opportunities for improvement. The core goals and architecture will remain stable, but implementation details are subject to refinement.
Phase 1: Core Framework
- Single
defkitpackage withNewComponent(),NewTrait(),NewPolicy(),NewWorkflowStep()functions VelaCtx()API for runtime context (Name(),Namespace(),AppName(),ClusterVersion(), etc.)- Parameter type system: String, Int, Bool, Float, Enum, List, Object, Map, Struct, OneOf, Variant with validation
- Convenience types: StringList, IntList, StringKeyMap
- Schema-agnostic resource builder with
Set(),SetIf(),SpreadIf(),If()/EndIf(),VersionIf() - Collection operations:
Each(),Filter(),Map(),Pick(),Rename(),Wrap(),DefaultField(),Flatten(),Dedupe() - Multi-source collections:
FromFields(),MapBySource(),Nested(),Optional() - Helper builder pattern:
tpl.Helper(),StructArrayHelper(),ConcatHelper(),DedupeHelper() - Transform utilities:
Transform(),HasExposedPorts(),Format() - Expression helpers:
Lit(),Eq(),Ne(),Lt(),Le(),Gt(),Ge(),And(),Or(),Not() - IR→CUE compiler via
cuegen.go - CLI integration:
vela def apply,vela def vetfor Go files - CLI:
vela def init-module,vela def validate-module - Registry pattern with
defkit.Register(),All(),Components(),Clear(),Count(),ToJSON() - Status/Health helpers for all workload types:
- Deployment:
DeploymentStatus(),DeploymentHealth() - DaemonSet:
DaemonSetStatus(),DaemonSetHealth() - StatefulSet:
StatefulSetStatus(),StatefulSetHealth() - Job:
JobHealth() - CronJob:
CronJobHealth()
- Deployment:
- Composable Health Expressions via unified
Health()API (generic for any resource type):- Condition checks:
Health().Condition(),Health().AllTrue(),Health().AnyTrue() - Phase checks:
Health().Phase(),Health().PhaseField() - Field comparisons:
Health().Field(),Health().FieldRef(),Health().Exists(),Health().NotExists() - Combinators:
Health().And(),Health().Or(),Health().Not() - Always healthy:
Health().Always() - Policy generation:
Health().Policy(expr),HealthPolicyExpr(expr)
- Condition checks:
- Composable Status Expressions via unified
Status()API (generic for any resource type):- Field extraction:
Status().Field(),Status().SpecField(),Status().Exists() - Condition access:
Status().Condition().StatusValue(),.Message(),.Reason() - Message building:
Status().Format(),Status().Concat() - Conditional messages:
Status().Switch(),Status().Case(),Status().Default() - Health-aware:
Status().HealthAware(healthyMsg, unhealthyMsg) - Structured details:
Status().WithDetails(),Status().Detail() - Status generation:
Status().Build(),CustomStatusExpr(expr)
- Field extraction:
- TestContext for unit testing:
TestContext(),WithName(),WithNamespace(),WithParam(),WithClusterVersion(), etc. - Custom matchers for Gomega-based testing
- Example components: webservice, worker, task, cron-task, daemon, statefulset, k8s-objects, ref-objects
Phase 2: Complete Definition Support
- PolicyDefinition with
NewPolicy()- Example policies: topology, apply-once, garbage-collect, override, read-only, replication, resource-update, shared-resource, take-over
- WorkflowStepDefinition with
NewWorkflowStep()- Example workflow steps: deploy, suspend, apply-component, apply-deployment, apply-object, apply-terraform-config, apply-terraform-provider, build-push-image, check-metrics, clean-jobs, collect-service-endpoints, create-config, delete-config, depends-on-app, deploy-cloud-resource, export-data, export-service, export2config, export2secret, generate-jdbc-connection, list-config, notification, print-message-in-status, read-config, read-object, request, share-cloud-resource, step-group, webhook
- TraitDefinition with
NewTrait()- Container patches: command, env, container-image, container-ports, init-container, startup-probe, resource, securitycontext
- Pod-level: affinity, hostalias, lifecycle, podsecuritycontext, sidecar, topologyspreadconstraints
- Scaling: scaler, cpuscaler, hpa
- Networking: expose, gateway, pure-ingress, service-account, service-binding
- Storage: storage
- Metadata: labels, annotations
- Advanced: json-merge-patch, json-patch, k8s-update-strategy, nocalhost
RawCUE()escape hatch for complex CUE patterns- Trait patterns: PatchContainer helper, SetRawPatchBlock, SetRawOutputsBlock
Phase 3: Distribution & Ecosystem
- Addon integration: Support
godef/folder in addon structure for Go-based definitions - Module dependencies: Enable defkit modules to import definitions from other Go modules
- CLI addon commands:
vela addon enabledetects and compiles Go definitions - Addon validation:
vela addon validateincludes Go definition validation - OCI registry support for distributing compiled definitions
- Migration tooling (
vela def gen-gofor CUE→Go) - Enhanced documentation and tutorials
Phase 4: Advanced Features
- Other languages based on community demand
- IDE plugins
- Definition composition
Compatibility
Coexistence with CUE
- CUE definitions remain fully supported
- defkit is an alternative path, not a replacement
- Both produce valid X-Definition CRs
Migration
# Optional: Import existing CUE to Go (inverse of gen-api, NEW subcommand)
vela def gen-go ./legacy-definitions/*.cue --output ./converted/
FAQ
Q: Can I still write CUE definitions? A: Yes. CUE definitions remain fully supported. defkit is an alternative.
Q: How do I access runtime values like readyReplicas?
A: Use defkit.Status().Field("status.readyReplicas") when defining custom status expressions via CustomStatus(). This generates CUE that accesses context.output.status.readyReplicas at runtime. For common workloads, use pre-built helpers like defkit.DeploymentStatus().Build().
Q: What about CUE unification (&)?
A: Common patterns like defaults with runtime values are handled automatically. For complex unification, use RawCUE().
Q: Why Go first? A: Go is KubeVela's implementation language and widely used in the Kubernetes ecosystem. Other languages may follow based on community demand.
Q: How do I test definitions without a cluster?
A: Use defkit.TestContext() to create mock contexts with parameters, cluster version, and output status. All testing can be done with standard Go testing frameworks.
Q: Can I see the generated CUE?
A: Yes. Use vela def render ./definition.go --output cue or def.ToCUE() in tests.