Serve API

6 min read

The Serve API is the Gateway’s intent delivery surface. It accepts flat, human-readable fields from a caller and produces a fully validated, provenance-stamped Kubernetes object applied to the cluster — without the caller needing any knowledge of CRDs, API groups, namespaces, or Kubernetes object shape.

The caller declares what they want. The Gateway handles the rest.


Intent delivery chain

Every create or update request passes through six stages in sequence:

1. Target resolution — The caller names a target. The Gateway resolves it to the specific CRD declared in the Katalog. In CR mode, the target is inferred from the submitted CR’s apiVersion and kind.

2. Token check — The named token is verified against the target and operation. Tokens declare which targets they can reach, which operations they can perform, and which namespaces they can operate in. A request with an invalid or insufficiently scoped token is denied before any CR is constructed.

3. CR construction — The flat intent fields are mapped to CR fields using the serve.fields declarations in the Katalog. serve.name and serve.namespace template expressions are resolved. Required fields are enforced. The result is a fully shaped Kubernetes object.

4. Provenance — The Gateway stamps orkestra.orkspace.io/serve-target, orkestra.orkspace.io/serve-alias, and orkestra.orkspace.io/serve-source annotations on the CR. These record the delivery surface that created the object and survive through the full object lifecycle.

5. Admission validation — Validation and mutation rules are evaluated against the constructed CR. Deny-action violations return an error immediately — the CR is never written. Mutation rules apply their defaults and overrides.

6. Apply and respond — The CR is applied to the cluster via server-side apply. The response is shaped by serve.config.response — either the full CR, a payload subset, or both.


Targets

Every CRD exposes one or more named targets through serve.target. Exactly one entry carries primary: true; any other entries are additional surfaces on the same CRD:

serve:
  target:
    myapp:
      primary: true
    preview:
      tokens:
        ci-pipeline:
          permissions:
            global: [create, update]
    internal:
      tokens:
        platform-team:
          permissions:
            global: ["*"]

Callers reference any of these by name in POST /api/v1/apply:

{"target": "preview", "name": "my-service", ...}

All targets share the same CRD, the same operator, and the same Kubernetes resource. Each target can independently restrict which tokens are valid for it, override apply and response behaviour, and enable or disable the surface without affecting the others.

The primary entry acts as the CRD-level config authority — its tokens and config serve as fallbacks when an entry declares neither.


Tokens

Tokens are declared in serve.tokens (CRD level) or in a target entry’s tokens map. A token absent from a target’s map is denied for that target, even if it is valid at the CRD level. Entry tokens can only narrow access, not widen it.

serve:
  tokens:
    platform-team:
      permissions:
        global: ["*"]
    ci-read:
      permissions:
        resources: [get, list]
      namespaces: [staging, preview]

Each token entry takes permissions and optionally namespaces:

KeyEffect
permissions.globalApplies to all endpoint classes that declare no class-specific list
permissions.schemaOverrides global for GET /api/v1/schema/... endpoints
permissions.resourcesOverrides global for /api/v1/resources/... endpoints
namespacesRestricts the token to these namespaces. Empty means all

Valid operations: get, list, create, update, delete, *.


Fields

serve.fields, serve.labels, and serve.annotations all use the same map-keyed-by-field-name shape. The only difference is the destination:

MapWritten to
serve.fieldsspec.* — type inferred from CRD schema
serve.labelsmetadata.labels — declare type explicitly
serve.annotationsmetadata.annotations — declare type explicitly
serve:
  fields:
    environment:
      path: spec.environment
      required: true
      enum: [staging, production]

    image:
      values:
        spec.image.registry:    '{{ imageRegistry   .value }}'
        spec.image.repository:  '{{ imageRepository .value }}'
        spec.image.tag:         '{{ imageTag        .value }}'

    ttl:
      path: spec.ttl
      default: "24h"

  labels:
    team:
      required: true
      type: string

  annotations:
    costCenter:
      type: string

Key behaviours (same across all three maps):

  • path — dot-notation destination path. Defaults to the field name when absent.
  • required — enforced both browser-side (form) and server-side (Gateway API).
  • value — template expression applied to the submitted value before writing. .value is the submitted field, .request is the full intent payload.
  • values — fanout: one submitted field writes to multiple paths. Mutually exclusive with value. path is ignored when values is present.
  • default / override — synthesise mutation rules. default sets when absent; override always sets.
  • enum — generates an in membership check.
  • when / or — conditional visibility in the form. Evaluated client-side.
  • disabled — renders the field greyed out with the given reason string.
  • order — form position. Also determines which field’s error is shown first when multiple fail.

serve.ignore lists field names hidden from the serve form entirely.


Name and namespace

serve.name and serve.namespace are template expressions the Gateway resolves at apply time:

serve:
  name: '{{ repoSlug .spec.repository }}'
  namespace: '{{ .request.team }}'

name overrides whatever the caller submitted (or generates one when the caller omitted it). Use it when instances are 1:1 with a stable caller-supplied identity and redeploys should update the same CR rather than create a new one.

namespace is required for namespaced CRDs when serve.enabled: true. Callers never submit a namespace — the Gateway resolves it from the intent.


Modes

Two submission modes are independently controllable:

ModeWhat it meansDefault
serve.modes.targetSubmit flat fields with a target identifierenabled
serve.modes.crSubmit a full Kubernetes CR (apiVersion + kind)enabled
serve:
  modes:
    cr: false    # disallow raw CR submission; only target mode accepted

Per-target overrides are set on the target entry’s modes block.


Response shaping

serve.config.response controls what the caller receives. Each target entry can declare its own config.response independently:

serve:
  config:
    response:
      default: true
      payload:
        phase:      '{{ .status.phase }}'
        serviceURL: 'https://{{ .metadata.name }}.myorg.io'
      exclude:
        - metadata.managedFields

  target:
    preview:
      primary: false
      config:
        response:
          default: false        # payload fields only, no raw CR
          payload:
            phase: '{{ .status.phase }}'
FieldEffect
defaulttrue (default) starts the response with the full CR; false starts empty
payloadNamed template expressions merged into the response
excludeDot-notation paths stripped after payload is applied
poll.fieldAppends ?field=<value> to the derived poll URL
poll.urlReplaces the default poll URL entirely

Read operations

For get, list, and delete the Gateway performs a token check and target resolution, then proxies the operation to the cluster. The same response shaping applies to get and list — a caller using a read-scoped token sees only the payload fields declared for their surface.


Operations

OperationWhat the Gateway does
createConstructs CR, runs admission, applies via SSA
updateSame as create — SSA is idempotent
getToken check, then cluster GET with response shaping
listToken check, then cluster LIST with response shaping per item
deleteToken check, then cluster DELETE

Where to go next

Concepts

Schema reference