serve.target operatorBox

4 min read

Each named target in serve.target can carry its own operatorBox. When a CR is applied through a specific target, the reconciler uses that target’s operatorBox instead of the CRD-level one — a different set of child resources, different lifecycle hooks, and optionally a different set of preReconcile gates.

This makes the target the unit of runtime execution: the same CRD can behave differently depending on which surface delivered the intent, without branching on when: conditions inside one shared box.


Declaration

spec:
  crds:
    website:
      operatorBox:               # CRD-level fallback — used by kubectl apply / unknown targets
        onCreate:
          deployments:
            - name: "{{ .metadata.name }}"

      serve:
        enabled: true
        target:
          web:
            primary: true
            operatorBox:         # used when CR arrives via the "web" target
              preReconcile:
                enqueueGate:
                  when:
                    - field: "{{ .spec.image }}"
                      notEquals: ""
              onCreate:
                deployments:
                  - name: "{{ .metadata.name }}-web"
                    image: "{{ .spec.image }}"
                    replicas: "{{ .spec.replicas }}"

          regional:
            operatorBox:         # used when CR arrives via the "regional" target
              preReconcile:
                reconcileGate:
                  when:
                    - field: "{{ len .spec.regions }}"
                      notEquals: "0"
              onCreate:
                deployments:
                  - name: "{{ .metadata.name }}-{{ .item }}"
                    image: "{{ .spec.image }}"
                    forEach:
                      field: spec.regions
                      as: item
                namespaces:
                  - name: "{{ .metadata.name }}-{{ .item }}"
                    forEach:
                      field: spec.regions
                      as: item

Resolution order

When the reconciler is invoked, it selects the operatorBox by checking the CR’s annotations (most specific first):

  1. Target entry whose name matches orkestra.orkspace.io/serve-alias (alias wins over primary)
  2. Target entry whose name matches orkestra.orkspace.io/serve-target
  3. CRD-level operatorBox — fallback when no annotation or no matching target

CRs applied via kubectl apply carry no gateway annotations, so they always use the CRD-level box.


preReconcile gates at the target level

Per-target operatorBox blocks support the same preReconcile shape as the CRD-level box:

operatorBox:
  preReconcile:
    enqueueGate:       # evaluated before the item enters the work queue
      when:
        - field: "{{ .spec.image }}"
          notEquals: ""

    reconcileGate:     # evaluated after dequeue, before reconciler runs
      when:
        - field: "{{ len .spec.regions }}"
          notEquals: "0"
      anyOf:
        - field: '{{ .spec.tier }}'
          equals: premium
GateEvaluated byEffect when condition fails
enqueueGateInformer (before work queue)CR is dropped from the queue — no reconcile cycle starts
reconcileGateKordinator (after dequeue)Reconcile is skipped for this cycle — item is requeued

Gate semantics are identical to those at the CRD level — the only difference is they apply only when this target’s box is active. See operatorBox preReconcile for the full gate reference including external: calls.


Surface switch and resource cleanup

When a CR is re-submitted via a different target (a surface switch), the reconciler detects the change by comparing two annotations:

AnnotationWritten byValue
orkestra.orkspace.io/serve-aliasApply handler (per-request)The alias name, or "" for the primary target
orkestra.orkspace.io/last-surfaceReconciler (after each cycle)The effective target active at the end of the previous reconcile

A mismatch between last-surface and the current effective target triggers a cleanup sweep before the new target’s operatorBox runs. The sweep finds resources stamped with orkestra-owner=<name>.<prevTarget> and deletes them — both namespaced and cluster-scoped types.

Why a sweep and not template expansion? When the gateway routes a CR away from the old target, the spec fields that drove that target’s forEach declarations may already be absent (e.g. spec.regions is not included in the new POST body). Template-based deletion would expand forEach to nothing and silently miss the orphans. The label-selector sweep is immune to spec changes.

After cleanup, last-surface is updated to the current target and reconciliation proceeds with the new box.


keepPreviousSurface

To retain old-target resources after a switch (e.g. a canary scenario where both targets run simultaneously), set keepPreviousSurface: true. It can be declared at the CRD level or per-target:

# CRD level — applies to all targets
serve:
  apply:
    overrides:
      keepPreviousSurface: true

# Per-target — applies only when this target becomes active
target:
  canary:
    apply:
      overrides:
        keepPreviousSurface: true
    operatorBox:
      onCreate:
        deployments:
          - name: "{{ .metadata.name }}-canary"

CRD-level wins if set; per-target applies otherwise.

ValueBehaviour
false (default)Previous-surface resources are deleted on the first reconcile after a target switch
truePrevious-surface resources are left alive — no sweep runs

What stays fixed at the CRD level

Per-target operatorBox overrides lifecycle templates and gates. These fields are always taken from the CRD-level operatorBox:

  • Worker counts, resync intervals, and autoscale config
  • finalizers
  • rollBackOnError
  • reconciler.constructor and reconciler.hooks (custom reconciler wiring)

Only onCreate, onReconcile, onDelete, preReconcile, status, and external/cross blocks are resolved per-target.


Simulating a specific target

Use spec.target in the simulate file, or --target on the CLI, to route the simulation through a named target’s box:

# simulate-regional.yaml
spec:
  katalog: ./katalog.yaml
  cr: ./cr.yaml
  target: regional
  expect:
    ops:
      - cycle: 1
        verb: create
        resource: deployments
        name: my-app-eu-west
      - cycle: 1
        verb: create
        resource: namespaces
        name: my-app-eu-west

preReconcile gates are evaluated in simulation. A reconcileGate that blocks (e.g. len .spec.regions != 0 when regions is empty) will cause the simulate cycle to produce no ops — use expect.crds with steady: false to assert the gate fired rather than asserting specific resources.