v0.7.16 — Per-target operatorBox, lifecycle:, controller-runtime compatibility

3 min read

Breaking: anyOf: renamed to or:

Global rename across the entire schema — gates, autoscale conditions, validation rules, simulation specs, serve fields. The old name is rejected at load time.

# before
anyOf:
  - field: spec.env
    equals: production

# after
or:
  - field: spec.env
    equals: production

Breaking: resources: renamed to managedResources: in the constructor block

constructor.resources: and hooks.resources: are now managedResources:, which distinguishes managed resource kinds from the watch: block. Old name rejected at load time.

Breaking: domain.Reconciler interface signature

Typed operators implementing domain.Reconciler must update their Reconcile method:

// before
Reconcile(ctx context.Context, key string) error

// after
Reconcile(ctx context.Context, req domain.Request) (domain.Result, error)

req.Key is namespace/name. req.NamespacedName is available directly. domain.Result.RequeueAfter propagates to the work queue — ctrl.Result{RequeueAfter: X} returned from a domain.ReconcilerFrom-wrapped reconciler now works without any changes inside Reconcile.

ork migrate handles this automatically. Native mode operators must update manually.

Per-target operatorBox

Each serve target can now declare its own operatorBox — resources, hooks, and pre-reconcile gates scoped to that surface:

serve:
  target:
    standard:
      operatorBox:
        reconciler:
          hooks: true
    enterprise:
      operatorBox:
        preReconcile:
          enqueueGate:
            when:
              - field: "{{ isBusinessHours }}"
                equals: "true"

reconciler.default: false wires the target’s constructor from ReconcilerRegistry. A missing entry is a load-time error.

lifecycle: block

lifecycle:
  maturity: beta          # alpha | beta | stable | deprecated

  deprecation:
    message: "Replaced by task-runner"
    migratedTo: task-runner:v1.0.0
    timeline:
      from: "2026-01-01"
      to:   "2027-01-01"

  compatibility:
    orkestra: ">= 0.7.0"
    kubernetes: ">= 1.28"

A Komposer must declare lifecycle.accept.patterns to consume a deprecated Katalog. Without it, the deprecated operator blocks startup.

requeue: — per-object requeue scheduling

operatorBox:
  requeue:
    - when:
        - field: .status.phase
          equals: Pending
      after: "{{ timeUntil .status.certExpiresAt }}"

Different objects get different requeue intervals based on their own state. Complements resync: (uniform, whole-CRD period) — requeue: is conditional and per-object.

timeUntil note

Returns the duration until a timestamp as a Go duration string ("72h0m0s"), or "0s" if past or invalid. Primary use: requeue.after: for cert-expiry-style scheduling.

failPolicy: on pre-reconcile gates

operatorBox:
  preReconcile:
    reconcileGate:
      external:
        - name: featureCheck
          url: "{{ .spec.flagUrl }}"
      failPolicy: closed   # deny reconcile when external call fails

open (default) passes through when the external call fails. closed denies.

observe.events — Kubernetes Events as reconciliation triggers [EXPERIMENTAL]

observe.events extends the observe: block with a second observation mechanism alongside observe.watch. Kubernetes Event objects can now trigger primary CR reconciliation, with the event’s own properties available as resolver context at the enqueueGate.

operatorBox:
  observe:
    events:
      dbReady:
        reason: DatabaseReady
        type: Normal
        regarding:
          kind: Database
          name: my-database
        enqueueGate:
          when:
            - field: "{{ .events.dbReady.reportingController }}"
              equals: "database.myorg.io/controller"

The event entry matches on reason, action, type, reportingController, reportingInstance, regarding, and related. Matched events resolve the primary CR key via regarding (default) or broadcast to all managed CRs when regarding is absent.

The enqueueGate has access to the full event context as .events.<name>.*reason, action, type, reportingController, reportingInstance, regarding, related. The gate can reason about what the event says, not just that it happened. The same when:/or: condition machinery applies — no new syntax.

observe.watch and observe.events are siblings under pkg/runtime/informer/observe. Both share the same enqueue path, the same gate evaluation, and the same resolver construction. The only difference is what they observe and what context they inject.

This is marked experimental. ork validate emits a warning when event entries are declared. The regarding / keyFrom relationship and edge case handling are being settled in follow-up PRs before the experimental flag is removed.

Use when a secondary controller already emits Events and you want to react without polling, without watching the secondary CRD, and without writing a bridge controller.