Usage

7 min read

Can Orkestra manage built-in Kubernetes resources?

Yes. kind: Deployment, kind: Pod, kind: Service, and 30+ other built-in Kubernetes kinds are supported without declaring group, version, or plural — Orkestra enriches them automatically from its internal registry:

- name: deployment-governance
  apiTypes:
    kind: Deployment   # ← only field needed for built-in kinds
  validation:
    - field: metadata.labels.team
      operator: exists
      message: "all deployments must declare a team owner"
      action: warn
Governance without a separate policy engine
This is how you apply governance to Kubernetes built-in resources without OPA, Kyverno, or a separate admission controller. Orkestra watches the resource, validates it at reconcile time, and optionally intercepts at admission time when ENABLE_ADMISSION_WEBHOOK=true or security.webhooks.admission.enabled=true.

Run ork validate --full to see exactly what Orkestra resolves for a kind-only declaration.


What is the difference between validation and mutation?

Validation evaluates rules against a CR and either blocks it (action: deny) or surfaces an advisory (action: warn).

Mutation applies defaults and normalisations to a CR before it is stored. Fields declared with default: are set only when absent. Fields declared with override: are always set.

Both run at two points:

  • Admission time — when ENABLE_ADMISSION_WEBHOOK=true, synchronously during kubectl apply
  • Reconcile time — every reconcile cycle, regardless of webhook configuration

Declare once, enforced at both points.

Roll out rules safely
Deploy new validation rules with action: warn first. Observe controller_admission_validation_violations_total in Prometheus to understand which CRs would be affected. When you are confident, change to action: deny. The Katalog change takes effect on the next Orkestra restart.

Does ENABLE_ADMISSION_WEBHOOK=true block the API server if Orkestra is down?

No — by design. The webhook configuration uses FailurePolicy: Ignore by default. If Orkestra is unreachable when the API server calls /validate or /mutate, the operation is allowed through. Validation catches violations at reconcile time when Orkestra restarts.

# To change to blocking behaviour (requires high-availability Orkestra deployment):
webhooks:
  failurePolicy: Fail    # default: Ignore
Before setting Fail
FailurePolicy: Fail means Orkestra’s availability directly gates all CR deployments. Set it only with multiple Orkestra replicas, a PodDisruptionBudget, and confidence that your admission rules are correct. Start with Ignore.

How do I use when: conditions?

when: creates a resource only when a condition is met:

services:
  - name: "{{ .metadata.name }}-public"
    port: "80"
    targetPort: "{{ .spec.port }}"
    when:
      - field: spec.exposePublicly
        equals: "true"

Multiple conditions in the same when: block are ANDed. To express OR, create two separate resource entries with different when: conditions and the same name — only one will be created, whichever condition is met first.

See When Conditions for the full reference.


How does dependsOn ordering work?

dependsOn tells Orkestra to hold one CRD’s workers until another reaches a condition:

spec:
  crds:
    database:
      workers: 8
    application:
      dependsOn:
        database:
          condition: healthy   # wait until database workers are running and failure-free
    analytics:
      dependsOn:
        database:
          condition: started   # wait only until database workers are running

condition: healthy waits until the dependency’s workers are running and its consecutive failure count is zero. condition: started waits only until workers are running — useful when you want ordering without blocking on health.

The bare list form defaults to started:

dependsOn:
  - database

See CRD Entry Schema for the full reference.


What does forEach do?

forEach expands one resource declaration into N resources — one per element in a list field on the CR spec. Each iteration element is bound to a name and available as a template variable alongside the parent CR’s fields.

# CR spec
spec:
  replicationFactor: 3
  shards:
    - name: us-east
      region: us-east-1
      size: 50Gi
    - name: eu-west
      region: eu-west-1
      size: 50Gi

# Katalog
onCreate:
  custom:
    - apiVersion: storage.example.io/v1alpha1
      kind: Shard
      forEach:
        field: spec.shards   # iterate over this list
        as: shard            # bind each element as .shard
      metadata:
        name: "{{ .metadata.name }}-{{ .shard.name }}"
        namespace: "{{ .metadata.namespace }}"
        namespaced: true
      spec:
        region: "{{ .shard.region }}"
        size: "{{ .shard.size }}"
        replicationFactor: "{{ .spec.replicationFactor }}"

