Skip to content

Development

Biowilko edited this page Aug 5, 2026 · 1 revision

Development

Install

pip install -e ".[dev]"

Pulls in the runtime deps (click, kubernetes, PyYAML) plus pytest, pytest-cov, and ruff.

Running the tests

pytest                  # unit tests only (the default)
pytest --cov            # with coverage

Unit 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).

Linting

ruff check .

Configured for E/F/I rules (pycodestyle errors, pyflakes, import sorting) at a 120-character line length.

Integration tests

SQUAREPEG_INTEGRATION=1 pytest -m integration

These 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.

CI

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

Project layout

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

Design notes worth knowing before touching the code

  • Manifests are plain dicts, not typed kubernetes.client.V1* models. This is what makes arbitrary config passthrough a straightforward dict merge, and makes --dry-run output exactly what gets sent — no serialization round-trip to diverge from reality.
  • One merge function, three uses. merge.deep_merge is 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 the claims set (empty for config layering; populated from CLI flags for passthrough).
  • kubernetes.spec/metadata in config is always expressed as a Pod spec, even in --mode jobmanifest.build_job re-homes it under the Job's pod template. Get this right if you ever touch build_job; it's the reason the same config file works unchanged in either mode.

Clone this wiki locally