v0.7.11 — Workload autoscaler, user-defined notes, and three new example packs

5 min read

Notes — the template vocabulary of a Katalog

ork init --pack intermediate/09-notes

Four sub-examples: built-in notes for live cluster queries and fallbacks (01-built-in), user-defined notes declared inline (02-user-defined), notes packaged into a Motif for team distribution via spec.imports (03-motifs), and Komposer-level note override (04-komposer). A root simulate.yaml and e2e.yaml run all four in sequence.

Temporal — time-dependent or time-aware operators

ork init --pack use-cases/temporal

Four sub-examples: business-hours provisioning (01-business-hours), weekly maintenance window (02-maintenance-window), per-region peak-hour replica scaling (03-regional-peak), and business-hours autoscaling driven by a user-defined note (04-autoscale). No CronJobs in any example.

Workload Autoscaler — replica control driven by time, external metrics, or cross-operator state

ork init --pack use-cases/workload-autoscaler

Three sub-examples: time-based jump scaling (01-time-based), external API step scaling via external: with the dev server (02-external-api), and cross-operator step scaling via cross: reading a sibling CRD’s queue depth (03-cross-operator).

autoscale: — replica control for Deployments, StatefulSets, and ReplicaSets

Adds an autoscale: block to deployments:, statefulsets:, and replicasets: declarations. On every reconcile, the autoscaler evaluates scale-up and scale-down conditions and patches spec.replicas when they pass. The reconciler’s drift correction is suppressed for any workload that declares autoscale: so the two never fight.

deployments:
  - name: "{{ .metadata.name }}"
    replicas: 2          # baseline — used as the starting point, not enforced once autoscale owns replicas
    autoscale:
      min: 2
      max: 10
      cooldown: 3m       # minimum gap between any two scale events
      scaleUp:
        conditions:
          when:
            - field: external.queue.queue.pendingJobs
              greaterThan: "100"
        increment: 2     # step scaling — adds 2 replicas per tick
      scaleDown:
        conditions:
          when:
            - field: external.queue.queue.pendingJobs
              lessThan: "20"
        decrement: 1

Scale modes — each direction supports either step or jump scaling:

  • increment: N / decrement: N — add or remove N replicas per tick, clamped to min/max
  • target: N — jump directly to N replicas in one reconcile

Condition sources — any data in the resolver context is a valid scaling signal:

  • time: / dayOfWeek: — time-window and weekday conditions (built-in notes: weekday, weekend, timeInWindow, nextCron)
  • field: referencing external.* — scale on live HTTP metrics fetched via external: each reconcile
  • field: referencing cross.* — scale on a sibling CRD’s status fields via cross: (informer cache, no HTTP call)

negate: true on any condition — inverts the result, enabling “not in business hours” and similar patterns without writing a separate note:

when:
  - dayOfWeek:
      weekday: true
    negate: true   # passes on weekends

External JSON auto-parsing — when an external: call returns a JSON object body, top-level keys are merged into external.<name> so nested fields are navigable with dot-path syntax (external.queue.queue.pendingJobs).

ork validate — validates autoscale: declarations: checks that min ≤ max, that each direction has exactly one of target, increment, or decrement, and that cooldown is a valid duration.

Dev server /workload-metricsork run --dev-server exposes a stateful metrics endpoint for testing external API autoscale locally. POST /workload-metrics/flip toggles between low-load (8 pending jobs) and high-load (152 pending jobs) without touching the cluster.

User-defined notes — named template expressions for Katalogs and Motifs

Adds a notes: block to Katalog and Motif. Each entry is a named Go template expression that becomes a callable function in every {{ }} context across the Katalog — status fields, when: conditions, onReconcile templates, normalize rules.

notes:
  - name: serviceHost
    description: Fully-qualified cluster hostname for this workload
    expression: "{{ .metadata.name }}.{{ .metadata.namespace }}.svc.cluster.local"

  - name: fullImage
    expression: "{{ .spec.image }}:{{ .spec.tag | default \"latest\" }}"

spec:
  crds:
    workload:
      operatorBox:
        status:
          fields:
            - path: host
              value: "{{ serviceHost }}"   # calls the note
        onReconcile:
          deployments:
            - image: "{{ fullImage }}"

Notes declared in a Motif are distributed the same way as profiles — via spec.imports at the Katalog level, available to every CRD without re-declaring them:

spec:
  imports:
    - motif: ./org-standards.yaml   # contributes notes and profiles Katalog-wide

A Komposer can override any note inline without touching the Katalog:

# komposer.yaml
notes:
  - name: serviceHost
    expression: "{{ .metadata.name }}.{{ .metadata.namespace }}.svc.prod-cluster.example.com"

CLI: ork validate --notes prints the merged note registry after validation. ork notes -f katalog.yaml appends user-defined notes to the built-in catalog.

Fix: ork patterns surfaces auth errors instead of silently showing 0 patterns

ork patterns without a valid GHCR login previously rendered an empty table (“0 patterns”) with no explanation. The catalog listing API requires auth even on public registries — unlike ork run which uses anonymous artifact pulls — so the two commands appeared to behave differently without any hint as to why.

The error is now written to stderr with a login hint:

warning: could not list patterns from ghcr.io/orkspace/orkestra-registry/patterns/katalogs
  → unauthorized: authentication required
  hint: try logging in with: docker login ghcr.io

Same fix applied to the motif URL listing path.

spec.imports — katalog-wide profile scoping for Motifs

Profiles declared in a Motif can now be imported at the Katalog level via spec.imports, making them available to every CRD in the Katalog:

spec:
  imports:
    - motif: ./org-standards.yaml
  crds:
    application:
      operatorBox:
        onCreate:
          deployments:
            - resources:
                profile: org-standard   # ✓ available to all CRDs
    database:
      operatorBox:
        onCreate:
          deployments:
            - resources:
                profile: org-standard   # ✓ same profile, no import needed here

spec.crds[name].imports still works for resources, status, and admission — profiles declared in those Motifs are ignored at the CRD level. This removes the previous behaviour where profiles from a CRD-level import leaked into the Katalog-wide registry, which caused conflicts when more than one CRD imported the same Motif.