v0.7.14 — Aliases and Intent Provenance

13 min read

Serve aliases

A CRD can now expose multiple named entry points — aliases — alongside its primary target. Each alias shares the same CRD, operator, and Kubernetes resource, but can restrict which tokens are valid for it and shape its response independently.

Aliases are declared in serve.target. The entry with primary: true is the primary surface; all other entries are aliases:

serve:
  target:
    apifixture:
      primary: true
    preview:
      include: ./serve/aliases/preview.yaml
    internal:
      tokens:
        platform-team:
          permissions:
            global: ["*"]

Callers use aliases the same way they use the primary target — by name in POST /api/v1/apply or via ork serve:

curl -X POST /api/v1/apply \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"target": "preview", "name": "my-service", ...}'

CRD-level serve.tokens and serve.config serve as fallback for any alias that does not declare its own. Each entry accepts enabled: false to close a surface without removing its config block.

Intent provenance annotations

Every CR applied through the gateway is stamped with three annotations by the apply handler:

AnnotationValue
orkestra.orkspace.io/serve-targetThe primary target name (e.g. apifixture)
orkestra.orkspace.io/serve-aliasThe alias name, or "" for the primary target
orkestra.orkspace.io/serve-sourceVerified OIDC sub claim of the caller, or "" for static token auth

Seven built-in notes expose provenance in any template expression:

NoteReturns
getServeTarget .Primary target name
getServeAlias .Alias name — "" for primary target
getServeSource .Delivery source
hasServeTarget .true when submitted via the Gateway API
hasServeAlias .true when a named alias was used
hasServeSource .true when a webhook source integration was used
isDirectApply .true when none of the three annotations are present

isDirectApply is true when a CR arrived via kubectl or CI direct apply, bypassing all gateway routing.

These notes work in when: conditions at both reconcile time (operatorBox resource templates) and admission time (validation and mutation rules).

Immutable routing surface

A resource’s routing surface — the target or alias it was created through — is immutable without explicit intent. Once a CR is applied via preview, only preview can update it:

{
  "accepted": false,
  "message": "routing surface conflict: resource was created via \"preview\", cannot update via \"apifixture\" without ?override=true"
}

Pass ?override=true to intentionally change the surface. The gateway logs a warning with the before/after values. New CRs and CRs created by direct kubectl apply are not subject to this check.

Alias-aware response shaping

Each alias can configure its own response config — what the apply response includes and what GET /api/v1/resources/... returns:

serve:
  target:
    apifixture:
      primary: true
    preview:
      config:
        response:
          default: false
          payload:
            phase: '{{ .status.phase }}'
            alias: '{{ getServeAlias . }}'

On GET, the gateway reads the serve-alias annotation stamped on the stored CR and applies the matching alias response config automatically — the caller does not need to pass ?target=. CRs applied via the primary target use the CRD-level response config.

Alias token permission model

Token permissions resolve in a three-layer chain, most specific first:

  1. Alias tokens — when the alias declares tokens:, only listed tokens are checked. A token absent from the alias map is denied, even if valid at the CRD level.
  2. CRD tokens — when no alias tokens are declared, the CRD-level restrictions apply.
  3. Allow all — when neither level declares restrictions.

serve.target[*].include

Each target entry supports include: — a path relative to the katalog file pointing to a YAML file with tokens: and/or config: at the top level. Inline fields take precedence on merge. Convention: ./serve/aliases/<name>.yaml.

Admission-time gating by alias or target

Provenance notes are available in when: conditions on validation.rules and mutation.rules. The gateway stamps the annotations before the SSA patch, so the webhook sees them on every create and update:

validation:
  rules:
    - field: spec.replicas
      lessThanOrEqualTo: 10
      message: "preview environments are capped at 10 replicas"
      action: deny
      when:
        - field: '{{ eq (getServeAlias .) "preview" }}'
          equals: "true"

--alias flag on ork serve subcommands

ork serve schema, ork serve fields, ork serve aliases, and ork serve can-i all accept --alias as an alternative to --target. Passing an alias name resolves the CRD through that surface and shows alias-specific context in the output header.

Gateway schema API includes aliases

