Skip to content

Configuration

Biowilko edited this page Aug 5, 2026 · 4 revisions

Configuration

squarepeg reads config from a layered set of YAML files, resolved Nextflow-style: several sources are located, loaded, and deep-merged together before any CLI flag is applied — rather than there being just one config file.

Resolution order

Lowest precedence to highest — each layer can override just the keys it cares about, without having to restate everything from the layers below it:

Order Source Notes
1 Built-in defaults Not a file — hardcoded fallbacks (mode: pod, timeout: 300, cleanup: true, etc.)
2 ~/.config/squarepeg/config.yaml The single default location. Silently skipped if it doesn't exist. Skip it entirely with --no-default-config.
3 $SQUAREPEG_CONFIG A colon-separated list of paths (same convention as $PATH/$KUBECONFIG), loaded left-to-right. A listed-but-missing file is an error (unlike the default location).
4 --config PATH Repeatable CLI flag, loaded in the order given, left-to-right.
5 Individual CLI flags -e, --cpus, -n, etc. — always win over everything above.

Every layer (2–4) is deep-merged key by key, not replaced wholesale — a later file overriding just mode: job doesn't wipe out a namespace or tolerations set by an earlier one. See How merging works below for the exact algorithm.

# Site-wide defaults plus a per-user override, composed automatically:
SQUAREPEG_CONFIG=/etc/squarepeg/site.yaml squarepeg run --config ~/my-overrides.yaml alpine true

Top-level schema

Key Type Description
namespace string Target namespace.
mode pod | job Default execution mode.
cleanup bool false behaves as if --keep were always passed.
timeout int Seconds to wait for the container to start (see Execution Lifecycle).
quiet bool Suppress squarepeg's own status messages.
allow_host_path_mounts bool Opt in to -v /host:/container bind mounts (only sensible against a local cluster).
split_streams bool Reserved for splitting stdout/stderr into separate streams (depends on an apiserver feature gate; not fully wired up yet).
defaults mapping See defaults below.
volumes mapping Named volume sources — see Volume forms.
kubernetes mapping Arbitrary Kubernetes passthrough — see below.
job mapping Job-level passthrough (ignored in --mode pod) — see below.
profiles mapping Named config overlays, selected with --profile — see Profiles.

Unknown top-level keys are a hard error, naming the offending file — this is deliberate typo protection (a silently-ignored namesapce: would be a bad day). The kubernetes and job sections are the exception: they're deliberately free-form and not validated by squarepeg at all — the Kubernetes apiserver validates them, and --dry-run lets you inspect the result before it gets there.

The defaults section

Applied whenever the equivalent CLI flag isn't given:

Key Maps to Units
cpus --cpus docker-style (e.g. "1", "0.5")
memory --memory docker-style (e.g. 2g)
image_pull_policy --pull Kubernetes-native (Always/IfNotPresent/Never, already-cased)
workdir -w/--workdir
env -e/--env mapping of KEY: value; CLI -e entries win over these on a key collision, but config-only keys still get included

The kubernetes and job passthrough sections

These merge directly into the generated manifest, letting you set any Kubernetes field squarepeg has no dedicated flag for — node selectors, tolerations, service accounts, image pull secrets, security contexts, GPU resources, and so on.

kubernetes.spec (and kubernetes.metadata) is always a Pod spec/metadata, even when --mode job is selected. squarepeg re-homes it under the Job's pod template automatically, so the same config file works unchanged in either mode — without this rule, switching --mode would silently drop every toleration and node selector you'd configured.

kubernetes:
  metadata:
    labels: {team: bioinf}
  spec:
    serviceAccountName: squarepeg
    nodeSelector: {"kubernetes.io/arch": amd64}
    tolerations:
      - {key: dedicated, operator: Equal, value: batch, effect: NoSchedule}
    imagePullSecrets:
      - {name: regcred}
    containers:
      - name: main                                # matched by name; merges into squarepeg's own container
        securityContext: {allowPrivilegeEscalation: false}

job:                                                # Job-level only; ignored in --mode pod
  spec:
    backoffLimit: 0
    ttlSecondsAfterFinished: 3600

CLI flags always win over passthrough on the same field. If you pass -e FOO=bar on the command line, a kubernetes.spec.containers[].env entry for FOO in your config is silently overridden for that field only — every other field you set in passthrough still applies. This is what "layered, not replaced" means in practice; see How merging works.

Profiles

profiles:
  gpu:
    defaults: {cpus: "8", memory: 32g}
    kubernetes:
      spec:
        nodeSelector: {"nvidia.com/gpu.present": "true"}
        containers:
          - name: main
            resources: {limits: {"nvidia.com/gpu": 1}}
squarepeg run --profile gpu my-training-image train.py

A profile is itself a config document (using the same schema, minus a nested profiles key — that's rejected) that gets deep-merged over the rest of the resolved config when selected with --profile NAME.

Two things worth knowing:

  • Same-named profiles across multiple config layers deep-merge, last-loaded wins per key — not an error. This lets a site-wide file own a profile's scheduling details (node selector, toleration) while a per-user file adds just a resource size to the same profile name, without either file needing to know about the other's contents.
  • Profile bodies are validated and interpolated eagerly, regardless of whether you select them. A typo or an unset ${VAR} reference inside profiles.gpu is a hard error on every invocation of squarepeg against that config file, not just ones using --profile gpu. This makes "is my config file valid?" a property of the file, not of the flags you happen to pass.

How merging works

Every layering step — config file over config file, profile over base config, and Kubernetes passthrough over the generated manifest — uses the same deep-merge routine:

  1. Dicts merge key by key, recursively.
  2. Lists where every element has a name key merge by name (this is what makes containers, env, volumes, volumeMounts behave intuitively — a later layer adding one env var doesn't wipe out the others).
  3. Any other list is replaced wholesale by the later layer — this is deliberate: tolerations is a list without a stable key to merge by, and replacement is the only way a later layer can remove an inherited toleration (an append-only merge would give no way to say "actually, none of these").
  4. CLI-set fields are "claimed" and cannot be overridden by config passthrough, even if the passthrough is loaded after the CLI flags are parsed — e.g. -e FOO=bar always beats a kubernetes...env entry for FOO, but a passthrough BAR env var still lands untouched.

Example

See examples/config.yaml in the repo for a complete worked example covering every section above, including ${VAR} references (see Environment Variable Interpolation).

Clone this wiki locally