Normalize Patterns
Schedule format unification
Users write cron schedules as a string or a structured object. Normalize collapses both into a canonical cron string once — onCreate, onReconcile, and status fields all use .spec.schedule with no branching.
normalize:
spec:
schedule: "{{ cronFromAny .spec.schedule }}"
# Both are valid — downstream never sees the difference:
spec:
schedule: "*/5 * * * *"
spec:
schedule:
minute: "*/5"
hour: "*"
dayOfMonth: "*"
month: "*"
dayOfWeek: "*"
Try it:
ork init --pack use-cases
cd crd-conversion/without-webhooks
ork run
Image tag normalization
Make the tag explicit so child resource templates never receive an ambiguous image reference:
normalize:
spec:
image: >
{{ if contains .spec.image ":" }}
{{ .spec.image }}
{{ else }}
{{ .spec.image }}:latest
{{ end }}
"nginx" → "nginx:latest". Every onCreate and status template uses the same resolved value.
Registry prefix enforcement
Enforce a private registry prefix without requiring users to always write it:
normalize:
spec:
image: >
{{ if hasPrefix .spec.image "registry.internal/" }}
{{ .spec.image }}
{{ else }}
registry.internal/{{ trimPrefix .spec.image "/" }}
{{ end }}
Try it — all three image forms handled in one operator:
ork init --pack use-cases
cd normalize/02-image-normalization
ork run
kubectl apply -f cr-bare.yaml # nginx → registry.internal/nginx:latest
kubectl apply -f cr-tagged.yaml # nginx:1.25 → registry.internal/nginx:1.25
kubectl apply -f cr-full.yaml # registry.internal/nginx:1.25 → unchanged
Environment name canonicalization
Accept any casing — produce one canonical value downstream:
normalize:
spec:
environment: "{{ toLower (trimSpace .spec.environment) }}"
" Production ", "PRODUCTION", "production" → "production". Validation rules, label templates, and routing logic all see a consistent value.
Domain cleanup
Strip protocol and trailing slash from user-provided URLs:
normalize:
spec:
domain: >
{{ trimSuffix (trimPrefix (trimPrefix (trimSpace .spec.domain) "https://") "http://") "/" }}
"https://my-app.example.com/" → "my-app.example.com".
Try it — messy and clean inputs producing the same status:
ork init --pack use-cases
cd normalize/01-string-cleanup
ork run
kubectl apply -f cr-messy.yaml # " Production ", "https://acme.example.com/"
kubectl apply -f cr-clean.yaml # already canonical
Composite field assembly
Build a stable internal identifier from multiple spec fields:
normalize:
spec:
internalName: '{{ toLower (replace (printf "%s-%s" .spec.tenant .spec.environment) " " "-") }}'
tenant: "Acme Corp", environment: "Production" → internalName: "acme-corp-production". Every child resource template references .spec.internalName — the assembly logic lives in one place.
Try it — internalName used across secrets, configmap, and all deployments:
ork init --pack use-cases
cd normalize/04-webservice
ork run
kubectl apply -f cr-simple.yaml
Nested path normalization
Dot-notation paths reach deep fields without touching siblings:
normalize:
spec:
resources.requests.cpu: '{{ resourceCPU . | default "100m" }}'
resources.requests.memory: '{{ resourceMemory . | default "128Mi" }}'
Only the declared paths are overwritten. Other fields under spec.resources are untouched.
Why resourceCPU instead of direct path: {{ default "100m" .spec.resources.requests.cpu }} evaluates the argument before default can act. If spec.resources is absent, navigating .requests panics — nil pointer evaluating interface {}.requests. resourceCPU navigates each level safely inside the note function and returns "" at the first missing key. default "100m" then supplies the fallback via pipe.
Try it — minimal CR with no resources, full CR with explicit values, identical Deployments:
ork init --pack use-cases
cd normalize/03-defaults-without-webhook
ork run
kubectl apply -f cr-minimal.yaml # no resources declared
kubectl apply -f cr-full.yaml # explicit cpu/memory
Boolean coercion
Normalize string booleans so boolTernary and typed comparisons always receive a real bool:
normalize:
spec:
suspend: "{{ toBool .spec.suspend }}"
monitoring: "{{ toBool .spec.monitoring }}"
"yes", "1", "true", true all produce true.
Scalar or list unification
Accept either a single string or a list — produce a list for forEach: loops:
normalize:
spec:
regions: >
{{ if typeString .spec.regions }}
{{ list .spec.regions | toJson }}
{{ else }}
{{ .spec.regions | toJson }}
{{ end }}
After normalize, .spec.regions is always a list. forEach: in onReconcile requires no type check.