Resources

4 min read

resources is the block where a Motif declares what it creates and manages. The structure mirrors the Katalog operatorBox resource blocks. Motif resources are merged into the CRD entry at reconcile time alongside the Katalog’s own operatorBox.


Block types

resources:
  # Special — runs once on CR creation, never on reconcile
  onCreate:
    secrets:
      - ...

  # All other blocks — run on every reconcile
  statefulSets:
    - ...
  deployments:
    - ...
  services:
    - ...
  configMaps:
    - ...
  ingresses:
    - ...
  hpa:
    - ...
  pdb:
    - ...
  namespaces:
    - ...
  serviceAccounts:
    - ...
  roles:
    - ...
  roleBindings:
    - ...
  cronJobs:
    - ...
  custom:
    - ...

resources.onCreate

Resources declared under onCreate are applied exactly once — when the CR is first created. They are never updated on subsequent reconciles.

Use onCreate for:

  • Secrets with generated values (passwords, API keys) — generate once, never overwrite.
  • Seed data that must not be reset on operator restart.
resources:
  onCreate:
    secrets:
      - name: "{{ .metadata.name }}-creds"
        once: true
        rotateAfter: "{{ .inputs.passwordRotation }}"
        data:
          password: "{{ randomAlphanumeric 16 }}"

onCreate and once: true are distinct guards:

  • onCreate — run this block only on CR creation; skip it on all subsequent reconciles.
  • once: true — check-before-generate idempotency guard. Before evaluating any template, Orkestra checks whether the secret already exists. If it does, skip template evaluation entirely (no-op). If it does not exist, evaluate templates and create.

once: true is required when using Orkestra’s random notesrandomAlphanumeric, randomHex, randomBase64 — because these are pure non-idempotent functions: they produce a new value on every call. Without once: true, the reconcile loop (which runs on every resync) would call the random note on every cycle, generating a new password every 30 seconds and breaking every application using it.

resources:
  onCreate:
    secrets:
      - name: "{{ .metadata.name }}-creds"
        once: true
        data:
          password: "{{ randomAlphanumeric 32 }}"
          apiKey:   "{{ randomHex 16 }}"
ConditionBehaviour
Secret does not existEvaluate templates → create with generated values
Secret already existsSkip entirely — no template evaluation, no update

Reconciled resource blocks

All blocks outside onCreate are merged into the CRD entry’s onReconcile phase and run on every reconcile.

A full example from the postgres Motif:

resources:
  statefulSets:
    - name: "{{ .metadata.name }}-postgres"
      image: "{{ .inputs.image }}"
      replicas: "{{ .inputs.replicas }}"
      serviceName: "{{ .metadata.name }}-postgres-headless"
      port: 5432
      resources:
        profile: "{{ .inputs.resourceProfile }}"
      probes:
        startup:
          type: tcp
          profile: slow-start
        liveness:
          type: tcp
          profile: standard
        readiness:
          type: tcp
          profile: standard
      volumeClaimTemplates:
        - name: data
          storageClass: "{{ .inputs.storageClass }}"
          storageSize: "{{ .inputs.volumeSize }}"
          mountPath: /var/lib/postgresql/data
      env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: "{{ .metadata.name }}-creds"
              key: password

  services:
    - name: "{{ .metadata.name }}-postgres-headless"
      headless: true
      port: 5432

    - name: "{{ .metadata.name }}-postgres"
      port: 5432

  deployments:
    - name: "{{ .metadata.name }}-pgadmin"
      image: "{{ .inputs.pgAdminImage }}"
      port: 80
      when:
        - field: inputs.enableUI
          equals: "true"

Merge semantics

Resources from all imports are merged into a single CRD entry in declaration order. Name collisions are a hard error — each resource should use {{ .metadata.name }} as a prefix to guarantee uniqueness across imports.

SourceMerge order
imports[0] resourcesFirst
imports[1] resourcesSecond
operatorBox resourcesLast

The operatorBox block always wins on name conflict because it is the Katalog author’s own resources.


status

status.fields declared in a Motif are contributed to the parent CRD’s status and written after each reconcile.

status:
  fields:
    - path: postgresReady
      value: "{{ allReplicasReady .children.statefulset }}"
    - path: connectionSecret
      value: "{{ .metadata.name }}-creds"
    - path: connectionString
      value: "postgresql://{{ .inputs.username }}@{{ .metadata.name }}-postgres.{{ .metadata.namespace }}.svc.cluster.local:5432/{{ .inputs.database }}"
    - path: pgAdminUrl
      value: "http://{{ .metadata.name }}-pgadmin-svc.{{ .metadata.namespace }}.svc.cluster.local"

Status fields from multiple imports are merged. A duplicate path is a hard error.

→ Status field schema: 05-status


admission

A Motif can declare its own validation and mutation rules. These are merged with the parent CRD’s admission rules.

admission:
  validation:
    rules:
      - field: spec.image
        prefix: "myregistry.io/"
        action: deny

  mutation:
    rules:
      - field: spec.replicas
        default: "1"

→ Validation schema: 07-validation
→ Mutation schema: 08-mutation