Self-Service
Every Orkestra operator is self-service by default. The same gateway that the platform team uses to manage CRDs is the API every caller — a browser form, a CI pipeline, a Slack bot, a CLI — uses to interact with them. No separate portal. No secondary system to wire up.
The serve layer is what makes it work. It sits between callers and the operator, translating intent into Kubernetes objects without the caller knowing what an apiVersion is.
The model
A platform team defines a CRD and a Katalog. That Katalog already declares what the operator does — what children it creates, what validation rules apply, what the status looks like. Adding serve: to the CRD entry turns the same Katalog into a self-service API:
spec:
crds:
application:
serve:
enabled: true
target: app
namespace: '{{ teamName }}'
fields:
image:
label: "Container Image"
required: true
environment:
label: "Environment"
type: enum
enum: ["staging", "production"]
That is the whole declaration. One addition to an existing file. What it produces:
GET /api/v1/schema— listsappas a self-service targetGET /api/v1/schema?target=app— returns the flat field contractPOST /api/v1/apply— accepts{"target":"app","name":"...","image":"...","environment":"staging"}and creates the CRGET /api/v1/resources/app/...— reads status back- The Control Center shows a
[+ Create]button and a generated form
Every caller hits the same API. Every request goes through the same validation rules and token checks before anything reaches etcd.
What the serve layer does
A caller submits flat fields. The serve layer:
- Resolves the target to a CRD
- Checks the caller’s token and permissions
- Builds the full CR from the submitted fields (
spec,metadata.name,metadata.namespace, labels, annotations) - Stamps provenance annotations
- Evaluates
validation.rules - Applies via server-side apply
- Returns the response (default: the built CR, or a shaped payload if
serve.config.responseis declared)
The caller never sees the CRD schema, the Kubernetes object structure, or any of the operator internals. They see what they submitted and what the status says.
Field translation
The serve layer can transform submitted fields before writing to the CRD. Callers speak one vocabulary; the CRD speaks another. serve.fields.value and serve.fields.values bridge them:
fields:
schedule:
label: "Schedule (cron)"
required: true
values:
schedule.minute: '{{ cronMinute .value }}'
schedule.hour: '{{ cronHour .value }}'
schedule.dayOfMonth: '{{ cronDom .value }}'
schedule.month: '{{ cronMonth .value }}'
schedule.dayOfWeek: '{{ cronDow .value }}'
Caller submits "0 2 * * 1-5". CRD receives a structured schedule object. Neither side sees the other’s format.
Token scoping
Not every caller gets the same access. serve.tokens restricts which gateway tokens can reach a CRD, with per-token operation and namespace permissions:
serve:
tokens:
ci-pipeline:
namespaces: ["team-payments-staging"]
permissions:
resources: ["create", "update"]
Multiple surfaces, one CRD
A CRD can expose multiple named targets — aliases — alongside the primary. A preview alias creates lightweight environments; an internal alias returns a richer response. The same CR, the same operator, different surfaces.
→ Aliases and Intent Provenance
Testing without a cluster
ork serve validate # check serve config
ork serve validate --full # show target, fields, token map
ork serve play -f katalog.yaml --token dev -i intent.yaml # run the full chain locally
Use cases
The serve layer is general-purpose. The same mechanism works for:
- Internal Developer Platform — developer self-service for infrastructure and application CRDs
- CI/CD pipelines submitting deploy intents via token
- Slack bots posting to the gateway on user request
- Any caller that needs to create or update Kubernetes resources without knowing the CRD schema
Where to go next
Target Mode — how flat fields become a CR
Additional Fields — labels, annotations, field hints
Field Translation —
value,values, intent gating