External

4 min read

The external: block makes HTTP calls before any resource group runs. Results are injected into the template context as .external.<name>.* — every resource template, when: condition, and status field that follows can reference them.

operatorBox:
  onReconcile:
    external:
      - name: healthCheck
        url: "{{ .spec.serviceUrl }}/health"
        expectedStatus: 200
        continueOnError: true
        timeout: 5s

    deployments:
      - name: "{{ .metadata.name }}"
        when:
          - field: external.healthCheck.status
            equals: "200"

Where external sits in the pipeline

informer cache → DeepCopy → normalize → mutation → validation
    → cross-CRD reads
    → external calls  ← you are here
    → resource groups (deployments, services, configMaps, …)
    → read children → enrich
    → status fields

External runs after cross-CRD context is available (.cross.* is accessible in url: and body: fields) and before any resource is created or updated. Every call’s result is available to everything that comes after it.


The primary design decision

Every external call is either required or optional. This is controlled by continueOnError:.

BehaviourSettingUse when
Failure halts the reconcilecontinueOnError: false (default)The call is a prerequisite — don’t create anything if it fails
Failure is logged, reconcile continuescontinueOnError: trueThe call is optional — the system should keep running without it

A failed required call records Ready=False on the CR condition and retries on the next resync. A failed optional call sets .external.<name>.error but the reconcile succeeds and resources are updated normally.


Results in template context

After a call completes, four fields are available under .external.<name>:

FieldValue
.statusHTTP status code string ("200", "404", "503")
.bodyFirst 4096 bytes of the response body
.errorError message on failure; "" on success
.called"true" when the call ran; "false" when skipped by when:

Access via dot notation in any template expression:

# In a when: condition
- field: external.healthCheck.status
  equals: "200"

# In a value template
value: "{{ .external.appConfig.body }}"

Try it

ork init --pack use-cases
cd external/01-health-gate     # gate a Deployment on a live health check
cd external/02-config-inject   # embed an API response body into a ConfigMap every reconcile
cd external/03-image-signing   # sign an image once, re-sign only when the image changes
cd external/04-chained         # chain two calls — second call uses the first call's token
ork run

Best practices

Gate calls with when: to avoid unnecessary API calls

External calls run on every reconcile by default. For calls that don’t need to run every cycle, use a when: condition to skip them. Write the result to a status field on first success — subsequent reconciles check the status field instead of calling the API.

external:
  - name: signImage
    url: "{{ .spec.serviceUrl }}/sign"
    method: POST
    when:
      # Only call when the image has not been signed yet.
      # status.signedImage is written after a successful sign (see status.fields below).
      - field: status.signedImage
        notEquals: "{{ .spec.image }}"

status:
  fields:
    - path: signedImage
      value: "{{ .spec.image }}"
      when:
        - field: external.signImage.status
          equals: "200"

The pattern: call the API → write the result to status → gate future calls on the status field. No annotations, no counters — just a status field and a condition.

Use continueOnError: false for hard dependencies, true for optional enrichment

If a resource must not be created without the call succeeding, use continueOnError: false. If the call enriches or optimises but the system should keep running without it, use continueOnError: true and gate the enrichment path with when: - field: external.<name>.error / operator: notExists.

Keep tokens in environment variables

Never put bearer tokens or API keys directly in the Katalog. Use $ENV_VAR in the token: field — the runtime expands it via os.ExpandEnv at call time.

token: "$API_TOKEN"        # correct — read from env at runtime
token: "abc123secret"      # never — visible in the Katalog YAML

In production, mount the secret into the Orkestra runtime pod via values.yaml:

# charts/orkestra/values.yaml
runtime:
  extraEnvFrom:
    - secretRef:
        name: external-api-credentials
  # or per-key:
  extraEnv:
    - name: API_TOKEN
      valueFrom:
        secretKeyRef:
          name: external-api-credentials
          key: API_TOKEN

Create the secret once:

kubectl create secret generic external-api-credentials \
  --from-literal=API_TOKEN=your-token-here

For local development, export the variable before ork run:

export API_TOKEN="your-token-here"
ork run

Match timeout: to your resync period

If resync: 15s and the external call can take up to 10s, the operator spends most of each cycle waiting. Set timeout: to a fraction of the resync period — typically no more than 20–30%.

Name calls with camelCase

Call names must be valid Go identifiers. Hyphens break template access.

name: healthCheck      # correct   → {{ .external.healthCheck.status }}
name: health-check     # broken    → {{ .external.health-check.status }} fails to parse

Treat the body as opaque text until parsed

.external.<name>.body is a raw string. Use contains: conditions to check for substrings, or inject it as-is into a ConfigMap for the app to parse. The operator does not parse JSON — that is intentional.


Where to go next

  • Patterns — health gates, config injection, image signing, chaining, notifications
  • Reference — full field table, constraints, sleep:, env var expansion