Target Mode
The Apply API accepts a simplified request format where callers submit a target and flat fields instead of a full Kubernetes CR. The gateway builds the CR from the IDP configuration.
Why target mode exists
Every self-service caller — a browser form, a CI pipeline, a Slack bot — has the same problem: they want to describe what they need, not construct a Kubernetes object. A developer knows the repository and the image tag; they shouldn’t need to know apiVersion, kind, metadata, or the difference between spec and labels.
Target mode hides Kubernetes behind the IDP contract. The platform team defines the fields. The gateway handles the rest.
How it works
The Katalog declares idp.target and idp.fields:
idp:
enabled: true
target: app
name: '{{ repoSlug .repository }}'
namespace: '{{ teamName }}-{{ environment }}'
fields:
repository:
label: "Repository"
type: string
required: true
image:
label: "Container Image"
type: string
required: true
Callers submit target + fields:
POST /api/v1/apply
{
"target": "app",
"repository": "myorg/payments-api",
"image": "ghcr.io/myorg/app:v1.0.0",
"team": "team-payments",
"environment": "staging"
}
The gateway:
- Looks up the CRD by
target - Routes fields to
spec,metadata.labels, ormetadata.annotationsbased onidp.fieldsandidp.additionalFields - Resolves
idp.nameandidp.namespace - Applies the full CR via SSA
The caller never sees the CR.
Two modes, one API
| Mode | Request format | When to use |
|---|---|---|
| Target mode | {"target": "...", fields...} | Self-service callers who don’t know Kubernetes |
| Full CR mode | {"apiVersion": "...", "kind": "...", ...} | Advanced callers, existing clients, kubectl compatibility |
# Target mode — submit fields
curl -X POST /api/v1/apply \
-d '{"target":"app","repository":"myorg/app","image":"..."}'
# Full CR mode — submit a complete CR
curl -X POST /api/v1/apply \
-d '{"apiVersion":"platform.myorg.io/v1","kind":"App",...}'
Both modes produce the same result. The gateway detects which format you’re using based on the presence of target or apiVersion+kind.
The schema contract
Callers discover available targets and fields through the schema API:
# List all available targets
curl -X GET /api/v1/schema \
-H "Authorization: Bearer $TOKEN"
# Get fields for a specific target
curl -X GET /api/v1/schema?target=app \
-H "Authorization: Bearer $TOKEN"
The schema API returns a flat list of fields:
{
"target": "app",
"title": "Application",
"fields": {
"repository": {
"label": "Repository",
"type": "string",
"required": true
},
"image": {
"label": "Container Image",
"type": "string",
"required": true
}
}
}
Callers don’t need to know about spec, labels, or annotations — they just see fields.
idp.target — the caller-facing identifier
idp.target decouples the caller-facing identifier from the Kubernetes kind.
idp:
enabled: true
target: app # callers use this, not "App" or "apprequests"
If omitted, defaults to the lowercased kind (e.g., kind: App → target: app).
ork validate ensures targets are unique across the Katalog.
idp.name and idp.namespace
Target mode resolves idp.name and idp.namespace server-side, so callers don’t need to know them:
idp:
enabled: true
name: '{{ repoSlug .repository }}' # → "payments-api"
namespace: '{{ teamName }}-{{ environment }}' # → "team-payments-staging"
Callers never supply metadata.name or metadata.namespace in target mode.
When idp.name is not declared, the caller must supply a name. When idp.namespace is not declared on a namespaced CRD, the gateway rejects the request — self-service creation has no way to know where the CR belongs.
Nested fields with path
Fields can map to nested locations in the CRD spec using path:
idp:
fields:
repository:
path: app.repository
label: "Repository"
cpu:
path: app.resources.cpu
label: "CPU Request"
Callers submit flat field names:
{
"target": "app",
"repository": "myorg/app",
"cpu": "500m"
}
The gateway maps to:
spec:
app:
repository: myorg/app
resources:
cpu: 500m
→ Nested fields with path reference
Response: pollUrl and payload
A successful target-mode apply returns:
{
"accepted": true,
"name": "payments-api",
"namespace": "team-payments-staging",
"kind": "AppRequest",
"apiVersion": "platform.myorg.io/v1",
"pollUrl": "/api/v1/resources/AppRequest/team-payments-staging/payments-api?field=status.phase",
"payload": {
"phase": "",
"serviceURL": "https://payments-api.staging.myorg.io",
"nextSteps": "Waiting for resources to be provisioned..."
}
}
pollUrl— where to GET the resource (configurable viaidp.config.response.poll)payload— the platform team’s curated view (idp.config.response.payload)
At apply time, .status is not yet available. Callers should poll pollUrl to see status updates.
→ idp.config.response reference
Try it
ork init --pack use-cases/idp
Follow the README — it walks through target mode from schema discovery to apply to polling.