GET /api/v1/schema and GET /api/v1/schema?target=<t> now include an aliases array in each response entry. GET /api/v1/resources/... accepts alias names in the {kind} path segment — aliases are routable the same way targets are.

Control Center surface selector

When a CRD has aliases, the create form in the Control Center shows a surface tab strip before the fields. Selecting a tab routes the submission through that alias. Single-surface CRDs stamp the target silently with no UI change.

CRDEntry.AliasNames()

A new AliasNames() []string method on CRDEntry returns a sorted slice of alias names. Used by the gateway schema response, the kordinator /katalog health endpoint, and the CC.

OIDC token authentication

gateway.api.auth.tokens now supports short-lived OIDC tokens as a credential source. Three named presets and a generic oidc: type are available:

  • githubOIDC: — GitHub Actions tokens. Issuer is hardcoded to token.actions.githubusercontent.com. The allow: block constrains by repository, repositoryOwner, ref, workflow, environment, and jobWorkflowRef.
    - name: gh-ci
      githubOIDC:
        allow:
          repository: myorg/payments
          ref: refs/heads/main
    
  • gitlabOIDC: — GitLab CI tokens. Issuer hardcoded to gitlab.com. Allow fields: namespacePath, refProtected, environment.
    - name: gl-ci
      gitlabOIDC:
        allow:
          namespacePath: mygroup/infra
          refProtected: "true"
    
  • vaultOIDC: — HashiCorp Vault identity OIDC tokens. url: is the Vault server root; the effective issuer and JWKS discovery base are {url}/v1/identity/oidc. Allow fields: entityName, entityID, namespace, plus a free-form allow: map for custom claim templates.
    - name: vault-ci
      vaultOIDC:
        url: https://vault.myorg.io
        allow:
          entityName: ci-agent
    
  • oidc: — any OIDC-compliant provider. issuer: is required. Discovery follows the standard {issuer}/.well-known/openid-configuration path. Free-form allow: map.
    - name: internal-ci
      oidc:
        issuer: https://auth.myorg.io
        audience: orkestra
        allow:
          sub: "system:serviceaccount:ci:runner"
    

All four types are mutually exclusive with token: and secretRef:. When an OIDC token authenticates a request, the verified sub claim is stamped as orkestra.orkspace.io/serve-source and available in templates via getServeSource ..

ork token — inspect and verify gateway tokens

New CLI namespace for working with gateway.api.auth.tokens entries without a running cluster.

  • ork token list — tabular list of all configured token entries: name, type (oidc/static), provider, and allow summary.
  • ork token verify — verify a JWT against the katalog locally. Fetches JWKS from the real provider, checks signature, expiry, issuer, and claim matching. Reports which entry matched and prints the verified claims.
  • ork token probe -n <name> — probe the OIDC discovery endpoint and JWKS for a named entry. Reports reachability, jwks_uri, key count, and algorithms. Useful for confirming Vault’s non-standard discovery path is reachable before deploying.
ork token list
ork token verify -t token.jwt
ork token probe -n vault-ci
ork token verify --api http://localhost:8443 -t token.jwt   # live mode via ork proxy

Gateway token validation at ork validate

gateway.api.auth.tokens entries are now validated statically at ork validate time:

  • Each token: value must be an ${ENV_VAR} reference — literals are rejected.
  • Each secretRef: entry must supply both name and key.
  • token and secretRef are mutually exclusive per entry.
  • oidc.issuer is required for the generic oidc: type.
  • githubOIDC.allow, gitlabOIDC.allow, and vaultOIDC.allow must not be empty — an empty block accepts any valid token from that issuer.
  • vaultOIDC.url is required.

ork serve validate --full — aliases block

ork serve validate --full prints the aliases block per CRD, showing token restriction status and response config presence for each alias.

ork serve play

New subcommand that runs the full gateway apply chain locally from a flat intent file — no cluster, no running gateway required.

ork serve play --token control-center

Reads intent.yaml (or intent.json) from the current directory and runs six stages in-process: target resolution, token permission check, CR construction from serve field declarations, provenance annotation stamping, admission rule evaluation, and response payload evaluation. Each stage prints its result; the trace stops at the first failure with a clear error.

