v0.7.15 — Gateway Webhook Intake + Artifact Signing
Artifact signing — Cosign keyless, publish: block, local testing
Patterns now carry cryptographic proof of origin via Cosign keyless signing. No key management — the OIDC token CI already issues is the credential.
ork pattern sign / ork pattern verify — new ork pattern subcommand group, mirroring docker image. Sign a pushed artifact; verify its signature and print the signer subject.
ork push --sign — convenience flag that signs immediately after push, equivalent to ork push followed by ork pattern sign.
ork push --sign-local / ork pattern sign --local — push to ttl.sh and sign for local testing. No real registry or CI credentials required. Prints the verify and inspect commands with the TTL on the artifact. Use --ttl to control how long the artifact lives (default: 1h).
ork inspect — always shows Signed: row. No flag required. Add --verbose to expand the issuer and Rekor log entry beneath the row.
publish: block — new top-level katalog field. Declares the signing policy for a pattern and controls which quality gates are required.
publish:
signing:
verify: true # ork pull --verify enforces this
expectedIdentities:
- github.com/myorg/postgres/.github/workflows/release.yaml@refs/heads/main
tests:
e2e: true # default
simulate: true # default
intent: false # opt-in; requires gateway.api.enabled: true and intent.yaml
verify: true — ork pull --verify refuses unsigned artifacts or artifacts signed by an unexpected identity. expectedIdentities uses OIDC subject claims; the issuer is inferred from the subject prefix (GitHub Actions, GitLab CI supported).
Cosign binary is resolved from $PATH, then ~/.orkestra/tools/cosign, then downloaded automatically from GitHub releases on first use — no manual install step.
Gateway webhook intake — GitHub, GitLab, Slack, generic
gateway.webhooks adds four inbound, push-based delivery sources that resolve through the exact same target-mode pipeline POST /api/v1/apply does — the GitOps counterpart to ork serve apply’s CLI/CI-driven pull model.
gateway:
webhooks:
github:
- name: payments-repo
enabled: true
path: /webhooks/github/payments
branch: main
watch:
- "services/*/intent.yaml"
secretRef: { name: ork-payments-github-secret, key: secret }
contentTokenRef: { name: ork-payments-github-app-token, key: token }
gitlab: [ ... same shape ... ]
slack:
- name: platform-workspace
enabled: true
path: /webhooks/slack
signingSecretRef: { name: ork-slack-signing-secret, key: secret }
commands: ["/deploy"]
generic:
- name: pagerduty
enabled: true
path: /webhooks/generic/pagerduty
secretRef: { name: ork-pagerduty-webhook-secret, key: secret }
GitHub / GitLab — a push to branch touching a file matching watch (glob patterns, same shape as GitHub Actions’ own on.push.paths) fetches that file’s content via the Contents API / Repository Files API and applies it as a target-mode intent. A push can match several files; each is applied independently. contentTokenRef — a separate credential from secretRef — reads the file content, since push payloads carry only changed paths, never content. reportStatus: true optionally posts the apply outcome back as a commit/pipeline status.
Slack — a slash command’s text ("<target> key=value ...") becomes the intent. Acks within Slack’s 3-second window, then applies on a bounded background worker pool and posts the outcome to response_url.
Generic — any caller that can POST JSON and sign it with HMAC-SHA256 (PagerDuty, Datadog, an internal system). The body is the intent directly.
Every entry’s own name authorizes under serve.tokens — the same identity model a gateway.api.auth.tokens bearer token uses — and is stamped as the serve-source provenance annotation on every CR it applies. secretRef/contentTokenRef/signingSecretRef all reuse the APISecretRef shape gateway.api.auth.tokens[].secretRef already uses, so every webhook credential gets the same self-bootstrap-if-missing and rotateAfter rotation behavior for free. ork validate enforces entry names unique across all four sources (not just within one — this is also what lets ork webhook play resolve --source from --webhook alone), unique paths across every source, required credentials per source, and that every serve.tokens key resolves to either a gateway.api.auth.tokens entry or a gateway.webhooks entry’s name.
Multi-cluster routing and bootstrap
One gateway instance can now route intents to multiple Kubernetes clusters. The cluster that receives each CR is determined per-CRD, per-target, or dynamically at apply time from the intent payload.
gateway.clusters — new block registering remote clusters with their endpoint and credentials:
gateway:
clusters:
prod:
endpoint: https://prod.internal:6443
tokenRef: { name: orkestra-prod, namespace: default, key: token }
caRef: { name: orkestra-prod, namespace: default, key: ca.crt }
serve.cluster — routes all applies for a CRD to a named cluster by default. Per-target overrides (serve.target.<name>.cluster) route specific targets to different clusters. Template expressions ({{ if eq .request.env "prod" }}prod{{ else }}staging{{ end }}) resolve at apply time from the intent payload.
ork clusters validate — validates gateway.clusters offline: endpoint, credentials, static cluster references, and template expressions against the full funcMap.
ork clusters check — goes online: connects to each cluster, reads credentials from the gateway cluster, and verifies the katalog’s CRDs are installed.
ork clusters bootstrap — provisions least-privilege access on a target cluster and stores the credentials in the gateway cluster. Refactored as a generic tool: works for Orkestra and for any other system (ArgoCD, Flux) that needs a scoped ServiceAccount + token on a remote cluster.
- Single cluster:
ork clusters bootstrap --context kind-prod --name prod - Multiple clusters:
ork clusters bootstrap --config cluster-config.yaml - Validate a config file without touching any cluster:
ork clusters bootstrap --validate cluster-config.yaml --no-hintsuppresses the Orkestra snippet for non-Orkestra consumers--dry-runpreviews every resource without applying- Config file
rules:field for explicit ClusterRole rules (generic path) - SA name override (
sa-name) for non-Orkestra SA naming - Verb validation in
--validate: unknown verbs are caught before any cluster is touched
ork generate rbac / ork generate bundle — generate per-cluster RBAC files alongside the local one. Template-routed CRDs appear in all cluster files with a warning to remove inapplicable rules.
gateway-<name>-rbac.yamlgenerated per registered cluster- Without
-o, files are written to the current directory (no stdout pipe required) -o -for explicit stdout
ork gate run — local gateway for Serve layer development
ork gate run -f katalog.yaml starts the gateway in HTTP-only mode without a cluster deployment. The Gateway API (POST /api/v1/apply, GET /api/v1/resources/, intake webhooks) runs on the health port (default :8080). Admission and conversion webhooks are skipped — they require TLS and a live cluster.
Use this to test serve routing, apply flows, and intake payloads before pushing a Helm deployment. Admission rules are still covered by ork gate -f katalog.yaml --cr cr.yaml.
This is backed by a build-tag split in cmd/internal/:
gateway.go(//go:build gateway) — production; hard exit outside a podgateway_dev.go(//go:build !runtime && !gateway) — dev; HTTP-only Serve layer
kubeclient.Interface — renamed from KubeClient
kubeclient.KubeClient is now kubeclient.Interface, matching the Go convention used by kubernetes.Interface and dynamic.Interface. All signatures, struct fields, type assertions, and return types updated across the codebase. No behaviour change.
ork simulate --envtest — declarative integration testing
ork simulate -f simulate.yaml --envtest runs the same simulate.yaml against a real kube-apiserver + etcd spun up locally — no cluster, no deployed operator. The reconciler, CR, and expect: assertions are unchanged; only the backend switches from fake in-memory clients to a real API server. Envtest binaries auto-download to ~/.ork/envtest-bins on first use; KUBEBUILDER_ASSETS overrides this.
New simulate.yaml fields declare the CRD schema to install:
spec:
crd: ./crds/my-operator.yaml # single CRD file
crdFiles: # or multiple
- ./crds/website.yaml
- ./crds/database.yaml
crFiles: # multiple CR files (supplement cr:)
- ./crs/a.yaml
- ./crs/b.yaml
--envtest requires at least one crd or crdFiles entry.
Op recording uses an HTTP transport interceptor (not reactor chains) so all kubeclient paths — typed clientset SSA patches, dynamic client, controller-runtime client — are captured with the correct verb (apply, patch, create, delete).
tests/simulate-envtest/ ships the first suite: basic reconcile, status subresource patch, and namespace filter — the same scenarios covered by tests/integration/kubeclient/ and tests/integration/informer/, expressed as YAML.
ork webhook — list and locally play webhook entries
New CLI namespace mirroring ork token/ork serve play for the webhook intake surface.
ork webhook list
ork webhook play -f katalog.yaml --webhook payments-repo \
--event push-event.json \
--fetch services/payments/intent.yaml=local-intent.yaml \
--simulate
ork webhook play runs the real entry’s declared branch/watch/commands through the exact chain ork serve play uses — target resolution, token check, CR construction, provenance stamping, admission validation — with no cluster, no HTTP server, and no real GitHub/GitLab/Slack account. Signature/token verification is skipped; --fetch <path>=<local-file> supplies what the Contents/Repository Files API would have returned for a matched path. --simulate extends the chain into ork simulate, same as ork serve play --simulate. --source is optional — webhook entry names are unique across all four sources, so it’s resolved from --webhook automatically when omitted.
Pre-reconcile gates — operatorBox.preReconcile
Two gate points under operatorBox.preReconcile, each firing at a different stage of the pipeline:
operatorBox:
preReconcile:
enqueueGate: # informer layer — before the item enters the queue
when:
- field: "{{ .spec.active }}"
equals: "true"
reconcileGate: # kordinator layer — after dequeue, before reconciler
when:
- field: "{{ .spec.enabled }}"
equals: "true"
or:
- field: "{{ .spec.environment }}"
equals: "production"
- field: "{{ .spec.environment }}"
equals: "staging"
enqueueGate — evaluated by the informer in handleEvent. Object is silently dropped before it ever enters the work queue. No health state change; no kordinator involvement. Zero queue pressure for objects that should be completely ignored.
reconcileGate — evaluated by the kordinator after dequeue. When conditions fail the reconcile cycle is skipped and CRD health is set to gated (idle, not degraded). Clears on the next successful reconcile.
Both gates support external: calls — at the gate level or at the shared preReconcile: level:
operatorBox:
preReconcile:
external: # shared — results available to both gates
- name: featureFlag
url: "{{ .spec.flagUrl }}"
enqueueGate:
external: # gate-specific calls, run after shared
- name: quota
url: "{{ .spec.quotaUrl }}"
when:
- field: "{{ .external.featureFlag.body }}"
equals: "true"
reconcileGate:
when:
- field: "{{ .external.quota.body }}"
equals: "available"
Calls run in order — shared first, then gate-level. Each call’s results are injected into the resolver before the next call runs, so later calls can reference earlier results.
Both gates use the full resolver chain (.spec, .metadata, serve intent, profiles, notes). Logic lives in pkg/katalog (EvaluatePreReconcile, EvaluateEnqueueFilter) and is called via registered closures so neither the informer factory nor the kordinator has a direct katalog dependency.
EvaluateWhen renamed to EvaluateConditions — the function evaluates both when: (AND) and or: (OR), so the name now reflects what it does.
gated state in Control Center — separate from healthy/degraded. Purple badge with gate reason. StatusCounts.Gated propagates through the full CC chain.
crdFiles / crFiles added to E2ESpec. tests/simulate-envtest/04-conditional-reconciliation/ covers gate-pass and gate-discard via envtest simulate. examples/intermediate/05-when-conditions/conditional-reconciliation/ — App (reconcileGate) + Route (unconditional) pack.
Per-target operatorBox — surface-specific reconciliation
serve.target.<name>.operatorBox overrides the CRD-level operatorBox for CRs routed through that surface. The gateway stamps orkestra.orkspace.io/serve-target on every applied CR; the runtime reads that annotation at reconcile time and uses the matching target’s templates, hooks, and gates instead of the shared CRD-level ones. CRs applied via kubectl apply (no annotation) fall back to the CRD-level operatorBox.
This applies equally to declarative and typed (hooks/constructor) operators. A per-target reconciler.hooks.args block means the same binary receives different resolved values depending on which surface delivered the intent — no code change, no separate operator:
operatorBox:
onCreate:
deployments:
- name: "{{ .metadata.name }}"
services:
- name: "{{ .metadata.name }}-svc"
serve:
enabled: true
target:
web:
primary: true
operatorBox:
onCreate:
deployments:
- name: "{{ .metadata.name }}-web"
apifixture:
operatorBox:
onCreate:
deployments:
- name: "{{ .metadata.name }}-apifixture"
preReconcile, status, and reconciler.hooks.args follow the same pattern — a target may declare its own values, with the CRD-level config as the fallback when absent. Reconciler settings (workers, resync, autoscale) are fixed at CRD level.
Cleanup on target switch is handled automatically via a label-selector sweep on orkestra-owner=<name>.<prevTarget> — immune to spec fields being cleared before cleanup runs. keepPreviousSurface: true skips the sweep when both surfaces should run simultaneously.
ork simulate --target <name> — simulates a specific target’s operatorBox. Also declarable in simulate.yaml via spec.target:. CLI flag takes precedence over the spec field.
simulate.yaml spec.target: — new field. Pins the simulated reconciliation to a named target’s operatorBox, equivalent to passing --target on the CLI.
ork simulate refactored — CLI simulate helpers now take a cliSimulateOptions struct instead of a flat parameter list, reducing signature length across runSimulate, runSimulateFromSpec, runSimulateDiscovery, and simulateOne.
Serve modes, apply-time controls, and field selectors
Three new blocks under serve and per target give platform teams granular control over the Gateway API surface, override behaviour, and full CR routing.
serve.modes — controls which apply modes are available for a CRD. Both default to true.
serve:
enabled: true
modes:
target: true # target mode — submit fields with a target identifier
cr: false # full CR mode — submit a complete Kubernetes CR
targets:
staging:
primary: true
modes:
target: false # disable target mode in staging
At least one mode must be enabled. ork validate enforces this. Can be set at the CRD level and per target.
serve.apply.overrides — controls whether request-level overrides (?overwrite=true and ?override=true) are honoured. Both default to true (allow overrides). This is a second line of defence — even if the caller passes the override parameter, the gateway can reject it based on the configuration.
serve:
enabled: true
apply:
overrides:
resourceConflict: true # allow ?overwrite=true (SSA field ownership)
targetConflict: false # disallow ?override=true (routing surface changes)
targets:
staging:
primary: true
apply:
overrides:
targetConflict: true # only staging allows routing changes
production:
primary: false
# inherits CRD-level
resourceConflict (previously forceConflict) — when true, callers can pass ?overwrite=true to force field ownership on server-side apply. When false, the override is rejected regardless of the request.
targetConflict (previously targetOverride) — when true, callers can pass ?override=true to change the routing surface (target/alias) of an existing CR. When false, the override is rejected and routing surface changes are always disallowed.
Both settings can be set at the CRD level (fallback) and per target. Target-level wins when set.
serve.targets[<name>].fieldSelector — links full CRs to a target based on field values. When a CR matches ALL key-value pairs, it is automatically routed to that target — enabling per-target response config, tokens, permissions, and mode enforcement for full CR mode.
serve:
enabled: true
targets:
internal:
fieldSelector:
spec.workloadType: app
modes:
cr: false # internal disallows full CRs
apply:
targetOverride: false
fieldSelector — a map of dot-notation field paths to values (max 3 per target). This is a true selector — like Service → Pod selection. Each target must have a unique selector. ork validate enforces uniqueness and warns if a target has cr: false but no field selector.
Validation rules:
- Max 3 field selectors per target
- Unique across targets — no two targets can share the same
path:valuepair - Must be valid dot-notation paths (e.g.,
spec.mealPlan: dinner) - Values must be non-empty
The target becomes the owner of the matched CR — controlling its mode, response config, tokens, and provenance.