Serve Nested Spec
Nested Spec Paths
serve.fields.path
By default, serve.fields maps field names directly to top-level spec paths. Use path to map a field to a nested location in the CRD spec.
serve:
fields:
# Flat field — maps to spec.repository
repository:
label: "Repository"
# Nested field — maps to spec.app.repository
repository:
path: app.repository
label: "Repository"
# Deeply nested — maps to spec.app.resources.cpu
cpu:
path: app.resources.cpu
label: "CPU Request"
Callers submit flat field names — they don’t need to know about nesting. The gateway maps the field to the correct location in the CRD.
curl -X POST /api/v1/apply \
-d '{
"target": "smartapp",
"repository": "myorg/payments-api", # → spec.app.repository
"cpu": "500m" # → spec.app.resources.cpu
}'
Why Use path
Without path | With path |
|---|---|
| Fields must match CRD structure | Fields are flat and caller-friendly |
| Callers must know nested paths | Callers submit simple field names |
| CRD evolution breaks callers | Gateway maps to new paths |
| UI fields show dot-paths | UI fields show clean names |
Validation
ork validate enforces:
- Unique paths — no two fields can map to the same
speclocation - Valid format — path segments must be valid Kubernetes names (alphanumeric,
_,-,.) - No empty segments —
app..repositoryis rejected - No leading/trailing dots —
.app.repositoryis rejected
Schema Validation (Not Yet Implemented)
Path existence in the CRD schema is not yet validated by ork validate. The platform team must verify that nested paths exist in the CRD spec. This will be added in a future release when OpenAPI schemas are loaded into the Katalog.
Example
CRD:
spec:
app:
repository: string
image: string
resources:
cpu: string
memory: string
scaling:
replicas: integer
minReplicas: integer
maxReplicas: integer
Serve Config:
serve:
fields:
repository:
path: app.repository
label: "Repository"
image:
path: app.image
label: "Container Image"
cpu:
path: app.resources.cpu
label: "CPU Request"
memory:
path: app.resources.memory
label: "Memory Request"
replicas:
path: scaling.replicas
label: "Replicas"
Caller Request:
{
"target": "smartapp",
"repository": "myorg/payments-api",
"image": "ghcr.io/myorg/app:v1",
"cpu": "500m",
"memory": "512Mi",
"replicas": 3
}
Generated CR:
spec:
app:
repository: myorg/payments-api
image: ghcr.io/myorg/app:v1
resources:
cpu: 500m
memory: 512Mi
scaling:
replicas: 3
Related
→ serve.fields — field configuration reference
→ serve labels/annotations — labels and annotations as fields
→ Target Mode API — submitting fields instead of CRs
target.clusters
Scopes the apply fan-out for a specific named target or alias to a subset of
serve.clusters. When absent, fan-out uses all of serve.clusters. Reads and
lists always use the local cluster regardless of this setting.
serve:
clusters:
- prod
- staging
- eu-west
target:
prod-release:
primary: true
clusters:
- prod # restricts to prod only — staging and eu-west skipped
canary:
clusters:
- '{{ if eq .request.region "eu" }}eu-west{{ else }}staging{{ end }}'
Every entry in target.clusters must appear in serve.clusters — ork validate
rejects any name that is not declared there. Template expressions skip this check
at validate time; the resolved name is validated against serve.clusters at apply time.
| Value type | Validation | Resolution |
|---|---|---|
| Static name | Checked against gateway.clusters AND serve.clusters at ork validate time | Looked up in registry at apply time |
| Template expression | Parse + function existence checked at ork validate time | Resolved against intent fields at apply time |
| Absent | — | Fan-out uses all of serve.clusters; falls back to local if that is also absent |
→ serve.clusters — CRD-level routing whitelist and default fan-out
→ gateway.clusters — registering clusters
→ Multi-cluster routing — concept overview