rig major updates

This commit is contained in:
2026-09-22 05:15:49 -03:00
parent 9c963514f1
commit 2a0a793f19
64 changed files with 1762 additions and 645 deletions

View File

@@ -15,7 +15,13 @@ 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. `docker save` it and you have the whole installer as one file to carry into an air-gapped network.
`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
```
Each is verified against its pin on the way out, and again when an addon uses it. The addons' container images still have to be preloaded into the local registry: the manifests reference quay.io and registry.k8s.io, and registry mirroring covers docker.io only.
## Packages

View File

@@ -1,33 +1,35 @@
# ctrl/Dockerfile.example
# examples/starter/Dockerfile.example
## Naming
EXAMPLE — a component image. Copy, rename, replace. Named like the manifest it feeds and the resource it becomes:
```
ctrl/Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
```
That image string is the ONLY thing connecting the three. Nothing checks it; a typo shows up as a pod stuck in ImagePullBackOff pulling from the public index, which reads like a network problem and is not one.
## COPY paths are repo-root relative (the one that catches everyone)
## COPY paths are relative to the build context
The Tiltfile passes two paths with DIFFERENT bases, in adjacent arguments:
The overlay's Tiltfile runs from the overlay's own folder (rig's ctrl/Tiltfile includes it), so both paths in `docker_build` are relative to the overlay:
```
context='..' the REPO ROOT (the Tiltfile is in ctrl/)
dockerfile='Dockerfile.api' relative to the TILTFILE, so ctrl/Dockerfile.api
context='.' the overlay folder (or a subfolder, e.g. 'repodir/api')
dockerfile='Dockerfile.api' relative to the overlay's Tiltfile too
```
So every COPY is resolved against the repo root, NOT against the Dockerfile's directory. A file sitting right beside it is still reached as `ctrl/`:
Every COPY is resolved against the context, NOT against the Dockerfile's directory. With `context='.'` and the Dockerfile in a subfolder, a file sitting right beside it is still reached through that subfolder:
```
COPY ctrl/nginx.conf /etc/nginx/conf.d/default.conf # correct
COPY nginx.conf /etc/nginx/conf.d/default.conf # fails — no such file
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf # correct, Dockerfile in docker/
COPY nginx.conf /etc/nginx/conf.d/default.conf # fails — no such file in the context
```
Nothing warns you. The build just cannot find a file that is visibly there.
Before overlays, rig's own ctrl/Tiltfile built with `context='..'` (the repository root) and the Dockerfile in ctrl/, which is the same trap one level up.
## Dependency layer
Dependencies first, in their own layer: they change far less often than the code, so a source edit does not reinstall them on every rebuild.
@@ -37,7 +39,7 @@ Dependencies first, in their own layer: they change far less often than the code
The sync in the Tiltfile's `docker_build` must land where this image expects it:
```
live_update=[sync('../api', '/app/api')]
live_update=[sync('api', '/app/api')]
```
matches `COPY api/ ./api/` with `WORKDIR /app`. If the two disagree, Tilt syncs into a path nothing reads and the container keeps serving the built copy — edits appear to do nothing, with no error anywhere.

View File

@@ -13,21 +13,23 @@ make tilt / docs work on it, read about it
The logic lives in the scripts, never here: `make cluster up` -> ctrl/cluster.sh up.
Config layers, weakest first: built-in defaults < ctrl/versions.env (pinned toolchain) < ctrl/env.d/<profile>.env (optional) < ctrl/.env (local, gitignored) < the environment. So `make cluster up PROFILE=<name>` beats them all. See [config.md](config.md).
Config layers, weakest first: built-in defaults < ctrl/versions.env (pinned toolchain) < ctrl/env.d/<profile>.env (optional) < <overlay>/rig.env (optional) < ctrl/.env (local, gitignored) < the environment. So `make cluster up PROFILE=<name>` beats them all. See [config.md](config.md) and [overlay.md](overlay.md).
Start with: `make check && make deps && make cluster up`
## FACTS
Identity follows the FOLDER NAME, so this directory can be copied elsewhere, renamed, and run as a separate environment with no edits. ctrl/.env overrides it when you want a name that differs from the directory.
Identity follows the FOLDER NAME the overlay's when one is named, else this directory's so either can be copied elsewhere, renamed, and run as a separate environment with no edits. ctrl/.env overrides it when you want a name that differs from the folder.
Asked once, of ctrl/ports.sh, which resolves it through lib/config.sh:
```
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
```
Read positionally, so the order is a contract; ctrl/selftest.sh pins it.
Read positionally, so the order is a contract; ctrl/selftest.sh pins it. The two paths are absolute, or `-` when there is none, so the count never shifts.
`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.

View File

@@ -2,23 +2,19 @@
## Purpose and ownership
This file ships with rig and works unedited: rig's own k8s/base already boots, so `make tilt` comes up with a running cluster and no editing at all. What it deploys is two EXAMPLES — replace them, and add your own images and resources in the two marked sections near the bottom. The catalogue after them has the blocks to paste, with the parts that are easy to get wrong already commented.
rig's half of the dev loop, and rig's file: who we are, the context guard, the registry, the overlay's manifests and the namespaces they use. The workload's half — images, resource names and order, port-forwards — is the overlay's own `Tiltfile`, which this one includes at the end (see [overlay.md](overlay.md)). With no overlay named that is `examples/starter/Tiltfile`, so `make tilt` on a fresh clone comes up with the two examples running and nothing to edit.
rig supplies this file; it does not own it. Nothing in rig reads it back, and nothing here is regenerated — edit it freely, the way you would edit k8s/base/example-mock.yaml. rig owns the machine, you own the workload.
Splitting it is what lets rig be replaced as a whole (✖ S1 in STALE.md).
## Nothing hardcoded to this directory
Nothing in the Tiltfile is hardcoded to this directory, deliberately. Every other project here writes its slug into the Tiltfile five or six times by hand, so a copy of the project deploys into the original's cluster until someone remembers to edit all of them. A rig is meant to be copied and renamed, so it asks instead.
Nothing in the Tiltfile is hardcoded to this directory, deliberately. Every other project here writes its slug into the Tiltfile five or six times by hand, so a copy of the project deploys into the original's cluster until someone remembers to edit all of them. A rig is meant to be copied and renamed, and an overlay moved, so it asks instead.
## Who we are, and on which ports
One question to rig, answered by ctrl/ports.sh, which resolves it through lib/config.sh — the same path every other rig script takes. That is the point: the cluster name is NOT the bare directory name (it is lowercased and reduced to a DNS label), and the ports honour anything pinned in ctrl/.env. Recomputing either of those here in Starlark is how two copies end up disagreeing about which cluster they are talking to.
## Where the manifests live
rig's own manifests are the default; point MANIFESTS_DIR in ctrl/.env at an overlay versioned somewhere else and rig stops owning them — see k8s/README.md. Real manifests usually change on a different cadence, by different people, under different review.
The value is REPO-ROOT relative, because that is the root everything else in rig is expressed against. The Tiltfile runs in ctrl/, so prefix rather than assume: `'../' + 'ctrl/k8s/overlays/dev'` and `'../' + '../platform/overlays/dev'` are both right, where stripping a leading `ctrl/` would only fix the first.
The manifests and the overlay arrive as absolute paths, or `-` when there is none.
## Refuse to deploy into the wrong cluster
@@ -28,23 +24,20 @@ Tilt snapshots the kubectl context at startup, BEFORE parsing this file, so it c
Fail closed. Tilt can usually infer the kind registry on its own, but "usually" is an inference, and when it misses, an unqualified name like `app` quietly means docker.io/library/app — a push to the public index instead of the registry two lines away. rig runs that registry; name it.
## Catalogue
The catalogue holds the shapes that recur across every project here, with the reasoning kept next to them. They are comments so the file runs as-is.
## Catalogue: build an image
## Namespaces
The one genuinely non-obvious thing in the whole corpus: `context` and `dockerfile` are relative to DIFFERENT directories, in adjacent arguments, and nothing warns you.
Every namespace the manifests use has to exist before anything lands in it, and kustomize does not guarantee ordering across resources, so the Tiltfile creates them first (idempotent). The Namespaces the manifests declare are grouped as the `infra` resource, whatever they are called.
- `context=` the REPO ROOT — the Tiltfile is in ctrl/, so `'..'`
- `dockerfile=` relative to THIS file — so `'Dockerfile.api'` is ctrl/Dockerfile.api
Nothing here assumes a namespace is named after the cluster (✖ S4).
Every COPY inside those Dockerfiles is therefore repo-root relative: a file sitting BESIDE the Dockerfile is still reached as `COPY ctrl/nginx.conf`.
## Handing over to the overlay
## Catalogue: reload the gateway when its config changes
The facts are published as environment variables (`os.putenv`) and the overlay's Tiltfile is `include()`d. An included Tiltfile runs from its own folder: `os.getcwd()`, `local()` and every relative path in it resolve from the overlay, so it needs no path back into rig and reads the facts with `os.getenv`:
A Caddyfile arriving via configMapGenerator with disableNameSuffixHash does NOT roll the pod — the ConfigMap name never changes, so nothing tells the Deployment anything happened. Without the `gateway-reload` local_resource you edit the routes and watch nothing take effect.
```
RIG_CLUSTER RIG_CONTEXT RIG_HTTP_PORT RIG_HTTPS_PORT RIG_TILT_PORT RIG_REGISTRY RIG_OVERLAY_DIR
```
## Catalogue: reach a service directly, bypassing the gateway
## Catalogue
For a DB client or an admin UI. Prefer routing through the gateway: host ports are a single shared namespace across every project on this machine, which is why rig derives a block per environment in the first place. If you do need one, take it from this environment's own block rather than picking a number.
The blocks that recur across projects moved with the workload's half: `examples/starter/Tiltfile` carries them, commented, with the parts that are easy to get wrong explained next to them — building an image, a shared base built once, naming and ordering resources, reloading a gateway on a config change, kustomize flags, and reaching a service directly.

View File

@@ -2,18 +2,12 @@
## addons.sh
Each addon is its own idempotent script in `ctrl/addons/` — adding one is adding
a file, not editing a dispatcher.
Each addon is its own idempotent script — adding one is adding a file, not editing a dispatcher. `ADDONS` names them, in install order; the overlay's `addons/<name>.sh` is found before rig's `ctrl/addons/<name>.sh`, and every one runs from rig's `ctrl/` with `RIG_CTRL` exported, wherever its file lives (see [overlay.md](overlay.md)).
## airflow.sh
## What rig ships, and what it does not
Airflow needs a metadata database before it will start at all, so the script
refuses rather than rolls a pod that will CrashLoopBackOff while the real problem
(postgres missing from `ADDONS`) stays invisible in the logs.
rig's own addons make the *cluster* work, and are useless outside one: metallb, cert-manager, metrics-server. Things a workload happens to need — a database, a cache, a scheduler — are the workload's, and which workload needs which is not rig's business, so they live with the overlay. `examples/data/addons/` has postgres, redis and airflow as a worked example; an overlay that wants them copies them in.
One pod on `standalone`, matching the compose cabinet: migration, admin user,
scheduler and webserver in a single container. The official chart's five
deployments model an installation; switching this on means wanting pipelines.
## cert-manager.sh
@@ -47,27 +41,3 @@ kind nodes serve kubelet metrics over a self-signed cert, so the standard
manifest never becomes ready without `--kubelet-insecure-tls`. That is fine here
(it is a local cluster) and is the single most common reason metrics-server sits
at 0/1 on kind.
## postgres.sh — cabinets
A cabinet is a public service dropped into the environment as-is — the upstream
image, unmodified, reachable at a known address. `postgres.sh` is the cluster
half of it; the compose half is a `service.yml` beside a `cabinet.json`. The
declaration is made once and both paths read it, so nothing is remembered twice.
## postgres.sh — plain manifests, one replica
Plain manifests rather than a helm chart, matching the other addons: a chart repo
is a network dependency, and the offline example profile exists precisely so
there is a path with none. The image is pinned in `ctrl/versions.env` and can be
preloaded into a local registry like every other image here.
One replica on a PVC. This models a dependency for local work, not a
highly-available database, and pretending otherwise on a kind node would be a
more elaborate lie rather than a more useful one.
## redis.sh
Cache, and the broker anything queue-shaped runs on. No persistence: a broker
that loses its queue on restart is the honest local model, and a PVC here buys
nothing but a volume to clean up.

View File

@@ -42,3 +42,9 @@ ports, registry, addons — and adds detail only where something needs attention
as the full report did before 2026-09-17. `deps.sh detect all` is the same switch for the host part,
so the standalone `rigdeps.sh detect` is short too. Changed because the long report buried the few
lines that mattered.
## overlay and kind config
The rig block names the overlay when one is set (with `all`: what it provides — rig.env, manifests, kind config, addons, Tiltfile — and where the manifests and kind config resolved to).
Two `!` lines belong to overlays. An older ctrl/.env that still pins `MANIFESTS_DIR=ctrl/k8s/overlays/dev` — rig's examples, before they moved — is reported; load_config ignores it until then. And a kind config without the containerd `config_path` patch is reported whenever a registry mode needs it: registry.sh writes per-host config into certs.d, containerd only reads it if the cluster was created with that patch, and an overlay's own kind file replaces rig's whole template, so dropping it is easy and fails silently.

View File

@@ -7,7 +7,8 @@ The ecosystem convention is that scripts are standalone with no shared log libra
```
built-in defaults in load_config; fill only what nothing else set
ctrl/versions.env pinned toolchain + image digests (committed)
ctrl/env.d/<profile> addons, registry (OPTIONAL, examples ship as *.env.example)
ctrl/env.d/<profile> how this machine reaches the world (OPTIONAL, examples ship as *.env.example)
<overlay>/rig.env what runs: addons, namespaces, images (OPTIONAL, lives with the overlay)
ctrl/.env machine-local values and secrets (gitignored)
the caller's env `make cluster up PROFILE=<name>` (always wins)
```
@@ -20,11 +21,15 @@ Run from ctrl/.
Values a user can reasonably override per-invocation. Anything set in the environment when load_config runs is restored after the files are read. NODES is deliberately NOT here: it is read back out of the kind config, so the file is the one place that decides it.
REGISTRY_PORT and MANIFESTS_DIR were missing here while ctrl/.env set them, so the caller's env silently LOST to the file for those two, breaking the one precedence rule the header states. Both are now listed; the other twelve are unchanged.
REGISTRY_PORT and MANIFESTS_DIR were missing here while ctrl/.env set them, so the caller's env silently LOST to the file for those two, breaking the one precedence rule the header states. Both are now listed. OVERLAY joined with overlays, for the same reason: it is chosen per machine or per call.
## default_cluster_name
The containing folder's name, reduced to something kind accepts as a cluster name (a DNS label: lowercase alphanumerics and dashes). Run from ctrl/, so the repo root is the parent.
The environment's folder name — the overlay's when one is named, else rig's own — reduced to something kind accepts as a cluster name (a DNS label: lowercase alphanumerics and dashes). Run from ctrl/, so rig's folder is the parent.
## _from_ctrl, _abs_from_ctrl
Paths in the config are relative to rig's folder (MANIFESTS_DIR, OVERLAY) or to ctrl/ (KIND_CONFIG), or absolute. Scripts run from ctrl/, so `_from_ctrl` turns a rig-relative path into one usable from there, and `_abs_from_ctrl` into an absolute one for consumers outside bash (ports.sh active, the kind config's hostPath entries).
## derive_port_base
@@ -38,9 +43,15 @@ RIG_PORTABLE skips the machine-local layer. config_snapshot sets it, so a genera
A profile is an optional overlay, never a prerequisite. rig assumes no configuration: with no profile named, or no env.d/ at all, it runs on the built-in defaults. What IS an error is naming a profile that does not exist, because a typo must not quietly fall back to something else.
## load_config: overlays
An overlay is one folder, outside rig's version control, that holds what runs ([overlay.md](overlay.md)). `OVERLAY` names it; a named overlay that does not exist is an error, like a named profile. With none named, rig's own `examples/starter` is used if it is present — it sets nothing, so a plain rig resolves as it did before overlays — and a rig copied without `examples/` still resolves, with no manifests.
Its `rig.env` is layered after the profile and before ctrl/.env. It may not set PROFILE or OVERLAY, which are chosen before it loads, and the paths it sets are relative to the overlay (load_config rewrites them as it loads the file), so an overlay can be moved without editing it.
## load_config: identity follows the folder
Identity follows the FOLDER, so copying this directory somewhere else and renaming it yields a distinct environment with no further edits. Without this, two copies would share one cluster and `make cluster down` in either would destroy the other's.
Identity follows the FOLDER — the overlay's when one is named, else rig's — so copying either somewhere else and renaming it yields a distinct environment with no further edits. Without this, two copies would share one cluster and `make cluster down` in either would destroy the other's. It is also what lets a project carry rig at `<project>/rig/` without every such project's cluster being called `rig`.
## load_config: host ports
@@ -48,7 +59,7 @@ Host ports are a single shared namespace, so unlike the cluster name they cannot
## load_config: MANIFESTS_DIR
Where the workload's manifests live, repo-root relative. Defaulted here so it is always resolved rather than sometimes-set: it is the seam that lets the real manifests be versioned away from the installer, and a consumer should not have to know whether anyone filled it in. See k8s/README.md.
Where the workload's manifests live, relative to rig's folder or absolute: the overlay's `k8s/overlays/dev` unless something names another. It is the seam that lets the real manifests be versioned away from the installer. `none` means rig applies none (the overlay's Tiltfile does). A named folder that does not exist is an error; the old default `ctrl/k8s/overlays/dev`, pinned by older .env files, is ignored while that folder does not exist and reported by `make check`.
## load_config: NODE_MB
@@ -60,7 +71,7 @@ It is set here rather than in check.sh because the memory tool and every standal
Renders the kind config to stdout. sed rather than envsubst: envsubst is gettext-base, absent from a minimal Debian, and Docker is meant to be the only prerequisite. The variable list is explicit so a template cannot quietly start depending on something the caller does not set.
hostPath entries are resolved by the HOST dockerd, so HOST_WORKDIR must stay a host path even when this runs inside the installer container.
hostPath entries are resolved by the HOST dockerd, so HOST_WORKDIR and OVERLAY_DIR must stay host paths even when this runs inside the installer container. `${OVERLAY_DIR}` renders to the overlay's absolute path, for mounting its folders into the nodes.
## What a standalone kit needs to know

View File

@@ -156,3 +156,7 @@ The "put OUT_BIN on PATH" advice is only worth giving when something actually la
## main: argument shift
Read the command, THEN shift — and shift only if there is something there. A bare `shift` with no positional parameters returns 1, and under `set -e` that ended the script before a single line was printed: running this with no arguments at all, the documented default, did nothing and said nothing.
## manifest / manifests
The manifests rig's own addons apply are pinned in versions.env like the binaries, and fetched by the same code: `resolve_url` for the source (upstream, artifactory, baked), `verify` for the sum. `manifest <NAME>` makes one present in `vendor/manifests/` (rig's folder, gitignored) and prints only its path, so an addon can apply it; a cached copy whose sum still matches is reused, one that does not is fetched again. `manifests [--to DIR]` fetches all three, which is how the deps-full image bakes them and how an offline machine is given them. Why the addons stopped applying URLs is in versions.md.

View File

@@ -12,12 +12,18 @@ The kubectl context becomes kind-<CLUSTER>. LEAVE THIS UNSET unless you need a n
LEAVE UNSET — they derive from the directory name so several environments coexist without negotiating (see ctrl/ports.sh). `make check` shows this environment's block; `bash ctrl/ports.sh persist` writes it into ctrl/.env so it stops being derived and becomes fixed. Set a value only to override.
## ctrl/.env.example: OVERLAY
The folder that holds what runs — its settings (`rig.env`), manifests, addons, Tiltfile — kept outside rig's version control: `local/<name>` (gitignored), or a repo of its own anywhere. Relative to rig's folder, or absolute. Unset, rig runs its own `examples/starter`. The cluster, context and port block follow the overlay's folder name. See [overlay.md](overlay.md).
## ctrl/.env.example: MANIFESTS_DIR
Where the application manifests live. The real ones are expected to be versioned separately from this installer — they change on a different cadence, by different people. Repoint this at their repo and rig stops owning them:
Where the manifests live. Leave it unset: the overlay's `k8s/overlays/dev` is the default. Set it only to point somewhere else, relative to rig's folder or absolute:
MANIFESTS_DIR=../platform-manifests/overlays/dev
Older copies of this file set `MANIFESTS_DIR=ctrl/k8s/overlays/dev`, rig's examples before they moved to `examples/`. That value is ignored while the folder does not exist, and `make check` says to delete the line.
## ctrl/.env.example: DEPS_SOURCE
Where the installer fetches the pinned binaries from.
@@ -36,11 +42,13 @@ Corporate root CA, if Artifactory is fronted by an internal CA (it usually is).
## env.d/*.env.example: profiles in general
EXAMPLE PROFILES. rig needs none of these: with no profile it runs on its built-in defaults (lib/config.sh). To use one, copy it to <name>.env in ctrl/env.d/ and name it — PROFILE=<name> in ctrl/.env, or on the command line. It then overlays the defaults; anything it does not set, they still supply.
EXAMPLE PROFILES. rig needs none of these: with no profile it runs on its built-in defaults (lib/config.sh). To use one, copy it to <name>.env in ctrl/env.d/ and name it — PROFILE=<name> in ctrl/.env, or on the command line. It then overlays the defaults; anything it does not set, they still supply. An activated `<name>.env` is gitignored: it is this machine's choice.
## env.d/client.env.example
A profile says how this machine reaches the world — a registry mirror, an air-gapped install. What runs is an overlay's business ([overlay.md](overlay.md)); its `rig.env` layers above the profile.
client — images through a pull-through cache of the corporate registry, with TLS and metrics addons. More nodes or port mappings: edit k8s/kind-config.yaml.tpl.
## env.d/mirror.env.example
mirror — images through a pull-through cache of an internal registry, with TLS and metrics addons. More nodes or port mappings: edit the kind config (rig's, or the overlay's).
### Real ports (80/443)
@@ -48,27 +56,9 @@ Ports derive from the directory name by default (see ctrl/ports.sh), so several
Opt in to the real ports only when this is the ONLY environment and nothing else owns :80. They fail to bind otherwise, and docker reports it as an opaque "failed to bind host port 0.0.0.0:80/tcp: address already in use" halfway through cluster creation. `make check` checks before you spend the time. Uncommenting also means only one environment can exist at a time.
## env.d/data.env.example
data — databases and a scheduler for an environment that needs them: postgres, redis and airflow, each an upstream image run unmodified.
Everything lands in the `data` namespace (DATA_NAMESPACE to move it), so `make cluster reset` on the app namespace leaves the databases alone.
Costs roughly 2-3 GB with airflow, under 1 without. Airflow's first boot runs the whole metadata migration, so expect a few minutes before it is ready.
### Postgres password
The password is not in the profile: postgres.sh generates one on first install and keeps it across re-runs, so re-running the addon never rotates the credential out from under whatever is already connected.
### Reaching the databases
Ports derive from the directory name by default — see ctrl/ports.sh. Reach the databases with port-forward rather than binding more host ports:
kubectl -n data port-forward svc/postgres 5432:5432
kubectl -n data port-forward svc/airflow 8080:8080
## env.d/offline.env.example
offline — air-gapped. Everything comes from a local registry that was loaded ahead of time; nothing reaches the internet. Pair with the deps-full image (DEPS_SOURCE=baked) so the toolchain install is offline too.
offline — air-gapped. Everything comes from a local registry that was loaded ahead of time; nothing reaches the internet. Pair with the deps-full image (DEPS_SOURCE=baked) so the toolchain install is offline too, and so the manifests metallb and the other addons apply come from the image, verified, rather than from GitHub (see Dockerfile.deps.md). Their container images still have to be preloaded.
The heavier addons are left out to keep first boot viable.

View File

@@ -4,11 +4,17 @@
The cluster: one node by default — add nodes or port mappings by editing the file, then `make cluster reset`.
It is a TEMPLATE rather than a plain kind-config.yaml because a rig is copied and renamed to make a second environment, and both the cluster name and the host port follow the directory. A checked-in literal would make every copy collide on both. ctrl/cluster.sh renders it with sed — not envsubst, which is gettext-base and absent from a minimal Debian, and rig's whole premise is that Docker is the only prerequisite.
It is a TEMPLATE rather than a plain kind-config.yaml because a rig (or an overlay) is copied and renamed to make a second environment, and both the cluster name and the host port follow the folder. A checked-in literal would make every copy collide on both — which is exactly why every other project here, with its literal kind-config.yaml, has only one of itself. ctrl/cluster.sh renders it with sed — not envsubst, which is gettext-base and absent from a minimal Debian, and rig's whole premise is that Docker is the only prerequisite.
A kind config is fixed at creation: to change the cluster, edit the file, then `make cluster reset`. lib/config.sh reads the node count back out of it, so nothing restates it.
## An overlay's own kind config
An overlay may carry its own `kind-config.yaml.tpl` (see [overlay.md](overlay.md)); it replaces this whole file, rendered the same way, so start from a copy of this one. Keep the containerd `config_path` patch: `make check` reports its absence whenever a registry mode needs it. A project that builds its own cluster through rig can also pass any file as `KIND_CONFIG=<path>`.
## Substituted variables
Substituted by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR. The header comment names them without the `${...}` braces so that line survives the substitution.
Substituted by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR (rig's folder), OVERLAY_DIR (the overlay's folder, for mounts). The header comment names them without the `${...}` braces so that line survives the substitution.
## Node count

171
rig/docs/notes/overlay.md Normal file
View File

@@ -0,0 +1,171 @@
# Overlays: what runs lives outside rig
## Why
rig is the machine: the toolchain, the cluster, the registry, the port block, the
dev loop's plumbing. What runs on it — the services, their manifests, their
images, their settings — belongs to whoever owns that work, changes at a
different rate, and often cannot be shared at all. Keeping both in one tree meant
editing rig's own files to use it, and then carrying those edits into every copy.
So the use case is one folder, the **overlay**, kept outside rig's version
control. rig reads it; rig never writes into it and never knows what is in it.
The dependency points one way: an overlay knows about rig, rig knows about
overlays in general and about none in particular.
## Where an overlay lives
```
rig/local/<name>/ gitignored by rig: an overlay with no version control of its own,
or a clone of its own repo
anywhere/<name>/ a repo of its own, named by path
<project>/ a project folder that carries rig at <project>/rig/ (the vendored
layout, below)
```
Name it with `OVERLAY` — in `ctrl/.env` for this machine, or per call:
```bash
OVERLAY=local/myenv make cluster up
```
Relative paths are relative to rig's folder. With none named, rig uses its own
`examples/starter`, which sets nothing, so a plain rig behaves as it always did.
An overlay that is named and missing is an error; it never falls back.
## What rig reads from it
Every piece is optional.
| in the overlay | what rig does with it |
| --- | --- |
| `rig.env` | a config layer (below). Any key a profile could set. |
| `k8s/overlays/dev/` | the default `MANIFESTS_DIR`: rig's Tiltfile applies it with kustomize |
| `kind-config.yaml.tpl` | the default `KIND_CONFIG`: the cluster's shape, rendered like rig's own |
| `addons/<name>.sh` | an addon, found before rig's `ctrl/addons/<name>.sh` of the same name |
| `Tiltfile` | the workload's half of the dev loop, included by rig's `ctrl/Tiltfile` |
Anything else in the folder is the overlay's own business: Dockerfiles, DAGs,
folders of repos or data it mounts, its `.gitignore`, the secrets its kustomize
generators read. rig does not look.
## Layers
```
built-in defaults < ctrl/versions.env < ctrl/env.d/<profile>.env < <overlay>/rig.env < ctrl/.env < the caller
```
A profile says how this machine reaches the world (a registry mirror, an
air-gapped install); an overlay says what runs. `ctrl/.env` is still this
machine's, and the caller still wins over everything.
`rig.env` may not set `PROFILE` or `OVERLAY`: both are chosen before it loads.
The paths it sets (`MANIFESTS_DIR`, `KIND_CONFIG`) are relative to the overlay.
`MANIFESTS_DIR=none` means rig applies no manifests and the overlay's Tiltfile
does, e.g. when kustomize needs flags.
## Identity
With an overlay named, the cluster, the kubectl context and the port block
follow the overlay folder's name, sanitised the same way a rig folder's name is.
One rig can therefore serve several overlays, each in its own cluster, and a
project that carries rig at `./rig` does not name every cluster `rig`.
`ports.sh persist` refuses while an overlay is set: it writes to rig's
`ctrl/.env`, and a pin there would follow every overlay.
## The Tiltfile handoff
rig's `ctrl/Tiltfile` does rig's part — the context guard, `default_registry`,
the manifests, the namespaces they use — then publishes the facts and includes
the overlay's `Tiltfile`:
```
RIG_CLUSTER RIG_CONTEXT RIG_HTTP_PORT RIG_HTTPS_PORT RIG_TILT_PORT RIG_REGISTRY RIG_OVERLAY_DIR
```
Read them with `os.getenv`. An included Tiltfile runs from its own folder, so
every relative path in it (`docker_build` contexts, `sync`, `deps`, `local`) is
relative to the overlay — it never needs a path back into rig.
## Addons
An addon is a bash script run by `ctrl/addons.sh` from rig's `ctrl/`, with
`RIG_CTRL` exported. It starts like this, sources the config and does its work:
```bash
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
source ./lib/config.sh
load_config
```
`OVERLAY_DIR` is set, so an addon can find files beside it
(`$(_from_ctrl "$OVERLAY_DIR")/...`). rig's own `ctrl/addons/` holds only what
makes a cluster work (metallb, cert-manager, metrics-server);
`examples/data/addons/` shows workload ones.
## The kind config
An overlay's `kind-config.yaml.tpl` replaces rig's whole file, so start from a
copy of `ctrl/k8s/kind-config.yaml.tpl` and keep its containerd `config_path`
patch: `registry.sh` needs it, and `make check` says so when it is missing.
`${OVERLAY_DIR}` renders to the overlay's absolute path, for mounts:
```yaml
extraMounts:
- hostPath: ${OVERLAY_DIR}/datadir
containerPath: /rig/datadir
```
A kind config is fixed when the cluster is created: after changing it,
`make cluster reset`.
## The vendored layout
A project folder can carry rig inside it and be the overlay itself:
```
<project>/
Makefile the forwarder below
rig.env k8s/ Tiltfile kind-config.yaml.tpl addons/ ...
rig/ rig, placed as it is; tracked or ignored by the project, its call
```
The forwarder runs rig with `OVERLAY` set to this folder. It passes `OVERLAY` in
the environment, not as a make argument, so rig's own `$(shell ...)` sees it
under make 4.3 as well:
```make
HERE := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
ifeq ($(wildcard $(HERE)/rig/Makefile),)
$(error rig/ is missingthis folder is an overlay; put rig in ./rig)
endif
GOALS := $(or $(MAKECMDGOALS),help)
.PHONY: $(GOALS)
$(firstword $(GOALS)):
@OVERLAY='$(HERE)' $(MAKE) --no-print-directory -C '$(HERE)/rig' $(GOALS)
$(wordlist 2,$(words $(GOALS)),$(GOALS)):
@:
```
The cluster is then named after `<project>`, exactly as a copied rig named
`<project>` was, so moving a copied rig to this layout keeps its cluster and ports.
`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

@@ -20,12 +20,18 @@ Derivation is a default, not a decision. On first use the resolved block is writ
The resolved facts a consumer outside bash needs, machine-readable:
```
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
```
Identity and ports together, because they are one fact set: both derive from the directory name so that copies never collide. A consumer needs all of them or none, and fetching them separately is how two end up disagreeing. MANIFESTS_DIR rides along because the one consumer that needs the addressing is the one that needs to know what to deploy.
Identity and ports together, because they are one fact set: both derive from a folder name (the overlay's when one is named) so that copies never collide. A consumer needs all of them or none, and fetching them separately is how two end up disagreeing. MANIFESTS_DIR and OVERLAY_DIR ride along because the one consumer that needs the addressing is the one that needs to know what to deploy and whose Tiltfile to include.
Space-separated, so MANIFESTS_DIR must not contain spaces. Everything else in rig already assumes that of paths; kind, docker and kubectl all do.
The two paths are absolute, or `-` when there is none: an empty field would shift every later one. OVERLAY_DIR was appended rather than inserted, so readers that take fields by position kept their indexes.
Space-separated, so the paths must not contain whitespace; `active` refuses rather than print a line that splits wrong. Everything else in rig already assumes that of paths; kind, docker and kubectl all do.
## persist, with an overlay
`persist` writes into ctrl/.env, which belongs to this rig, not to an overlay. With OVERLAY set, a block pinned there would follow every overlay this rig later runs, and two of them would then share ports — the collision the derivation exists to prevent. So it refuses and says so; an overlay's ports stay derived from its folder name.
`derive` answers a DIFFERENT question (what the directory name alone implies) and deliberately ignores ctrl/.env. Configuring anything from it would silently contradict the rule that "anything already in ctrl/.env wins". `active` is what anything downstream should read.

View File

@@ -18,7 +18,7 @@ rig assumes no configuration. A profile is an overlay on built-in defaults, so a
## 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]..[6]`. 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.
`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
@@ -44,7 +44,21 @@ Not a change-detector. The block is derived, never stored, so if the derivation
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.
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
@@ -62,6 +76,16 @@ Each kit's Makefile exists so nothing wrapping these scripts has to GUESS how to
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.
## optional: Tiltfile evaluates
## 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
Parsing the Tiltfile for real is the only way to know it still evaluates, but Tilt snapshots a kubectl context before parsing, so it cannot run without a cluster. Skipped rather than failed when there is none, the same way docgen skips its graphgen section.
`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.

View File

@@ -34,8 +34,21 @@ The distro docker packages ship the daemon and the CLI but frequently not this,
## Node images
Node images shipped with `KIND_VERSION`, pinned by digest so a kind upgrade can never silently move the k8s version. Profiles select one via `K8S_VERSION`. Older entries are kept deliberately: running a trailing-edge control plane is part of simulating a legacy estate.
Node images shipped with `KIND_VERSION`, pinned by digest so a kind upgrade can never silently move the k8s version. `K8S_VERSION` selects one (a profile or an overlay's rig.env may set it; the default is the newest pinned). Older entries are kept deliberately, for targets that run an older Kubernetes.
## Cabinets
## Workload images are not pinned here
Public services dropped in as-is, the upstream image unmodified. The same declaration installs on compose or in the cluster, so a dependency is named once and works either way. Pinned by tag rather than digest because they are ordinary upstream images with no supply chain claim attached — bump freely, and preload them for the offline profile.
What an overlay runs is pinned by the overlay: `examples/data/rig.env` carries its postgres, redis and airflow images. This file holds what rig itself needs — the toolchain, the node images, the registry and rig's own addons — so it never says what any particular environment runs.
## The addons' manifests
metallb, cert-manager and metrics-server are installed from their upstream manifests. Those are pinned here by URL and SHA256 like the binaries, fetched through the same `DEPS_SOURCE` resolver (upstream, artifactory, baked) by `deps.sh manifest <NAME>`, verified, and applied from `vendor/manifests/` — never a URL applied directly. That is what lets the offline example profile install its addons with no network: the deps-full image carries them.
cert-manager and metrics-server publish their manifests as release assets, and GitHub reports each asset's SHA256 (`digest` in the release API); those are the pinned sums. metallb does not: its manifest is a file in the repository at the release tag, with no published sum. Its pin was taken from a download whose git blob id matched the one GitHub serves for `config/manifests/metallb-native.yaml` at that tag, and the blob id is kept beside it (`METALLB_MANIFEST_GIT_BLOB`) so the next bump is checked the same way:
```
curl -sSL "https://api.github.com/repos/metallb/metallb/contents/config/manifests/metallb-native.yaml?ref=<tag>" | jq -r .sha
(printf 'blob %d\0' "$(wc -c < metallb-native.yaml)"; cat metallb-native.yaml) | sha1sum
```
Bumping an addon's version means bumping its manifest sum in the same edit; `deps.sh manifest` refuses a mismatch.