The intent file is the same flat key-value document you would POST to /api/v1/apply:

target: apifixture
name: my-payment-service
workloadType: app
team: platform
environment: staging
repoURL: https://github.com/myorg/payments

Stage 5 (admission validation) evaluates validation.rules and mutation.rules — including synthesized rules from serve.fields marked required: true — using the same EvaluateConditions + EvaluateValidationRule logic as the webhook and reconciler. A deny-action violation stops the chain before simulate handoff. Mutation rules that would fire are previewed inline.

--simulate hands the built CR to ork simulate after all six stages pass. --simulate simulate.yaml uses an existing simulate spec for katalog, cycles, and expect: assertions while substituting the play-built CR. This makes a simulate spec a full contract from caller intent to child resource ops — testable locally in one command.

Useful for testing token permissions, verifying serve.name/serve.namespace expression resolution, confirming field routing, catching admission violations, and previewing the response payload — all before wiring up a real delivery surface or GitOps webhook.

ork gate

New command that evaluates admission rules locally against a CR — no cluster, no webhook server required.

ork gate -f katalog.yaml --cr cr.yaml

Runs EvaluateConditions + EvaluateValidationRule for every validation.rule in the Katalog against the provided CR. Deny-action violations exit non-zero; warn-action violations are printed as advisories and exit zero. mutation.rules are also evaluated and previewed — showing which fields would be defaulted or overridden and what value they would receive.

When mutateFirst: true is set, ork gate applies mutation rules to a copy of the CR before running validation — matching the real webhook pipeline order. A CR with absent fields that mutation would fill in passes validation locally just as it would at admission time.

Two operators are skipped in local mode — unique: (needs an informer cache) and external: (needs a real endpoint) — and are noted in the output.

Multi-document CR files are supported; each document is matched to a CRD by kind.

The gateway is an intent runner. The runtime is a CR runner. ork gate is what closes the local loop on the admission side: the same validation and mutation logic the webhook enforces, runnable anywhere without a cluster. Combined with ork serve play --simulate simulate.yaml, the full path from intent to child resource ops is testable locally end to end.

ork serve apply

New subcommand that applies an intent or CR to a live gateway via POST /api/v1/apply.

ork serve apply -f intent.yaml --api https://gateway.myorg.io --token "$ORK_TOKEN"

Accepts a flat intent file (target mode) or a full CR (apiVersion + kind). Both YAML and JSON are supported. Defaults to intent.yaml or intent.json in the current directory when --file is not set.

The gateway handles everything on the other side — target resolution, token validation, admission, provenance stamping, SSA delivery. The command sends the body, prints the structured response, and exits non-zero on rejection.

--dry-run runs the full admission pipeline at the gateway without writing to etcd — useful for validating a token’s permissions or an intent’s shape before committing.

Dependency updates

  • golang.org/x/net v0.55.0 → v0.56.0 (CVE-2026-46600)
  • golang.org/x/crypto v0.52.0 → v0.53.0

ork push --add-intent

ork push now accepts --add-intent <file> (YAML or JSON). Before pushing, it runs the full ork serve play chain — target resolution, token check (if token: is set in the intent file), CR construction, and admission validation — and bakes the result into the artifact as OCI annotations:

AnnotationValue
io.orkestra.intent.statuspassed / failed
io.orkestra.intent.targetTarget name played
io.orkestra.intent.tested_atRFC3339 timestamp

Both intent.yaml and intent.json are recognised as optional files in the artifact bundle. ork inspect shows the intent play result alongside Simulate and E2E.

Deprecation timeline and runtime enforcement

metadata.deprecation now supports a scheduled deprecation window and two-gate runtime enforcement.

Timelinetimeline.from opens the deprecation warning window with a days-until-EOL countdown; timeline.to marks the end-of-life date. Both are YYYY-MM-DD strings. ork validate rejects missing message, invalid dates, and from ≥ to.

Accept gatesork run and ork gate refuse to start a deprecated or EOL Katalog unless the operator has explicitly acknowledged it in the file:

