-
Notifications
You must be signed in to change notification settings - Fork 0
Configuration
squarepeg reads config from a layered set of YAML files, resolved preferentially: several sources are located, loaded, and deep-merged together before any CLI flag is applied — rather than there being just one config file.
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, orphan_sweep: true, orphan_sweep_min_age: 300, 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| 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). |
orphan_sweep |
bool | Delete your own leftover squarepeg pods/jobs in the namespace as part of each run's cleanup — both finished-but-undeleted and never-started-and-stuck. false disables it. Requires list on pods/jobs (broader than the create/delete/get squarepeg otherwise needs) — see Execution Lifecycle → Clean up. |
orphan_sweep_min_age |
int (seconds) | Default 300. How long a resource must have been finished — or, for a never-started pod, how long it must have existed — before it's eligible for the sweep. |
defaults |
mapping | See defaults below. |
volumes |
mapping | Named volume sources; can also declare auto-mounts via mount_path — see Volumes below. |
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.
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 |
Each entry under volumes.NAME holds a Kubernetes volume source (persistentVolumeClaim, emptyDir, etc.) plus two optional squarepeg-owned fields:
| Key | Type | Description |
|---|---|---|
mount_path |
string | If present, this volume is mounted at this path on every run, with no -v flag needed at all. If absent, the entry behaves as it always has — available for a user to reference with -v NAME:/path, not mounted otherwise. |
read_only |
bool | Whether the auto-mount is read-only. Default false. Only meaningful alongside mount_path. |
volumes:
refdata: # only mounted if the user does -v refdata:/x
persistentVolumeClaim: {claimName: refdata-pvc, readOnly: true}
shared-tools: # auto-mounted at /opt/tools on EVERY run, no -v needed
persistentVolumeClaim: {claimName: shared-tools-pvc, readOnly: true}
mount_path: /opt/tools
read_only: trueA few things worth knowing:
-
A volume can be both auto-mounted and separately
-v-referenced. If a user does-v shared-tools:/somewhere-elsefor a volume already auto-mounted above, both mounts exist side by side in the container — Kubernetes allows mounting the same volume at multiple paths, so this needs no special handling. -
mount_path/read_onlynever leak into the generated manifest — squarepeg strips them before emitting the Kubernetes volume source, whether the volume was auto-mounted or resolved via-v. -
No CLI opt-out. There's no
--no-auto-mountflag; auto-mounted volumes are controlled purely by whoever owns the config layer that setmount_path. A user who wants different behaviour edits their own--configlayer (which can override or removemount_pathfor that name, sincevolumes.NAMEis a normal dict that deep-merges across layers same as everything else) or selects a different--profile. -
Auto-mounts are config-driven, not CLI-driven, so they aren't "claimed" —
kubernetes.specpassthrough in the same resolved config can still override an auto-mount by name, consistent with how other config defaults (e.g.defaults.cpus) can be overridden by passthrough in the same config. - This is friendlier sugar over something you could already do by hand-writing matching
kubernetes.spec.volumes+containers[].volumeMountsentries — see below — themount_pathfield just saves you from writing both halves of that yourself.
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: 3600CLI 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:
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.pyA 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 insideprofiles.gpuis 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.
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:
- Dicts merge key by key, recursively.
-
Lists where every element has a
namekey merge by name (this is what makescontainers,env,volumes,volumeMountsbehave intuitively — a later layer adding one env var doesn't wipe out the others). -
Any other list is replaced wholesale by the later layer — this is deliberate:
tolerationsis 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"). -
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=baralways beats akubernetes...enventry forFOO, but a passthroughBARenv var still lands untouched.
See examples/config.yaml in the repo for a complete worked example covering every section above, including ${VAR} references (see Environment Variable Interpolation).