-
Notifications
You must be signed in to change notification settings - Fork 0
Development
pip install -e ".[dev]"Pulls in the runtime deps (click, kubernetes, PyYAML) plus pytest, pytest-cov, and ruff.
pytest # unit tests only (the default)
pytest --cov # with coverageUnit tests are fully hermetic — they mock the Kubernetes client (unittest.mock) rather than touching a real cluster, so they run offline and require no kubeconfig at all. By default, pytest excludes integration tests (addopts = "-m 'not integration'" in pyproject.toml).
ruff check .Configured for E/F/I rules (pycodestyle errors, pyflakes, import sorting) at a 120-character line length.
SQUAREPEG_INTEGRATION=1 pytest -m integrationThese exercise the real create → watch → stream → exit-code → cleanup path against an actual cluster. They're gated by three independent checks, deliberately layered — an accidental run against a real cluster is a real hazard:
| Guard | What it does |
|---|---|
| pytest marker |
-m integration is required; a bare pytest never runs them. |
SQUAREPEG_INTEGRATION=1 |
Must be set explicitly, in addition to the marker. |
| Live connectivity check | A fixture pings the cluster's version endpoint before anything runs, and prints the resolved kubeconfig context so you can see exactly what you're about to hit. |
Test coverage includes: success/nonzero exit codes, byte-for-byte stdout, env vars, workdir, volumes, both Pod and Job mode, cleanup actually removing the resource, --keep retaining it, --dry-run creating nothing, image-pull failures failing fast (not hanging out the timeout), config passthrough landing on the live object, and incremental log streaming (guarding specifically against a regression where log reads get buffered until the container exits instead of streaming live).
Locally, integration tests need a small test image (busybox:1.36) reachable by your cluster and a namespace you can create/delete in — the fixtures create and tear down a uniquely-named test namespace per session, never reusing default.
Three jobs, all defined in .github/workflows/ci.yml:
| Job | What it runs |
|---|---|
python-tests |
pytest --cov across a Python 3.11/3.12/3.13 matrix |
lint |
ruff check . |
integration |
Spins up a kind cluster (helm/kind-action), loads the test image into it, and runs SQUAREPEG_INTEGRATION=1 pytest -m integration
|
| Path | Contents |
|---|---|
squarepeg/cli.py |
Click CLI: flag parsing, config resolution, dispatch to manifest building and the runner |
squarepeg/dockerargs.py |
Docker-flag semantics: env/entrypoint parsing, the unsupported-flag table |
squarepeg/runspec.py |
RunSpec — the normalized, in-memory representation of "what to run" |
squarepeg/config.py |
Config file location/loading/validation, layering, coerce_int/coerce_bool
|
squarepeg/env_interpolation.py |
${VAR}/${VAR:-default} substitution |
squarepeg/merge.py |
The one deep-merge routine shared by config layering, profiles, and k8s passthrough |
squarepeg/manifest.py |
Builds Pod/Job manifests as plain dicts |
squarepeg/quantities.py |
Docker ↔ Kubernetes CPU/memory unit conversion |
squarepeg/volumes.py |
-v/--volume flag parsing |
squarepeg/naming.py |
Resource name generation/sanitization, RFC1123 validation, label sanitization |
squarepeg/errors.py |
The exception hierarchy and their exit codes |
squarepeg/k8s/session.py |
Kubeconfig/client/namespace resolution |
squarepeg/k8s/runner.py |
Create → wait → exit-code → cleanup orchestration, signal handling |
squarepeg/k8s/logs.py |
Log streaming, reconnect, dedupe |
squarepeg/k8s/events.py |
Surfacing Kubernetes events as diagnostics |
tests/ |
Unit tests (mirroring the module layout above) plus test_integration.py and conftest.py
|
examples/config.yaml |
A fully worked example config file covering every schema section |
-
Manifests are plain dicts, not typed
kubernetes.client.V1*models. This is what makes arbitrary config passthrough a straightforwarddictmerge, and makes--dry-runoutput exactly what gets sent — no serialization round-trip to diverge from reality. -
One merge function, three uses.
merge.deep_mergeis used identically for config-file-over-config-file layering, profile-over-base-config, and Kubernetes-passthrough-over-generated-manifest. The only thing that differs between uses is theclaimsset (empty for config layering; populated from CLI flags for passthrough). -
kubernetes.spec/metadatain config is always expressed as a Pod spec, even in--mode job—manifest.build_jobre-homes it under the Job's pod template. Get this right if you ever touchbuild_job; it's the reason the same config file works unchanged in either mode.