deprecation:
  accept:
    beforeEol: true   # required during the deprecation warning window
    eol: true         # additionally required after the end-of-life date

eol: true alone is not accepted — beforeEol must also be set.

Touchpoints — the deprecation state is surfaced at ork push (author sees the exact consumer message before upload), ork validate, ork inspect, and ork pull. Display state is computed from today vs the timeline at each call site.

Validatorork validate warns without blocking; enforcement is at runtime startup only, after validation passes and before the operator begins reconciling.

Serve field translation — value, values, .request

The serve layer can now transform what a caller submits before it reaches the CRD. Two new fields on serve.fields.<name> control this:

value — single transform. One intent field → one spec field, expression-evaluated:

serve:
  fields:
    image:
      value: '{{ trimPrefix "docker.io/" .value }}'

.value is the raw submitted value. The result replaces it at the declared spec path (or spec.<fieldName> when no path: is set).

values — fanout. One intent field → multiple spec fields:

serve:
  fields:
    schedule:
      label: "Schedule (cron)"
      required: true
      values:
        schedule.minute:     '{{ cronMinute .value }}'
        schedule.hour:       '{{ cronHour   .value }}'
        schedule.dayOfMonth: '{{ cronDom    .value }}'
        schedule.month:      '{{ cronMonth  .value }}'
        schedule.dayOfWeek:  '{{ cronDow    .value }}'

The caller submits "0 2 * * 1-5". The CR receives five structured fields. Neither side sees the other’s format. The Katalog is the contract between them.

Both value and values expressions have access to .value (the submitted field value) and .request (the full raw intent payload), so cross-field references work naturally:

values:
  image.tag: '{{ .request.version }}'

Expression failures are hard errors. If a value or values expression fails at serve time (template error, missing function, unresolvable reference), the apply is rejected immediately — the CR is never built with a partial spec.

Flat keys in values are supported. A values key does not need to be dotted — imageTag: '{{ .value }}' writes to spec.imageTag.

.request available throughout the reconciler

The raw serve intent (from the orkestra.orkspace.io/serve-intent annotation) is now available as .request.<field> in all template contexts during reconcile — operatorBox templates, mutation.rules, validation.rules, and status.fields. Previously it was only injected for validation at the webhook boundary.

Intent gating in validation rules — fires.reconcile: false

validation.rules and mutation.rules now support fires.reconcile: false, limiting a rule to the admission path only:

validation:
  rules:
    - field: "{{ cronValid .request.schedule }}"
      equals: true
      link: schedule
      fires:
        reconcile: false
      message: 'schedule must be a valid cron expression'
      action: deny

Use this for rules that read .request.* — the raw intent annotation is present at admission but not guaranteed at every reconcile. Rules without fires: continue to fire at both admission and reconcile (existing behavior unchanged).

fires.reconcile on ExternalCall (under validation.external and mutation.external) predates this; the same field is now available on individual inline rules as well.

Example pack — use-cases/crd-api-evolution

New example pack showing how to evolve a CRD’s API surface without breaking existing consumers:

  • Demonstrates adding fields, changing field shapes, and renaming spec paths across operator versions using the serve layer as the translation boundary
  • Each step is runnable with ork serve play locally before deploying
ork init --pack use-cases/crd-api-evolution

Example pack — use-cases/crd-conversion

New example pack demonstrating CRD conversion patterns without a conversion webhook:

  • basic/ — single-version CronJob operator; no conversion, no serve layer
  • with-serve-translation/ — same operator with serve.fields.values fanout: callers submit a flat cron string, the serve layer fans it out to five structured schedule fields before the CR reaches the API server
ork init --pack use-cases/crd-conversion

The with-serve-translation variant includes ork serve play and ork simulate as local test steps — runnable before any cluster is involved.

ork validate — field translation checks

New static checks for serve.fields at ork validate time:

  • value and values are mutually exclusive per field
  • path and values are mutually exclusive per field — write full paths in values directly
  • Field names must not contain hyphens — they cannot be used as Go template identifiers in .request.<name> expressions; use camelCase or underscores
  • values expressions must compile against the full FuncMap (including user-defined notes)