Skip to content

Environment Variable Interpolation

Biowilko edited this page Aug 5, 2026 · 2 revisions

Environment Variable Interpolation

Any string value anywhere in a config file — including inside the otherwise-unvalidated kubernetes/job passthrough sections — can reference an environment variable from the shell squarepeg is running in.

namespace: "${SQUAREPEG_NAMESPACE:-my-namespace}"

kubernetes:
  spec:
    serviceAccountName: "${SERVICE_ACCOUNT:-squarepeg}"
    imagePullSecrets:
      - {name: "${REGISTRY_SECRET}"}

volumes:
  refdata:
    persistentVolumeClaim: {claimName: "${REFDATA_PVC}", readOnly: true}

Syntax

Form Behaviour
${VAR} Substitutes the value of $VAR. Hard error if VAR is not set, naming the variable, the exact key path (e.g. kubernetes.spec.imagePullSecrets[0].name), and the config file it came from.
${VAR:-default} Substitutes $VAR if it is set and non-empty, else the literal default. Never errors.
$$ A literal $.
$VAR (no braces) Not a reference — passed through completely untouched.

A handful of things worth knowing:

  • Multiple references in one string all resolve: "${A}-${B}", "job-${RUN_TAG}".
  • The bash :- nuance applies: a variable set to the empty string counts as unset for ${VAR:-default} purposes, same as bash. This is the single most surprising part of the syntax if you've not hit it before — MYVAR="" will still fall through to default.
  • A default is a literal, never re-scanned. ${VAR:-ghcr.io/foo/bar:1.2} works fine (colons and slashes in the default are just text) — and critically, if $VAR's value happens to contain ${OTHER}-looking text, it is never itself expanded. One substitution pass, no recursion.
  • Malformed references are a hard error, not silently passed through: ${}, ${1BAD}, ${A B}, an unterminated ${VAR — all raise, naming the bad token. This matches squarepeg's fail-fast posture everywhere else (missing config files, unknown config keys, unsupported flags all raise rather than degrade quietly) — a ${NAMSEPACE}-class typo is exactly the thing this feature exists to catch.

When it runs

Interpolation happens per config file, immediately after parsing and before validation — before files are merged together, and before profile selection. Two consequences worth knowing:

  • Error messages name the exact file, since interpolation happens before layers get flattened together.
  • It's eager over the whole document, including profiles.* bodies you haven't selected with --profile. A ${VAR} reference inside an unused profile is still a hard error on every invocation. This matches how squarepeg already validates every profile's schema regardless of selection — see Configuration → Profiles.

Type coercion

Substitution always produces a Python string — it never tries to guess that ${N} should become an integer just because N happens to hold "3". squarepeg's own settings that aren't naturally strings (timeout, quiet, cleanup, allow_host_path_mounts) are explicitly coerced from a string if needed (e.g. timeout: "${TIMEOUT:-300}" works fine, and cleanup: "${CLEANUP:-false}" correctly becomes the boolean False, not the string "false").

This coercion does not extend into the kubernetes/job passthrough sections. An integer or boolean field there — job.spec.backoffLimit, a container's readOnly — must stay literal YAML (backoffLimit: 3), not an interpolated string (backoffLimit: "${N}"), or the apiserver will reject the manifest. --dry-run shows you the quoted value immediately if this happens, so the failure is cheap to diagnose.

config show and secrets

Both squarepeg config show and --dry-run print resolved (post-interpolation) values — that's the entire point, showing you what will actually be used. This means:

If a config value resolves from a secret-bearing environment variable, it will appear in that output. Treat config show/--dry-run output the same way you'd treat a shell history containing secrets — don't paste it into a bug report or a shared terminal recording without checking first.

squarepeg does not redact anything, and there's no plan to guess which env vars are "sensitive" by name pattern — that would give false confidence while missing the ones that matter. For real secrets, the better mechanism is to reference a Kubernetes Secret directly from kubernetes.spec (e.g. via envFrom/secretKeyRef), which never passes through squarepeg's own process at all.

Quick reference

You write MYVAR is... Result
${MYVAR} "prod" prod
${MYVAR} unset error
${MYVAR:-staging} "prod" prod
${MYVAR:-staging} unset staging
${MYVAR:-staging} "" (set but empty) staging
$$ (anything) $ (literal)
$MYVAR (anything) $MYVAR (untouched — no braces, not a reference)

Clone this wiki locally