8.7 KiB
ctrl/selftest.sh
Purpose
What rig has settled, written down as assertions.
These are documentation that runs. Each check is ONE decision that has already been made, with the reason above it: not coverage, and deliberately not an exhaustive sweep of use cases. rig's own index says a rule without its reason gets overridden the first time it is inconvenient; a rule nobody can restate is worse. So the test says what was decided, and failing it should read as "you are about to undo this" rather than "something broke".
Scope, on purpose:
- No cluster, no docker, no network. It must be cheap enough to actually run.
- It asserts about RIG.
make checkasserts about the MACHINE and never fails; this exits 1, the waymake standalone checkdoes. - What actually deploys is not testable here.
tilt cistays a manual step.
rig needs no profile
rig assumes no configuration. A profile is an overlay on built-in defaults, so a rig with no env.d/ at all must resolve, report, and still generate a kit. Naming a profile that does not exist must still be an error, because a typo that silently fell back to the defaults would be worse than a failure.
the ports.sh active contract
ports.sh active is read POSITIONALLY by two other files: the Makefile takes $(word 2) and $(word 5), the Tiltfile takes _facts[0]..[7]. Insert a field in the middle and nothing errors: Tilt simply guards on the wrong context or binds the wrong port. The field count and order are the contract, so they are pinned here rather than left to whoever edits ports.sh next. The two paths are absolute or -, never empty: an empty field would shift the ones after it just the same.
the caller's env beats the files
lib/config.sh states one precedence rule: versions.env < env.d/<profile> < ctrl/.env < the caller's env. It is enforced by CONFIG_OVERRIDABLE, a hand-maintained list, and a key missing from it loses to the file SILENTLY. REGISTRY_PORT and MANIFESTS_DIR were both missing on 2026-09-13 and were found by accident.
So the loop is generated FROM the list: add a key to CONFIG_OVERRIDABLE and the test starts asking about it without anyone remembering to come here. Three keys name something that must exist and are validated at load, so they get a real alternative rather than a sentinel.
one derivation, not three
The Makefile used to compute the cluster name itself and sed TILT_PORT out of ctrl/.env: a second derivation of values lib/config.sh already owns, which could disagree with it after ports.sh persist. It now reads ports.sh active. Nothing structurally prevents the sed coming back, so the agreement is asserted against the real make -n output rather than against the source.
--no-print-directory and a grep, not tail -1: run from make selftest this is a RECURSIVE make, and the "Entering/Leaving directory" lines go to STDOUT. tail -1 then reads "make[1]: Leaving directory ..." and both checks fail, but only when invoked through make, never when the script is run directly. A test that passes one way and fails the other is worse than no test.
identity follows the folder, safely
The cluster name is NOT the bare directory name: kind needs a DNS label, so default_cluster_name lowercases it and replaces everything outside [a-z0-9-]. Re-deriving that anywhere else is how a copy ends up guarding the wrong context, which is exactly why the Tiltfile asks instead of computing.
ports are stable across versions
Not a change-detector. The block is derived, never stored, so if the derivation shifts then every EXISTING environment's ports move underneath it: a running cluster keeps its old ports while rig starts reporting new ones, and ports.sh show stops describing reality. Anchored to three known names.
rig stays standalone
rig sits inside a host project's tree but must be copyable straight out of it: no imports, no paths, no assumption the host is there. This grep is the whole test of that claim, and until it was added it lived only in prose and in whoever remembered to run it.
The pattern is assembled from fragments so the file does not match ITSELF. Writing it literally would fail forever; excluding the file instead would put a blind spot in the one check that guards the boundary. It includes the host project's word for a backing service, which rig's workload addons carried until they left, and skips local/, where overlays live and may say anything.
scratch copies
Every check that changes something does it in a copy made by copy_rig: without local/ (overlays, possibly someone else's, possibly large) and def/, and without this machine's PROFILE, OVERLAY, CLUSTER and MANIFESTS_DIR choices, so a check sets exactly what it tests.
what runs is an overlay; rig only reads it
The overlay decisions (docs/notes/overlay.md), each against a throwaway overlay in a scratch copy: with nothing named, the same cluster, ports, addons, node count and kind config as before overlays existed; rig.env between the profile and ctrl/.env, the caller above all; identity from the overlay's folder; its paths relative to itself; a named overlay that does not exist, or a rig.env that tries to choose the profile or the overlay, is an error; an overlay's addon found before rig's own and run from rig's ctrl/; persist refusing; make -n tilt OVERLAY=... asking for the overlay's context (make before 4.4 would not pass it to $(shell)).
And the two that make an overlay safe to hold someone else's work: rig writes nothing into it (a checksum of the folder before and after active, addons list, a kind render, standalone write and export), and nothing from it — neither a value nor its path — reaches a committed kit.
withdrawn stays withdrawn
One check per entry in STALE.md, each asserting that the withdrawn thing has not come back. The reasoning stays in STALE.md; the check is what makes it more than prose.
the Tiltfile hardcodes nothing
Every other Tiltfile on this machine writes its slug in five or six times by hand, so a copied project deploys into the original's cluster until someone edits all of them. rig's asks ports.sh. A literal kind-<name> in it would mean that has been undone.
standalone kits are generated and current
The kits under standalone/<profile>/ are rig flattened into single files, one per profile. A kit left behind by a change to rig is exactly the drift they replaced (rigmini.sh once said 2 GB per node long after rig measured 800 MB), so a stale kit fails here rather than waiting to be noticed on another machine.
kit Makefiles call only real verbs
Each kit's Makefile exists so nothing wrapping these scripts has to GUESS how to call them. A generated Makefile once did guess: rigmini.sh on, not a verb, and a bare rigdeps.sh for "check and report", which installs. So every target's default verb must be one its script's own dispatch accepts, read from that dispatch, not from a list that could drift from it.
export carries choices, not credentials
An export is "take the setup I have here somewhere else", so it carries this machine's CHOICES (profile, ports, manifest dir) and never its credentials: ctrl/.env can hold registry and mirror logins next to those choices. The committed per-profile kits carry neither, since they must be the same on any machine. Proven with sentinel values in a scratch copy, because the real ctrl/.env may have those keys empty, and an empty value proves nothing.
the dev loop parses — tilt and kubectl, no cluster
Parsing the Tiltfile for real is the only way to know it still evaluates. Tilt snapshots a kubectl context first, but it never contacts the cluster while evaluating: given a throwaway kubeconfig whose entries are kind-named (Tilt only runs local() freely for contexts it recognises as local) and a kubectl that swallows apply, tilt alpha tiltfile-result evaluates rig's Tiltfile with the starter overlay included, and reports the resources it would deploy.
It runs as rig, as a copy under another name — the case that used to stop at load (✖ S4) — and with the data overlay, whose namespace is used but not declared. Skipped, not failed, without tilt or kubectl.
rig's addons apply verified files, never URLs
The offline example profile must install rig's addons with no network. Each addon therefore asks deps.sh manifest <NAME> for a pinned manifest, verified on disk, instead of applying a URL; the check fails if a URL comes back or a manifest is asked for without a pinned sum (versions.md). Their container images still need preloading, and nothing here pretends otherwise.
the examples are overlays that work as shipped
examples/ is what real overlays are copied from, so every addon there must parse, and every DAG must be valid Python. What they deploy is exercised by the parse checks above, not here.