serve.fields.values

3 min read

serve.fields.values runs at the Gateway before the CR is written. Callers submit a simplified intent; the Katalog fans the fields out to the CRD’s internal schema. The CRD never sees the caller’s vocabulary — the translation is declared once in the serve block and runs transparently on every apply.

Use it when the caller should not be coupled to the CRD schema. The CRD is an implementation detail; the intent is the contract.


How it works

Declare serve.fields in the Katalog. Each field entry maps one intent field to one or more CRD paths via values:. The expressions run against .value — the raw value the caller submitted.

serve:
  fields:
    schedule:
      values:
        schedule.minute:     '{{ cronMinute .value }}'
        schedule.hour:       '{{ cronHour   .value }}'
        schedule.dayOfMonth: '{{ cronDom    .value }}'
        schedule.month:      '{{ cronMonth  .value }}'
        schedule.dayOfWeek:  '{{ cronDow    .value }}'

The caller submits:

target: cronjob-tutorial
name: daily-backup
schedule: "0 2 * * 1-5"
image: "gcr.io/google-containers/busybox:latest"

What reaches the API server:

spec:
  schedule:
    minute:     "0"
    hour:       "2"
    dayOfMonth: "*"
    month:      "*"
    dayOfWeek:  "1-5"
  image: gcr.io/google-containers/busybox:latest

The structured schedule is reconstructed from the cron string entirely within the serve layer. Nothing downstream — not the CRD schema, not the reconciler, not etcd — ever sees the flat string.


The pipeline order

Intent submitted (ork serve apply)
serve.fields.values   ← flat intent → CRD-shaped spec fields (at the Gateway)
validation            ← intent gate fires on the raw request fields
CR written to API server  ← structured spec only; caller's vocabulary gone
normalize / mutation / reconcile  ← see the CRD shape throughout

serve.fields.values runs before the CR reaches the API server. Validation rules on request.* fields fire against the caller’s raw input — before the fanout — so error messages speak the caller’s vocabulary, not the CRD’s.

validation:
  rules:
    - field: request.schedule
      operator: exists
      message: "schedule is required — use a cron expression (e.g. \"*/5 * * * *\")"
      action: deny

One field to many CRD paths

A single intent field can fan out to any number of CRD paths. Each key under values: is a dot-notation path into spec:

serve:
  fields:
    schedule:
      values:
        schedule.minute:     '{{ cronMinute .value }}'
        schedule.hour:       '{{ cronHour   .value }}'
        schedule.dayOfMonth: '{{ cronDom    .value }}'
        schedule.month:      '{{ cronMonth  .value }}'
        schedule.dayOfWeek:  '{{ cronDow    .value }}'

Five CRD fields from one intent field. The template functions (cronMinute, cronHour, cronDom, cronMonth, cronDow) each extract one component from the cron string.


Comparing the three approaches

normalize:conversion.paths:serve.fields.values
Translation pointReconcilerAPI server (..//convert)Gateway (before CR is written)
CRD versionsOneTwo or moreOne
Caller submits viakubectl applykubectl applyork serve apply
Caller sees CRD schemaYesYesNo
Gateway requiredNoYesYes

serve.fields.values is the only approach where the CRD schema is entirely hidden from callers. The other two require the caller to know the CRD’s field names, even if the format is flexible.


Try it

ork init --pack use-cases/crd-conversion/with-serve-translation
cd with-serve-translation

Test the field fanout locally without a cluster:

ork serve play -i intent.yaml -t dev

Prints the built CR with spec.schedule as the structured object. Try an invalid cron string:

ork serve play -i intent-invalid.yaml -t dev

The intent gate fires on request.schedule and returns the error in the caller’s vocabulary.


Where to go next