This creates two Shard CRs — my-store-us-east and my-store-eu-west — from a single declaration. If the parent CR’s spec.shards is updated at runtime, Orkestra adds new entries and deletes removed ones on the next reconcile.

forEach works on the custom: block (Custom Resources) and on standard resource blocks (deployments:, services:, etc.).

Try it:

ork init --pack advanced/16-custom-resources
cd 05-forEach-sharding
# Follow the steps in README

What is the difference between normalize: and conversion.paths:?

Both use note expressions to transform CR fields, but they solve different problems.

normalize: — runs in-memory before every reconcile. The raw CR in etcd is unchanged. Kubernetes never sees two versions of the CRD. Use it when you want to tolerate input shape variation within a single schema version:

normalize:
  spec:
    schedule: "{{ cronFromAny .spec.schedule }}"
    # accepts both "*/5 * * * *" and {minute:"*/5", hour:"*",...}
    # downstream templates always see a plain cron string

No webhook registration. No TLS. No infrastructure cost.

conversion.paths: — runs in the gateway’s /convert HTTPS endpoint, called by the Kubernetes API server. Use it when you have a real multi-version CRD (v1, v2) and need Kubernetes to translate between stored and requested versions:

conversion:
  storageVersion: v1
  updateCRD: true
  paths:
    - from: v1
      to: v2
      spec:
        schedule: "{{ cronToMap .spec.schedule }}"

The rule: if you control the CRD and want to tolerate different input shapes — normalize:. If you have committed to multiple API versions and need the API server to convert between them — conversion.paths:.

Schema Evolution


When do I need Go hooks?

The external: block handles HTTP already — GET, POST, bearer tokens, chained calls where each result flows into the next via .external.{name}.body. when: and anyOf: handle conditional logic. For most operators, those are enough.

Hooks become necessary when the work is genuinely outside HTTP:

  • Non-HTTP protocols — gRPC, database connections, message queue operations, anything that isn’t an HTTP endpoint
  • Complex computed fields from non-HTTP sources — deriving values that require SDK calls, database queries, or binary protocols where there is no HTTP endpoint to call

For AWS, GCP, and Stripe — providers are in development for common SDK integrations (aws:, gcp:, stripe: blocks in the Katalog). Until a provider covers your case, hooks are the path.

Hooks are additive: the hook runs, then Orkestra applies onCreate/onReconcile templates as normal. The template layer does not disappear.

operatorBox:
  reconciler:
    hooks:
      location: github.com/myorg/database-operator/hooks
      function: DatabaseHooks

Try it:

ork init --pack advanced/09-hooks
cd 09-hooks
# Follow the steps in README

Typed Operators — Hooks · Migration Guide — Hybrid · Migration Guide — Hooks Only


When do I need a constructor?

When you need to own the full reconcile loop — typically when integrating an existing operator without rewriting it.

operatorBox:
  reconciler:
    default: false   # GenericReconciler is replaced; constructor owns everything

reconciler.default: false is the one field change that replaces the entire reconciler. Your constructor receives Orkestra’s KubeClient and informer — no controller-runtime required. Declarative templates (onCreate, onReconcile, status.fields) are not applied when reconciler.default: false; the constructor is responsible for all state.

Try it:

ork init --pack advanced/10-constructor
cd 10-constructor
# Follow the steps in README

Typed Operators — Constructor · Migration Guide — Constructor · ork migrate


Can I mix declarative, hooks, and constructor operators in one binary?

Yes. Each CRD in a Katalog or Komposer can use a different mode. One binary, one process:

# komposer.yaml
spec:
  crds:
    database:          # typed hooks — Go SDK calls alongside declarative templates
      workers: 5
    website:           # dynamic — pure YAML, no Go
      dependsOn:
        database:
          condition: healthy
    pipeline:          # constructor — full custom reconciler
      dependsOn:
        website:
          condition: started

You promote along a single axis: start pure YAML, add reconciler.hooks: when you need Go logic, flip reconciler.default: false when you need full control. No project restructure. No framework switch.

Try it:

ork init --pack advanced/11-mixed-operator-pattern
cd 11-mixed-operator-pattern
# Follow the steps in README

Typed Operators — Mixing all three