external

5 min read

The external: list under onReconcile: declares HTTP calls to make before any resource group is processed. Results are injected into the template resolver under .external.<name>.* and are available in every subsequent template expression, when: condition, and status field.

Calls run sequentially in declaration order. The resolver is updated after each call — later calls can reference earlier results via template expressions in their url:, token:, or body: fields.

Wire format

operatorBox:
  onReconcile:
    external:
      - name: healthCheck
        url: "{{ .spec.serviceUrl }}/health"
        method: GET
        body: ""
        token: "$API_TOKEN"
        headers:
          X-Request-Source: orkestra
        timeout: 5s
        expectedStatus: 200
        continueOnError: false
        when:
          - field: status.phase
            notEquals: "Ready"
        sleep: ""

Fields

FieldRequiredDefaultDescription
nameyesResult identifier. Must be a valid Go identifier (camelCase). Used as {{ .external.<name>.status }}.
urlyesEndpoint URL. Template expressions are resolved against the current CR.
methodnoGETHTTP method: GET, POST, PUT, PATCH, DELETE.
bodyno""Request body. Template expressions supported. Sets Content-Type: application/json when non-empty.
tokenno""Bearer token. Use $ENV_VAR syntax — expanded via os.ExpandEnv. Never put raw secrets here.
headersno{}Additional HTTP headers as map[string]string.
timeoutno10sPer-call deadline. Go duration: "5s", "1m", "500ms".
expectedStatusno0When set: any other status code is a failure. When 0: 4xx/5xx is a failure, 2xx succeeds.
continueOnErrornofalsefalse: failure halts reconcile, writes Ready=False. true: failure logged, reconcile continues.
whenno[]AND gate conditions. If any fail, the call is skipped and .called = "false".
orno[]OR gate conditions. At least one must pass. Combined with when: using AND semantics.
sleepno""Delay before this call. Go duration. For development and sequencing async side-effects — not for production rate limiting.
fires.reconcilenotrueWhen false, the call is skipped during reconcile — it only runs at admission time. Applies when the call is declared under validation.external or mutation.external. No effect on onReconcile.external calls.
includenoPath to a YAML file with a top-level calls: list. When set, this entry is replaced in-place by the listed calls. Resolved relative to the katalog file. Cleared after expansion.
retryBackoffnoRetry this specific call with exponential backoff before returning an error. Shorthand ("2s") sets initial only; full form: initial, max, multiplier, maxAttempts. See retry backoff.

Result context

FieldDescription
.external.<name>.statusHTTP status code string ("200", "503"). Empty on pre-response failure.
.external.<name>.bodyFirst 4096 bytes of response body.
.external.<name>.errorError message on failure; "" on success.
.external.<name>.called"true" when the call ran; "false" when skipped by when:/or:.
.external.<name>.<key>When the response body is a valid JSON object, its top-level keys are merged in and navigable by dot path.

If the response is { "queue": { "pendingJobs": 8 } } and the call is named metrics, then external.metrics.queue.pendingJobs is directly usable in field: conditions and {{ .external.metrics.queue.pendingJobs }} in template expressions. .body is always present alongside parsed fields.

Access pattern

# Gate a resource on a successful call
deployments:
  - name: "{{ .metadata.name }}"
    when:
      - field: external.healthCheck.status
        equals: "200"

# Embed the body in a ConfigMap
configMaps:
  - name: "{{ .metadata.name }}-config"
    data:
      config.json: "{{ .external.appConfig.body }}"

# Use a previous call's result in the next call's token
external:
  - name: tokenFetch
    url: "{{ .spec.authUrl }}/token"
    method: POST

  - name: protectedCall
    url: "{{ .spec.serviceUrl }}/resource"
    token: "{{ .external.tokenFetch.body }}"

include: in external lists

When the external: list grows long, individual entries can delegate to a shared file:

onReconcile:
  external:
    - include: ./shared/auth-calls.yaml
    - name: healthCheck
      url: "{{ .spec.serviceUrl }}/health"

./shared/auth-calls.yaml:

calls:
  - name: tokenFetch
    url: "{{ .spec.authUrl }}/token"
    method: POST
  - name: featureFlags
    url: "{{ .spec.flagsUrl }}/flags"
    continueOnError: true

The include entry is replaced in-place by the calls: list. The path is resolved relative to the katalog file’s directory. Works in onReconcile.external, onCreate.external, validation.external, and mutation.external.

Placement

External calls can be declared in three locations:

LocationWhen it fires
onReconcile.externalEvery reconcile cycle, before resource groups are processed
validation.externalBefore validation rules — at admission (always) and at reconcile (unless fires.reconcile: false)
mutation.externalBefore mutation rules — at admission (always) and at reconcile (unless fires.reconcile: false)

fires.reconcile: false is useful when an external call is expensive or irrelevant after the CR is persisted — for example, a pre-admission health check that only makes sense at kubectl apply time.

validation:
  external:
    - name: healthCheck
      url: "{{ .spec.serviceUrl }}/health"
      expectedStatus: 200
      continueOnError: true
      fires:
        reconcile: false   # checked once at apply — not repeated every resync

  rules:
    - field: "{{ .external.healthCheck.status }}"
      equals: "200"
      action: deny
      message: "health check failed — deployment blocked"

Constraints

  • Body cap: 4096 bytes — larger responses are truncated silently.
  • Sequential: no parallel execution; declaration order is execution order.
  • camelCase names: hyphens in name break template access.
  • Every reconcile: calls run on every reconcile unless gated by when:.
  • Token expansion: $VAR and ${VAR} only — template expressions in token: resolve first, then env expansion applies.

Example

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

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

  status:
    fields:
      - path: phase
        value: "Degraded"
        when:
          - field: external.healthCheck.status
            notEquals: "200"
      - path: phase
        value: "Ready"
        when:
          - field: external.healthCheck.status
            equals: "200"
          - field: "{{ allReplicasReady .children.deployment }}"
            equals: "true"
      - path: lastHealthCheck
        value: "{{ .external.healthCheck.status }}"

See the External concept doc for patterns and best practices.