Authoring Blueprints
A blueprint is a KCL package — a directory containing at minimum a main.k file. An optional blueprint.yaml manifest declares the blueprint's metadata, inputs, expected outputs, and status expressions. The Platspec Operator executes main.k at reconciliation time, passing it a structured context built from the Platform and its associated resources.
Package Structure
my-blueprint/
├── blueprint.yaml # Manifest: metadata, inputs, outputs, status (optional but recommended)
└── main.k # KCL logic: receives context, produces Kubernetes manifestsThe blueprint.yaml Manifest
The manifest describes the blueprint's interface. See the Blueprint specification for the full field reference.
A minimal manifest:
apiVersion: blueprints.platformspec.io/v1alpha1
kind: Blueprint
metadata:
name: my-blueprint
capability: my-capability
version: 0.1.0
description: What this blueprint provisions.
author: Your Name
maintainer: team@example.com
providers: [] # empty = provider-agnostic
minOperatorVersion: 0.1.0
tags: []
inputs:
context:
required:
- platform
- environment
optional: []
config:
- field: replicas
type: int
default: 1
description: Number of replicas.
outputs:
generates:
- apiVersion: apps/v1
kind: Deployment
- apiVersion: v1
kind: Namespace
status:
target: Platform
fields:
- field: ready
type: bool
description: True when all replicas are ready.
expr: |
deploy = childResources["apps/v1/Deployment"][0]
desired = deploy.spec.replicas if deploy?.spec?.replicas else 1
ready = deploy.status.readyReplicas if deploy?.status?.readyReplicas else 0
ready >= desiredThe main.k KCL File
main.k is a KCL program that receives input context and returns a list of Kubernetes manifests. The operator injects context via option("ctx").
Input context
The operator assembles a structured context object from the Platform and its associated resources. Access it via option("ctx"):
ctx = option("ctx")
# Top-level keys always present if declared in inputs.context.required:
platform = ctx.platform # Platform resource spec
environment = ctx.environment # Environment resource spec (when running per-environment)
provider = ctx.provider # Provider resource spec (when scoped to a provider)
# Blueprint config (merged from BlueprintBinding config + Platform overrides):
config = ctx.config
# Convenience accessors
replicas = config.replicas or 1Returning manifests
main.k must output a list of Kubernetes manifests. Return them as KCL schema instances or plain dicts:
ctx = option("ctx")
config = ctx.config
platform = ctx.platform
ns_name = "${platform.metadata.name}-ns"
replicas = config.replicas or 1
[
{
apiVersion = "v1"
kind = "Namespace"
metadata = {
name = ns_name
labels = {
"app.kubernetes.io/managed-by" = "platspec-operator"
}
}
}
{
apiVersion = "apps/v1"
kind = "Deployment"
metadata = {
name = "${platform.metadata.name}-echo"
namespace = ns_name
}
spec = {
replicas = replicas
selector = {matchLabels = {app = "echo"}}
template = {
metadata = {labels = {app = "echo"}}
spec = {
containers = [{
name = "echo"
image = config.image or "nginx:latest"
ports = [{containerPort = 80}]
}]
}
}
}
}
]Accessing provider config
When a blueprint targets a specific cloud provider, the provider's config is available via the provider context key:
ctx = option("ctx")
provider = ctx.provider
config = ctx.config
account_id = provider.spec.config.accountId
region = provider.spec.config.region or "us-east-1"Generating one resource per environment-provider pair
Blueprints like cloud-account iterate over providerRefs on the Environment to emit one output resource per provider:
ctx = option("ctx")
environment = ctx.environment
config = ctx.config
[
{
apiVersion = "example.yourproject.io/v1alpha1"
kind = "CloudAccount"
metadata = {
name = "${environment.metadata.name}-${ref.name}"
namespace = environment.metadata.namespace
}
spec = {
provider = ref.name
emailDomain = config.emailDomain
}
}
for ref in environment.spec.providerRefs or []
]Status Expressions
The status.fields[].expr field in blueprint.yaml is a KCL expression evaluated after each reconciliation. It reads the live state of the resources your blueprint applied and writes derived values to the Platform's .status.
The childResources variable is a dict keyed by "<apiVersion>/<kind>" (e.g. "apps/v1/Deployment"). Each value is a list of live Kubernetes objects.
See the Status Expressions reference for detailed examples.
Key rules:
- All lines except the last are intermediate KCL assignments.
- The last line is the return value — it must match the declared
type. - Use
?.(safe-navigation) everywhere —.statusfields may be absent on the first reconciliation pass. - Expression errors log a warning and set the field to
null. They do not block the Platform from reachingReady.
Development Workflow
1. Install KCL locally
# macOS
brew install kcl-lang/tap/kcl
# or via pip
pip install kcl-lang2. Iterate on main.k locally
Run KCL directly to validate your output before deploying:
kcl main.k -D ctx='{"platform":{"metadata":{"name":"test"},"spec":{}},"environment":{"metadata":{"name":"dev"},"spec":{}},"config":{"replicas":1}}'3. Use the filesystem registry for development
Point a BlueprintRegistry at your local directory:
apiVersion: core.platformspec.io/v1alpha1
kind: BlueprintRegistry
metadata:
name: local-dev
namespace: platspec-system
spec:
type: filesystem
path: /path/to/your/blueprintsFilesystem registries bypass the cache
type: filesystem registries always fetch live — the operator never caches them. This means your changes are picked up on every reconciliation without needing to bump the version. Switch to oci or another remote type before publishing.
4. Trigger a reconciliation
Force the operator to re-reconcile by touching your BlueprintBinding:
kubectl -n platspec-system annotate blueprintbinding my-platform-bindings \
platspec.io/reconcile-trigger="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--overwrite5. Inspect generated resources
# See what resources were applied
kubectl -n platspec-system get blueprintbinding my-platform-bindings \
-o jsonpath='{.metadata.annotations.platspec\.io/generated-resources}' | jq .
# Tail operator logs
kubectl -n platspec-system logs -l app.kubernetes.io/name=platspec-operator -fPublishing a Blueprint
OCI registry (recommended)
Package your blueprint directory as an OCI artifact:
kcl registry push oci://ghcr.io/your-org/blueprints/my-blueprint:0.1.0 ./my-blueprint/Then declare a BlueprintRegistry pointing at your OCI registry and reference the blueprint by name and version from BlueprintBinding.
Git registry
Publish via a Git repository. The operator clones the repo and resolves blueprints by path:
spec:
type: git
url: https://github.com/your-org/blueprints.git
ref: mainBest Practices
- Declare all required context keys in
inputs.context.required. If a key is absent at runtime the execution fails with a clear error rather than a silent wrong value. - Provide defaults for all config fields. Blueprints should work with minimal configuration so that bindings only need to override what differs.
- Use safe-navigation everywhere in status expressions. Resources may not have
.statuspopulated on the first reconciliation pass. - Pin versions in production. Use
version: latestonly during development. Pinned versions are cached and deterministic. - Keep blueprints single-capability. One blueprint, one capability. Compose at the
BlueprintBindinglevel, not inside a blueprint. - Label generated resources consistently. Follow the operator's label conventions so resources can be discovered and cleaned up correctly.