proper docs and .version

This commit is contained in:
2026-09-28 10:02:55 -03:00
parent 100f389719
commit f00055033e
13 changed files with 94 additions and 214 deletions

9
rig/.gitignore vendored
View File

@@ -6,14 +6,7 @@ def
.env.local
ctrl/.env
# generated: the .dot is a build artifact rendered from arch/*.json, never hand-edited.
# The .svg IS committed — onboarding material should render in a repo browser.
arch/*.dot
# ctrl/Tiltfile.gen was here for a generator that no longer exists. ctrl/Tiltfile
# is now a real, committed file that derives its values when Tilt parses it, so
# there is nothing generated to ignore.
# binaries pulled by `make deps-bundle` for the air-gapped installer image
# the manifests the air-gapped installer image unpacks (docs/notes/Dockerfile.deps.md)
vendor
# Overlays that live with no version control of their own, or clones of their own

1
rig/.version Normal file
View File

@@ -0,0 +1 @@
0.1.1

View File

@@ -83,9 +83,8 @@ fetches with `curl`. A stock `debian:trixie-slim` has no curl — detection runs
fine, then the first download dies with `curl: command not found` and an exit
code of 127. That is the bootstrap paradox `ctrl/Dockerfile.deps` exists
to kill — the installer carries its own toolchain so the host needs only Docker —
but building the image and running it are two different things, and only the
build has a Makefile target today. **On a genuinely bare machine, run it by
hand:**
but `make deps image` only builds the image; running it is the step below.
**On a genuinely bare machine, run it by hand:**
```bash
make deps image # builds rig-deps:deps
@@ -207,13 +206,13 @@ never tracks it), or keep it anywhere else and name it by path.
## Run it
```bash
make kind-up # idempotent create, then selects the context
make tilt-up # context + your assigned port
make cluster up # idempotent create, then selects the context
make tilt # context + your assigned port
```
`tilt-up` passes `--context kind-<name>` every time, which is the point of going
`make tilt` passes `--context kind-<name>` every time, which is the point of going
through `make` at all: tilt cannot deploy into whichever cluster you last looked
at.
`make tilt-down` and `make kind-down` close the loop, and `make kind-reset` is
`make tilt down` and `make cluster down` close the loop, and `make cluster reset` is
delete-and-recreate for when a cluster wedges.

View File

@@ -1,200 +1,99 @@
# rig
A runnable local model of a large, regulated estate — legacy and new side by
side. Its job is onboarding and exploration, not a production replica: most
services are deliberately mocked, because what has to be faithful is the
topology, not the workloads.
rig builds a disposable Kubernetes environment on your machine, so you can see how a system
fits together without access to any of it. It is for **onboarding and exploration**, not a
production replica.
## Prerequisite
**Docker.** Nothing else — no curl, no jq, no python, no apt repositories.
### Starting from plain Windows
Everything here is bash and runs *inside* a Linux shell, so on a Windows machine
that means WSL. Nothing in rig installs WSL, and nothing will: `wsl --install`
enables Windows features and requires a reboot, which is not something a script
should do to a machine on your behalf — and there is no tested undo for it.
From an elevated PowerShell or Command Prompt, once:
Most services in it are **not real**. What has to be faithful is the topology: names, ports,
dependency order, who can reach whom, and how it fails. That is what makes a whole estate fit on
a laptop. A real 20-service platform does not fit once in 14 GB; stubs of about 30 MB each fit
three times.
```powershell
wsl --install
```
Under the hood it is a kind cluster, a local image registry and a Tilt dev loop, each with its
own name and ports, so several environments run side by side. What runs on it is an
**overlay**: a folder outside rig that holds one use case.
Then reboot and open the Linux shell it installed.
## Using rig from a project
**If you cloned this on the Windows side, copy it into WSL before carrying on.**
WSL can reach the Windows drives at `/mnt/c`, and working from there mostly
functions — slowly — but file watching does not: that filesystem raises no
inotify events, so anything watching for edits silently stops seeing them.
You don't need to read rig to use it. Get a release (`rig-<version>.tar.gz`, or a checkout at
the tag `rig-<version>`), extract it anywhere, and point the project at it (`RIG_DIR`). Then run
`make check`: it says what the machine still lacks and which command comes next. `.version`
names the release; a project records the one it was last run against. See
[`docs/notes/overlay.md`](docs/notes/overlay.md).
```bash
cp -r /mnt/c/Users/<you>/rig ~/rig
cd ~/rig
```
## Prerequisite
`make deps` reports it if you are running from `/mnt/...`. Then carry on below.
**Docker.** Nothing else: no curl, jq, python or apt repositories.
If it fails, the usual causes give unhelpful messages:
On Windows, rig runs inside WSL. Run `wsl --install` once, from an elevated shell, then reboot.
Work inside the Linux filesystem, not `/mnt/c`: file watching doesn't work there, and
`make deps` warns about it. Git Bash, MSYS and Cygwin don't work (no `/proc`, no docker socket).
| symptom | cause |
| --- | --- |
| "the virtual machine could not be started" | virtualization disabled in BIOS/UEFI |
| the command is not recognised | Windows build too old — needs 2004 or later |
| the command is not recognised | Windows build too old (needs 2004 or later) |
| the install starts, then nothing works | a reboot is still pending |
Running the scripts from **Git Bash, MSYS or Cygwin does not work** — those look
close enough to a Linux shell to get started and then fail without `/proc` or a
docker socket. `ctrl/deps.sh` detects that and says so rather than letting you
find out the slow way.
## Read the docs first
```bash
make docs # serves on localhost, prints the URL
```
They run before anything is installed, which matters because they are the
instructions for everything else. No cluster and no toolchain required.
Why the code is the way it is — the reasoning, measurements and gotchas — lives
in [`docs/notes/`](docs/notes/), one file per script, so the code keeps short comments.
## Then
```bash
make check # is this machine ready? short; `make check all` for every detail
make deps # install the toolchain (add `core` on a managed machine)
make cluster up # cluster + registry + the profile's addons
```
On a machine with nothing but Docker, [BOOTSTRAP.md](BOOTSTRAP.md) runs the toolchain through
the installer container.
That is the whole setup. `make cluster up` also starts this environment's local
registry and wires it into the node, so an image built locally is pullable by the
cluster without going near docker.io. `make check` shows its port, among
everything else:
## What it does
```bash
make check # ... registry localhost:<port> (running)
docker build -t localhost:<port>/app:1 .
docker push localhost:<port>/app:1
kubectl --context kind-$(basename $PWD) run app --image=localhost:<port>/app:1
```
| command | does |
| --- | --- |
| `make docs` | serves the docs locally; works before anything is installed |
| `make check` | is this machine ready? memory, toolchain, cluster, the next command (`check all` for every detail) |
| `make deps` | installs the pinned toolchain (`deps core` on a managed machine) |
| `make cluster up` | the cluster, its registry and the configured addons (`down`, `reset`, `list`, `free`) |
| `make tilt` | the dev loop: rig's part, then the overlay's Tiltfile |
| `make selftest` | checks rig itself |
| `make help` | every target |
The port block is derived from the directory name, so two copies of rig never
collide — nothing to configure. `make check all` lists it; `bash ctrl/ports.sh persist`
pins it into `ctrl/.env` if you want it fixed:
Every verb is a `Makefile` target calling one script in `ctrl/`. The aliases (`kind-up`,
`tilt-up`, …) call the same scripts.
```bash
make cluster list # every cluster on this machine, with memory
make cluster free # stop the others if memory is tight
make cluster down # remove this cluster and its registry
```
## Overlays
**The verbs are yours to change.** `cluster` is the script — `ctrl/cluster.sh` —
and every spelling above is a `Makefile` target that calls it. `make kind-up` is
an alias for `make cluster up`, kept because the other projects on this machine
answer to that spelling and muscle memory spans repos rather than stopping at
one. Nothing outside the `Makefile` reads these names, so rename them, drop the
ones you never type, or add whatever your own projects already say: each alias
is two lines at the bottom of the file, calling the same script the canonical
target does.
**`make tilt` works on a fresh copy, unedited.** rig's `ctrl/Tiltfile` does
rig's part — identity, context guard, registry, the manifests — and then includes
the overlay's own Tiltfile. With no overlay named that is `examples/starter`, so
the dev loop comes up with its two examples running and nothing to configure.
It hardcodes nothing. It asks `ctrl/ports.sh active` for this environment's
cluster name, kube context, ports and paths — the same values every other rig
script resolves through `ctrl/lib/config.sh` — so a copied and renamed rig, or a
moved overlay, deploys into its own cluster with no edits.
`make help` lists every target. `make selftest` checks rig itself, the installer's
detection included; `make selftest install` runs the installer on clean containers
([`docs/notes/installer-testing.md`](docs/notes/installer-testing.md)).
On a machine where Docker really is the only thing installed, `make deps` has
nothing to download with — see [BOOTSTRAP.md](BOOTSTRAP.md), which runs the
toolchain through the installer container and carries on to scaffolding and running
a new project.
## What runs is an overlay
rig is the machine: toolchain, cluster, registry, port block, the dev loop's
plumbing. What runs on it is an **overlay** — one folder, outside rig's version
control, holding a use case: its settings (`rig.env`), its manifests
(`k8s/overlays/dev`), its images and Tiltfile, its addons, its kind config if it
needs its own. rig reads it and never writes into it. See
[`docs/notes/overlay.md`](docs/notes/overlay.md).
An overlay holds its settings (`rig.env`), its manifests (`k8s/overlays/dev`), its Tiltfile and
images, its addons, and its kind config if it needs one. rig reads it and never writes into it.
```bash
cp -r examples/starter local/myenv # local/ is gitignored
OVERLAY=local/myenv make cluster up # or OVERLAY=local/myenv in ctrl/.env
cp -r examples/starter local/myenv # local/ is ignored
OVERLAY=local/myenv make cluster up
OVERLAY=local/myenv make tilt
```
An overlay can also be a repo of its own, anywhere, or a project folder that
carries rig at `<project>/rig/` with a three-line forwarding Makefile.
With no overlay named, rig runs `examples/starter`. [`examples/data`](examples/data/) adds
postgres, redis and airflow as plain manifests. A project can carry rig at `./rig`, or keep it
anywhere and name it with `RIG_DIR`.
## One environment per folder
The cluster name, context, image tags and ports derive from the overlay folder's name (rig's
own, with no overlay), so environments never collide, and one's teardown never touches another.
Cluster name, kubectl context, image tags and the host port block all derive
from a folder name — the overlay's when one is named, else rig's own — so copies
never collide and neither one's teardown can touch the other. Two overlays run
side by side from one rig; two copies of rig do too.
## Configuration
## Profiles
Layers, weakest first: built-in defaults < `ctrl/versions.env` < the profile < the overlay's
`rig.env` < `ctrl/.env` < the command line. A profile (`ctrl/env.d/`) is optional and says how
this machine reaches the world: `mirror` (a pull-through cache) and `offline` (air-gapped) ship
as examples. The cluster itself is one kind template: the overlay's, or rig's one-node default.
**rig needs no profile.** With none named it runs on built-in defaults: one node,
no addons, a local registry, the newest Kubernetes version it pins. A profile is
an optional file in `ctrl/env.d/`, named by `PROFILE`, that says how this machine
reaches the world. rig ships two as **examples**; copy one to use it (the copy is
gitignored):
rig's own addons make the cluster work: `metallb` (LoadBalancer addresses), `cert-manager` (a
local CA), `metrics-server` (`kubectl top`). A workload's database, cache or scheduler belongs to
its overlay. There is no ingress controller: services are reached through LoadBalancer
addresses, or through the overlay's own gateway on the environment's HTTP port.
| example | what it changes |
| --- | --- |
| `mirror.env.example` | images through a pull-through cache of an internal registry |
| `offline.env.example` | air-gapped: everything from a preloaded local registry |
## Where things are
```bash
cp ctrl/env.d/mirror.env.example ctrl/env.d/mirror.env
PROFILE=mirror make cluster up
```
The layers, weakest first: built-in defaults < `ctrl/versions.env` < the profile
< the overlay's `rig.env` < `ctrl/.env` < your command line.
The **cluster itself** is one file: the overlay's `kind-config.yaml.tpl` if it has
one, else rig's `ctrl/k8s/kind-config.yaml.tpl` (one node). To change it — more
nodes, other port mappings, mounts — edit it and `make cluster reset`. The node
count is read back out of it, so there is nothing to drift.
## Addons
Each addon is its own idempotent script, and `ADDONS` names the ones to install,
in order. Adding one is adding a file — there is no dispatcher to edit. An
overlay's `addons/<name>.sh` is found before rig's own.
**There is no ingress controller, deliberately.** They pin a narrow window of
Kubernetes versions, so depending on one would constrain which k8s a rig can be
built with — and running a trailing-edge control plane is often the point.
Services are reached through MetalLB and `type: LoadBalancer`, which carries no
such constraint and is also what a real cluster does.
rig's own addons make the cluster work:
| Addon | Does |
| --- | --- |
| `metallb` | gives `type: LoadBalancer` an address it can actually reach |
| `cert-manager` | a local CA, so TLS works offline |
| `metrics-server` | makes `kubectl top` work on kind |
What a workload needs — a database, a cache, a scheduler — belongs to its
overlay. [`examples/data`](examples/data/) carries postgres, redis and airflow as
plain manifests (no helm: a chart repo is a network dependency), with passwords
generated on first install and kept across re-runs:
```bash
OVERLAY=examples/data make cluster up
Makefile the verbs
.version this release: patch and minor steps are edits here; milestones get a tag
ctrl/ one script per verb; lib/config.sh (settings and layers); Tiltfile (rig's
half of the dev loop); k8s/kind-config.yaml.tpl; addons/; env.d/; versions.env
docs/ index.html (make docs); notes/, one per script: why the code is as it is
examples/ starter (the default overlay), data (postgres, redis, airflow)
standalone/ single-file kits generated from ctrl/ (make standalone); never edited by hand
tests/ the installer's host fixtures
BOOTSTRAP.md from a bare machine with only Docker
STALE.md assumptions withdrawn, each checked by selftest
```

View File

@@ -1 +0,0 @@
0.1.0

View File

@@ -1,5 +1,5 @@
# Machine-local config. Copy to ctrl/.env (gitignored) and edit.
# Cluster SHAPE: an optional profile in ctrl/env.d/. Architecture MODEL: arch/<name>.json.
# Cluster SHAPE: an optional profile in ctrl/env.d/.
# Notes: docs/notes/env.md
# A profile in ctrl/env.d/ to build. Empty means rig's built-in defaults, which

View File

@@ -386,7 +386,8 @@ docker load &lt; rig.tgz &amp;&amp; make cluster up PROFILE=offline <span clas
derived from the cluster's Docker network at install time, because Docker picks
that subnet and it differs between machines.</p>
<p>Those addresses are reachable from the Linux side, including inside WSL. A
browser on Windows has no route to them: use the ingress host ports instead.</p>
browser on Windows has no route to them: use the environment's HTTP port instead, where
an overlay's gateway listens (its kind config maps it to the host).</p>
<h3>Networking and memory</h3>
<p>The cluster uses kind's built-in networking, which does enforce standard
@@ -417,9 +418,9 @@ OVERLAY=local/myenv make cluster up
</code></pre>
<p>With none named, rig runs its own <code>examples/starter</code>.
<code>examples/data</code> carries postgres, redis and airflow as an overlay's own
addons. A project can also carry rig at <code>&lt;project&gt;/rig/</code> and be
the overlay itself, with a three-line forwarding Makefile — see
<code>docs/notes/overlay.md</code>.</p>
addons. A project can carry rig at <code>./rig</code>, or keep it anywhere and name
it with <code>RIG_DIR</code>. <code>.version</code> names the release; the project
records the one it was last run against. See <code>docs/notes/overlay.md</code>.</p>
<h3>What runs in it</h3>
<p>Each component is one of three things, and switching between them is a

View File

@@ -15,7 +15,7 @@ docker build -f ctrl/Dockerfile.deps --target deps -t <slug>-deps .
docker build -f ctrl/Dockerfile.deps --target deps-full -t <slug>-deps:full .
```
`deps-full` bakes every pinned binary in at build time, and the manifests rig's own addons apply (metallb, cert-manager, metrics-server). `docker save` it and you have the whole installer as one file to carry into an air-gapped network. There, put the manifests where the addons look for them:
`make deps image full` builds the second. `deps-full` bakes every pinned binary in at build time, and the manifests rig's own addons apply (metallb, cert-manager, metrics-server). `docker save` it and you have the whole installer as one file to carry into an air-gapped network. There, put the manifests where the addons look for them:
```
docker run --rm -v "$PWD/vendor:/out/vendor" rig-deps:full manifests --to /out/vendor/manifests

View File

@@ -31,7 +31,7 @@ Read positionally, so the order is a contract; ctrl/selftest.sh pins it. The two
`OVERLAY` and `CLUSTER` given as make arguments (`make tilt OVERLAY=local/x`) are handed to that `$(shell ...)` explicitly. Before make 4.4, `$(shell)` runs with make's own environment and does not see command-line variables, while the recipes do: tilt would then be told one context and the Tiltfile would guard on another. An overlay's forwarder avoids the question by putting `OVERLAY` in the environment.
This used to be sed over ctrl/.env plus a slug computed in the Makefile, which is a SECOND derivation of values lib/config.sh already owns, and the two could disagree about the port after `ports.sh persist`, or about the name for any directory whose sanitised form differs from its raw one. One source now; the Tiltfile reads the same line.
One source: lib/config.sh owns these values, and the Tiltfile reads the same line. A second derivation in the Makefile could disagree about the port after `ports.sh persist`, or about the name of any directory whose sanitised form differs from its raw one.
## CLUSTER / KCTX fallback
@@ -39,7 +39,7 @@ The fallback matters: ports.sh sources config.sh, and if a profile or .env is br
## ARGS as .PHONY
Words after the target become the script's subcommand; each gets a no-op rule so make does not treat them as goals. They are also marked PHONY, because some of those words name real directories. `cfg`, `ctrl`, `docs`, `gen` and `init` all exist at this level, and make considers a target that is an existing directory already built, so `make build ctrl` ran the build and then printed "make: 'ctrl' is up to date". The empty rule is not enough on its own; only .PHONY stops make consulting the filesystem.
Words after the target become the script's subcommand; each gets a no-op rule so make does not treat them as goals. They are also marked PHONY, because some of those words name real directories (`ctrl`, `docs`, `examples`), and make considers a target that is an existing directory already built: without it, `make docs ctrl` would print "make: 'ctrl' is up to date". The empty rule is not enough on its own; only .PHONY stops make consulting the filesystem.
## tilt: --port guard

View File

@@ -54,7 +54,7 @@ switch for the host part, so the standalone `rigdeps.sh detect` is short too.
rig knows what a node costs (NODE_MB), not what a workload does, so the parts come from the
overlay's rig.env: `MEMORY_BUDGET="name=MB ..."`, in the order listed; a 0 is left out, and an
entry that is not `name=MB` is a `!` line. rig.env is sourced, so an overlay can size a part by
its own knob (poc_rig's airflow follows its mode). When one part outweighs the rest together
its own knob (an airflow sized by the mode the overlay runs it in). When one part outweighs the rest together
(and is at least 1 GB), check names it as the first thing to switch off; `MEMORY_BUDGET_TIP`
says how, in the overlay's words. No budget: one `?` row says where to add it.

View File

@@ -2,7 +2,7 @@
## ctrl/.env.example: header
Machine-local config. Copy to ctrl/.env (gitignored) and edit. The cluster SHAPE is an optional profile in ctrl/env.d/ — see the *.env.example there. The architecture MODEL lives in arch/<name>.json — not in .env either.
Machine-local config. Copy to ctrl/.env (gitignored) and edit. The cluster SHAPE is an optional profile in ctrl/env.d/ — see the *.env.example there.
## ctrl/.env.example: CLUSTER

View File

@@ -168,13 +168,16 @@ No search path: an unset `RIG_DIR` means `./rig`, and nothing else is tried. Mov
changes nothing in the cluster's name, context or ports; moving or renaming the overlay
folder does.
`VERSION`, at rig's top, names the rig. It travels with the folder, vendored, a
worktree or a plain copy, and needs no git. A project records the one it was last run
against and warns on another; it does not refuse:
`.version`, at rig's top, names the rig. It travels with the folder, vendored, a
worktree, a plain copy or a `git archive` release, and needs no git. Patch and minor
steps are edits to that file, with no tag; a `rig-<version>` tag marks a milestone only.
A project records the version it was last run against and warns on another; it does not
refuse. Releases before 0.1.1 carry `VERSION` instead, so the project reads that as a
fallback:
```make
RIG_REF := 0.1.0
RIG_AT := $(strip $(shell cat '$(RIG_DIR)/VERSION' 2>/dev/null))
RIG_REF := 0.1.1
RIG_AT := $(strip $(shell cat '$(RIG_DIR)/.version' 2>/dev/null || cat '$(RIG_DIR)/VERSION' 2>/dev/null))
ifneq ($(RIG_AT),$(RIG_REF))
$(warning rig at $(RIG_DIR) is $(or $(RIG_AT),unversioned); this overlay was last run against rig $(RIG_REF))
endif
@@ -183,18 +186,3 @@ endif
`rig.env` holds no secrets, by this contract. A repository whose `.gitignore` has a
broad `*.env` (a common secrets rule) would still hide it, so an overlay living in
such a repo re-includes it in its own `.gitignore`: `!rig.env`.
## Moving a copied rig to an overlay
A rig copied into a project and edited there splits cleanly:
| was, in the copy | goes to |
| --- | --- |
| `ctrl/k8s/base`, `ctrl/k8s/overlays` | `k8s/` |
| the workload parts of `ctrl/Tiltfile` | `Tiltfile` (paths now relative to the overlay) |
| `ctrl/env.d/<name>.env` | `rig.env` |
| edits to `ctrl/k8s/kind-config.yaml.tpl` | `kind-config.yaml.tpl` (`${HOST_WORKDIR}` → `${OVERLAY_DIR}`) |
| workload addons | `addons/` |
| Dockerfiles for the workload | beside the Tiltfile |
| `ctrl/.env` | `rig/ctrl/.env` (this machine's; drop a `MANIFESTS_DIR=ctrl/k8s/overlays/dev` line) |
| everything else of rig's | replaced by rig as it is |

View File

@@ -28,7 +28,7 @@ So the loop is generated FROM the list: add a key to `CONFIG_OVERRIDABLE` and th
## 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.
The Makefile reads `ports.sh active` rather than deriving the cluster name and `TILT_PORT` itself: a second derivation of values `lib/config.sh` already owns could disagree with it after `ports.sh persist`. Nothing structurally prevents one 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.