Declarative — Zero Go
The same WebApp operator as the baseline — same CRD, same Deployment, same Service, same status — expressed as a pure Katalog. No Go. No binary. No image to build.
This is the most common surprise for people coming from controller-runtime: not all operators need Go. If your operator creates Kubernetes resources, applies drift correction, writes status, and enforces admission rules, the declarative system handles all of it. The Orkestra runtime is the binary.
ork init --pack from-controller-runtime
cd from-controller-runtime/02-declarative
What you will learn
- How to declare Deployments, Services, and status fields in a Katalog
- How
reconcile: trueenables drift correction without writing a single reconcile loop - How admission rules work without a webhook server setup
- The complete list of what the runtime provides automatically — metrics, leader election, panic recovery, health endpoints — when you declare rather than build
What changed
Everything in the controller-runtime project that was machinery is gone:
main.go— not neededDockerfile— not needed- Helm chart — not needed
webapp_controller.go— replaced by declarations inkatalog.yaml
The reconcile logic is now a declaration:
operatorBox:
reconcile:
onCreate:
deployments:
- name: "{{ .metadata.name }}"
image: "{{ .spec.image }}"
replicas: "{{ .spec.replicas }}"
port: "{{ .spec.port }}"
reconcile: true
Template expressions — {{ .spec.image }}, {{ .metadata.name }} — have access to the full CR, its status, child resource state (.children.*), cross-CRD observations (.cross.*), HTTP call results (.external.*), and live runtime metrics (.metrics.*). See Orkestra Notes and Conditionals for what is expressible declaratively before reaching for Go.
Try it
ork init --pack from-controller-runtime
cd from-controller-runtime/02-declarative
# Follow steps in README
Want to add another operator?
A new business concern — a background worker, a certificate issuer, a provisioner — is just another CRD entry in the same Katalog. No new project, no new binary, no new Helm chart.
Add crd-with-secret.yaml and cr-with-secret.yaml alongside the existing files. Then add an entry under spec.crds::
spec:
crds:
webapp:
# ... existing webapp entry ...
worker:
crdFile: ./crd-with-secret.yaml
crFiles:
- ./cr-with-secret.yaml
operatorBox:
reconcile:
onCreate:
secrets:
- name: "{{ .metadata.name }}-token"
once: true
rotateAfter: 30d
data:
token: "{{ randomAlphanumeric 32 }}"
deployments:
- name: "{{ .metadata.name }}"
image: "{{ .spec.image }}"
replicas: "{{ .spec.replicas }}"
reconcile: true
env:
- name: WORKER_TOKEN
valueFrom:
secretKeyRef:
name: "{{ .metadata.name }}-token"
key: token
emit:
status:
fields:
- path: phase
value: "Running"
runtime:
allowedNamespaces:
- default
This is the same pattern for all options — hooks, constructors, or mixed. The only difference is what goes inside operatorBox:. The CRD declaration, file layout, and the idea of adding a new entry are identical.
When to reach for more
If the logic can live outside the cluster — a script, a service, a function — Remote lets you write it in any language without a Kubernetes client. No Go, no image build.
If any resource needs computed logic that’s awkward to declare, Hybrid keeps the declarative side for the straightforward resources and adds a single Go hook for the rest. When in doubt, try declaring it first — the system keeps expanding.