Compare commits
2 Commits
rig-0.1.0
...
f4b2c15a9f
| Author | SHA1 | Date | |
|---|---|---|---|
| f4b2c15a9f | |||
|
|
aeeb26f4e9 |
@@ -14,7 +14,7 @@
|
||||
# KIND_CONFIG spr's own kind config, which maps the rooms' gateway NodePorts
|
||||
# REGISTRY_MODE=none rooms load images straight into the node
|
||||
# PROFILE= ADDONS= set empty here, so rig's own ctrl/.env can never quietly
|
||||
# OVERLAY= pick a profile, an overlay or addons for spr's cluster
|
||||
# pick a profile or add addons to spr's cluster
|
||||
#
|
||||
# status stays here: it answers a question about rooms, not about the cluster.
|
||||
set -e
|
||||
@@ -27,7 +27,6 @@ rig() {
|
||||
KIND_CONFIG="$SCRIPT_DIR/k8s/kind-config.yaml" \
|
||||
REGISTRY_MODE=none \
|
||||
PROFILE= \
|
||||
OVERLAY= \
|
||||
ADDONS= \
|
||||
bash "$RIG_CTRL/cluster.sh" "$@"
|
||||
}
|
||||
|
||||
14
rig/.gitignore
vendored
14
rig/.gitignore
vendored
@@ -16,14 +16,6 @@ arch/*.dot
|
||||
# binaries pulled by `make deps-bundle` for the air-gapped installer image
|
||||
vendor
|
||||
|
||||
# Overlays that live with no version control of their own, or clones of their own
|
||||
# repos: rig reads them and never tracks them (docs/notes/overlay.md).
|
||||
/local/
|
||||
|
||||
# A profile you activated (cp env.d/<name>.env.example env.d/<name>.env) is this
|
||||
# machine's choice; only the examples are committed.
|
||||
ctrl/env.d/*.env
|
||||
|
||||
# Only the default kit is committed; a kit for a local profile is this machine's.
|
||||
standalone/*
|
||||
!standalone/default/
|
||||
# Client rigs are NOT ignored here. A copy is a SIBLING of this directory
|
||||
# (../acme-rig), so a rule in this file cannot see it — the rules live in the
|
||||
# parent repo's .gitignore, anchored at its root, where `*-rig/` matches them.
|
||||
|
||||
112
rig/BOOTSTRAP.md
112
rig/BOOTSTRAP.md
@@ -1,9 +1,18 @@
|
||||
# From a machine with nothing on it to an environment you can work in
|
||||
# From a machine with nothing on it to a project you can work in
|
||||
|
||||
The README says the prerequisite is Docker and nothing else. This is what that
|
||||
actually looks like end to end: a bare Linux box, and an overlay running under
|
||||
actually looks like end to end: a bare Linux box, and a new project running under
|
||||
Tilt at the end of it.
|
||||
|
||||
A copy of this directory is a sibling of it, named after the environment it
|
||||
models (`acme-rig`). Paths below are relative to the parent checkout.
|
||||
|
||||
It spans three repos because the work does. **rig** prepares the machine — the
|
||||
pinned toolchain, the cluster, the port arithmetic. **all** owns the shape a
|
||||
project takes, in `all/projects/templates/conventions.md` and the `broad`
|
||||
scaffold beside it. **ppl** owns everything after local, and is where this
|
||||
document stops.
|
||||
|
||||
Read it once before running anything. Three of the steps below need root and one
|
||||
needs a logout, so knowing about them in advance is cheaper than meeting them
|
||||
halfway through.
|
||||
@@ -99,7 +108,7 @@ docker run --rm \
|
||||
```
|
||||
|
||||
The image name follows the directory, like everything else here: in `rig`
|
||||
it is `rig-deps`, in a copy called `other-rig` it is `other-rig-deps`. The
|
||||
it is `rig-deps`, in a copy called `acme-rig` it is `acme-rig-deps`. The
|
||||
tag is `deps` (or `full`, below), not `latest`.
|
||||
|
||||
None of the four arguments are guessable, so:
|
||||
@@ -143,10 +152,6 @@ From here on this machine has curl, so **`make deps` is the short form** for
|
||||
every later run and every later copy of this directory. The container path is
|
||||
the first-time path.
|
||||
|
||||
How the installer itself is tested — machine fixtures, clean containers, a
|
||||
Workspace snapshot, a throwaway WSL distro — is in
|
||||
[`docs/notes/installer-testing.md`](docs/notes/installer-testing.md).
|
||||
|
||||
|
||||
## Prove the machine before blaming the project
|
||||
|
||||
@@ -178,30 +183,80 @@ worth reading before rather than after. `make cluster free <names>` stops
|
||||
clusters without deleting them; `docker start` brings them back untouched.
|
||||
|
||||
|
||||
## Start an overlay
|
||||
## Scaffold the project
|
||||
|
||||
The canonical layout is [`all/projects/templates/conventions.md`](../all/projects/templates/conventions.md).
|
||||
Read it — it is short, opinionated, and exists precisely so nobody
|
||||
reverse-engineers a layout from whichever repo they happened to open. What
|
||||
follows is only the mechanical part.
|
||||
|
||||
```bash
|
||||
SLUG=<slug> # short, lowercase, no separators
|
||||
cp -r ~/wdir/semester/all/projects/templates/broad ~/wdir/semester/"$SLUG"
|
||||
cd ~/wdir/semester/"$SLUG"
|
||||
grep -rl '<slug>' ctrl | xargs sed -i "s/<slug>/$SLUG/g"
|
||||
cp ctrl/k8s/.env.example ctrl/k8s/.env
|
||||
git init && git add -A && git commit -m "scaffold $SLUG from broad"
|
||||
```
|
||||
|
||||
`<slug>` is the only placeholder and it lives only under `ctrl/` — cluster name,
|
||||
namespace, ConfigMap name, and the `NAME=` in `kind-up.sh` / `kind-down.sh`. One
|
||||
sed does all of it.
|
||||
|
||||
What runs lives outside rig, in an overlay — see
|
||||
[`docs/notes/overlay.md`](docs/notes/overlay.md). Start from rig's own:
|
||||
The slug is the folder name, lowercase and short — `mpr`, `unt`, `nvi`. The
|
||||
cluster takes that name and the context becomes `kind-<slug>`, derived by the
|
||||
scaffold's Makefile from the directory, so there is nothing to edit for either.
|
||||
|
||||
**Pick the Tilt port deliberately.** `ctrl/k8s/.env.example` ships a value that
|
||||
is already in use, so copying it unchanged puts two projects on one port:
|
||||
|
||||
```bash
|
||||
cp -r examples/starter local/myenv # local/ is gitignored by rig
|
||||
echo 'OVERLAY=local/myenv' >> ctrl/.env
|
||||
make check # shows the overlay, its cluster and ports
|
||||
grep -h '^TILT_PORT=' ~/wdir/semester/*/ctrl/k8s/.env 2>/dev/null | sort
|
||||
```
|
||||
|
||||
Choose a free one in `10300–10399` — the range ALL reserves in
|
||||
`projects/index.json` under `policy` — avoiding `10350`, which is Tilt's own
|
||||
default. Currently taken: `nvi` 10330, `unt` 10340, `mpr` 10360, `mlv` 10370,
|
||||
`eth` 10380, `lng` 10390. This is the Tilt *web UI* port, not a service port;
|
||||
each project owns its own service ports separately. The scaffold ships it blank
|
||||
on purpose, so there is nothing to collide with until you choose.
|
||||
|
||||
The scaffold's `ctrl/k8s/` is the same shape as every other project here, and it
|
||||
builds as shipped:
|
||||
|
||||
```
|
||||
kind-config.yaml one node; gateway NodePort 30080 -> hostPort 8080
|
||||
base/ namespace, configmap, app (Deployment + Service)
|
||||
overlays/dev/ promotes the app Service to NodePort 30080
|
||||
```
|
||||
|
||||
The cluster takes the overlay's folder name and the context becomes
|
||||
`kind-<name>`, so there is nothing to edit for either. Replace the two example
|
||||
components under `k8s/base/`, and add your images and resources to the
|
||||
overlay's `Tiltfile`. Check the manifests before `kind` spends minutes on anything
|
||||
— this renders the whole tree without a cluster and catches a broken patch
|
||||
immediately:
|
||||
Check it before `kind` spends minutes on anything — this renders the whole tree
|
||||
without a cluster and catches a broken patch immediately:
|
||||
|
||||
```bash
|
||||
kubectl kustomize local/myenv/k8s/overlays/dev
|
||||
kubectl kustomize ctrl/k8s/overlays/dev
|
||||
```
|
||||
|
||||
If the overlay is to be versioned, make `local/myenv` a repository of its own (rig
|
||||
never tracks it), or keep it anywhere else and name it by path.
|
||||
The workload is an nginx placeholder so a fresh copy reaches something that
|
||||
answers; replace it. Keep `30080` in step between the overlay patch and
|
||||
`kind-config.yaml`'s `containerPort` — the hostPort is this project's to pick.
|
||||
Reachability is a plain kind port mapping: no ingress controller and no MetalLB.
|
||||
Caddy maps `<slug>.local.ar` onto the host port (`~/wdir/semester/ppl/local/Caddyfile`),
|
||||
with `*.local.ar` resolving to 127.0.0.1 through dnsmasq. That is the whole chain.
|
||||
|
||||
**For `ctrl/Tiltfile`, copy rig's** rather than a live project's. rig ships one
|
||||
that derives its cluster, context, ports and manifest directory from
|
||||
`ctrl/ports.sh active` instead of hardcoding a slug, and carries a catalogue of
|
||||
the blocks every project here ends up needing. Copying from `unt` or `nvi` is
|
||||
what the estate did until now, and it is why the same Tiltfile preamble exists
|
||||
in six places with the slug typed in by hand five times each.
|
||||
|
||||
> **Two things in this document disagree with rig and are not settled.** It
|
||||
> mandates Tilt ports in `10300–10399`, while rig derives a block from the
|
||||
> directory name at `20000+` so copies cannot collide — a rig-managed project
|
||||
> takes rig's. And it names `ctrl/k8s/.env.example`, which is the `broad`
|
||||
> scaffold's layout; rig's is `ctrl/.env.example`. Both are this document
|
||||
> describing the house scaffold from inside rig's tree.
|
||||
|
||||
|
||||
## Run it
|
||||
@@ -211,9 +266,20 @@ make kind-up # idempotent create, then selects the context
|
||||
make tilt-up # context + your assigned port
|
||||
```
|
||||
|
||||
`tilt-up` passes `--context kind-<name>` every time, which is the point of going
|
||||
`tilt-up` passes `--context kind-<slug>` 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
|
||||
delete-and-recreate for when a cluster wedges.
|
||||
|
||||
|
||||
## Register it
|
||||
|
||||
The project exists; now it is findable. Add an entry to
|
||||
`~/wdir/semester/all/projects/index.json` and write its `projects/<slug>.md` beside the
|
||||
others. Structured fields in the index, prose in the markdown.
|
||||
|
||||
Putting it on the CI server and deploying it is `ppl`'s half, and it starts at
|
||||
`~/wdir/semester/ppl/ctrl/init-repo.sh` — gitea remote, then Woodpecker. That is a
|
||||
different document.
|
||||
|
||||
10
rig/Makefile
10
rig/Makefile
@@ -4,10 +4,8 @@
|
||||
# Notes: docs/notes/Makefile.md
|
||||
|
||||
# Identity and ports, asked once of ctrl/ports.sh, read positionally (selftest pins the order):
|
||||
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
|
||||
# OVERLAY/CLUSTER given as make arguments are handed over explicitly: before make 4.4,
|
||||
# $(shell) does not see them, and the context would follow the wrong environment.
|
||||
FACTS := $(shell $(if $(OVERLAY),OVERLAY='$(OVERLAY)') $(if $(CLUSTER),CLUSTER='$(CLUSTER)') bash ctrl/ports.sh active 2>/dev/null)
|
||||
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
||||
FACTS := $(shell bash ctrl/ports.sh active 2>/dev/null)
|
||||
SLUG := $(shell echo '$(notdir $(CURDIR))' | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/^-*//; s/-*$$//')
|
||||
# Fall back to the folder name, not empty, if ports.sh fails on a broken config.
|
||||
CLUSTER := $(or $(word 1,$(FACTS)),$(SLUG))
|
||||
@@ -70,8 +68,8 @@ tilt: ## dev loop [up|down] (default
|
||||
# The counterpart to check: that one asks about the MACHINE and never fails,
|
||||
# this one asks about RIG and exits 1. Checks are written as the decisions they
|
||||
# defend, so a failure names what is being undone.
|
||||
selftest: ## does rig still do what it says? exits 1 if not [install]
|
||||
bash ctrl/selftest.sh $(ARGS)
|
||||
selftest: ## does rig still do what it says? exits 1 if not
|
||||
bash ctrl/selftest.sh
|
||||
|
||||
# The one-file versions of rig's tools, one folder per profile, for machines the
|
||||
# full rig is not going to. Generated from rig as it is, never edited by hand;
|
||||
|
||||
122
rig/README.md
122
rig/README.md
@@ -100,101 +100,107 @@ 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.
|
||||
**`make tilt` works on a fresh copy, unedited.** rig ships `ctrl/Tiltfile`, and
|
||||
`k8s/base` already boots, so the dev loop comes up with the two examples running
|
||||
and nothing to configure first.
|
||||
|
||||
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.
|
||||
cluster name, kube context, ports and manifest directory — the same values every
|
||||
other rig script resolves through `ctrl/lib/config.sh` — so a copied and renamed
|
||||
rig deploys into its own cluster with no edits. Every other project here writes
|
||||
its slug into the Tiltfile five or six times by hand, which is exactly the
|
||||
collision `kind-config.yaml.tpl` exists to avoid.
|
||||
|
||||
`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)).
|
||||
What it deploys is `MANIFESTS_DIR`, defaulting to rig's own `ctrl/k8s/overlays/dev`.
|
||||
Point that at an overlay versioned elsewhere and rig stops owning the manifests.
|
||||
|
||||
Replace the examples, then add your images and resources in the two marked
|
||||
sections. The catalogue below them holds the blocks that recur across every
|
||||
project here — `docker_build`, resource ordering, gateway reload, port-forwards —
|
||||
with the parts that are easy to get wrong already commented.
|
||||
|
||||
`make help` lists every target.
|
||||
|
||||
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).
|
||||
|
||||
```bash
|
||||
cp -r examples/starter local/myenv # local/ is gitignored
|
||||
OVERLAY=local/myenv make cluster up # or OVERLAY=local/myenv in ctrl/.env
|
||||
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.
|
||||
## One directory is one environment
|
||||
|
||||
## One environment per folder
|
||||
Copy this directory, rename it, run it. Cluster name, kubectl context, image
|
||||
tags and the host port block all derive from the directory name, so copies never
|
||||
collide and neither one's teardown can touch the other.
|
||||
|
||||
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.
|
||||
A copy of this directory is a **sibling** of it, named after the environment it
|
||||
models (`acme-rig`). That is why the ignore rules for copies sit in the *parent*
|
||||
repo's `.gitignore` rather than here: a rule in this directory cannot see a
|
||||
directory beside it.
|
||||
|
||||
## Profiles
|
||||
|
||||
**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):
|
||||
an optional overlay — a file in `ctrl/env.d/`, named by `PROFILE` — for when you
|
||||
want different addons or registry.
|
||||
|
||||
rig ships **examples**, not active profiles, because each one is a use case rather
|
||||
than something every rig needs. Copy one to use it:
|
||||
|
||||
| example | what it changes |
|
||||
| --- | --- |
|
||||
| `mirror.env.example` | images through a pull-through cache of an internal registry |
|
||||
| `client.env.example` | images through a mirror of a corporate registry |
|
||||
| `offline.env.example` | air-gapped: everything from a preloaded local registry |
|
||||
| `data.env.example` | databases and a scheduler: postgres, redis, airflow |
|
||||
|
||||
```bash
|
||||
cp ctrl/env.d/mirror.env.example ctrl/env.d/mirror.env
|
||||
PROFILE=mirror make cluster up
|
||||
cp ctrl/env.d/data.env.example ctrl/env.d/data.env
|
||||
PROFILE=data make cluster up # installs the profile's addons too
|
||||
PROFILE=data make check # what the profile wants, and what exists
|
||||
```
|
||||
|
||||
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.
|
||||
The **cluster itself** is one file, `ctrl/k8s/kind-config.yaml.tpl` (one node).
|
||||
To change it — more nodes, other port mappings — edit it and `make cluster
|
||||
reset`. The node count is read back out of it, so there is nothing to drift. The layout under `ctrl/k8s/` is the
|
||||
same as every other project here — a kind config, a kustomize `base/`, an
|
||||
`overlays/dev/` — see [`ctrl/k8s/README.md`](ctrl/k8s/README.md).
|
||||
|
||||
## 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.
|
||||
Each addon is its own idempotent script in `ctrl/addons/`, and a profile names
|
||||
the ones it wants in `ADDONS`. Adding one is adding a file — there is no
|
||||
dispatcher to edit.
|
||||
|
||||
**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:
|
||||
built with — and running a trailing-edge control plane to model a legacy estate
|
||||
is the whole point. Services are reached through MetalLB and
|
||||
`type: LoadBalancer`, which carries no such constraint and is also what a real
|
||||
cluster does.
|
||||
|
||||
| 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 |
|
||||
| `postgres` | database, in the `data` namespace |
|
||||
| `redis` | cache and broker |
|
||||
| `airflow` | scheduled pipelines; needs postgres and redis |
|
||||
|
||||
The last three are **cabinets**: a public service dropped in as-is, the upstream
|
||||
image unmodified, reachable at a known address. A cabinet is declared once and
|
||||
installs on either target — a `service.yml` composes it for a laptop, and these
|
||||
install the same one here. The names match on purpose: each cabinet carries a
|
||||
`rig_addon` field pointing at `ctrl/addons/<name>.sh`.
|
||||
|
||||
Plain manifests rather than helm charts, like every other addon: a chart repo is
|
||||
a network dependency, and the `offline` profile exists precisely so there is a
|
||||
path with none. Images are pinned in `ctrl/versions.env` and can be preloaded.
|
||||
|
||||
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:
|
||||
Passwords are generated on first install and kept across re-runs, so re-running
|
||||
an addon never rotates a credential out from under something already connected:
|
||||
|
||||
```bash
|
||||
OVERLAY=examples/data make cluster up
|
||||
kubectl -n data get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d
|
||||
kubectl -n data port-forward svc/airflow 8080:8080
|
||||
```
|
||||
|
||||
68
rig/STALE.md
68
rig/STALE.md
@@ -1,68 +0,0 @@
|
||||
# rig — withdrawn assumptions
|
||||
|
||||
**Everything in this file is no longer true.**
|
||||
|
||||
It exists so the live docs stay short and a withdrawn assumption cannot quietly
|
||||
return: each entry carries a **check**, and `ctrl/selftest.sh` runs every one of
|
||||
them under "withdrawn stays withdrawn". A retraction that is only prose is one
|
||||
nobody re-reads.
|
||||
|
||||
- **Do not restate these** in a plan, a README or a comment; point here (`✖ S2`).
|
||||
- **Ids are stable.**
|
||||
- **Only withdrawn things belong here.** A warning that is still actionable is a
|
||||
live rule and stays where it is.
|
||||
|
||||
---
|
||||
|
||||
**✖ S1 — "rig supplies `ctrl/Tiltfile` but does not own it: replace the examples and
|
||||
add your images in its marked sections."** *(ctrl/Tiltfile, README, 2026-09-13)*
|
||||
Withdrawn 2026-09-22. Every project that used rig edited rig's own file, so no
|
||||
update to rig could land without merging those edits by hand. rig's `ctrl/Tiltfile`
|
||||
is now rig's: identity, context guard, registry, manifests, namespaces. The
|
||||
workload's half is the overlay's own `Tiltfile`, which rig's includes.
|
||||
**Check:** `ctrl/Tiltfile` includes the overlay's Tiltfile and has no "Images" section
|
||||
of its own.
|
||||
|
||||
**✖ S2 — "A second environment is a copy of rig renamed after it (`../<name>-rig`), and
|
||||
its use case is written into the copy."** *(README, BOOTSTRAP, .gitignore, 2026-08)*
|
||||
Withdrawn 2026-09-22. A use case is an overlay, kept outside rig's version control
|
||||
(`docs/notes/overlay.md`); rig itself is replaced as a whole. Copying rig still gives
|
||||
a separate environment, but carries no use case of its own.
|
||||
**Check:** rig's `.gitignore` ignores `/local/`; no `acme` example name remains in rig.
|
||||
|
||||
**✖ S3 — "rig's addons include the services a workload needs (a database, a cache, a
|
||||
scheduler), described in the host project's vocabulary."** *(ctrl/addons/, README,
|
||||
versions.env, 2026-08)* Withdrawn 2026-09-22. Which services a workload needs is
|
||||
not rig's business. `ctrl/addons/` holds what makes a cluster work; workload addons
|
||||
live with overlays, and `examples/data` carries them as an example.
|
||||
**Check:** `ctrl/addons/` holds exactly cert-manager, metallb and metrics-server;
|
||||
`versions.env` pins no workload image.
|
||||
|
||||
**✖ S4 — "The dev loop's namespace is named after the cluster."** *(ctrl/Tiltfile,
|
||||
2026-09-13)* Withdrawn 2026-09-22. The Tiltfile created and grouped
|
||||
`<CLUSTER>:namespace` while the example manifests declared `rig`, so every copy not
|
||||
named `rig` stopped at load: `No object identified by the fragment
|
||||
"acme-rig:namespace"`. The first real use worked around it with a Namespace named
|
||||
`to_be_replaced` that its overlay renamed. The Tiltfile now creates the namespaces
|
||||
the manifests use and groups the ones they declare.
|
||||
**Check:** `ctrl/Tiltfile` does not build a namespace name from `CLUSTER`.
|
||||
|
||||
**✖ S5 — "`MANIFESTS_DIR` defaults to `ctrl/k8s/overlays/dev`, rig's own examples."**
|
||||
*(lib/config.sh, .env.example, 2026-09-13)* Withdrawn 2026-09-22. The default is the
|
||||
overlay's `k8s/overlays/dev`; rig's examples are `examples/starter`. The old value,
|
||||
still pinned by older `.env` files, is ignored while that folder does not exist, and
|
||||
`make check` says to delete it.
|
||||
**Check:** `ctrl/k8s/overlays` does not exist; `.env.example` does not set
|
||||
`MANIFESTS_DIR`.
|
||||
|
||||
**✖ S6 — "The example profiles are `client`, `data` and `offline`."** *(ctrl/env.d/,
|
||||
2026-09-17)* Withdrawn 2026-09-22. "client" names the customer, not a registry
|
||||
mode: the example is `mirror`. `data` was a workload, not a way of reaching the
|
||||
world: it is the `examples/data` overlay.
|
||||
**Check:** `ctrl/env.d/` holds no `client` or `data` example.
|
||||
|
||||
**✖ S7 — "BOOTSTRAP spans three repos: rig prepares the machine, the house repos own the
|
||||
project's shape and everything after local."** *(BOOTSTRAP.md, 2026-08)* Withdrawn
|
||||
2026-09-22. rig's docs describe rig. The house scaffold and registration sections
|
||||
moved out of rig; BOOTSTRAP now ends with starting an overlay.
|
||||
**Check:** no house path or host name (`semester`, `local.ar`) in rig.
|
||||
@@ -1 +0,0 @@
|
||||
0.1.0
|
||||
@@ -6,11 +6,6 @@
|
||||
# need no profile at all. Copy an env.d/*.env.example to <name>.env to add one.
|
||||
PROFILE=
|
||||
|
||||
# The overlay: one folder, outside rig's version control, holding what runs —
|
||||
# its settings (rig.env), manifests, addons, Tiltfile. Relative to rig's folder,
|
||||
# or absolute. Unset: rig's own examples/starter. See docs/notes/overlay.md.
|
||||
# OVERLAY=local/<name>
|
||||
|
||||
# Cluster name; the kubectl context becomes kind-<CLUSTER>.
|
||||
# LEAVE UNSET: it defaults to this folder's name, which keeps the folder copyable.
|
||||
# CLUSTER=
|
||||
@@ -22,8 +17,9 @@ PROFILE=
|
||||
# TILT_PORT=
|
||||
# REGISTRY_PORT=
|
||||
|
||||
# Where the manifests live. Leave unset: the overlay's k8s/overlays/dev.
|
||||
# Where the application manifests live; repoint at their own repo, e.g.
|
||||
# MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
MANIFESTS_DIR=ctrl/k8s/overlays/dev
|
||||
|
||||
# Where the installer fetches the pinned binaries from:
|
||||
# upstream (needs internet) | artifactory (generic repo) | baked (in the image)
|
||||
|
||||
@@ -27,10 +27,8 @@ CMD ["install"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# deps-full — same image, binaries and the addons' manifests baked in, works with no network.
|
||||
# deps-full — same image, binaries baked in, works with no network at all.
|
||||
FROM deps AS deps-full
|
||||
RUN /work/rigdeps.sh fetch --to /opt/rig/bin \
|
||||
&& /work/rigdeps.sh manifests --to /opt/rig/manifests
|
||||
RUN /work/rigdeps.sh fetch --to /opt/rig/bin
|
||||
ENV DEPS_SOURCE=baked \
|
||||
BAKED_BIN=/opt/rig/bin \
|
||||
BAKED_MANIFESTS=/opt/rig/manifests
|
||||
BAKED_BIN=/opt/rig/bin
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# EXAMPLE component image. Copy, rename, replace:
|
||||
# Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
|
||||
# COPY paths are relative to the build context the overlay's Tiltfile names (context='.').
|
||||
# Notes: rig's docs/notes/Dockerfile.example.md
|
||||
# ctrl/Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
|
||||
# COPY paths are relative to the REPO ROOT (Tilt context='..'), not this directory.
|
||||
# Notes: docs/notes/Dockerfile.example.md
|
||||
|
||||
FROM python:3.12-slim
|
||||
|
||||
@@ -11,7 +11,7 @@ WORKDIR /app
|
||||
COPY api/requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Context-relative — see above.
|
||||
# Repo-root relative — see above.
|
||||
COPY api/ ./api/
|
||||
|
||||
# Match this with the containerPort in the manifest and the target of the
|
||||
@@ -20,5 +20,5 @@ EXPOSE 8000
|
||||
|
||||
CMD ["python", "-m", "api"]
|
||||
|
||||
# live_update: the Tiltfile's sync('api', '/app/api') must match COPY api/ + WORKDIR /app,
|
||||
# live_update: Tiltfile's sync('../api', '/app/api') must match COPY api/ + WORKDIR /app,
|
||||
# or edits silently do nothing.
|
||||
@@ -1,6 +1,6 @@
|
||||
# The dev loop, rig's half. `make tilt` from rig's folder, or from an overlay's forwarder.
|
||||
# rig owns this file: who we are, the context guard, the registry, the overlay's manifests.
|
||||
# The workload's half is the overlay's own Tiltfile, included at the end; edit that one.
|
||||
# The dev loop. `make tilt` from the project root, or `cd ctrl && tilt up`.
|
||||
# Works unedited; replace the two EXAMPLES and add yours in the marked sections.
|
||||
# rig supplies this file but does not own it, and nothing here names this directory.
|
||||
# Notes: docs/notes/Tiltfile.md
|
||||
|
||||
# ── who we are, and on which ports ─────────────────────────────────────────
|
||||
@@ -12,9 +12,10 @@ HTTP = _facts[2]
|
||||
HTTPS = _facts[3]
|
||||
TILT = _facts[4]
|
||||
REGISTRY = _facts[5]
|
||||
# Absolute paths, or '-' when there is none.
|
||||
MANIFESTS = '' if _facts[6] == '-' else _facts[6]
|
||||
OVERLAY = '' if _facts[7] == '-' else _facts[7]
|
||||
|
||||
# Where the manifests live (MANIFESTS_DIR, see k8s/README.md).
|
||||
# The value is REPO-ROOT relative and this file runs in ctrl/, so prefix '../'.
|
||||
MANIFESTS = '../' + _facts[6]
|
||||
|
||||
# ── refuse to deploy into the wrong cluster ────────────────────────────────
|
||||
# Tilt fixes the context before parsing this file, so it can only be refused here.
|
||||
@@ -24,44 +25,73 @@ if k8s_context() != CTX:
|
||||
fail("Wrong kubectl context: '%s'. This is %s — run: make tilt, or tilt up --context %s"
|
||||
% (k8s_context(), CLUSTER, CTX))
|
||||
|
||||
# The namespace has to exist before anything lands in it, and kustomize does not
|
||||
# guarantee ordering across resources. Creating it here is idempotent.
|
||||
local('kubectl --context %s create namespace %s --dry-run=client -o yaml | kubectl --context %s apply -f -'
|
||||
% (CTX, CLUSTER, CTX), quiet=True)
|
||||
|
||||
# ── images go to this environment's own registry ───────────────────────────
|
||||
# Fail closed: name the registry rather than let Tilt infer it, or a miss pushes
|
||||
# an unqualified image to docker.io.
|
||||
default_registry('localhost:' + REGISTRY)
|
||||
|
||||
# ── the overlay's manifests ────────────────────────────────────────────────
|
||||
# Every namespace they use must exist before anything lands in it, and kustomize
|
||||
# does not order resources, so create them here (idempotent). The Namespaces they
|
||||
# declare are grouped as 'infra', whatever they are named.
|
||||
if MANIFESTS:
|
||||
_yaml = kustomize(MANIFESTS)
|
||||
k8s_yaml(_yaml)
|
||||
_declared = []
|
||||
_namespaces = {}
|
||||
for _o in decode_yaml_stream(_yaml):
|
||||
if not _o:
|
||||
continue
|
||||
_md = _o.get('metadata') or {}
|
||||
if _o.get('kind') == 'Namespace':
|
||||
_declared.append(_md.get('name'))
|
||||
_namespaces[_md.get('name')] = True
|
||||
elif _md.get('namespace'):
|
||||
_namespaces[_md.get('namespace')] = True
|
||||
for _ns in sorted(_namespaces.keys()):
|
||||
local('kubectl --context %s create namespace %s --dry-run=client -o yaml | kubectl --context %s apply -f -'
|
||||
% (CTX, _ns, CTX), quiet=True)
|
||||
if _declared:
|
||||
k8s_resource(objects=[_n + ':namespace' for _n in _declared], new_name='infra')
|
||||
k8s_yaml(kustomize(MANIFESTS))
|
||||
|
||||
# ── Images ─────────────────────────────────────────────────────────────────
|
||||
# (nothing yet — rig's examples run upstream images. Add docker_build calls here.)
|
||||
|
||||
|
||||
# ── Resources ──────────────────────────────────────────────────────────────
|
||||
# (nothing yet — add k8s_resource calls here to name and order what you deploy.)
|
||||
|
||||
|
||||
# Everything with no dev loop of its own, gathered so it does not clutter the UI.
|
||||
k8s_resource(
|
||||
objects=[CLUSTER + ':namespace'],
|
||||
new_name='infra',
|
||||
)
|
||||
|
||||
# ── the workload's half: the overlay's Tiltfile ────────────────────────────
|
||||
# Included, so its relative paths resolve from the overlay's own folder. It reads
|
||||
# these facts with os.getenv and never needs a path back into rig.
|
||||
os.putenv('RIG_CLUSTER', CLUSTER)
|
||||
os.putenv('RIG_CONTEXT', CTX)
|
||||
os.putenv('RIG_HTTP_PORT', HTTP)
|
||||
os.putenv('RIG_HTTPS_PORT', HTTPS)
|
||||
os.putenv('RIG_TILT_PORT', TILT)
|
||||
os.putenv('RIG_REGISTRY', 'localhost:' + REGISTRY)
|
||||
os.putenv('RIG_OVERLAY_DIR', OVERLAY)
|
||||
if OVERLAY and os.path.exists(OVERLAY + '/Tiltfile'):
|
||||
include(OVERLAY + '/Tiltfile')
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Catalogue — paste what you need, delete the rest.
|
||||
# Commented out so this file runs as-is.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# ── build an image ─────────────────────────────────────────────────────────
|
||||
# context= is the REPO ROOT ('..'); dockerfile= is relative to THIS file (ctrl/).
|
||||
# So every COPY is repo-root relative, even for files beside the Dockerfile.
|
||||
#
|
||||
# docker_build(
|
||||
# CLUSTER + '-api', # must match `image:` in the manifest —
|
||||
# context='..', # that string is the only thing
|
||||
# dockerfile='Dockerfile.api', # connecting the two
|
||||
# ignore=['.git', 'def', '.venv', 'node_modules', '__pycache__'],
|
||||
# live_update=[sync('../api', '/app/api')],
|
||||
# )
|
||||
#
|
||||
# ── name and order a resource ──────────────────────────────────────────────
|
||||
# k8s_resource('api', resource_deps=['postgres'], labels=['app'])
|
||||
# k8s_resource('gateway', resource_deps=['api', 'ui'], labels=['app'])
|
||||
#
|
||||
# ── reload the gateway when its config changes ─────────────────────────────
|
||||
# A hash-less configMapGenerator ConfigMap never changes name, so edits do NOT
|
||||
# roll the pod on their own.
|
||||
#
|
||||
# local_resource(
|
||||
# 'gateway-reload',
|
||||
# cmd='kubectl --context %s -n %s rollout restart deployment/gateway' % (CTX, CLUSTER),
|
||||
# deps=['k8s/base/Caddyfile'],
|
||||
# resource_deps=['gateway'],
|
||||
# auto_init=False,
|
||||
# )
|
||||
#
|
||||
# ── an overlay whose secretGenerator reads outside its own directory ───────
|
||||
# kustomize refuses to read above the kustomization root unless told to. Only
|
||||
# add this if you actually have such a generator; it loosens a safety check.
|
||||
#
|
||||
# k8s_yaml(kustomize(MANIFESTS, flags=['--load-restrictor=LoadRestrictionsNone']))
|
||||
#
|
||||
# ── reach a service directly, bypassing the gateway ────────────────────────
|
||||
# Prefer the gateway; host ports are shared machine-wide. If you need one, take it
|
||||
# from this environment's own port block.
|
||||
#
|
||||
# k8s_resource('postgres', port_forwards=[str(int(HTTP) + 5) + ':5432'])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install the addons the configuration asks for (ADDONS), in the order listed.
|
||||
# One idempotent script per addon: the overlay's addons/<name>.sh first, then rig's ctrl/addons/.
|
||||
# Install the addons the active profile asked for, in the order listed.
|
||||
# One idempotent script per addon in ctrl/addons/.
|
||||
# Usage: addons.sh install | list
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
@@ -9,51 +9,26 @@ cd "$(dirname "$0")"
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
# Every addon runs from here, wherever its file lives, so it can source ./lib/config.sh.
|
||||
export RIG_CTRL="$PWD"
|
||||
|
||||
# The script for one addon name: the overlay's, else rig's; empty if neither.
|
||||
addon_path() {
|
||||
local ov=""
|
||||
if [ -n "$OVERLAY_DIR" ]; then ov="$(_from_ctrl "$OVERLAY_DIR")/addons/$1.sh"; fi
|
||||
if [ -n "$ov" ] && [ -f "$ov" ]; then
|
||||
echo "$ov"
|
||||
elif [ -f "addons/$1.sh" ]; then
|
||||
echo "addons/$1.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
if [ -z "${ADDONS// /}" ]; then
|
||||
echo "no addons asked for (ADDONS is empty)"
|
||||
echo "no addons in profile '$PROFILE_NAME'"
|
||||
return
|
||||
fi
|
||||
local a p
|
||||
local a
|
||||
for a in $ADDONS; do
|
||||
p=$(addon_path "$a")
|
||||
if [ -z "$p" ]; then
|
||||
echo "no such addon: $a (looked in the overlay's addons/ and ctrl/addons/)" >&2
|
||||
if [ ! -f "addons/${a}.sh" ]; then
|
||||
echo "no such addon: addons/${a}.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "addon: $a"
|
||||
bash "$p"
|
||||
bash "addons/${a}.sh"
|
||||
done
|
||||
}
|
||||
|
||||
list() {
|
||||
echo "wanted: ${ADDONS:-none}"
|
||||
echo "profile '$PROFILE_NAME' wants: ${ADDONS:-none}"
|
||||
echo "available:"
|
||||
local f
|
||||
if [ -n "$OVERLAY_DIR" ]; then
|
||||
for f in "$(_from_ctrl "$OVERLAY_DIR")"/addons/*.sh; do
|
||||
[ -e "$f" ] || continue
|
||||
printf ' %-16s overlay\n' "$(basename "$f" .sh)"
|
||||
done
|
||||
fi
|
||||
for f in addons/*.sh; do
|
||||
[ -e "$f" ] || continue
|
||||
printf ' %-16s rig\n' "$(basename "$f" .sh)"
|
||||
done
|
||||
ls addons/*.sh 2>/dev/null | xargs -n1 basename | sed 's/\.sh$//' | sed 's/^/ /'
|
||||
}
|
||||
|
||||
case "${1:-list}" in
|
||||
|
||||
109
rig/ctrl/addons/airflow.sh
Executable file
109
rig/ctrl/addons/airflow.sh
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apache Airflow — the cluster half of the airflow cabinet (one `standalone` pod).
|
||||
# Requires the postgres addon; refuses to install without it.
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
if ! $K get deployment -n "$NS" postgres >/dev/null 2>&1; then
|
||||
echo " ! airflow needs the postgres addon, and it is not installed" >&2
|
||||
echo " add it before airflow in the profile's ADDONS:" >&2
|
||||
echo " ADDONS=\"... postgres airflow\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reuse the credential postgres generated rather than storing a second copy.
|
||||
db_user=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_USER}' | base64 -d)
|
||||
db_pass=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)
|
||||
db_name=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_DB}' | base64 -d)
|
||||
|
||||
if $K get secret -n "$NS" airflow >/dev/null 2>&1; then
|
||||
echo " secret exists, keeping the current admin password and fernet key"
|
||||
else
|
||||
admin_password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
|
||||
# Airflow requires a 32-byte urlsafe-base64 key; without a fixed one every
|
||||
# restart invalidates every stored connection.
|
||||
fernet_key=$(head -c 32 /dev/urandom | base64 | tr '+/' '-_')
|
||||
$K create secret generic airflow -n "$NS" \
|
||||
--from-literal=ADMIN_USER="${AIRFLOW_ADMIN_USER:-admin}" \
|
||||
--from-literal=ADMIN_PASSWORD="$admin_password" \
|
||||
--from-literal=FERNET_KEY="$fernet_key" \
|
||||
--from-literal=SQL_ALCHEMY_CONN="postgresql+psycopg2://${db_user}:${db_pass}@postgres:5432/${db_name}" \
|
||||
>/dev/null
|
||||
echo " generated an admin password (read it back with the command below)"
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
selector:
|
||||
app: airflow
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: airflow
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: airflow
|
||||
spec:
|
||||
containers:
|
||||
- name: airflow
|
||||
image: ${AIRFLOW_IMAGE}
|
||||
args: ["standalone"]
|
||||
env:
|
||||
- name: AIRFLOW__CORE__EXECUTOR
|
||||
value: LocalExecutor
|
||||
- name: AIRFLOW__CORE__LOAD_EXAMPLES
|
||||
value: "false"
|
||||
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: SQL_ALCHEMY_CONN}
|
||||
- name: AIRFLOW__CORE__FERNET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: FERNET_KEY}
|
||||
- name: _AIRFLOW_WWW_USER_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_USER}
|
||||
- name: _AIRFLOW_WWW_USER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
# First boot runs the whole migration before it serves anything.
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 20
|
||||
YAML
|
||||
|
||||
echo " waiting for airflow (the first boot migrates the database, so this is slow)..."
|
||||
$K rollout status deployment/airflow -n "$NS" --timeout=600s
|
||||
|
||||
echo " in-cluster: http://airflow.${NS}.svc.cluster.local:8080"
|
||||
echo " reach it: kubectl --context ${KUBECONTEXT} -n ${NS} port-forward svc/airflow 8080:8080"
|
||||
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret airflow -o jsonpath='{.data.ADMIN_PASSWORD}' | base64 -d"
|
||||
@@ -2,7 +2,7 @@
|
||||
# cert-manager plus a self-signed cluster issuer (offline local CA).
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
@@ -12,9 +12,7 @@ K="kubectl --context ${KUBECONTEXT}"
|
||||
if $K get deployment -n cert-manager cert-manager >/dev/null 2>&1; then
|
||||
echo " already installed"
|
||||
else
|
||||
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||
manifest=$(bash ./deps.sh manifest CERT_MANAGER)
|
||||
$K apply -f "$manifest"
|
||||
$K apply -f "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml"
|
||||
fi
|
||||
|
||||
echo " waiting for cert-manager..."
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
# The pool is derived from the kind Docker network at install time.
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
|
||||
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
@@ -47,9 +46,7 @@ echo " kind network $subnet → pool ${pool_start}-${pool_end}"
|
||||
if $K get deployment -n metallb-system controller >/dev/null 2>&1; then
|
||||
echo " already installed"
|
||||
else
|
||||
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||
manifest=$(bash ./deps.sh manifest METALLB)
|
||||
$K apply -f "$manifest"
|
||||
$K apply -f "https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml"
|
||||
fi
|
||||
|
||||
# `rollout status`, not `kubectl wait`: wait errors out while the pod doesn't exist yet.
|
||||
@@ -62,7 +59,27 @@ $K rollout status daemonset/speaker -n metallb-system --timeout=240s
|
||||
# on a race that resolves itself in seconds.
|
||||
echo " configuring the address pool"
|
||||
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if render_manifest "$ADDON_DIR/metallb.yaml" | $K apply -f - >/dev/null 2>&1
|
||||
if $K apply -f - >/dev/null 2>&1 <<YAML
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: IPAddressPool
|
||||
metadata:
|
||||
name: default
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- ${pool_start}-${pool_end}
|
||||
---
|
||||
# Layer 2 mode: one node answers ARP for each address. No BGP peer needed, which
|
||||
# is what makes this work on a laptop.
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: L2Advertisement
|
||||
metadata:
|
||||
name: default
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
ipAddressPools:
|
||||
- default
|
||||
YAML
|
||||
then
|
||||
echo " pool ready: ${pool_start}-${pool_end}"
|
||||
exit 0
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: IPAddressPool
|
||||
metadata:
|
||||
name: default
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
addresses:
|
||||
- ${pool_start}-${pool_end}
|
||||
---
|
||||
# Layer 2 mode: one node answers ARP for each address. No BGP peer needed, which
|
||||
# is what makes this work on a laptop.
|
||||
apiVersion: metallb.io/v1beta1
|
||||
kind: L2Advertisement
|
||||
metadata:
|
||||
name: default
|
||||
namespace: metallb-system
|
||||
spec:
|
||||
ipAddressPools:
|
||||
- default
|
||||
@@ -2,7 +2,7 @@
|
||||
# metrics-server — makes `kubectl top` work (patched with --kubelet-insecure-tls for kind).
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
@@ -10,9 +10,7 @@ load_config
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
|
||||
if ! $K get deployment -n kube-system metrics-server >/dev/null 2>&1; then
|
||||
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||
manifest=$(bash ./deps.sh manifest METRICS_SERVER)
|
||||
$K apply -f "$manifest"
|
||||
$K apply -f "https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml"
|
||||
fi
|
||||
|
||||
$K patch deployment metrics-server -n kube-system --type=json \
|
||||
|
||||
40
rig/examples/data/addons/postgres.yaml → rig/ctrl/addons/postgres.sh
Normal file → Executable file
40
rig/examples/data/addons/postgres.yaml → rig/ctrl/addons/postgres.sh
Normal file → Executable file
@@ -1,3 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL — the cluster half of the postgres cabinet: plain manifests, one replica on a PVC.
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
|
||||
|
||||
# The password is generated once and then left alone, so re-running this does
|
||||
# not rotate the credential out from under whatever is already connected.
|
||||
if $K get secret -n "$NS" postgres >/dev/null 2>&1; then
|
||||
echo " secret exists, keeping the current password"
|
||||
else
|
||||
password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
|
||||
$K create secret generic postgres -n "$NS" \
|
||||
--from-literal=POSTGRES_DB="${POSTGRES_DB:-postgres}" \
|
||||
--from-literal=POSTGRES_USER="${POSTGRES_USER:-postgres}" \
|
||||
--from-literal=POSTGRES_PASSWORD="$password" >/dev/null
|
||||
echo " generated a password (read it back with the command printed below)"
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
@@ -55,15 +84,22 @@ spec:
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["sh", "-c", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||
command: ["sh", "-c", "pg_isready -U \$POSTGRES_USER -d \$POSTGRES_DB"]
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-data
|
||||
YAML
|
||||
|
||||
echo " waiting for postgres..."
|
||||
$K rollout status deployment/postgres -n "$NS" --timeout=240s
|
||||
|
||||
echo " in-cluster: postgres.${NS}.svc.cluster.local:5432"
|
||||
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d"
|
||||
22
rig/examples/data/addons/redis.yaml → rig/ctrl/addons/redis.sh
Normal file → Executable file
22
rig/examples/data/addons/redis.yaml → rig/ctrl/addons/redis.sh
Normal file → Executable file
@@ -1,3 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Redis — the cluster half of the redis cabinet: cache/broker, no persistence.
|
||||
# Notes: docs/notes/addons.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
|
||||
|
||||
echo " applying manifests"
|
||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
@@ -33,3 +49,9 @@ spec:
|
||||
command: ["redis-cli", "ping"]
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 5
|
||||
YAML
|
||||
|
||||
echo " waiting for redis..."
|
||||
$K rollout status deployment/redis -n "$NS" --timeout=180s
|
||||
|
||||
echo " in-cluster: redis://redis.${NS}.svc.cluster.local:6379/0"
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Readiness check: is this machine ready to run rig? Reports and instructs; never fixes.
|
||||
# Usage: check.sh [all | mem [status|push|all|backup|restore]] (all = every detail)
|
||||
# Prints the memory budget and what to do next first, then the configuration as it is.
|
||||
# Notes: docs/notes/check.md
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
@@ -13,11 +12,13 @@ if [ "${1:-}" = mem ]; then
|
||||
exec bash ./mem.sh "${@:-status}"
|
||||
fi
|
||||
|
||||
# Facts only with `all`; problems (!) always print.
|
||||
# Compact by default: facts only with `all`; problems (!) always print.
|
||||
VERBOSE=""
|
||||
if [ "${1:-}" = all ]; then VERBOSE=1; fi
|
||||
fact() { if [ -n "$VERBOSE" ]; then echo "$@"; fi; }
|
||||
|
||||
bash ./deps.sh detect ${VERBOSE:+all}
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
@@ -65,64 +66,25 @@ port_busy() {
|
||||
grep -qi "^ *[0-9]*: [0-9A-F]*$hex " /proc/net/tcp /proc/net/tcp6 2>/dev/null
|
||||
}
|
||||
|
||||
# Who listens on a port, as "pid name", when ss can tell (only for our own processes).
|
||||
port_owner() {
|
||||
command -v ss >/dev/null 2>&1 || return 0
|
||||
ss -ltnpH "sport = :$1" 2>/dev/null \
|
||||
| sed -n 's/.*users:(("\([^"]*\)",pid=\([0-9]*\).*/\2 \1/p' | head -1
|
||||
}
|
||||
|
||||
# This environment's Tilt: a tilt started from this ctrl/ (the Makefile runs it there).
|
||||
tilt_pid=""
|
||||
if tilt_owner=$(port_owner "$TILT_PORT") && [ -n "$tilt_owner" ]; then
|
||||
pid=${tilt_owner%% *}
|
||||
if [ "${tilt_owner#* }" = tilt ] && [ "$(readlink "/proc/$pid/cwd" 2>/dev/null)" = "$(pwd -P)" ]; then
|
||||
tilt_pid=$pid
|
||||
fi
|
||||
echo
|
||||
echo "rig"
|
||||
echo " cluster ${CLUSTER} (${KUBECONTEXT}) profile ${PROFILE_NAME}, ${NODES} node(s), registry ${REGISTRY_MODE}"
|
||||
fact " ingress ${INGRESS_MODE}"
|
||||
if [ ! -f ./.env ]; then
|
||||
fact " .env none — built-in defaults (cp ctrl/.env.example ctrl/.env to set values)"
|
||||
fi
|
||||
|
||||
# none | stopped | up | unknown (no docker)
|
||||
cluster_state() {
|
||||
docker info >/dev/null 2>&1 || { echo unknown; return; }
|
||||
local f="label=io.x-k8s.kind.cluster=${CLUSTER}"
|
||||
if [ -n "$(docker ps -q --filter "$f")" ]; then echo up
|
||||
elif [ -n "$(docker ps -aq --filter "$f")" ]; then echo stopped
|
||||
else echo none; fi
|
||||
}
|
||||
state=$(cluster_state)
|
||||
|
||||
# ── memory budget: what this environment needs, against what the machine has ──
|
||||
# The workload's share comes from the overlay: MEMORY_BUDGET="name=MB ..." in its rig.env,
|
||||
# and MEMORY_BUDGET_TIP, optionally, says how to switch its largest part off.
|
||||
# rig only knows what a node costs. Warns; never blocks.
|
||||
# ── memory: does this cluster fit right now? Warns; never blocks. ──────────
|
||||
total_mb=$(mb_of MemTotal)
|
||||
avail_mb=$(mb_of MemAvailable)
|
||||
swap_used_mb=$(( $(mb_of SwapTotal) - $(mb_of SwapFree) ))
|
||||
overcommit=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||
node_mb=$(( NODES * NODE_MB ))
|
||||
|
||||
budget="" work_mb=0 big_mb=0 big=""
|
||||
for item in ${MEMORY_BUDGET:-}; do
|
||||
name=${item%=*} mb=${item##*=}
|
||||
if [ "$name" = "$item" ] || ! [[ "$mb" =~ ^[0-9]+$ ]]; then
|
||||
echo " ! budget '$item' in MEMORY_BUDGET is not name=MB"
|
||||
continue
|
||||
fi
|
||||
[ "$mb" -gt 0 ] || continue
|
||||
budget+=$(printf ' %-18s %6d MB' "$name" "$mb")$'\n'
|
||||
work_mb=$(( work_mb + mb ))
|
||||
if [ "$mb" -gt "$big_mb" ]; then big_mb=$mb big=$name; fi
|
||||
done
|
||||
need_mb=$(( work_mb + node_mb ))
|
||||
need_mb=$(( NODES * NODE_MB ))
|
||||
|
||||
rows=$(container_mb)
|
||||
# If our cluster is already up, its memory is already out of MemAvailable.
|
||||
# If our cluster is already up, its memory is already out of MemAvailable: need nothing more.
|
||||
ours_mb=$(awk -F'\t' -v c="$CLUSTER" '$2 == c { s += $1 } END { print s + 0 }' <<< "$rows")
|
||||
if [ "$state" = up ]; then
|
||||
still_mb=$(( need_mb > ours_mb ? need_mb - ours_mb : 0 ))
|
||||
else
|
||||
still_mb=$need_mb
|
||||
fi
|
||||
still_mb=$(( ours_mb > 0 ? 0 : need_mb ))
|
||||
headroom=$(( avail_mb - still_mb ))
|
||||
|
||||
# The biggest things holding memory, other than this cluster: kind clusters summed, the rest by name.
|
||||
@@ -131,41 +93,24 @@ others=$(awk -F'\t' -v c="$CLUSTER" '
|
||||
$2 == "-" { k["container \x27" $3 "\x27"] += $1 }
|
||||
END { for (n in k) printf "%d\t%s\n", k[n], n }' <<< "$rows" | sort -rn)
|
||||
|
||||
echo
|
||||
echo "memory budget (${CLUSTER})"
|
||||
printf '%s' "$budget"
|
||||
if [ -z "$budget" ]; then
|
||||
echo " workload ? (the overlay lists none: MEMORY_BUDGET in its rig.env)"
|
||||
fi
|
||||
printf ' %-18s %6d MB (kind control plane, kube-system; %s node(s))\n' "node overhead" "$node_mb" "$NODES"
|
||||
echo " ---------"
|
||||
printf ' %-18s %6d MB\n' "required" "$need_mb"
|
||||
if [ "$state" = up ]; then
|
||||
printf ' %-18s %6d MB (%s is up)\n' "held by it now" "$ours_mb" "$CLUSTER"
|
||||
fi
|
||||
printf ' %-18s %6d MB\n' "this machine has" "$total_mb"
|
||||
printf ' %-18s %6d MB (right now)\n' "available" "$avail_mb"
|
||||
echo
|
||||
|
||||
if [ "$headroom" -ge 512 ]; then
|
||||
if [ "$state" = up ]; then
|
||||
echo "fits: ${CLUSTER} holds ${ours_mb} MB of its ${need_mb} MB; ${avail_mb} MB still available — ${headroom} MB headroom."
|
||||
else
|
||||
echo "fits: ${need_mb} MB needed, ${avail_mb} MB available — ${headroom} MB headroom."
|
||||
fi
|
||||
if [ "$still_mb" -eq 0 ] && [ "$headroom" -ge 512 ]; then
|
||||
printf " memory up, holding %d MB — %d MB headroom for what you deploy\n" "$ours_mb" "$headroom"
|
||||
elif [ "$still_mb" -eq 0 ]; then
|
||||
printf " ! memory up, but only %d MB headroom for anything you deploy\n" "$headroom"
|
||||
elif [ "$headroom" -ge 512 ]; then
|
||||
printf " memory fits — ~%d MB for %s node(s), %d MB headroom\n" "$need_mb" "$NODES" "$headroom"
|
||||
elif [ "$headroom" -ge 0 ]; then
|
||||
echo "! tight: ${still_mb} MB still needed, ${avail_mb} MB available — only ${headroom} MB headroom."
|
||||
printf " ! memory fits, but only %d MB headroom (~%d MB for %s node(s))\n" "$headroom" "$need_mb" "$NODES"
|
||||
else
|
||||
echo "! does not fit: ${still_mb} MB still needed, ${avail_mb} MB available."
|
||||
printf " ! memory does not fit: ~%d MB needed, %d MB available\n" "$still_mb" "$avail_mb"
|
||||
# Two failures with opposite fixes, and telling them apart is the point.
|
||||
if [ "$need_mb" -le "$total_mb" ]; then
|
||||
echo " Something else holds the memory (below): stopping it helps, a bigger machine would not."
|
||||
if [ "$still_mb" -le "$total_mb" ]; then
|
||||
echo " something else holds it (below) — stopping that helps, a bigger VM would not."
|
||||
if grep -q 'kind cluster' <<< "$others"; then
|
||||
echo " 'make cluster free' stops the other kind clusters. It stops, never deletes."
|
||||
fi
|
||||
else
|
||||
echo " The machine itself is too small: ${total_mb} MB in all. Give it more memory"
|
||||
echo " ('make check mem' has the WSL steps), or switch parts of the workload off."
|
||||
echo " the machine itself is too small: ${total_mb} MB total."
|
||||
fi
|
||||
fi
|
||||
if [ -n "$others" ] && { [ -n "$VERBOSE" ] || [ "$headroom" -lt 512 ]; }; then
|
||||
@@ -176,12 +121,6 @@ if [ -n "$others" ] && { [ -n "$VERBOSE" ] || [ "$headroom" -lt 512 ]; }; then
|
||||
echo " ... and $((n_others - 6)) more"
|
||||
fi
|
||||
fi
|
||||
# One part that outweighs the rest is the first thing to switch off on a smaller machine.
|
||||
if [ "$work_mb" -gt 0 ] && [ $(( big_mb * 2 )) -gt "$work_mb" ] && [ "$big_mb" -ge 1024 ]; then
|
||||
echo
|
||||
echo "${big} is ${big_mb} of the ${work_mb} MB the workload needs — more than the rest"
|
||||
echo "combined. If you are not working on it, ${MEMORY_BUDGET_TIP:-switching it off in the overlay} gets that back."
|
||||
fi
|
||||
if [ "$swap_used_mb" -gt 0 ]; then
|
||||
fact " ${swap_used_mb} MB already in swap, which 'available' does not count: expect slow before failing"
|
||||
fi
|
||||
@@ -189,111 +128,46 @@ if [ "$overcommit" = "1" ]; then
|
||||
fact " overcommit=1: allocations never fail, so read 'fits' as a ceiling (OOM killer settles up)"
|
||||
fi
|
||||
|
||||
# ── where things stand, and the next step ──────────────────────────────────
|
||||
echo
|
||||
case "$state" in
|
||||
none)
|
||||
echo "${CLUSTER} does not exist yet. 'make cluster up' creates it${ADDONS:+ (with ${ADDONS})},"
|
||||
echo "then 'make tilt' starts the dev loop." ;;
|
||||
stopped)
|
||||
echo "${CLUSTER} exists but is stopped (nothing is lost). Start it again with:"
|
||||
echo " docker start \$(docker ps -aq -f label=io.x-k8s.kind.cluster=${CLUSTER})" ;;
|
||||
up)
|
||||
if [ -n "$tilt_pid" ]; then
|
||||
echo "${CLUSTER} is up, and its Tilt is running (pid ${tilt_pid}): http://localhost:${TILT_PORT}"
|
||||
else
|
||||
echo "${CLUSTER} is up. 'make tilt' starts the dev loop, at http://localhost:${TILT_PORT}."
|
||||
fi
|
||||
echo "Through the gateway: http://localhost:${HTTP_PORT}" ;;
|
||||
*)
|
||||
echo "docker is not reachable, so the cluster's state is unknown (the machine block below says why)." ;;
|
||||
esac
|
||||
|
||||
# ── the configuration, as it is ────────────────────────────────────────────
|
||||
echo
|
||||
bash ./deps.sh detect ${VERBOSE:+all}
|
||||
|
||||
echo
|
||||
echo "config"
|
||||
echo " cluster ${CLUSTER} ($([ "$state" = none ] && echo 'not created' || echo "$state"))"
|
||||
echo " context ${KUBECONTEXT}"
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
echo " overlay $(_abs_from_ctrl "$OVERLAY_DIR")"
|
||||
elif [ -n "$OVERLAY_DIR" ]; then
|
||||
echo " overlay none named — rig's own ${OVERLAY_DIR}"
|
||||
fi
|
||||
if [ -n "$VERBOSE" ] && [ -n "$OVERLAY_DIR" ]; then
|
||||
ov=$(_from_ctrl "$OVERLAY_DIR") pieces=""
|
||||
for piece in rig.env k8s/overlays/dev kind-config.yaml.tpl addons Tiltfile; do
|
||||
[ -e "$ov/$piece" ] && pieces+="$piece "
|
||||
done
|
||||
echo " provides: ${pieces:-nothing rig reads}"
|
||||
fi
|
||||
echo " profile ${PROFILE_NAME}"
|
||||
echo " nodes ${NODES}"
|
||||
echo " kind config ${KIND_CONFIG}"
|
||||
echo " manifests ${MANIFESTS_DIR:-none}"
|
||||
echo " ingress ${INGRESS_MODE}"
|
||||
if [ ! -f ./.env ]; then
|
||||
fact " .env none — built-in defaults (cp ctrl/.env.example ctrl/.env to set values)"
|
||||
fi
|
||||
if [ -n "${STALE_MANIFESTS_DIR:-}" ]; then
|
||||
echo " ! .env MANIFESTS_DIR=${STALE_MANIFESTS_DIR} is the old default; rig's examples moved"
|
||||
echo " to examples/ — delete that line from ctrl/.env (ignored until then)"
|
||||
fi
|
||||
# registry.sh points containerd at certs.d, which only works if the kind config says so,
|
||||
# and a kind config is fixed at creation: a project's own file that drops it fails silently.
|
||||
if [ "$REGISTRY_MODE" != none ] && ! grep -q 'config_path *= *"/etc/containerd/certs.d"' "$KIND_CONFIG"; then
|
||||
echo " ! kind ${KIND_CONFIG} lacks the containerd config_path patch that registry mode"
|
||||
echo " '${REGISTRY_MODE}' needs — copy it from ctrl/k8s/kind-config.yaml.tpl"
|
||||
fi
|
||||
|
||||
# ── ports: checked before creation; docker reports a clash only halfway through. ──
|
||||
# Ports held by our own cluster are not clashes: its node containers, and its registry,
|
||||
# which is a container of its own. Second grep, not `tr -d ':->'` (a tr range).
|
||||
ours=$({ docker ps --filter "label=io.x-k8s.kind.cluster=${CLUSTER}" --format '{{.Ports}}'
|
||||
docker ps --filter "name=^${CLUSTER}-registry\$" --format '{{.Ports}}'; } 2>/dev/null \
|
||||
| tr ',' '\n' | grep -oE ':[0-9]+->' | grep -oE '[0-9]+' || true)
|
||||
|
||||
clash=0 list=""
|
||||
for entry in "http:${HTTP_PORT}" "https:${HTTPS_PORT}" \
|
||||
"tilt:${TILT_PORT}" "registry:${REGISTRY_PORT}"; do
|
||||
# Ports held by our own cluster are not clashes. Second grep, not `tr -d ':->'` (a tr range).
|
||||
ours=$(docker ps --filter "label=io.x-k8s.kind.cluster=${CLUSTER}" \
|
||||
--format '{{.Ports}}' 2>/dev/null | tr ',' '\n' \
|
||||
| grep -oE ':[0-9]+->' | grep -oE '[0-9]+' || true)
|
||||
|
||||
clash=0 list="" mine=0
|
||||
for entry in "HTTP:${HTTP_PORT}" "HTTPS:${HTTPS_PORT}" \
|
||||
"TILT:${TILT_PORT}" "REGISTRY:${REGISTRY_PORT}"; do
|
||||
name="${entry%%:*}"; p="${entry#*:}"
|
||||
[ -n "$p" ] || continue
|
||||
list+="$name $p "
|
||||
list+="$p "
|
||||
if ! port_busy "$p"; then
|
||||
fact "$(printf " %-9s %-6s free" "$name" "$p")"
|
||||
elif echo "$ours" | grep -qx "$p"; then
|
||||
mine=1
|
||||
fact "$(printf " %-9s %-6s in use by this environment's cluster" "$name" "$p")"
|
||||
elif [ "$name" = tilt ] && [ -n "$tilt_pid" ]; then
|
||||
fact "$(printf " %-9s %-6s in use by this environment's Tilt" "$name" "$p")"
|
||||
else
|
||||
owner=$(port_owner "$p")
|
||||
if [ "$name" = tilt ] && [ "${owner#* }" = tilt ]; then
|
||||
printf " ! ports tilt %s is held by another Tilt (pid %s, in %s)\n" "$p" "${owner%% *}" \
|
||||
"$(readlink "/proc/${owner%% *}/cwd" 2>/dev/null || echo '?')"
|
||||
echo " stop it (kill ${owner%% *}) before 'make tilt'"
|
||||
else
|
||||
printf " ! ports %s %s IN USE by %s\n" "$name" "$p" "${owner:+pid ${owner}}${owner:-something else}"
|
||||
printf " ! ports %s %s IN USE by something else\n" "$name" "$p"
|
||||
clash=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo " ports ${list% }"
|
||||
if [ "$clash" -eq 1 ]; then
|
||||
echo " override it in ctrl/.env (e.g. HTTP_PORT=21080), or rename this directory"
|
||||
elif [ "$mine" -eq 1 ]; then
|
||||
echo " ports ${list% } held by this cluster"
|
||||
else
|
||||
echo " ports ${list% } free"
|
||||
fi
|
||||
fact " derived from the directory name; pin them: bash ctrl/ports.sh persist"
|
||||
|
||||
# ── what `make cluster up` wires in beside the cluster ─────────────────────
|
||||
REG_NAME="${CLUSTER}-registry"
|
||||
if reg_state=$(docker inspect -f '{{.State.Status}}' "$REG_NAME" 2>/dev/null); then
|
||||
echo " registry ${REGISTRY_MODE} localhost:${REGISTRY_PORT} (${reg_state})"
|
||||
if state=$(docker inspect -f '{{.State.Status}}' "$REG_NAME" 2>/dev/null); then
|
||||
echo " registry localhost:${REGISTRY_PORT} ($state)"
|
||||
else
|
||||
echo " registry ${REGISTRY_MODE} (no container yet)"
|
||||
fact " registry no container yet — 'make cluster up' starts it"
|
||||
fi
|
||||
echo " addons ${ADDONS:-none}"
|
||||
if [ -n "$VERBOSE" ]; then
|
||||
bash ./addons.sh list | sed -n '3,$p' | sed 's/^/ /'
|
||||
fi
|
||||
fact " available: $(ls addons/*.sh 2>/dev/null | xargs -n1 basename | sed 's/\.sh$//' | tr '\n' ' ')"
|
||||
|
||||
# The CA reaches three places and only one of them is ours. Report the other two.
|
||||
if [ -n "${REGISTRY_CA_FILE:-}" ]; then
|
||||
|
||||
@@ -15,7 +15,6 @@ up() {
|
||||
# Say what this profile locks in BEFORE spending minutes building it:
|
||||
# the kind config is fixed at creation and cannot be changed later.
|
||||
echo "creating cluster '$CLUSTER' from profile '$PROFILE_NAME'"
|
||||
echo " overlay ${OVERLAY_DIR:-none}"
|
||||
echo " kind config ${KIND_CONFIG}"
|
||||
echo " nodes $NODES"
|
||||
echo " image $NODE_IMAGE"
|
||||
|
||||
116
rig/ctrl/deps.sh
116
rig/ctrl/deps.sh
@@ -2,8 +2,7 @@
|
||||
# rig:standalone rigdeps detect
|
||||
# Toolchain installer: detect the host, install pinned tools into $OUT_BIN, report
|
||||
# host actions it will not perform (no sudo, no apt). Usually via `make deps`.
|
||||
# Usage: deps.sh [detect [all] | list | verify [core|dev] | fetch [core|dev] [--to DIR] | install [core|dev]
|
||||
# | manifest NAME | manifests [--to DIR] | snapshot [DIR]]
|
||||
# Usage: deps.sh [detect [all] | list | verify [core|dev] | fetch [core|dev] [--to DIR] | install [core|dev]]
|
||||
# Notes: docs/notes/deps.md
|
||||
|
||||
set -euo pipefail
|
||||
@@ -27,14 +26,6 @@ abspath() {
|
||||
|
||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||
HOST_ROOT="${HOST_ROOT:-/}"
|
||||
# A host fixture (docs/notes/installer-testing.md) is a root whose kernel files stand in
|
||||
# for this machine's; UNAME_S does the same for the one fact a file cannot carry.
|
||||
if [ "$HOST_ROOT" != / ]; then
|
||||
if [ -r "$HOST_ROOT/proc/meminfo" ]; then MEMINFO="${MEMINFO:-$HOST_ROOT/proc/meminfo}"; fi
|
||||
if [ -r "$HOST_ROOT/proc/sys/vm/overcommit_memory" ]; then
|
||||
OVERCOMMIT_FILE="${OVERCOMMIT_FILE:-$HOST_ROOT/proc/sys/vm/overcommit_memory}"
|
||||
fi
|
||||
fi
|
||||
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||
@@ -146,7 +137,7 @@ docker_pkg() {
|
||||
|
||||
# Windows outside WSL (Git Bash, MSYS, Cygwin) fails confusingly; name it instead.
|
||||
require_linux() {
|
||||
case "${UNAME_S:-$(uname -s)}" in
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
cat >&2 <<'EOF'
|
||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||
@@ -165,7 +156,7 @@ EOF
|
||||
esac
|
||||
}
|
||||
|
||||
is_wsl() { grep -qi microsoft "$(host_file /proc/version)" 2>/dev/null; }
|
||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||
|
||||
detect() {
|
||||
echo "host"
|
||||
@@ -728,99 +719,6 @@ require_linux
|
||||
cmd="${1:-install}"
|
||||
[ $# -gt 0 ] && shift
|
||||
|
||||
# ── manifests rig's own addons apply ───────────────────────────────────────
|
||||
# Pinned (URL + SHA256), fetched through the same DEPS_SOURCE resolver as the
|
||||
# binaries and verified, then applied from disk: an offline machine needs no
|
||||
# network for them. Default home: vendor/manifests/ in rig's folder (gitignored).
|
||||
MANIFESTS_HOME="${MANIFESTS_HOME:-$(cd .. && pwd)/vendor/manifests}"
|
||||
BAKED_MANIFESTS="${BAKED_MANIFESTS:-/opt/rig/manifests}"
|
||||
MANIFEST_NAMES="METALLB CERT_MANAGER METRICS_SERVER"
|
||||
|
||||
manifest_path() { # NAME dir
|
||||
local v="${1}_VERSION"
|
||||
echo "$2/$(echo "$1" | tr 'A-Z_' 'a-z-')-${!v}.yaml"
|
||||
}
|
||||
|
||||
# Make one pinned manifest present and verified in dir; print only its path.
|
||||
fetch_manifest() { # NAME dir
|
||||
local name="$1" dir="$2" url_var="${1}_MANIFEST_URL" sha_var="${1}_MANIFEST_SHA256" file
|
||||
if [ -z "${!url_var:-}" ] || [ -z "${!sha_var:-}" ]; then
|
||||
echo "no pinned manifest for $name (${url_var} / ${sha_var} unset)" >&2
|
||||
exit 1
|
||||
fi
|
||||
file=$(manifest_path "$name" "$dir")
|
||||
if [ -f "$file" ] && [ "$($SHA "$file" | awk '{print $1}')" = "${!sha_var}" ]; then
|
||||
echo "$file"
|
||||
return
|
||||
fi
|
||||
mkdir -p "$dir"
|
||||
if [ "$DEPS_SOURCE" = baked ]; then
|
||||
cp "$(manifest_path "$name" "$BAKED_MANIFESTS")" "$file.tmp"
|
||||
else
|
||||
download "$(resolve_url "${!url_var}")" "$file.tmp"
|
||||
fi
|
||||
verify "$file.tmp" "${!sha_var}" "$name manifest"
|
||||
mv "$file.tmp" "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
fetch_manifests() { # [--to DIR]
|
||||
local dest="$MANIFESTS_HOME" n
|
||||
if [ "${1:-}" = --to ]; then dest="$(abspath "${2:?--to needs a directory}")"; fi
|
||||
echo "fetching the addons' manifests into $dest (source: $DEPS_SOURCE)"
|
||||
for n in $MANIFEST_NAMES; do
|
||||
echo " $n $(fetch_manifest "$n" "$dest")"
|
||||
done
|
||||
}
|
||||
|
||||
# ── snapshot: this machine as a host fixture ───────────────────────────────
|
||||
# Writes what detect reads, cut down to what it needs — never the environment, the
|
||||
# home directory or the host name — plus the lines detect prints for it. A fact of
|
||||
# one machine: keep it with an overlay or in rig's local/, never in rig itself, and
|
||||
# replay it with ctrl/hosttest.sh. Notes: docs/notes/installer-testing.md
|
||||
snapshot() {
|
||||
local dest r f
|
||||
dest="$(abspath "${1:-host-snapshot}")"
|
||||
if [ -n "$(ls -A "$dest" 2>/dev/null)" ]; then
|
||||
echo "snapshot: $dest already holds something — pick an empty directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
r="$dest/root"
|
||||
mkdir -p "$r/etc" "$r/proc/sys/vm"
|
||||
f=$(host_file /etc/os-release)
|
||||
if [ -r "$f" ]; then grep -E '^(PRETTY_NAME|NAME|VERSION_ID|ID|ID_LIKE)=' "$f" > "$r/etc/os-release"; fi
|
||||
# The kernel release says WSL or not; the full build string names build hosts.
|
||||
echo "Linux version $(awk '{print $3; exit}' "$(host_file /proc/version)" 2>/dev/null || uname -r)" > "$r/proc/version"
|
||||
grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree):' "${MEMINFO:-/proc/meminfo}" > "$r/proc/meminfo"
|
||||
cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" > "$r/proc/sys/vm/overcommit_memory" 2>/dev/null || true
|
||||
f=$(host_file /etc/wsl.conf)
|
||||
if [ -r "$f" ]; then
|
||||
# Section headers and the two keys detect reads; a [user] default= names a person.
|
||||
grep -E '^[[:space:]]*(\[[a-z0-9]+\]|systemd[[:space:]]*=|generateResolvConf[[:space:]]*=)' "$f" > "$r/etc/wsl.conf" || true
|
||||
fi
|
||||
f=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||
if [ -n "$f" ] && grep -qE '^\s*memory\s*=' "$f"; then
|
||||
mkdir -p "$r/mnt/c/Users/user"
|
||||
{ echo "[wsl2]"; grep -E '^\s*memory\s*=' "$f"; } > "$r/mnt/c/Users/user/.wslconfig"
|
||||
fi
|
||||
{
|
||||
echo "# for the record; not replayed"
|
||||
echo "arch=$(arch)"
|
||||
echo "glibc=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || echo unknown)"
|
||||
echo "taken=$(date -u +%Y-%m-%d)"
|
||||
} > "$dest/facts.txt"
|
||||
# The lines the fixture itself decides, as detect prints them for it now.
|
||||
{
|
||||
echo "# Written by deps.sh snapshot: what detect said about this machine."
|
||||
env -u MEMINFO -u OVERCOMMIT_FILE HOST_ROOT="$r" bash "./$(basename "${BASH_SOURCE[0]}")" detect all 2>/dev/null \
|
||||
| grep -E '^ (distro|memory|overcommit|systemd|resolv\.conf|wslconfig) |^ ! systemd|^ - resolv\.conf' \
|
||||
| sed 's/^ /+ /'
|
||||
} > "$dest/expect.txt"
|
||||
echo "wrote $dest"
|
||||
(cd "$dest" && find . -type f | sort | sed 's|^\./| |')
|
||||
echo "keep it with an overlay or in rig's local/, never in rig; replay it with rig's hosttest.sh"
|
||||
}
|
||||
|
||||
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||
need_downloads() {
|
||||
require_amd64
|
||||
@@ -834,15 +732,9 @@ case "$cmd" in
|
||||
verify) verify_tools "${1:-dev}" ;;
|
||||
fetch) need_downloads; fetch "$@" ;;
|
||||
install) need_downloads; install "${1:-dev}" ;;
|
||||
manifest) need_downloads
|
||||
fetch_manifest "${1:?usage: $0 manifest <METALLB|CERT_MANAGER|METRICS_SERVER>}" "$MANIFESTS_HOME" ;;
|
||||
manifests) need_downloads; fetch_manifests "$@" ;;
|
||||
snapshot) snapshot "${1:-}" ;;
|
||||
*) echo "usage: $0 [detect [all]|list|verify|fetch|install|manifest NAME|manifests]" >&2
|
||||
*) echo "usage: $0 [detect [all]|list|verify|fetch|install]" >&2
|
||||
echo " install [core|dev] (default dev)" >&2
|
||||
echo " fetch [core|dev] [--to DIR]" >&2
|
||||
echo " manifests [--to DIR] the addons' pinned manifests, verified" >&2
|
||||
echo " snapshot [DIR] this machine as a host fixture (no secrets)" >&2
|
||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
@@ -16,30 +16,12 @@ serve() {
|
||||
echo "no docs/index.html" >&2
|
||||
exit 1
|
||||
fi
|
||||
# One worker and no access log: a few static pages need no more, and the
|
||||
# terminal then shows only real errors. The config is a temp file, gone on exit.
|
||||
# Global, not local: the trap runs after serve() has returned.
|
||||
DOCS_CONF=$(mktemp)
|
||||
trap 'rm -f "$DOCS_CONF"' EXIT
|
||||
cat > "$DOCS_CONF" <<'NGINX'
|
||||
worker_processes 1;
|
||||
error_log stderr error;
|
||||
events { worker_connections 64; }
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
access_log off;
|
||||
server { listen 80; root /usr/share/nginx/html; }
|
||||
}
|
||||
NGINX
|
||||
chmod 644 "$DOCS_CONF"
|
||||
echo "docs for '$CLUSTER' on http://localhost:${DOCS_PORT}"
|
||||
echo " (ctrl-c to stop; nothing is installed and nothing persists)"
|
||||
docker run --rm \
|
||||
--name "${CLUSTER}-docs" \
|
||||
-e NGINX_ENTRYPOINT_QUIET_LOGS=1 \
|
||||
-p "${DOCS_PORT}:80" \
|
||||
-v "$REPO/docs:/usr/share/nginx/html:ro" \
|
||||
-v "$DOCS_CONF:/etc/nginx/nginx.conf:ro" \
|
||||
nginx:alpine
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# EXAMPLE PROFILE (optional): copy to mirror.env, then PROFILE=mirror; overlays the defaults.
|
||||
# mirror — images via a pull-through cache of an internal registry, TLS and metrics addons.
|
||||
# A profile says how this machine reaches the world; what runs is an overlay's business.
|
||||
# EXAMPLE PROFILE (optional): copy to client.env, then PROFILE=client; overlays the defaults.
|
||||
# client — images via a pull-through cache of the corporate registry, TLS and metrics addons.
|
||||
# Notes: docs/notes/env.md
|
||||
|
||||
PROFILE_NAME=mirror
|
||||
PROFILE_NAME=client
|
||||
K8S_VERSION=v1_36
|
||||
ADDONS="metallb cert-manager metrics-server"
|
||||
REGISTRY_MODE=mirror
|
||||
@@ -16,6 +15,6 @@ DNS_MODE=hosts
|
||||
# HTTPS_PORT=443
|
||||
|
||||
# Set these in ctrl/.env (gitignored), not here:
|
||||
# REGISTRY_REMOTE_URL=https://registry.internal.example/api/docker/docker-virtual
|
||||
# REGISTRY_REMOTE_URL=https://artifactory.corp.example/artifactory/api/docker/docker-virtual
|
||||
# REGISTRY_USER / REGISTRY_PASSWORD
|
||||
# REGISTRY_CA_FILE=/path/to/internal-root-ca.crt
|
||||
# REGISTRY_CA_FILE=/path/to/corp-root-ca.crt
|
||||
26
rig/ctrl/env.d/data.env.example
Normal file
26
rig/ctrl/env.d/data.env.example
Normal file
@@ -0,0 +1,26 @@
|
||||
# EXAMPLE PROFILE (optional): copy to data.env, then PROFILE=data; overlays the defaults.
|
||||
# data — postgres, redis and airflow (upstream images) in the `data` namespace.
|
||||
# Notes: docs/notes/env.md
|
||||
|
||||
PROFILE_NAME=data
|
||||
K8S_VERSION=v1_36
|
||||
# Order matters: addons.sh installs in the order listed, and airflow refuses to
|
||||
# start without a metadata database, so postgres comes first.
|
||||
ADDONS="metallb postgres redis airflow"
|
||||
# local, not none — see the defaults in lib/config.sh: `none` has no outward-push guard.
|
||||
REGISTRY_MODE=local
|
||||
INGRESS_MODE=hostport
|
||||
DNS_MODE=hosts
|
||||
|
||||
# Namespace for the dependency containers.
|
||||
DATA_NAMESPACE=data
|
||||
|
||||
# Postgres identity. The password is generated once by postgres.sh and kept.
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_USER=app
|
||||
POSTGRES_STORAGE=2Gi
|
||||
|
||||
AIRFLOW_ADMIN_USER=admin
|
||||
|
||||
# Reach the databases with port-forward, e.g.
|
||||
# kubectl -n data port-forward svc/postgres 5432:5432
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Replay host fixtures: run `deps.sh detect all` against a stand-in machine and check
|
||||
# what it must (and must not) say. No docker, no network, no root.
|
||||
# Usage: hosttest.sh [FIXTURE_DIR...] default: tests/hosts/* exits 1 on a mismatch
|
||||
# A fixture is root/ (the files detect reads), expect.txt (+ must appear, - must not,
|
||||
# exit N), and an optional env (KEY=value lines). `deps.sh snapshot` writes one.
|
||||
# Notes: docs/notes/installer-testing.md
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
dirs=("$@")
|
||||
if [ ${#dirs[@]} -eq 0 ]; then dirs=(../tests/hosts/*/); fi
|
||||
|
||||
rc=0 passed=0 failed=0
|
||||
for d in "${dirs[@]}"; do
|
||||
d="${d%/}"
|
||||
if [ ! -f "$d/expect.txt" ]; then
|
||||
echo " FAIL $d: no expect.txt — not a host fixture" >&2
|
||||
rc=1; failed=$((failed + 1)); continue
|
||||
fi
|
||||
# Only the fixture's own settings: the caller's HOST_ROOT, MEMINFO or UNAME_S
|
||||
# must not leak into a replay.
|
||||
run=(env -u HOST_ROOT -u MEMINFO -u OVERCOMMIT_FILE -u UNAME_S)
|
||||
if [ -d "$d/root" ]; then run+=(HOST_ROOT="$(cd "$d/root" && pwd)"); fi
|
||||
if [ -f "$d/env" ]; then
|
||||
while IFS= read -r kv; do run+=("$kv"); done < <(grep -vE '^[[:space:]]*(#|$)' "$d/env")
|
||||
fi
|
||||
out=$("${run[@]}" bash ./deps.sh detect all 2>&1)
|
||||
code=$?
|
||||
|
||||
bad=""
|
||||
want_exit=0
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
'+ '*) grep -qF -- "${line#+ }" <<< "$out" || bad+=$'\n'" missing: ${line#+ }" ;;
|
||||
'- '*) grep -qF -- "${line#- }" <<< "$out" && bad+=$'\n'" present: ${line#- }" ;;
|
||||
'exit '*) want_exit="${line#exit }" ;;
|
||||
esac
|
||||
done < <(grep -vE '^[[:space:]]*(#|$)' "$d/expect.txt")
|
||||
if [ "$code" != "$want_exit" ]; then bad+=$'\n'" exit: $code, wanted $want_exit"; fi
|
||||
|
||||
if [ -z "$bad" ]; then
|
||||
printf ' ok %s\n' "$(basename "$d")"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
printf ' FAIL %s%s\n' "$(basename "$d")" "$bad"
|
||||
rc=1; failed=$((failed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%d host fixture(s) as expected, %d not\n' "$passed" "$failed"
|
||||
exit "$rc"
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# The installer on clean machines: the generated kit (standalone/default/rigdeps.sh) in
|
||||
# stock distro containers, as a non-root user, the way it reaches a real machine.
|
||||
# Needs docker and the network; takes minutes. Exits 1 on a failure.
|
||||
# Usage: installtest.sh [IMAGE...] default: ubuntu:22.04 debian:trixie-slim, then offline
|
||||
# Notes: docs/notes/installer-testing.md
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
KIT="$(cd .. && pwd)/standalone/default/rigdeps.sh"
|
||||
images=("$@")
|
||||
if [ ${#images[@]} -eq 0 ]; then images=(ubuntu:22.04 debian:trixie-slim); fi
|
||||
|
||||
rc=0
|
||||
passed=0
|
||||
check() { # name, expected, actual
|
||||
if [ "$2" = "$3" ]; then
|
||||
printf ' ok %s\n' "$1"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
printf ' FAIL %s\n expected: %s\n got: %s\n' "$1" "$2" "$3"
|
||||
rc=1
|
||||
fi
|
||||
}
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "installtest needs a running docker it can reach" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A stale kit would test yesterday's installer.
|
||||
if ! bash ./standalone.sh check >/dev/null 2>&1; then
|
||||
echo "the kit is stale — run: make standalone" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for img in "${images[@]}"; do
|
||||
printf '\n%s\n' "$img"
|
||||
|
||||
# A stock image has no curl or wget: the installer must say so and stop, not
|
||||
# half-install. This is the bootstrap paradox BOOTSTRAP.md describes.
|
||||
out=$(docker run --rm -v "$KIT:/kit/rigdeps.sh:ro" "$img" bash /kit/rigdeps.sh install dev 2>&1)
|
||||
code=$?
|
||||
check "bare: install refuses" "1" "$code"
|
||||
check "bare: and names what is missing" "yes" \
|
||||
"$(grep -q 'neither curl nor wget' <<< "$out" && echo yes || echo no)"
|
||||
|
||||
# The one root step a machine owner takes, then everything else as a plain user —
|
||||
# the Workspace's case: no sudo from the installer, tools in ~/.local/bin.
|
||||
out=$(docker run --rm -v "$KIT:/kit/rigdeps.sh:ro" "$img" bash -c '
|
||||
set -e
|
||||
apt-get update -qq >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl ca-certificates >/dev/null
|
||||
useradd -m t
|
||||
su t -s /bin/bash -c "
|
||||
set -e
|
||||
bash /kit/rigdeps.sh install dev
|
||||
echo ==verify; PATH=\$HOME/.local/bin:\$PATH bash /kit/rigdeps.sh verify dev
|
||||
echo ==manifests; bash /kit/rigdeps.sh manifests --to \$HOME/m
|
||||
echo ==bin; ls \$HOME/.local/bin
|
||||
echo ==m; ls \$HOME/m
|
||||
"' 2>&1)
|
||||
code=$?
|
||||
check "user: install, verify and manifests succeed" "0" "$code"
|
||||
check "user: the dev tier lands in ~/.local/bin" "ctlptl docker-compose jq kind kubectl tilt" \
|
||||
"$(sed -n '/^==bin$/,/^==m$/p' <<< "$out" | grep -vE '^==' | sort | xargs)"
|
||||
check "user: tells them to put it on PATH" "yes" \
|
||||
"$(grep -q 'Put the toolchain on your PATH' <<< "$out" && echo yes || echo no)"
|
||||
check "user: the three manifests, verified" "3" \
|
||||
"$(sed -n '/^==m$/,$p' <<< "$out" | grep -c '\.yaml$')"
|
||||
if [ "$code" -ne 0 ]; then printf '%s\n' "$out" | tail -15 | sed 's/^/ | /'; fi
|
||||
done
|
||||
|
||||
# The air-gapped path: everything baked into the image, then run with no network at all.
|
||||
printf '\noffline (deps-full, --network none)\n'
|
||||
tag="rig-installtest:full"
|
||||
if docker build -q -f Dockerfile.deps --target deps-full -t "$tag" .. >/dev/null 2>&1; then
|
||||
out=$(docker run --rm --network none --entrypoint bash "$tag" -c '
|
||||
set -e
|
||||
/work/rigdeps.sh install dev
|
||||
/work/rigdeps.sh manifests --to /tmp/m
|
||||
echo ==bin; ls /out/bin
|
||||
echo ==m; ls /tmp/m' 2>&1)
|
||||
code=$?
|
||||
check "offline: install and manifests succeed" "0" "$code"
|
||||
check "offline: the dev tier, from the image" "ctlptl docker-compose jq kind kubectl tilt" \
|
||||
"$(sed -n '/^==bin$/,/^==m$/p' <<< "$out" | grep -vE '^==' | sort | xargs)"
|
||||
check "offline: the three manifests, from the image" "3" \
|
||||
"$(sed -n '/^==m$/,$p' <<< "$out" | grep -c '\.yaml$')"
|
||||
if [ "$code" -ne 0 ]; then printf '%s\n' "$out" | tail -15 | sed 's/^/ | /'; fi
|
||||
docker rmi -f "$tag" >/dev/null 2>&1 || true
|
||||
else
|
||||
check "offline: the deps-full image builds" "yes" "no"
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
printf '%d install checks passed\n' "$passed"
|
||||
else
|
||||
printf 'FAILED — the installer did not do on a clean machine what it says\n' >&2
|
||||
fi
|
||||
exit "$rc"
|
||||
60
rig/ctrl/k8s/README.md
Normal file
60
rig/ctrl/k8s/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `ctrl/k8s` — the cluster, and what runs on it
|
||||
|
||||
Same layout as every other project here: a kind config, a kustomize `base/`,
|
||||
and an `overlays/dev/` that patches it.
|
||||
|
||||
```
|
||||
kind-config.yaml.tpl the cluster itself — nodes, ports
|
||||
base/ the components, as plain manifests
|
||||
overlays/dev/ how this rig differs from the base
|
||||
```
|
||||
|
||||
## Why the cluster config is a template
|
||||
|
||||
Every other project checks in a literal `kind-config.yaml`, because there is
|
||||
exactly one `unt` and one `nvi`. A rig is copied and renamed to make a second
|
||||
environment, and both the cluster name and the host port block follow the
|
||||
directory name — so a literal would make every copy collide on both.
|
||||
|
||||
`ctrl/cluster.sh` renders it with `sed`, substituting `${CLUSTER}`,
|
||||
`${NODE_IMAGE}`, `${HTTP_PORT}` and `${HOST_WORKDIR}`. Not `envsubst`: that is
|
||||
`gettext-base`, which a minimal Debian does not have, and Docker being the only
|
||||
prerequisite is the one promise rig makes.
|
||||
|
||||
**To change the cluster, edit this file** — more nodes, other port mappings —
|
||||
then `make cluster reset`: a kind config is fixed at creation, not re-applied.
|
||||
`lib/config.sh` reads the node count back out of it, so nothing restates it.
|
||||
|
||||
A project that builds its own cluster through rig passes its own file as
|
||||
`KIND_CONFIG=<path>`; it is rendered the same way.
|
||||
|
||||
## `base/` — replace these
|
||||
|
||||
**The two components in `base/` are examples, not the system.** They exist so
|
||||
the real manifests have a shape to be written against.
|
||||
|
||||
The real ones are expected to be versioned **separately from the installer** —
|
||||
they change on a different cadence, by different people, under different review.
|
||||
Point `MANIFESTS_DIR` in `ctrl/.env` at their overlay and rig stops owning them:
|
||||
|
||||
```
|
||||
MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||
```
|
||||
|
||||
Until then it defaults to `ctrl/k8s/overlays/dev`.
|
||||
|
||||
### The three states a component can be in
|
||||
|
||||
Switching between them should be a one-line change, never a rewrite. The DNS
|
||||
name stays the same in every case, so callers never know the difference:
|
||||
|
||||
| state | what exists | when |
|
||||
| --- | --- | --- |
|
||||
| **real** | an image built from source, hot-reloaded | the one thing you are working on |
|
||||
| **mock** | a stub returning canned responses (`example-mock.yaml`) | everything else — most of the estate |
|
||||
| **remote** | no pod at all, just a Service (`example-remote.yaml`) | when the real system is reachable and you want it |
|
||||
|
||||
Most components should be **mock**. What has to be faithful is the topology —
|
||||
names, ports, dependency order, who can reach whom, how it fails. The workloads
|
||||
are noise, and mocking them is what makes several copies of a large estate fit
|
||||
on one laptop.
|
||||
@@ -1,5 +1,5 @@
|
||||
# The cluster. Add nodes or port mappings here, then `make cluster reset`.
|
||||
# ctrl/cluster.sh substitutes (sed): CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR, OVERLAY_DIR
|
||||
# ctrl/cluster.sh substitutes (sed): CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
||||
# lib/config.sh reads the node count back from this file.
|
||||
# Notes: docs/notes/kind-config.md
|
||||
kind: Cluster
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
# Shared config loading: how the config layers compose. Sourced, never executed.
|
||||
# Precedence, weakest first:
|
||||
# defaults < versions.env < env.d/<profile> < <overlay>/rig.env < .env < caller's env.
|
||||
# Precedence, weakest first: defaults < versions.env < env.d/<profile> < .env < caller's env.
|
||||
# Run from ctrl/.
|
||||
# Notes: docs/notes/config.md
|
||||
|
||||
# Per-invocation overrides: restored after the files are read, so the caller wins.
|
||||
# NODES is deliberately not here (read from the kind config).
|
||||
CONFIG_OVERRIDABLE="PROFILE OVERLAY CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
||||
CONFIG_OVERRIDABLE="PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
||||
REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT
|
||||
SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT
|
||||
REGISTRY_PORT MANIFESTS_DIR"
|
||||
|
||||
# rig's own example, used when no overlay is named (relative to rig's root).
|
||||
DEFAULT_OVERLAY=examples/starter
|
||||
|
||||
# A rig-root-relative path as seen from ctrl/; absolute paths pass through.
|
||||
_from_ctrl() { case "$1" in /*) echo "$1" ;; *) echo "../$1" ;; esac; }
|
||||
|
||||
# The same, absolute. Empty if it does not exist.
|
||||
_abs_from_ctrl() { (cd "$(_from_ctrl "$1")" 2>/dev/null && pwd); }
|
||||
|
||||
# The environment's folder — the overlay's when one is named, else rig's —
|
||||
# reduced to a DNS label kind accepts as a cluster name.
|
||||
# The repo folder's name, reduced to a DNS label kind accepts as a cluster name.
|
||||
default_cluster_name() {
|
||||
local n
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
n=$(basename "$(_abs_from_ctrl "$OVERLAY_DIR")")
|
||||
else
|
||||
n=$(basename "$(cd .. && pwd)")
|
||||
fi
|
||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
|
||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
||||
echo "${n:-rig}"
|
||||
@@ -60,59 +45,15 @@ load_config() {
|
||||
_config_restore "$saved"
|
||||
|
||||
# A profile is optional; naming one that does not exist is an error.
|
||||
local profile="${PROFILE:-}" layered=""
|
||||
local profile="${PROFILE:-}"
|
||||
if [ -n "$profile" ] && [ "$profile" != default ]; then
|
||||
if [ ! -f "./env.d/${profile}.env" ]; then
|
||||
echo "no such profile: env.d/${profile}.env" >&2
|
||||
if [ -d "../examples/${profile}" ]; then
|
||||
echo " it is an example overlay now: OVERLAY=examples/${profile}" >&2
|
||||
fi
|
||||
echo "available: $(config_profiles | tr '\n' ' ')" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
source "./env.d/${profile}.env"
|
||||
set +a
|
||||
layered=1
|
||||
fi
|
||||
|
||||
# The overlay: one folder, outside rig, holding a use case (docs/notes/overlay.md).
|
||||
# Named ones must exist; with none named, rig's own example is used if present.
|
||||
OVERLAY_DIR=""
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
OVERLAY_DIR="${OVERLAY%/}"
|
||||
if [ ! -d "$(_from_ctrl "$OVERLAY_DIR")" ]; then
|
||||
echo "no overlay at OVERLAY=${OVERLAY} (relative to rig's folder, or absolute)" >&2
|
||||
exit 1
|
||||
fi
|
||||
elif [ -d "../${DEFAULT_OVERLAY}" ]; then
|
||||
OVERLAY_DIR="$DEFAULT_OVERLAY"
|
||||
fi
|
||||
# Its rig.env may not choose the profile or the overlay (both are chosen before
|
||||
# it loads), and the paths it sets are relative to the overlay.
|
||||
local ov_env="" m_before k_before
|
||||
if [ -n "$OVERLAY_DIR" ]; then ov_env="$(_from_ctrl "$OVERLAY_DIR")/rig.env"; fi
|
||||
if [ -n "$ov_env" ] && [ -f "$ov_env" ]; then
|
||||
if grep -qE '^[[:space:]]*(export[[:space:]]+)?(PROFILE|OVERLAY)=' "$ov_env"; then
|
||||
echo "$ov_env: an overlay's rig.env cannot set PROFILE or OVERLAY (they choose it)" >&2
|
||||
exit 1
|
||||
fi
|
||||
m_before="${MANIFESTS_DIR-}" k_before="${KIND_CONFIG-}"
|
||||
set -a
|
||||
source "$ov_env"
|
||||
set +a
|
||||
if [ "${MANIFESTS_DIR-}" != "$m_before" ]; then
|
||||
case "$MANIFESTS_DIR" in /*|none|"") ;; *) MANIFESTS_DIR="${OVERLAY_DIR}/${MANIFESTS_DIR}" ;; esac
|
||||
fi
|
||||
if [ "${KIND_CONFIG-}" != "$k_before" ]; then
|
||||
case "$KIND_CONFIG" in /*|"") ;; *) KIND_CONFIG="$(dirname "$ov_env")/${KIND_CONFIG}" ;; esac
|
||||
fi
|
||||
layered=1
|
||||
fi
|
||||
|
||||
# The machine and the caller still win over both.
|
||||
if [ -n "$layered" ]; then
|
||||
set -a
|
||||
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
||||
set +a
|
||||
_config_restore "$saved"
|
||||
@@ -145,25 +86,9 @@ load_config() {
|
||||
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
||||
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
||||
|
||||
# Where the workload's manifests live, relative to rig's folder (or absolute):
|
||||
# the overlay's k8s/overlays/dev unless something names another. `none`: rig
|
||||
# applies none (the overlay's Tiltfile does). A named folder must exist.
|
||||
if [ "${MANIFESTS_DIR:-}" = ctrl/k8s/overlays/dev ] && [ ! -d ../ctrl/k8s/overlays/dev ]; then
|
||||
# The old default, pinned by an older .env.example; rig's examples moved.
|
||||
STALE_MANIFESTS_DIR="$MANIFESTS_DIR"
|
||||
MANIFESTS_DIR=""
|
||||
fi
|
||||
if [ -z "${MANIFESTS_DIR:-}" ] && [ -n "$OVERLAY_DIR" ] \
|
||||
&& [ -d "$(_from_ctrl "$OVERLAY_DIR")/k8s/overlays/dev" ]; then
|
||||
MANIFESTS_DIR="$OVERLAY_DIR/k8s/overlays/dev"
|
||||
fi
|
||||
MANIFESTS_DIR="${MANIFESTS_DIR:-}"
|
||||
if [ "$MANIFESTS_DIR" = none ]; then
|
||||
MANIFESTS_DIR=""
|
||||
elif [ -n "$MANIFESTS_DIR" ] && [ ! -d "$(_from_ctrl "$MANIFESTS_DIR")" ]; then
|
||||
echo "no manifests at MANIFESTS_DIR=${MANIFESTS_DIR} (relative to rig's folder, or absolute)" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Where the workload's manifests live, repo-root relative; always resolved.
|
||||
# See k8s/README.md.
|
||||
MANIFESTS_DIR="${MANIFESTS_DIR:-ctrl/k8s/overlays/dev}"
|
||||
|
||||
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
||||
local var="NODE_IMAGE_${K8S_VERSION}"
|
||||
@@ -173,13 +98,9 @@ load_config() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The cluster is one file: the overlay's kind-config.yaml.tpl if it has one,
|
||||
# else rig's k8s/kind-config.yaml.tpl. KIND_CONFIG is "use this file instead",
|
||||
# for a project that builds its own cluster through rig (relative to ctrl/, or absolute).
|
||||
if [ -z "${KIND_CONFIG:-}" ] && [ -n "$OVERLAY_DIR" ] \
|
||||
&& [ -f "$(_from_ctrl "$OVERLAY_DIR")/kind-config.yaml.tpl" ]; then
|
||||
KIND_CONFIG="$(_from_ctrl "$OVERLAY_DIR")/kind-config.yaml.tpl"
|
||||
fi
|
||||
# The cluster is one file: k8s/kind-config.yaml.tpl. To change it, edit it.
|
||||
# KIND_CONFIG is only "use this file instead", for a project that builds its
|
||||
# own cluster through rig (a path relative to ctrl/, or absolute).
|
||||
KIND_CONFIG="${KIND_CONFIG:-./k8s/kind-config.yaml.tpl}"
|
||||
if [ ! -f "$KIND_CONFIG" ]; then
|
||||
echo "no kind config at KIND_CONFIG=${KIND_CONFIG}" >&2
|
||||
@@ -196,41 +117,16 @@ load_config() {
|
||||
}
|
||||
|
||||
# Render the kind config to stdout with sed (not envsubst) over an explicit variable list.
|
||||
# HOST_WORKDIR and OVERLAY_DIR must be host paths: the host dockerd resolves hostPath entries.
|
||||
# HOST_WORKDIR must be a host path: the host dockerd resolves hostPath entries.
|
||||
render_kind_config() {
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}" overlay_dir=""
|
||||
if [ -n "$OVERLAY_DIR" ]; then overlay_dir=$(_abs_from_ctrl "$OVERLAY_DIR"); fi
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}"
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" \
|
||||
-e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" \
|
||||
-e "s|\${HTTP_PORT}|${HTTP_PORT}|g" \
|
||||
-e "s|\${HOST_WORKDIR}|${host_workdir}|g" \
|
||||
-e "s|\${OVERLAY_DIR}|${overlay_dir}|g" \
|
||||
"$KIND_CONFIG"
|
||||
}
|
||||
|
||||
# Render an addon's manifest (a .yaml beside its .sh) to stdout. Every ${NAME}, or
|
||||
# ${NAME:-default}, is filled from the loaded config or the calling script's variables;
|
||||
# a NAME with neither is an error, so a typo never becomes an empty image. $VAR and
|
||||
# $(VAR) are left alone: they belong to the container. Notes: docs/notes/addons.md
|
||||
render_manifest() {
|
||||
local file="$1" text ref key def val
|
||||
text=$(<"$file") || return 1
|
||||
while IFS= read -r ref; do
|
||||
key=${ref#\$\{}; key=${key%\}}; def=""
|
||||
if [[ $key == *:-* ]]; then def=${key#*:-}; key=${key%%:-*}; fi
|
||||
if [ -n "${!key:-}" ]; then
|
||||
val=${!key}
|
||||
elif [[ $ref == *:-* ]]; then
|
||||
val=$def
|
||||
else
|
||||
echo "render_manifest: $file needs \${$key}, which nothing sets" >&2
|
||||
return 1
|
||||
fi
|
||||
text=${text//"$ref"/"$val"}
|
||||
done < <(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}' "$file" | sort -u)
|
||||
printf '%s\n' "$text"
|
||||
}
|
||||
|
||||
_config_restore() {
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
|
||||
@@ -20,7 +20,7 @@ BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess belo
|
||||
|
||||
# Refuse Git Bash / MSYS / Cygwin and kernels without /proc, with a clear message.
|
||||
require_linux() {
|
||||
case "${UNAME_S:-$(uname -s)}" in
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
cat >&2 <<'EOF'
|
||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||
|
||||
@@ -22,19 +22,11 @@ derive() {
|
||||
}
|
||||
|
||||
# Resolved facts for consumers outside bash, space-separated, positional:
|
||||
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
|
||||
# The two paths are absolute, or - when there is none. Read this, not `derive`.
|
||||
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
||||
# Read this, not `derive` (which ignores ctrl/.env).
|
||||
active() {
|
||||
load_config
|
||||
local m="-" o="-"
|
||||
if [ -n "$MANIFESTS_DIR" ]; then m=$(_abs_from_ctrl "$MANIFESTS_DIR"); fi
|
||||
if [ -n "$OVERLAY_DIR" ]; then o=$(_abs_from_ctrl "$OVERLAY_DIR"); fi
|
||||
case "$m$o" in
|
||||
*[[:space:]]*)
|
||||
echo "a path here holds whitespace, and these facts are split on spaces: $m $o" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
echo "$CLUSTER $KUBECONTEXT $HTTP_PORT $HTTPS_PORT $TILT_PORT $REGISTRY_PORT $m $o"
|
||||
echo "$CLUSTER $KUBECONTEXT $HTTP_PORT $HTTPS_PORT $TILT_PORT $REGISTRY_PORT $MANIFESTS_DIR"
|
||||
}
|
||||
|
||||
show() {
|
||||
@@ -63,13 +55,6 @@ _row() {
|
||||
# rewritten — an override stays an override.
|
||||
persist() {
|
||||
derive
|
||||
# ctrl/.env belongs to this rig, not to an overlay: a pin written now would
|
||||
# follow every overlay this rig later runs, and two of them would collide.
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
echo "not pinning: OVERLAY is set, and ctrl/.env would carry this block to every overlay" >&2
|
||||
echo " its ports stay derived from its folder name ($CLUSTER): $DERIVED_HTTP-$DERIVED_REGISTRY" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -f ./.env ] || cp ./.env.example ./.env
|
||||
|
||||
local wrote=0 key val
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# What rig has settled, written down as assertions: one decision per check.
|
||||
# No cluster, docker or network; exits 1 on failure (unlike `make check`).
|
||||
# Usage: make selftest [install] (or: bash ctrl/selftest.sh [install])
|
||||
# install: the installer in clean containers — docker, network, minutes (installtest.sh)
|
||||
# Usage: make selftest (or: bash ctrl/selftest.sh)
|
||||
# Notes: docs/notes/selftest.md
|
||||
set -uo pipefail # NOT -e: one failing check must not abort the rest
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ "${1:-}" = install ]; then shift; exec bash ./installtest.sh "$@"; fi
|
||||
|
||||
source ./lib/config.sh
|
||||
|
||||
rc=0
|
||||
@@ -26,17 +23,6 @@ check() { # name, expected, actual
|
||||
|
||||
note() { printf '\n%s\n' "$1"; }
|
||||
|
||||
# A scratch copy of rig for a check to change freely. local/ (overlays, possibly
|
||||
# someone else's) and def/ (scratch) never ride along, and neither do this
|
||||
# machine's PROFILE/OVERLAY/CLUSTER choices: a check sets what it tests.
|
||||
copy_rig() { # dest-dir
|
||||
mkdir -p "$1"
|
||||
tar -C .. --exclude=./local --exclude=./def -cf - . | tar -C "$1" -xf -
|
||||
if [ -f "$1/ctrl/.env" ]; then
|
||||
sed -i '/^PROFILE=/d; /^OVERLAY=/d; /^CLUSTER=/d; /^MANIFESTS_DIR=/d' "$1/ctrl/.env"
|
||||
fi
|
||||
}
|
||||
|
||||
# Resolve one key the way every rig script does, in a clean shell so the
|
||||
# caller's exported value is the only thing in play.
|
||||
resolved() {
|
||||
@@ -47,12 +33,12 @@ resolved() {
|
||||
note "rig needs no profile"
|
||||
# No env.d/ must still resolve and generate a kit; an unknown profile stays an error.
|
||||
NP="$(mktemp -d)"
|
||||
copy_rig "$NP/rig"; rm -rf "$NP/rig/ctrl/env.d"
|
||||
cp -r .. "$NP/rig"; rm -rf "$NP/rig/ctrl/env.d"; sed -i '/^PROFILE=/d' "$NP/rig/ctrl/.env" 2>/dev/null
|
||||
check "no env.d: config resolves" "default" \
|
||||
"$(cd "$NP/rig/ctrl" && bash -c 'source ./lib/config.sh; load_config >/dev/null && echo "$PROFILE_NAME"' 2>&1)"
|
||||
check "no env.d: the k8s version comes from the pins" "yes" \
|
||||
"$(cd "$NP/rig/ctrl" && bash -c 'source ./lib/config.sh; load_config >/dev/null && [ -n "$NODE_IMAGE" ] && echo yes' 2>&1)"
|
||||
check "no env.d: ports.sh active works" "8" \
|
||||
check "no env.d: ports.sh active works" "7" \
|
||||
"$(cd "$NP/rig/ctrl" && bash ports.sh active 2>/dev/null | wc -w)"
|
||||
check "no env.d: a kit is generated for the defaults" "yes" \
|
||||
"$( (cd "$NP/rig/ctrl" && rm -rf ../standalone/*/ && bash standalone.sh write >/dev/null 2>&1) && [ -f "$NP/rig/standalone/default/rigdeps.sh" ] && echo yes || echo no)"
|
||||
@@ -64,14 +50,13 @@ rm -rf "$NP"
|
||||
note "the ports.sh active contract"
|
||||
# ports.sh active is read positionally by the Makefile and Tiltfile: pin field count and order.
|
||||
FACTS="$(bash ports.sh active)"
|
||||
check "active: exactly 8 fields" "8" "$(printf '%s' "$FACTS" | wc -w)"
|
||||
read -r F_CLUSTER F_CTX F_HTTP F_HTTPS F_TILT F_REG F_MANIFESTS F_OVERLAY <<< "$FACTS"
|
||||
check "active: exactly 7 fields" "7" "$(printf '%s' "$FACTS" | wc -w)"
|
||||
read -r F_CLUSTER F_CTX F_HTTP F_HTTPS F_TILT F_REG F_MANIFESTS <<< "$FACTS"
|
||||
check "active: field 2 is kind-<cluster>" "kind-$F_CLUSTER" "$F_CTX"
|
||||
check "active: fields 3-6 are numeric" "yes" \
|
||||
"$([[ "$F_HTTP$F_HTTPS$F_TILT$F_REG" =~ ^[0-9]+$ ]] && echo yes || echo no)"
|
||||
# Absolute, or - when there is none: an empty field would shift every later one.
|
||||
check "active: fields 7-8 are absolute paths or -" "yes" \
|
||||
"$(for f in "$F_MANIFESTS" "$F_OVERLAY"; do case "$f" in -|/*) ;; *) echo no; exit; esac; done; echo yes)"
|
||||
check "active: field 7 is a path" "yes" \
|
||||
"$([ -n "$F_MANIFESTS" ] && [ "${F_MANIFESTS#-}" = "$F_MANIFESTS" ] && echo yes || echo no)"
|
||||
# derive answers a different question and must keep its own shape: it reports
|
||||
# what the directory name implies, ignoring ctrl/.env, so nothing should
|
||||
# configure itself from it.
|
||||
@@ -91,9 +76,7 @@ test_value() {
|
||||
KIND_CONFIG) echo "$PWD/k8s/kind-config.yaml.tpl" ;;
|
||||
*_PORT) echo "19999" ;;
|
||||
CLUSTER) echo "selftest-name" ;;
|
||||
# Named folders must exist, and must not be the default.
|
||||
MANIFESTS_DIR) echo "examples/starter/k8s/base" ;;
|
||||
OVERLAY) echo "examples/data" ;;
|
||||
MANIFESTS_DIR) echo "../elsewhere/overlays/dev" ;;
|
||||
ADDONS) echo "metallb" ;;
|
||||
*) echo "selftest-sentinel" ;;
|
||||
esac
|
||||
@@ -127,8 +110,8 @@ trap 'rm -rf "$TMP"' EXIT
|
||||
mkdir -p "$TMP/My_Proj"
|
||||
cp -r . "$TMP/My_Proj/ctrl"
|
||||
# A pinned CLUSTER in .env would be an override, not a derivation, and this
|
||||
# check is about the derivation. (An OVERLAY would be another derivation.)
|
||||
sed -i '/^CLUSTER=/d; /^OVERLAY=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null
|
||||
# check is about the derivation.
|
||||
sed -i '/^CLUSTER=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null
|
||||
COPY="$(cd "$TMP/My_Proj/ctrl" && bash ports.sh active)"
|
||||
check "a dir named My_Proj derives a DNS label" "my-proj" "$(awk '{print $1}' <<< "$COPY")"
|
||||
check "and a context to match" "kind-my-proj" "$(awk '{print $2}' <<< "$COPY")"
|
||||
@@ -146,88 +129,9 @@ check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)"
|
||||
note "rig stays standalone"
|
||||
# rig must be copyable out of its host project: no references to the host.
|
||||
# The pattern is assembled from fragments so this file does not match itself.
|
||||
# The host project's word for a backing service counts too: rig described its
|
||||
# workload addons with it until they left. local/ holds overlays, which may say anything.
|
||||
HOST_PAT="$(printf '%s' 'sole' 'print' '|\b' 'sp' 'r\b' '|' 'cab' 'inet')"
|
||||
HOST_PAT="$(printf '%s' 'sole' 'print' '|\b' 'sp' 'r\b')"
|
||||
check "no host-project references" "0" \
|
||||
"$(cd .. && grep -rIl -iE "$HOST_PAT" . --exclude-dir=def --exclude-dir=local 2>/dev/null | wc -l)"
|
||||
|
||||
|
||||
note "what runs is an overlay; rig only reads it"
|
||||
# docs/notes/overlay.md. Every check runs in a scratch copy with its own overlay.
|
||||
OV="$TMP/overlay-proof"; copy_rig "$OV/rig"
|
||||
OVR="$OV/rig"
|
||||
mkdir -p "$OVR/local/My_Env/addons" "$OVR/local/My_Env/k8s/prod" "$OVR/ctrl/env.d"
|
||||
printf 'ADDONS="from-profile"\nDATA_NAMESPACE=from-profile\n' > "$OVR/ctrl/env.d/selftest.env"
|
||||
cat > "$OVR/local/My_Env/rig.env" <<'EOF'
|
||||
ADDONS="metallb"
|
||||
DATA_NAMESPACE=from-overlay
|
||||
MANIFESTS_DIR=k8s/prod
|
||||
SELFTEST_SENTINEL=selftest-overlay-sentinel
|
||||
EOF
|
||||
printf 'resources: []\n' > "$OVR/local/My_Env/k8s/prod/kustomization.yaml"
|
||||
printf '#!/usr/bin/env bash\necho "overlay-metallb from $PWD with ${RIG_CTRL:-no RIG_CTRL}"\n' \
|
||||
> "$OVR/local/My_Env/addons/metallb.sh"
|
||||
in_ov() { (cd "$OVR/ctrl" && "$@"); }
|
||||
ov_key() { # key [env assignments...]
|
||||
local k="$1"; shift
|
||||
in_ov env "$@" bash -c 'source ./lib/config.sh; load_config >/dev/null 2>&1; printf "%s" "${!1}"' _ "$k"
|
||||
}
|
||||
|
||||
# With nothing named, rig behaves as it did before overlays: same name, ports, addons, nodes.
|
||||
check "no overlay: the cluster, ports and addons of before" "rig kind-rig 20310 20311 20312 20313" \
|
||||
"$(in_ov bash ports.sh active | awk '{print $1, $2, $3, $4, $5, $6}')"
|
||||
check "no overlay: no addons, one node, rig's own kind config" "|1|./k8s/kind-config.yaml.tpl" \
|
||||
"$(ov_key ADDONS)|$(ov_key NODES)|$(ov_key KIND_CONFIG)"
|
||||
|
||||
# The overlay's rig.env sits between the profile and ctrl/.env; the caller beats all.
|
||||
check "rig.env beats the profile" "from-overlay" \
|
||||
"$(ov_key DATA_NAMESPACE PROFILE=selftest OVERLAY=local/My_Env)"
|
||||
echo 'DATA_NAMESPACE=from-dotenv' >> "$OVR/ctrl/.env"
|
||||
check "ctrl/.env beats rig.env" "from-dotenv" \
|
||||
"$(ov_key DATA_NAMESPACE PROFILE=selftest OVERLAY=local/My_Env)"
|
||||
sed -i '/^DATA_NAMESPACE=from-dotenv$/d' "$OVR/ctrl/.env"
|
||||
check "the caller beats rig.env" "from-caller" \
|
||||
"$(ov_key ADDONS OVERLAY=local/My_Env ADDONS=from-caller)"
|
||||
|
||||
# Identity follows the overlay's folder, so one rig serves several without collisions.
|
||||
check "identity follows the overlay's folder" "my-env kind-my-env" \
|
||||
"$(in_ov env OVERLAY=local/My_Env bash ports.sh active | awk '{print $1, $2}')"
|
||||
check "paths in rig.env are relative to the overlay" "$OVR/local/My_Env/k8s/prod" \
|
||||
"$(in_ov env OVERLAY=local/My_Env bash ports.sh active | awk '{print $7}')"
|
||||
check "a named overlay that does not exist is an error" "yes" \
|
||||
"$(in_ov env OVERLAY=local/nope bash ports.sh active >/dev/null 2>&1 && echo no || echo yes)"
|
||||
printf 'PROFILE=x\n' > "$OV/bad-rig.env"; mkdir -p "$OVR/local/bad"; cp "$OV/bad-rig.env" "$OVR/local/bad/rig.env"
|
||||
check "rig.env may not choose the profile or the overlay" "yes" \
|
||||
"$(in_ov env OVERLAY=local/bad bash ports.sh active >/dev/null 2>&1 && echo no || echo yes)"
|
||||
|
||||
# Addons: the overlay's is found before rig's own, and runs from rig's ctrl/.
|
||||
check "an overlay's addon comes before rig's of the same name" \
|
||||
"overlay-metallb from $OVR/ctrl with $OVR/ctrl" \
|
||||
"$(in_ov env OVERLAY=local/My_Env ADDONS=metallb bash addons.sh install 2>&1 | grep '^overlay-metallb')"
|
||||
|
||||
# ctrl/.env is this rig's: a pinned block would follow every overlay.
|
||||
check "ports.sh persist refuses while an overlay is set" "yes" \
|
||||
"$(in_ov env OVERLAY=local/My_Env bash ports.sh persist >/dev/null 2>&1 && echo no || echo yes)"
|
||||
|
||||
# make's $(shell) must see an OVERLAY given as a make argument (make < 4.4 does not pass it).
|
||||
check "make -n tilt OVERLAY=... asks for the overlay's context" "kind-data" \
|
||||
"$(cd .. && make --no-print-directory -n tilt OVERLAY=examples/data 2>/dev/null | grep -m1 'tilt ' | sed -n 's/.*--context \([^ ]*\).*/\1/p')"
|
||||
|
||||
# rig reads an overlay and never writes into it; its values never reach a committed kit.
|
||||
sum_ov() { (cd "$OVR/local/My_Env" && find . -type f | sort | xargs sha256sum | sha256sum); }
|
||||
before=$(sum_ov)
|
||||
echo 'OVERLAY=local/My_Env' >> "$OVR/ctrl/.env"
|
||||
in_ov bash ports.sh active >/dev/null 2>&1
|
||||
in_ov bash addons.sh list >/dev/null 2>&1
|
||||
in_ov bash -c 'source ./lib/config.sh; load_config >/dev/null; render_kind_config >/dev/null' 2>/dev/null
|
||||
in_ov bash standalone.sh write >/dev/null 2>&1
|
||||
in_ov bash standalone.sh export "$OV/export" >/dev/null 2>&1
|
||||
check "rig writes nothing into an overlay" "$before" "$(sum_ov)"
|
||||
check "an overlay's values never reach a committed kit" "0" \
|
||||
"$(grep -rlE 'selftest-overlay-sentinel|local/My_Env' "$OVR/standalone" 2>/dev/null | wc -l)"
|
||||
check "a committed kit holds no path of this machine" "0" \
|
||||
"$(grep -rlF "$OVR" "$OVR/standalone" 2>/dev/null | wc -l)"
|
||||
"$(cd .. && grep -rIl -iE "$HOST_PAT" . --exclude-dir=def 2>/dev/null | wc -l)"
|
||||
|
||||
|
||||
note "the Tiltfile hardcodes nothing"
|
||||
@@ -235,7 +139,6 @@ note "the Tiltfile hardcodes nothing"
|
||||
check "no literal kind-<name>" "0" "$(grep -cE "['\"]kind-[a-z0-9]" Tiltfile)"
|
||||
check "guards on the variable" "1" "$(grep -c 'allow_k8s_contexts(CTX)' Tiltfile)"
|
||||
check "asks ports.sh for facts" "1" "$(grep -c "local('bash ports.sh active'" Tiltfile)"
|
||||
check "hands over to the overlay's Tiltfile" "1" "$(grep -c "include(OVERLAY + '/Tiltfile')" Tiltfile)"
|
||||
|
||||
|
||||
note "standalone kits are generated, current, and call only real verbs"
|
||||
@@ -265,8 +168,7 @@ check "there is a kit for every profile" "$(config_profiles | wc -l)" "$kits"
|
||||
|
||||
# An export carries this machine's choices but never its credentials; committed kits carry neither.
|
||||
# Proven with sentinel values in a scratch copy, since the real ctrl/.env may leave them empty.
|
||||
SX="$TMP/export-proof"; copy_rig "$SX/rig"
|
||||
mkdir -p "$SX/selftest-sentinel-choice/overlays/dev" # a named MANIFESTS_DIR must exist
|
||||
SX="$TMP/export-proof"; mkdir -p "$SX"; cp -r .. "$SX/rig"
|
||||
cat >> "$SX/rig/ctrl/.env" <<'EOF'
|
||||
REGISTRY_USER=selftest-sentinel-user
|
||||
REGISTRY_PASSWORD=selftest-sentinel-password
|
||||
@@ -285,117 +187,16 @@ check "export: refuses to write inside the repository" "yes" \
|
||||
"$( (bash standalone.sh export ../standalone/selftest-mine >/dev/null 2>&1) && echo no || echo yes)"
|
||||
|
||||
|
||||
note "rig's addons apply verified files, never URLs"
|
||||
# The offline profile must need no network for manifests: each addon asks deps.sh for a
|
||||
# pinned manifest, verified on disk (versions.md). Their images still need preloading.
|
||||
check "no rig addon applies a URL" "0" \
|
||||
"$(cat addons/*.sh | grep -cE 'apply -f "?https?://')"
|
||||
check "every manifest an addon asks for is pinned with a sum" "" \
|
||||
"$(for n in $(grep -ohE 'deps\.sh manifest [A-Z_]+' addons/*.sh | awk '{print $3}' | sort -u); do
|
||||
grep -q "^${n}_MANIFEST_URL=" versions.env && grep -q "^${n}_MANIFEST_SHA256=[0-9a-f]\{64\}$" versions.env \
|
||||
|| printf '%s ' "$n"; done)"
|
||||
|
||||
|
||||
note "withdrawn stays withdrawn (STALE.md)"
|
||||
# One check per entry; the reasoning is in STALE.md, not here.
|
||||
check "✖ S1 rig's Tiltfile has no Images section of its own" "0" "$(grep -c '^# ── Images' Tiltfile)"
|
||||
check "✖ S2 local/ is where overlays live, and ignored" "yes" \
|
||||
"$(grep -qx '/local/' ../.gitignore && echo yes || echo no)"
|
||||
# Patterns assembled from fragments so this file does not match itself.
|
||||
COPIES_PAT="$(printf '%s' 'ac' 'me-rig|ac' 'mebank')"
|
||||
HOUSE_PAT="$(printf '%s' 'semes' 'ter|local' '\.ar\b')"
|
||||
check "✖ S2 no example environment name from the copies era" "0" \
|
||||
"$(cd .. && grep -rIlE "$COPIES_PAT" . --exclude-dir=def --exclude-dir=local --exclude=STALE.md 2>/dev/null | wc -l)"
|
||||
check "✖ S3 ctrl/addons makes the cluster work, nothing more" "cert-manager metallb metrics-server" \
|
||||
"$(ls addons/*.sh | xargs -n1 basename | sed 's/\.sh$//' | sort | xargs)" # the scripts; their manifests sit beside them
|
||||
check "✖ S3 versions.env pins no workload image" "0" \
|
||||
"$(grep -cE '^(POSTGRES|REDIS|AIRFLOW)_IMAGE=' versions.env)"
|
||||
check "✖ S4 no namespace named after the cluster" "0" "$(grep -c "CLUSTER + ':namespace'" Tiltfile)"
|
||||
check "✖ S5 rig's examples left ctrl/k8s" "no" "$([ -d k8s/overlays ] && echo yes || echo no)"
|
||||
check "✖ S5 .env.example does not pin MANIFESTS_DIR" "0" "$(grep -c '^MANIFESTS_DIR=' .env.example)"
|
||||
check "✖ S6 no client or data example profile" "0" \
|
||||
"$(ls env.d/ | grep -cE '^(client|data)\.')"
|
||||
check "✖ S7 no house path or host name in rig" "0" \
|
||||
"$(cd .. && grep -rIlE "$HOUSE_PAT" . --exclude-dir=def --exclude-dir=local --exclude=STALE.md 2>/dev/null | wc -l)"
|
||||
|
||||
|
||||
note "the installer detects what each kind of machine needs"
|
||||
# Host fixtures (tests/hosts/): a stand-in root per machine, detect run against it. The
|
||||
# expensive machines — the Workspace, a WSL install — are exactly the ones you cannot
|
||||
# rebuild to test on, so their shapes are replayed here instead.
|
||||
out="$(bash ./hosttest.sh 2>&1)"
|
||||
check "every host fixture detects as expected" "$(ls -d ../tests/hosts/*/ | wc -l) host fixture(s) as expected, 0 not" \
|
||||
"$(tail -1 <<< "$out")"
|
||||
# A snapshot is carried off a machine that may be someone else's: it must hold only the
|
||||
# files detect reads, and nothing that names the machine or the person.
|
||||
SN="$TMP/snapshot/s"
|
||||
bash ./deps.sh snapshot "$SN" >/dev/null 2>&1
|
||||
check "snapshot writes only what detect reads" \
|
||||
"expect.txt facts.txt root/etc/os-release root/proc/meminfo root/proc/sys/vm/overcommit_memory root/proc/version" \
|
||||
"$( (cd "$SN" 2>/dev/null && find . -type f | sed 's|^\./||' | grep -vE '^root/(etc/wsl\.conf|mnt/c/Users/user/\.wslconfig)$' | sort | xargs) )"
|
||||
check "snapshot names no host, user or home" "0" \
|
||||
"$(grep -rlE "$(hostname)|${USER:-nobody}|/home/" "$SN" 2>/dev/null | wc -l)"
|
||||
check "and replays as the machine it was taken on" "yes" \
|
||||
"$(bash ./hosttest.sh "$SN" >/dev/null 2>&1 && echo yes || echo no)"
|
||||
|
||||
|
||||
note "the dev loop parses — needs tilt and kubectl, not a cluster"
|
||||
# A throwaway kubeconfig with kind-named entries (Tilt trusts kind contexts) and a
|
||||
# kubectl that swallows `apply`: the Tiltfile evaluates for real, nothing is contacted.
|
||||
# The second run is a copy under another name, the case that once failed at load.
|
||||
if ! command -v tilt >/dev/null || ! command -v kubectl >/dev/null; then
|
||||
printf ' skip tilt or kubectl is not installed\n'
|
||||
else
|
||||
FK="$TMP/fake-kube"; mkdir -p "$FK"
|
||||
real_kubectl=$(command -v kubectl)
|
||||
printf '#!/usr/bin/env bash\nfor a in "$@"; do [ "$a" = apply ] && { cat >/dev/null; exit 0; }; done\nexec %q "$@"\n' \
|
||||
"$real_kubectl" > "$FK/kubectl"
|
||||
chmod +x "$FK/kubectl"
|
||||
parses() { # cluster-name [env...] -> the manifests Tilt would deploy, or the error
|
||||
local name="$1"; shift
|
||||
cat > "$FK/kubeconfig" <<EOF
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters: [{name: kind-$name, cluster: {server: "https://127.0.0.1:9"}}]
|
||||
contexts: [{name: kind-$name, context: {cluster: kind-$name, user: kind-$name}}]
|
||||
users: [{name: kind-$name, user: {token: selftest}}]
|
||||
current-context: kind-$name
|
||||
EOF
|
||||
env "$@" KUBECONFIG="$FK/kubeconfig" PATH="$FK:$PATH" \
|
||||
timeout 120 tilt alpha tiltfile-result --context "kind-$name" > "$FK/out.json" 2> "$FK/err" \
|
||||
&& grep -o '"Name": *"[^"]*"' "$FK/out.json" | sed 's/.*"\([^"]*\)"$/\1/' | sort -u | xargs \
|
||||
|| grep -m1 -iE 'error|no object' "$FK/err"
|
||||
}
|
||||
check "the starter overlay parses" "example-service infra uncategorized" "$(parses rig)"
|
||||
check "and under another name" "example-service infra uncategorized" \
|
||||
"$(parses selftest-copy CLUSTER=selftest-copy)"
|
||||
check "the data overlay parses" "items-api uncategorized" "$(parses data OVERLAY=examples/data)"
|
||||
fi
|
||||
|
||||
|
||||
note "the examples are overlays that work as shipped"
|
||||
# They are what a real overlay is copied from, so they must at least parse.
|
||||
bad=""
|
||||
for f in ../examples/*/addons/*.sh; do [ -e "$f" ] && { bash -n "$f" 2>/dev/null || bad+="$f "; }; done
|
||||
check "every example addon parses" "" "$bad"
|
||||
# An addon's manifest may only name what the config or its own script sets, so a typo
|
||||
# fails here rather than as an empty value on a cluster.
|
||||
bad=""
|
||||
for y in addons/*.yaml ../examples/*/addons/*.yaml; do
|
||||
[ -e "$y" ] || continue
|
||||
for n in $(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*\}' "$y" | tr -d '${}' | sort -u); do
|
||||
grep -qE "(^|[[:space:]])$n=" "${y%.yaml}.sh" versions.env ../examples/*/rig.env 2>/dev/null || bad+="$y:$n "
|
||||
done
|
||||
done
|
||||
check "every addon manifest names only what is set" "" "$bad"
|
||||
if command -v python3 >/dev/null; then
|
||||
bad=""
|
||||
for f in ../examples/*/dags/*.py; do
|
||||
[ -e "$f" ] && { python3 -c 'import ast, sys; ast.parse(open(sys.argv[1]).read())' "$f" 2>/dev/null || bad+="$f "; }
|
||||
done
|
||||
check "every example DAG parses" "" "$bad"
|
||||
note "optional — needs tilt and this rig's cluster"
|
||||
# Tilt needs a cluster context to parse the Tiltfile, so this is skipped without one.
|
||||
if ! command -v tilt >/dev/null; then
|
||||
printf ' skip tilt is not installed\n'
|
||||
elif ! kubectl config get-contexts -o name 2>/dev/null | grep -qx "$F_CTX"; then
|
||||
printf " skip no %s context — run 'make cluster up' to include this\n" "$F_CTX"
|
||||
else
|
||||
printf ' skip python3 is not installed\n'
|
||||
out="$(tilt alpha tiltfile-result --context "$F_CTX" 2>&1)"
|
||||
check "Tiltfile evaluates" "yes" \
|
||||
"$(printf '%s' "$out" | grep -q '"Manifests"' && echo yes || echo "no: $(printf '%s' "$out" | tail -1)")"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
@@ -30,28 +30,25 @@ COMPOSE_VERSION=5.5.1
|
||||
COMPOSE_SHA256=db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576
|
||||
COMPOSE_URL=https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64
|
||||
|
||||
# Node images for KIND_VERSION, pinned by digest; K8S_VERSION picks one (default: the newest).
|
||||
# Older entries are kept deliberately, for targets that run an older Kubernetes.
|
||||
# Node images for KIND_VERSION, pinned by digest; profiles pick one via K8S_VERSION.
|
||||
# Older entries are kept deliberately (legacy-estate simulation).
|
||||
NODE_IMAGE_v1_36=kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5
|
||||
NODE_IMAGE_v1_35=kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
|
||||
NODE_IMAGE_v1_34=kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256
|
||||
NODE_IMAGE_v1_33=kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4
|
||||
|
||||
# Images pulled at runtime. Pinned by tag; the registry mode decides where they are pulled FROM.
|
||||
# Images pulled at runtime (registry, mocks). Pinned by tag; the registry mode
|
||||
# decides where they are pulled FROM.
|
||||
REGISTRY_IMAGE=registry:2
|
||||
STUB_IMAGE=python:3.12-slim
|
||||
|
||||
# rig's own addons (ctrl/addons/<name>.sh), installed when ADDONS names them.
|
||||
# Addons, installed by ctrl/addons/<name>.sh when listed in a profile's ADDONS.
|
||||
CERT_MANAGER_VERSION=v1.21.1
|
||||
METRICS_SERVER_VERSION=v0.9.0
|
||||
METALLB_VERSION=v0.16.0
|
||||
|
||||
# The manifests those addons apply, fetched and verified like the binaries
|
||||
# (`deps.sh manifest <NAME>`), so an offline machine needs no network for them.
|
||||
# Sums from the release's own asset digest; metallb publishes none, see versions.md.
|
||||
CERT_MANAGER_MANIFEST_URL=https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml
|
||||
CERT_MANAGER_MANIFEST_SHA256=5f6a499b8c1857d57f560f536e0dcc830914b45c420899fe7ad0692c8624e408
|
||||
METRICS_SERVER_MANIFEST_URL=https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml
|
||||
METRICS_SERVER_MANIFEST_SHA256=1cec29a5267809306a2c6ec74a3e449abbb705b4a8beed0c8a1963910f72c79b
|
||||
METALLB_MANIFEST_URL=https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml
|
||||
METALLB_MANIFEST_SHA256=b0b9be2802f10aa32d45308b4457d06cde0c70544712c8d0cf5511657ffd2b69
|
||||
METALLB_MANIFEST_GIT_BLOB=7fbda334cc3ac0aaabdcb081af4f543feb3c2f9f
|
||||
# Cabinets — unmodified upstream images, usable on compose or in the cluster.
|
||||
# Pinned by tag; bump freely, and preload them for the offline profile.
|
||||
POSTGRES_IMAGE=postgres:16-alpine
|
||||
REDIS_IMAGE=redis:7-alpine
|
||||
AIRFLOW_IMAGE=apache/airflow:2.10.4
|
||||
|
||||
@@ -5,12 +5,12 @@ digraph rig_environment {
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||
|
||||
label="One environment per folder — copies never collide"
|
||||
label="One environment per directory — copies never collide"
|
||||
labelloc=t
|
||||
fontsize=16
|
||||
fontcolor="#0066ff"
|
||||
|
||||
dirname [label="folder name\nthe overlay's, else rig's\ne.g. platform-v2/" fillcolor="#1f6feb" fontcolor="#ffffff" shape=octagon]
|
||||
dirname [label="directory name\ne.g. acmebank/" fillcolor="#1f6feb" fontcolor="#ffffff" shape=octagon]
|
||||
|
||||
subgraph cluster_derived {
|
||||
label="Everything below is derived from it"
|
||||
@@ -18,11 +18,11 @@ digraph rig_environment {
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
cname [label="cluster name\nplatform-v2" fillcolor="#121829"]
|
||||
ctx [label="kubectl context\nkind-platform-v2" fillcolor="#121829"]
|
||||
img [label="image tag\nplatform-v2-deps" fillcolor="#121829"]
|
||||
ports [label="port block\n20430–20439" fillcolor="#121829"]
|
||||
reg [label="registry container\nplatform-v2-registry" fillcolor="#121829"]
|
||||
cname [label="cluster name\nacmebank" fillcolor="#121829"]
|
||||
ctx [label="kubectl context\nkind-acmebank" fillcolor="#121829"]
|
||||
img [label="image tag\nacmebank-deps" fillcolor="#121829"]
|
||||
ports [label="port block\n21300–21309" fillcolor="#121829"]
|
||||
reg [label="registry container\nacmebank-registry" fillcolor="#121829"]
|
||||
}
|
||||
|
||||
subgraph cluster_config {
|
||||
@@ -32,10 +32,9 @@ digraph rig_environment {
|
||||
fontcolor="#8892a8"
|
||||
|
||||
versions [label="versions.env\npinned toolchain" fillcolor="#121829"]
|
||||
profile [label="env.d/<profile>.env\noptional: registry · mirror" fillcolor="#121829"]
|
||||
overlay [label="<overlay>/rig.env\noptional: what runs" fillcolor="#121829"]
|
||||
profile [label="env.d/<profile>.env\noptional: addons · registry" fillcolor="#121829"]
|
||||
localenv [label="ctrl/.env\nsecrets, overrides" fillcolor="#121829"]
|
||||
shell [label="the environment\nOVERLAY=local/x make …" fillcolor="#1a3a1a" fontcolor="#00c853"]
|
||||
shell [label="the environment\nPROFILE=client make …" fillcolor="#1a3a1a" fontcolor="#00c853"]
|
||||
}
|
||||
|
||||
dirname -> cname
|
||||
@@ -45,8 +44,7 @@ digraph rig_environment {
|
||||
dirname -> reg
|
||||
|
||||
versions -> profile [label="overridden by"]
|
||||
profile -> overlay [label="overridden by"]
|
||||
overlay -> localenv [label="overridden by"]
|
||||
profile -> localenv [label="overridden by"]
|
||||
localenv -> shell [label="overridden by" color="#00c853"]
|
||||
|
||||
cluster [label="kind cluster" fillcolor="#1a1a3a" fontcolor="#0066ff" shape=octagon]
|
||||
|
||||
@@ -4,181 +4,166 @@
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: rig_environment Pages: 1 -->
|
||||
<svg width="981pt" height="585pt"
|
||||
viewBox="0.00 0.00 981.00 585.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 580.74)">
|
||||
<svg width="962pt" height="481pt"
|
||||
viewBox="0.00 0.00 962.00 481.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 476.83)">
|
||||
<title>rig_environment</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-580.74 977,-580.74 977,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="486.5" y="-557.54" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">One environment per folder — copies never collide</text>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-476.83 958,-476.83 958,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="477" y="-453.63" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">One environment per directory — copies never collide</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_derived</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-65 8,-144.5 615,-144.5 615,-65 8,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="311.5" y="-125.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Everything below is derived from it</text>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-65 8,-144.5 596,-144.5 596,-65 8,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="302" y="-125.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Everything below is derived from it</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_config</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="623,-65 623,-541.24 965,-541.24 965,-65 623,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="794" y="-522.04" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Configuration — weakest first, later wins</text>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="604,-65 604,-437.33 946,-437.33 946,-65 604,-65"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="775" y="-418.13" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Configuration — weakest first, later wins</text>
|
||||
</g>
|
||||
<!-- dirname -->
|
||||
<g id="node1" class="node">
|
||||
<title>dirname</title>
|
||||
<polygon fill="#1f6feb" stroke="#1e2a4a" points="411.98,-203.49 411.98,-234.25 346.97,-255.99 255.03,-255.99 190.02,-234.25 190.02,-203.49 255.03,-181.75 346.97,-181.75 411.98,-203.49"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="301" y="-228.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">folder name</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="301" y="-215.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">the overlay's, else rig's</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="301" y="-201.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">e.g. platform-v2/</text>
|
||||
<polygon fill="#1f6feb" stroke="#1e2a4a" points="370.11,-197.44 370.11,-219.63 324.94,-235.33 261.06,-235.33 215.89,-219.63 215.89,-197.44 261.06,-181.75 324.94,-181.75 370.11,-197.44"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-211.59" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">directory name</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">e.g. acmebank/</text>
|
||||
</g>
|
||||
<!-- cname -->
|
||||
<g id="node2" class="node">
|
||||
<title>cname</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="104,-109 16,-109 16,-73 104,-73 104,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="60" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">cluster name</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="60" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">platform-v2</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="60" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank</text>
|
||||
</g>
|
||||
<!-- dirname->cname -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>dirname->cname</title>
|
||||
<path fill="none" stroke="#4a5568" d="M217.9,-193.66C183.86,-181.7 145.02,-165.31 113,-144.5 101.69,-137.15 90.82,-127.09 81.89,-117.75"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="84.65,-115.58 75.31,-110.58 79.49,-120.31 84.65,-115.58"/>
|
||||
<path fill="none" stroke="#4a5568" d="M228.68,-192.51C192.88,-182.36 148.52,-166.7 113,-144.5 101.4,-137.25 90.34,-127.04 81.36,-117.55"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="84.07,-115.32 74.76,-110.26 78.88,-120.02 84.07,-115.32"/>
|
||||
</g>
|
||||
<!-- ctx -->
|
||||
<g id="node3" class="node">
|
||||
<title>ctx</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="228,-109 122,-109 122,-73 228,-73 228,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="175" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">kubectl context</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="175" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">kind-platform-v2</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="223.75,-109 122.25,-109 122.25,-73 223.75,-73 223.75,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="173" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">kubectl context</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="173" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">kind-acmebank</text>
|
||||
</g>
|
||||
<!-- dirname->ctx -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>dirname->ctx</title>
|
||||
<path fill="none" stroke="#4a5568" d="M264.56,-181.46C244.01,-160.94 218.85,-135.8 200.43,-117.41"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="203.11,-115.13 193.56,-110.54 198.16,-120.08 203.11,-115.13"/>
|
||||
<path fill="none" stroke="#4a5568" d="M265.77,-181.32C245.77,-162.07 218.77,-136.06 199.05,-117.08"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="201.55,-114.63 191.91,-110.21 196.69,-119.67 201.55,-114.63"/>
|
||||
</g>
|
||||
<!-- img -->
|
||||
<g id="node4" class="node">
|
||||
<title>img</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="355.88,-109 246.12,-109 246.12,-73 355.88,-73 355.88,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="301" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">image tag</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="301" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">platform-v2-deps</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="344.12,-109 241.88,-109 241.88,-73 344.12,-73 344.12,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">image tag</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="293" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-deps</text>
|
||||
</g>
|
||||
<!-- dirname->img -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>dirname->img</title>
|
||||
<path fill="none" stroke="#4a5568" d="M301,-181.46C301,-162.23 301,-138.96 301,-120.97"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="304.5,-120.98 301,-110.98 297.5,-120.98 304.5,-120.98"/>
|
||||
<path fill="none" stroke="#4a5568" d="M293,-181.32C293,-163.19 293,-139.07 293,-120.47"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="296.5,-120.67 293,-110.67 289.5,-120.67 296.5,-120.67"/>
|
||||
</g>
|
||||
<!-- ports -->
|
||||
<g id="node5" class="node">
|
||||
<title>ports</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="462.38,-109 373.62,-109 373.62,-73 462.38,-73 462.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="418" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">port block</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="418" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">20430–20439</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="451.38,-109 362.62,-109 362.62,-73 451.38,-73 451.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="407" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">port block</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="407" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">21300–21309</text>
|
||||
</g>
|
||||
<!-- dirname->ports -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>dirname->ports</title>
|
||||
<path fill="none" stroke="#4a5568" d="M334.84,-181.46C353.92,-160.94 377.28,-135.8 394.38,-117.41"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="396.49,-120.29 400.73,-110.58 391.36,-115.52 396.49,-120.29"/>
|
||||
<path fill="none" stroke="#4a5568" d="M318.87,-181.32C337.78,-162.15 363.29,-136.3 382,-117.34"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="384.47,-119.81 389,-110.24 379.49,-114.9 384.47,-119.81"/>
|
||||
</g>
|
||||
<!-- reg -->
|
||||
<g id="node6" class="node">
|
||||
<title>reg</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="607.12,-109 480.88,-109 480.88,-73 607.12,-73 607.12,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="544" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">registry container</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="544" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">platform-v2-registry</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="588.38,-109 469.62,-109 469.62,-73 588.38,-73 588.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="529" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">registry container</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="529" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-registry</text>
|
||||
</g>
|
||||
<!-- dirname->reg -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>dirname->reg</title>
|
||||
<path fill="none" stroke="#4a5568" d="M373.79,-190.51C404.52,-177.93 440.23,-161.93 471,-144.5 485.65,-136.2 500.9,-125.58 513.66,-116.06"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="515.41,-119.13 521.26,-110.3 511.18,-113.56 515.41,-119.13"/>
|
||||
<path fill="none" stroke="#4a5568" d="M350.86,-190.3C383.87,-179.35 425.43,-163.64 460,-144.5 474.27,-136.6 488.83,-125.98 500.87,-116.36"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="502.85,-119.26 508.36,-110.21 498.41,-113.84 502.85,-119.26"/>
|
||||
</g>
|
||||
<!-- cluster -->
|
||||
<g id="node12" class="node">
|
||||
<g id="node11" class="node">
|
||||
<title>cluster</title>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="471.81,-10.54 471.81,-25.46 440.29,-36 395.71,-36 364.19,-25.46 364.19,-10.54 395.71,0 440.29,0 471.81,-10.54"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="418" y="-14.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#0066ff">kind cluster</text>
|
||||
<polygon fill="#1a1a3a" stroke="#1e2a4a" points="460.81,-10.54 460.81,-25.46 429.29,-36 384.71,-36 353.19,-25.46 353.19,-10.54 384.71,0 429.29,0 460.81,-10.54"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="407" y="-14.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#0066ff">kind cluster</text>
|
||||
</g>
|
||||
<!-- cname->cluster -->
|
||||
<g id="edge10" class="edge">
|
||||
<g id="edge9" class="edge">
|
||||
<title>cname->cluster</title>
|
||||
<path fill="none" stroke="#4a5568" d="M93.06,-72.61C99.54,-69.72 106.38,-67.01 113,-65 193.43,-40.54 289.9,-28.78 352.5,-23.34"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="352.6,-26.85 362.28,-22.53 352.02,-19.87 352.6,-26.85"/>
|
||||
<path fill="none" stroke="#4a5568" d="M93.35,-72.51C99.75,-69.67 106.48,-67 113,-65 189.55,-41.49 281.19,-29.58 341.58,-23.85"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="341.71,-27.35 351.35,-22.96 341.07,-20.38 341.71,-27.35"/>
|
||||
</g>
|
||||
<!-- ports->cluster -->
|
||||
<g id="edge11" class="edge">
|
||||
<g id="edge10" class="edge">
|
||||
<title>ports->cluster</title>
|
||||
<path fill="none" stroke="#4a5568" d="M418,-72.81C418,-65.23 418,-56.1 418,-47.54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="421.5,-47.54 418,-37.54 414.5,-47.54 421.5,-47.54"/>
|
||||
<path fill="none" stroke="#4a5568" d="M407,-72.81C407,-65.23 407,-56.1 407,-47.54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="410.5,-47.54 407,-37.54 403.5,-47.54 410.5,-47.54"/>
|
||||
</g>
|
||||
<!-- versions -->
|
||||
<g id="node7" class="node">
|
||||
<title>versions</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="764.38,-505.74 657.62,-505.74 657.62,-469.74 764.38,-469.74 764.38,-505.74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-490.79" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">versions.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-477.29" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">pinned toolchain</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="750.38,-401.83 643.62,-401.83 643.62,-365.83 750.38,-365.83 750.38,-401.83"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-386.88" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">versions.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-373.38" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">pinned toolchain</text>
|
||||
</g>
|
||||
<!-- profile -->
|
||||
<g id="node8" class="node">
|
||||
<title>profile</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="788.75,-422.49 633.25,-422.49 633.25,-386.49 788.75,-386.49 788.75,-422.49"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-407.54" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">env.d/<profile>.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-394.04" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">optional: registry · mirror</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="781.5,-318.58 612.5,-318.58 612.5,-282.58 781.5,-282.58 781.5,-318.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-303.63" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">env.d/<profile>.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="697" y="-290.13" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">optional: addons · registry</text>
|
||||
</g>
|
||||
<!-- versions->profile -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>versions->profile</title>
|
||||
<path fill="none" stroke="#4a5568" d="M711,-469.51C711,-459.24 711,-445.94 711,-434.13"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="714.5,-434.49 711,-424.49 707.5,-434.49 714.5,-434.49"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="742.5" y="-443.19" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
</g>
|
||||
<!-- overlay -->
|
||||
<g id="node9" class="node">
|
||||
<title>overlay</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="772.25,-339.24 649.75,-339.24 649.75,-303.24 772.25,-303.24 772.25,-339.24"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-324.29" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0"><overlay>/rig.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-310.79" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">optional: what runs</text>
|
||||
</g>
|
||||
<!-- profile->overlay -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>profile->overlay</title>
|
||||
<path fill="none" stroke="#4a5568" d="M711,-386.26C711,-375.99 711,-362.69 711,-350.88"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="714.5,-351.24 711,-341.24 707.5,-351.24 714.5,-351.24"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="742.5" y="-359.94" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
<path fill="none" stroke="#4a5568" d="M697,-365.59C697,-355.32 697,-342.03 697,-330.21"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="700.5,-330.58 697,-320.58 693.5,-330.58 700.5,-330.58"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="728.5" y="-339.28" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
</g>
|
||||
<!-- localenv -->
|
||||
<g id="node10" class="node">
|
||||
<g id="node9" class="node">
|
||||
<title>localenv</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="768.88,-236.87 653.12,-236.87 653.12,-200.87 768.88,-200.87 768.88,-236.87"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-221.92" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">ctrl/.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-208.42" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">secrets, overrides</text>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="752.88,-226.54 637.12,-226.54 637.12,-190.54 752.88,-190.54 752.88,-226.54"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="695" y="-211.59" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">ctrl/.env</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="695" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">secrets, overrides</text>
|
||||
</g>
|
||||
<!-- overlay->localenv -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>overlay->localenv</title>
|
||||
<path fill="none" stroke="#4a5568" d="M711,-302.76C711,-287.79 711,-265.95 711,-248.44"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="714.5,-248.67 711,-238.67 707.5,-248.67 714.5,-248.67"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="742.5" y="-276.69" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
<!-- profile->localenv -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>profile->localenv</title>
|
||||
<path fill="none" stroke="#4a5568" d="M696.61,-282.22C696.34,-269.76 695.96,-252.69 695.64,-238.23"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="699.14,-238.28 695.42,-228.36 692.14,-238.43 699.14,-238.28"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="727.68" y="-256.03" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
</g>
|
||||
<!-- shell -->
|
||||
<g id="node11" class="node">
|
||||
<g id="node10" class="node">
|
||||
<title>shell</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="791,-109 631,-109 631,-73 791,-73 791,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">the environment</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="711" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">OVERLAY=local/x make …</text>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="763.38,-109 614.62,-109 614.62,-73 763.38,-73 763.38,-109"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="689" y="-94.05" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">the environment</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="689" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">PROFILE=client make …</text>
|
||||
</g>
|
||||
<!-- localenv->shell -->
|
||||
<g id="edge9" class="edge">
|
||||
<g id="edge8" class="edge">
|
||||
<title>localenv->shell</title>
|
||||
<path fill="none" stroke="#00c853" d="M711,-200.63C711,-180.03 711,-145.27 711,-120.62"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="714.5,-120.95 711,-110.95 707.5,-120.95 714.5,-120.95"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="742.5" y="-155.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
<path fill="none" stroke="#00c853" d="M694.11,-190.49C693.16,-172.16 691.63,-142.72 690.49,-120.79"/>
|
||||
<polygon fill="#00c853" stroke="#00c853" points="694,-120.81 689.99,-111.01 687.01,-121.18 694,-120.81"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="724.21" y="-155.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
||||
</g>
|
||||
<!-- shell->cluster -->
|
||||
<g id="edge12" class="edge">
|
||||
<g id="edge11" class="edge">
|
||||
<title>shell->cluster</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M648.24,-72.58C638.47,-69.98 628.47,-67.37 619,-65 570.38,-52.84 514.82,-40.22 474.45,-31.28"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="475.23,-27.87 464.71,-29.13 473.72,-34.7 475.23,-27.87"/>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M628.29,-72.6C618.83,-69.99 609.17,-67.38 600,-65 553.72,-52.99 500.89,-40.48 462.22,-31.54"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="463.2,-28.18 452.67,-29.35 461.63,-35 463.2,-28.18"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
56
rig/docs/graphs/03-architecture.dot
Normal file
56
rig/docs/graphs/03-architecture.dot
Normal file
@@ -0,0 +1,56 @@
|
||||
// TODO: PLACEHOLDER — replace with the real estate topology.
|
||||
//
|
||||
// This is where the extracted platform diagrams land. The shape below is
|
||||
// illustrative only: it shows how a mocked dependency, a real service and a
|
||||
// remote system are meant to sit together, not what the system actually is.
|
||||
//
|
||||
// The intended end state is that this file stops being hand-written and is
|
||||
// generated from the running cluster, so the diagram becomes a report of what
|
||||
// exists rather than a drawing of what was once intended.
|
||||
digraph estate {
|
||||
rankdir=LR
|
||||
bgcolor="#0a0e17"
|
||||
fontname="Helvetica"
|
||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0" shape=box]
|
||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||
|
||||
label="Estate topology — PLACEHOLDER"
|
||||
labelloc=t
|
||||
fontsize=16
|
||||
fontcolor="#ffc107"
|
||||
|
||||
subgraph cluster_new {
|
||||
label="New"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
api [label="service under work\n(real: built and hot-reloaded)" fillcolor="#1a3a1a" fontcolor="#00c853"]
|
||||
}
|
||||
|
||||
subgraph cluster_core {
|
||||
label="Core (mocked)"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
svc_a [label="upstream service\n(mock: canned responses)" fillcolor="#121829"]
|
||||
db [label="datastore\n(mock)" fillcolor="#121829" shape=cylinder]
|
||||
}
|
||||
|
||||
subgraph cluster_legacy {
|
||||
label="Legacy estate (mocked)"
|
||||
style=dashed
|
||||
color="#1e2a4a"
|
||||
fontcolor="#8892a8"
|
||||
|
||||
batch [label="batch drop\n(mock: writes files on a timer)" fillcolor="#121829"]
|
||||
}
|
||||
|
||||
remote [label="external system\n(remote: ExternalName,\nreachable only from a VDI)" fillcolor="#3a1a1a" fontcolor="#ffc107" shape=octagon]
|
||||
|
||||
api -> svc_a [label="HTTP"]
|
||||
api -> db [label="query"]
|
||||
api -> batch [label="file handoff" style=dashed]
|
||||
api -> remote [label="only when reachable" style=dashed color="#ffc107"]
|
||||
}
|
||||
94
rig/docs/graphs/03-architecture.svg
Normal file
94
rig/docs/graphs/03-architecture.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 14.1.2 (0)
|
||||
-->
|
||||
<!-- Title: estate Pages: 1 -->
|
||||
<svg width="579pt" height="362pt"
|
||||
viewBox="0.00 0.00 579.00 362.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 357.62)">
|
||||
<title>estate</title>
|
||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-357.62 575.41,-357.62 575.41,4 -4,4"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="285.7" y="-334.42" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#ffc107">Estate topology — PLACEHOLDER</text>
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_new</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-130.12 8,-210.12 199,-210.12 199,-130.12 8,-130.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="103.5" y="-190.92" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">New</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_core</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="357.33,-172.12 357.33,-318.12 534.83,-318.12 534.83,-172.12 357.33,-172.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-298.92" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Core (mocked)</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_legacy</title>
|
||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="341.83,-84.12 341.83,-164.12 551.33,-164.12 551.33,-84.12 341.83,-84.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.58" y="-144.92" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Legacy estate (mocked)</text>
|
||||
</g>
|
||||
<!-- api -->
|
||||
<g id="node1" class="node">
|
||||
<title>api</title>
|
||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="191,-174.12 16,-174.12 16,-138.12 191,-138.12 191,-174.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="103.5" y="-159.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">service under work</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="103.5" y="-145.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">(real: built and hot-reloaded)</text>
|
||||
</g>
|
||||
<!-- svc_a -->
|
||||
<g id="node2" class="node">
|
||||
<title>svc_a</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="526.83,-282.12 365.33,-282.12 365.33,-246.12 526.83,-246.12 526.83,-282.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-267.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">upstream service</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-253.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(mock: canned responses)</text>
|
||||
</g>
|
||||
<!-- api->svc_a -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>api->svc_a</title>
|
||||
<path fill="none" stroke="#4a5568" d="M149.07,-174.56C167.54,-182.07 189.23,-190.7 209,-198.12 258.25,-216.6 270.12,-222.85 320.75,-237.12 331.43,-240.13 342.71,-243 353.94,-245.67"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="353.12,-249.07 363.66,-247.92 354.71,-242.25 353.12,-249.07"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-234.24" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">HTTP</text>
|
||||
</g>
|
||||
<!-- db -->
|
||||
<g id="node3" class="node">
|
||||
<title>db</title>
|
||||
<path fill="#121829" stroke="#1e2a4a" d="M480.7,-223.81C480.7,-226.22 465.18,-228.18 446.08,-228.18 426.97,-228.18 411.45,-226.22 411.45,-223.81 411.45,-223.81 411.45,-184.43 411.45,-184.43 411.45,-182.02 426.97,-180.06 446.08,-180.06 465.18,-180.06 480.7,-182.02 480.7,-184.43 480.7,-184.43 480.7,-223.81 480.7,-223.81"/>
|
||||
<path fill="none" stroke="#1e2a4a" d="M480.7,-223.81C480.7,-221.39 465.18,-219.43 446.08,-219.43 426.97,-219.43 411.45,-221.39 411.45,-223.81"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-207.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">datastore</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-193.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(mock)</text>
|
||||
</g>
|
||||
<!-- api->db -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>api->db</title>
|
||||
<path fill="none" stroke="#4a5568" d="M191.32,-168.36C257.83,-177.73 346.91,-190.28 399.91,-197.75"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="399.4,-201.22 409.79,-199.15 400.38,-194.29 399.4,-201.22"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-185.69" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">query</text>
|
||||
</g>
|
||||
<!-- batch -->
|
||||
<g id="node4" class="node">
|
||||
<title>batch</title>
|
||||
<polygon fill="#121829" stroke="#1e2a4a" points="537.33,-128.12 354.83,-128.12 354.83,-92.12 537.33,-92.12 537.33,-128.12"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-113.17" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">batch drop</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-99.67" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">(mock: writes files on a timer)</text>
|
||||
</g>
|
||||
<!-- api->batch -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>api->batch</title>
|
||||
<path fill="none" stroke="#4a5568" stroke-dasharray="5,2" d="M191.32,-144.39C237.67,-138.13 294.98,-130.39 343.4,-123.85"/>
|
||||
<polygon fill="#4a5568" stroke="#4a5568" points="343.61,-127.36 353.05,-122.55 342.67,-120.42 343.61,-127.36"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-143.94" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">file handoff</text>
|
||||
</g>
|
||||
<!-- remote -->
|
||||
<g id="node5" class="node">
|
||||
<title>remote</title>
|
||||
<polygon fill="#3a1a1a" stroke="#1e2a4a" points="571.41,-21.74 571.41,-52.5 497.99,-74.24 394.17,-74.24 320.75,-52.5 320.75,-21.74 394.17,0 497.99,0 571.41,-21.74"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-46.92" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">external system</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-33.42" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">(remote: ExternalName,</text>
|
||||
<text xml:space="preserve" text-anchor="middle" x="446.08" y="-19.92" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffc107">reachable only from a VDI)</text>
|
||||
</g>
|
||||
<!-- api->remote -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>api->remote</title>
|
||||
<path fill="none" stroke="#ffc107" stroke-dasharray="5,2" d="M149.55,-137.68C167.9,-130.34 189.37,-121.97 209,-114.87 254.45,-98.43 305.32,-81.49 348.17,-67.64"/>
|
||||
<polygon fill="#ffc107" stroke="#ffc107" points="349,-71.05 357.44,-64.65 346.85,-64.39 349,-71.05"/>
|
||||
<text xml:space="preserve" text-anchor="middle" x="255.88" y="-117.57" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">only when reachable</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -2,7 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="data:,">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>rig — local environment installer</title>
|
||||
<style>
|
||||
@@ -238,9 +237,12 @@
|
||||
<div class="nav-backdrop"></div>
|
||||
<nav>
|
||||
<a href="#start">Start here</a>
|
||||
<a href="#steps">The steps</a>
|
||||
<a href="#install">Installation</a>
|
||||
<a href="#environments">Environments</a>
|
||||
<a href="#overlays">Overlays</a>
|
||||
<a href="#profiles">Profiles</a>
|
||||
<a href="#registry">Registry</a>
|
||||
<a href="#architecture">Architecture</a>
|
||||
<a href="#troubleshooting">Troubleshooting</a>
|
||||
</nav>
|
||||
|
||||
@@ -248,68 +250,113 @@
|
||||
|
||||
<section class="section" id="start">
|
||||
<h2>Start here</h2>
|
||||
<p class="lede">A runnable local model of a large, regulated estate — legacy and new side by side.</p>
|
||||
<div class="prose">
|
||||
<p>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
|
||||
<b>onboarding and exploration</b>, not a production replica.</p>
|
||||
<p>rig builds a disposable Kubernetes environment on your machine so you can
|
||||
explore how a system fits together without needing access to any of it. Its
|
||||
job is <b>onboarding and exploration</b>, not a production replica.</p>
|
||||
|
||||
<p>Most services in it are <b>not real</b>. 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.</p>
|
||||
<p>Most services in it are deliberately <b>not real</b>. What has to be faithful
|
||||
is the topology — the names, the ports, the dependency order, who can reach whom,
|
||||
and how it fails. The workloads themselves are noise. This is what makes the
|
||||
whole estate fit on a laptop: a real 20-service platform will not fit even once
|
||||
on 14 GB, but mocks are about 30 MB each, so three faithful copies do.</p>
|
||||
|
||||
<h3>The only prerequisite</h3>
|
||||
<p><b>Docker.</b> No curl, no jq, no python, no apt repositories to configure.</p>
|
||||
<pre><code><span class="c"># in the environment directory:</span>
|
||||
make check <span class="c"># is this machine ready? reports, never changes anything</span>
|
||||
make deps <span class="c"># install the pinned toolchain; safe to re-run</span>
|
||||
<pre><code><span class="c"># then, in the environment directory:</span>
|
||||
make check <span class="c"># is this machine ready? reports, never fixes</span>
|
||||
make deps <span class="c"># install the pinned toolchain</span>
|
||||
make cluster up <span class="c"># cluster + registry + addons; ports derive by themselves</span>
|
||||
</code></pre>
|
||||
<p>Read <code>make check</code> before <code>make deps</code>: it lists what is
|
||||
missing, including the steps only you can do, such as anything needing
|
||||
<code>sudo</code> or a Windows-side restart.</p>
|
||||
|
||||
<h3>make cluster up</h3>
|
||||
<p>It prints what the kind config locks in before spending the time, because
|
||||
that config is fixed when the cluster is created. Re-running is safe and
|
||||
continues where an interrupted run stopped, rather than reporting that the
|
||||
cluster already exists and leaving every node <code>NotReady</code>.</p>
|
||||
<p>Read <code>make check</code> before <code>make deps</code>. It never changes
|
||||
anything — it prints what it found and, at the end, the steps it cannot perform
|
||||
for you.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="steps">
|
||||
<h2>The steps</h2>
|
||||
<p class="lede">Start to finish, in order, with what each one actually does.</p>
|
||||
<div class="prose">
|
||||
|
||||
<h3>1 · make check</h3>
|
||||
<p>Asks whether this machine is ready. It <b>changes nothing</b> — it
|
||||
reports what it found and, at the end, the things only a human can do
|
||||
(anything needing <code>sudo</code>, or a Windows-side restart). Read it
|
||||
before installing anything; it is faster than discovering the same problems
|
||||
one failure at a time.</p>
|
||||
<pre><code>make check</code></pre>
|
||||
|
||||
<h3>2 · make deps</h3>
|
||||
<p>Installs the pinned toolchain — only what is missing — and tells you
|
||||
if its directory is not on PATH yet. Running it twice is safe.</p>
|
||||
<pre><code>make deps</code></pre>
|
||||
|
||||
<h3>3 · make cluster up</h3>
|
||||
<p>Builds the cluster, starts its registry and installs the profile's
|
||||
addons — there is nothing else to run first. It prints what the profile
|
||||
locks in <i>before</i> spending the time, because the kind config is
|
||||
fixed at creation and cannot be changed afterwards.</p>
|
||||
<p>Re-running is safe and, more importantly, <b>convergent</b>: if a first
|
||||
attempt was interrupted before the CNI was installed, running it again
|
||||
finishes the job rather than reporting "already exists" and leaving every
|
||||
node permanently NotReady.</p>
|
||||
<pre><code>make cluster up <span class="c"># built-in defaults — no profile needed</span>
|
||||
make cluster up PROFILE=mirror <span class="c"># images through an internal registry</span>
|
||||
make cluster up OVERLAY=examples/data <span class="c"># an overlay: what runs, kept outside rig</span>
|
||||
make cluster reset <span class="c"># destroy and rebuild: how an edited kind config takes effect</span>
|
||||
make cluster up PROFILE=client <span class="c"># after copying env.d/client.env.example: cached registry</span>
|
||||
make cluster reset <span class="c"># destroy and rebuild — how an edited kind config takes effect</span>
|
||||
</code></pre>
|
||||
|
||||
<h3>4 · make docs</h3>
|
||||
<p>Serves this page from a throwaway container. Works with no cluster and
|
||||
no toolchain, which is deliberate: these pages are the instructions for
|
||||
building everything else, so they cannot depend on it.</p>
|
||||
<pre><code>make docs</code></pre>
|
||||
|
||||
<h3>Checking on things</h3>
|
||||
<dl>
|
||||
<dt>make cluster list</dt><dd>Every cluster on the machine, its memory cost and its port block. A new cluster usually fails to start because of an old one; <code>make cluster free</code> stops them without deleting.</dd>
|
||||
<dt>make check</dt><dd>Host, toolchain, and whether this cluster fits, its ports, registry and addons. Only problems print; <code>make check all</code> prints everything.</dd>
|
||||
<dt>make check mem</dt><dd>Memory in depth: what caps it, how far it climbs, and on WSL the <code>.wslconfig</code> backup and restore.</dd>
|
||||
<dt>make docs</dt><dd>Serves these pages from a throwaway container. It needs no cluster and no toolchain, because these are the instructions for building both.</dd>
|
||||
<dt>make cluster list</dt><dd>Every cluster on the machine, its memory cost and its port block. The usual reason a new one will not start is an old one you forgot about; <code>make cluster free</code> frees them without deleting.</dd>
|
||||
<dt>make check</dt><dd>Short: host, toolchain, and whether this cluster fits, its ports, registry and addons. Details only appear when something needs attention; <code>make check all</code> prints every one.</dd>
|
||||
<dt>make check mem</dt><dd>Memory in depth: what caps it, how far it really climbs, and on WSL the <code>.wslconfig</code> backup and restore.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Running more than one</h3>
|
||||
<p>Copy the directory, rename it, and run <code>make cluster up</code>. Cluster name,
|
||||
context, image tags and the port block all follow the directory name, so
|
||||
the second environment collides with nothing and neither one's teardown can
|
||||
reach the other.</p>
|
||||
<pre><code>cp -r rig ../platform-v2 && cd ../platform-v2
|
||||
make cluster up
|
||||
</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="install">
|
||||
<h2>Installation</h2>
|
||||
<p class="lede">A container installs onto the host and then gets out of the way.</p>
|
||||
<div class="graph-container">
|
||||
<a href="viewer.html?src=graphs/01-install.svg"><img src="graphs/01-install.svg" alt="How the installer works"></a>
|
||||
<a href="viewer.html?src=graphs/01-install.svg"><img src="graphs/01-install.svg" alt="Installation flow"></a>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>The installer is a container, not a shell script, because a stock slim Debian
|
||||
has no curl, wget, jq, python3 or CA bundle: a shell installer could not make a
|
||||
verified HTTPS request, let alone check one. The container carries its own
|
||||
toolchain, so the host needs only Docker. Nothing runs inside it afterwards —
|
||||
kind, kubectl, tilt and jq run natively.</p>
|
||||
|
||||
<p>Every tool is a single binary at a pinned version, checked against a published
|
||||
SHA256. Node images are pinned <b>by digest</b>, so upgrading kind cannot move
|
||||
your Kubernetes version.</p>
|
||||
|
||||
<h3>Two tiers</h3>
|
||||
<p>A managed or corporate-issued machine is not somewhere to install development
|
||||
tools by default, so installing cluster tooling is a decision rather than a side
|
||||
effect:</p>
|
||||
<p>The installer is a container, not a shell script, for a specific reason: a
|
||||
stock slim Debian has no <code>curl</code>, no <code>wget</code>, no
|
||||
<code>jq</code>, no <code>python3</code> and <b>no CA bundle</b>. A shell
|
||||
installer could not make a verified HTTPS request, let alone check one. The
|
||||
container carries its own toolchain, so the host needs nothing but Docker.</p>
|
||||
|
||||
<p>The cluster never runs inside that container. Everything it installs —
|
||||
kind, kubectl, tilt, jq — runs natively afterwards, so nothing pays a
|
||||
container tax during daily work.</p>
|
||||
|
||||
<h3>Pinned and verified</h3>
|
||||
<p>Every tool is a single binary fetched at a pinned version and checked
|
||||
against a published SHA256. Node images are pinned <b>by digest</b>, so
|
||||
upgrading kind cannot silently move your Kubernetes version.</p>
|
||||
|
||||
<h3>Not every machine should get cluster tooling</h3>
|
||||
<p>A managed or corporate-issued machine — the kind that holds the access
|
||||
you cannot get anywhere else — is not somewhere to install development
|
||||
tools by default. So the toolchain comes in two tiers:</p>
|
||||
<table>
|
||||
<tr><th>tier</th><th>installs</th><th>for</th></tr>
|
||||
<tr><td><code>core</code></td><td>kubectl, jq</td><td>talk to a cluster someone else runs</td></tr>
|
||||
@@ -318,6 +365,11 @@ make cluster reset <span class="c"># destroy and rebuild: ho
|
||||
<pre><code>make deps core <span class="c"># kubectl and jq only — nothing that creates a cluster</span>
|
||||
make deps <span class="c"># dev, the default</span>
|
||||
</code></pre>
|
||||
<p>Testing <i>in situ</i> on a managed machine is still possible — install
|
||||
the <code>dev</code> tier deliberately when you need it. The point is that
|
||||
it should be a decision rather than a side effect of installing.</p>
|
||||
<p>The documentation itself needs neither tier: <code>make docs</code>
|
||||
wants only Docker.</p>
|
||||
|
||||
<h3>Air-gapped</h3>
|
||||
<pre><code>make deps image full <span class="c"># bakes every binary into the image</span>
|
||||
@@ -330,129 +382,160 @@ docker load < rig.tgz && make cluster up PROFILE=offline <span clas
|
||||
|
||||
<section class="section" id="environments">
|
||||
<h2>Environments</h2>
|
||||
<p class="lede">One directory is one environment. Copy it, rename it, run it.</p>
|
||||
<div class="graph-container">
|
||||
<a href="viewer.html?src=graphs/02-environment.svg"><img src="graphs/02-environment.svg" alt="Environment derivation"></a>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>One folder is one environment. Running several versions of a system at once
|
||||
means several clusters on one machine, and everything that could collide is
|
||||
derived from the folder name (the overlay's, when one is named):</p>
|
||||
<p>Running several versions of a system at once means several clusters on one
|
||||
machine, not several machines. Everything that could collide is derived from
|
||||
the directory name:</p>
|
||||
<dl>
|
||||
<dt>cluster + context</dt><dd><code>platform-v2/</code> builds <code>platform-v2</code> on <code>kind-platform-v2</code>.</dd>
|
||||
<dt>port block</dt><dd>Ten ports from a hash of the name, above 20000 — clear of 80, 443, 3000, 5432, 8000 and 8080.</dd>
|
||||
<dt>cluster + context</dt><dd><code>acmebank/</code> builds <code>acmebank</code> on <code>kind-acmebank</code>.</dd>
|
||||
<dt>port block</dt><dd>Ten ports from a hash of the name, in the 20000+ range — clear of 80, 443, 3000, 5432, 8000 and 8080.</dd>
|
||||
<dt>registry + images</dt><dd>Named after the environment, so two copies never share one.</dd>
|
||||
</dl>
|
||||
<p>So two copies never collide, and neither one's <code>make cluster down</code>
|
||||
can touch the other. <code>make check</code> shows the block;
|
||||
<code>bash ctrl/ports.sh persist</code> freezes it into <code>ctrl/.env</code>.</p>
|
||||
<p>Two copies therefore never collide, and neither one's
|
||||
<code>make cluster down</code> can touch the other. <code>make check</code>
|
||||
shows the block; <code>bash ctrl/ports.sh persist</code> freezes it into
|
||||
<code>ctrl/.env</code> if you want it fixed rather than derived.</p>
|
||||
|
||||
<h3>Configuration layers</h3>
|
||||
<p>Weakest first, later wins: built-in defaults → pinned versions → a profile, if
|
||||
you name one → the overlay's <code>rig.env</code> → <code>ctrl/.env</code> → the
|
||||
caller's environment. So a value on the command line always beats every file.</p>
|
||||
<p>Weakest first, later wins: built-in defaults → pinned versions → a
|
||||
profile, if you name one → <code>ctrl/.env</code> → the environment. So
|
||||
<code>make cluster up PROFILE=<name></code> always beats every file.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<h3>Profiles</h3>
|
||||
<p>A profile says how this machine reaches the world. rig needs none.</p>
|
||||
<section class="section" id="profiles">
|
||||
<h2>Profiles</h2>
|
||||
<p class="lede">Optional overlays — rig needs none.</p>
|
||||
<div class="prose">
|
||||
<table>
|
||||
<tr><th>example</th><th>registry</th><th>for</th></tr>
|
||||
<tr><td>none</td><td><code>local</code></td><td>the built-in defaults</td></tr>
|
||||
<tr><td><code>mirror.env.example</code></td><td><code>mirror</code></td><td>images through an internal registry</td></tr>
|
||||
<tr><td><code>offline.env.example</code></td><td><code>local</code></td><td>air-gapped</td></tr>
|
||||
<tr><td><i>none</i></td><td>local</td><td>the built-in defaults; no profile needed</td></tr>
|
||||
<tr><td><code>client.env.example</code></td><td>mirror</td><td>images through a corporate registry</td></tr>
|
||||
<tr><td><code>offline.env.example</code></td><td>local</td><td>air-gapped</td></tr>
|
||||
<tr><td><code>data.env.example</code></td><td>local</td><td>postgres, redis, airflow</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Registry modes</h3>
|
||||
<table>
|
||||
<tr><th>mode</th><th>what it does</th></tr>
|
||||
<tr><td><code>none</code></td><td>images are built straight into the node</td></tr>
|
||||
<tr><td><code>local</code></td><td>a registry container wired into the cluster</td></tr>
|
||||
<tr><td><code>mirror</code></td><td>that container as a pull-through cache of the corporate registry</td></tr>
|
||||
<tr><td><code>remote</code></td><td>no local container; pull direct with an <code>imagePullSecret</code></td></tr>
|
||||
</table>
|
||||
<p><code>mirror</code> is what a locked-down network looks like: images come from
|
||||
the corporate registry, you do not hammer it, and you keep working when the
|
||||
connection drops. It is also only reachable from where that registry is routable —
|
||||
from a laptop, use <code>local</code> or <code>offline</code>.</p>
|
||||
<p><b>Corporate CA trust is the usual failure.</b> It has to reach three places:
|
||||
the host Docker daemon, every node's containerd (nodes do not inherit host trust),
|
||||
and any in-cluster client. Set <code>REGISTRY_CA_FILE</code>;
|
||||
<code>make check</code> reports which is still missing. Without it the error is
|
||||
<code>x509: certificate signed by unknown authority</code>.</p>
|
||||
<div class="note"><p><b>The kind config cannot be re-applied.</b> Edit
|
||||
<code>ctrl/k8s/kind-config.yaml.tpl</code>; it takes effect when the cluster is created. <code>cluster up</code> prints what it
|
||||
locks in before spending the time, and <code>make cluster reset</code> is
|
||||
the way out.</p></div>
|
||||
|
||||
<h3>LoadBalancer services</h3>
|
||||
<p>Real manifests use <code>type: LoadBalancer</code>. On a bare local cluster
|
||||
those Services sit at <code>EXTERNAL-IP <pending></code> forever, with no
|
||||
error: the deployment looks healthy and is unreachable. The <code>metallb</code>
|
||||
addon fixes that, so upstream manifests work unchanged. Its address pool is
|
||||
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>
|
||||
|
||||
<h3>Networking and memory</h3>
|
||||
<p>The cluster uses kind's built-in networking, which does enforce standard
|
||||
NetworkPolicy — verified against a no-policy control. A pluggable CNI was tried
|
||||
and removed: it added policy features nothing here needs, in exchange for a
|
||||
slower boot and one more thing to get right at creation time.</p>
|
||||
<p>Every cluster is a running container tree whether you use it or not.
|
||||
<code>make cluster list</code> shows what each costs, and
|
||||
<p>Real manifests use <code>type: LoadBalancer</code>, because a real
|
||||
cluster has one. On a bare local cluster those Services sit at
|
||||
<code>EXTERNAL-IP <pending></code> forever, with no error anywhere —
|
||||
the deployment looks healthy and simply is not reachable.</p>
|
||||
<p>The <code>metallb</code> addon fixes that, so the same manifests work
|
||||
here as upstream and nothing has to be rewritten to NodePort. Its address
|
||||
pool is derived from the cluster's Docker network at install time rather
|
||||
than hardcoded, because Docker picks that subnet and it differs between
|
||||
machines.</p>
|
||||
<div class="note"><p><b>Where those addresses are reachable from.</b> The
|
||||
pool lives on the Docker bridge, so LoadBalancer IPs work from the Linux
|
||||
side — including from inside WSL. A browser on Windows has no route to
|
||||
them. Use the ingress host ports for anything you need to open in a
|
||||
browser.</p></div>
|
||||
|
||||
<h3>Networking</h3>
|
||||
<p>The cluster uses kind's built-in networking, which <b>does</b> enforce
|
||||
standard NetworkPolicy — verified against a no-policy control, not assumed.
|
||||
The widely repeated claim that it accepts policies and silently ignores
|
||||
them is out of date.</p>
|
||||
<p>A pluggable CNI was tried and removed: it only added
|
||||
GlobalNetworkPolicy, policy tiers and egress-CIDR rules, none of which are
|
||||
needed yet, in exchange for a slower boot and one more thing that has to be
|
||||
right at creation time. Worth revisiting only when a policy the built-in
|
||||
cannot express actually comes up.</p>
|
||||
|
||||
<h3>Memory</h3>
|
||||
<p>Every cluster is a running container tree whether you are using it or not.
|
||||
<code>make cluster list</code> shows what exists and what it costs;
|
||||
<code>make cluster free</code> stops the others without deleting them.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="overlays">
|
||||
<h2>Overlays</h2>
|
||||
<section class="section" id="registry">
|
||||
<h2>Registry</h2>
|
||||
<p class="lede">Local, cached, or straight to the corporate registry.</p>
|
||||
<div class="prose">
|
||||
<p>An overlay is one folder, kept outside rig's version control, holding a use
|
||||
case. rig reads it and never writes into it. Every piece is optional:</p>
|
||||
<table>
|
||||
<tr><th>in the overlay</th><th>what rig does with it</th></tr>
|
||||
<tr><td><code>rig.env</code></td><td>a config layer: addons, namespaces, images — anything a profile could set</td></tr>
|
||||
<tr><td><code>k8s/overlays/dev/</code></td><td>the manifests the dev loop applies</td></tr>
|
||||
<tr><td><code>kind-config.yaml.tpl</code></td><td>the cluster's shape, when it needs its own (mounts, ports)</td></tr>
|
||||
<tr><td><code>addons/<name>.sh</code></td><td>addons, found before rig's own; each applies its own <code><name>.yaml</code></td></tr>
|
||||
<tr><td><code>Tiltfile</code></td><td>the workload's half of the dev loop, included by rig's</td></tr>
|
||||
<tr><th>mode</th><th>what it does</th></tr>
|
||||
<tr><td><code>none</code></td><td>images are built straight into the node</td></tr>
|
||||
<tr><td><code>local</code></td><td>a registry container wired into the cluster</td></tr>
|
||||
<tr><td><code>mirror</code></td><td>that container as a <b>pull-through cache</b> of the corporate registry</td></tr>
|
||||
<tr><td><code>remote</code></td><td>no local container; pull direct with an imagePullSecret</td></tr>
|
||||
</table>
|
||||
<pre><code>cp -r examples/starter local/myenv <span class="c"># local/ is gitignored</span>
|
||||
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><project>/rig/</code> and be
|
||||
the overlay itself, with a three-line forwarding Makefile — 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
|
||||
one-line change:</p>
|
||||
<p><code>mirror</code> is what a locked-down network actually looks like:
|
||||
images originate from the corporate registry, you do not hammer it, and you
|
||||
keep working when the connection drops.</p>
|
||||
|
||||
<div class="note"><p><b>The corporate CA will bite you.</b> A corporate
|
||||
registry is usually behind an internal CA, and trust has to reach
|
||||
<b>three</b> places: the host Docker daemon, every cluster node's containerd
|
||||
(nodes do <i>not</i> inherit host trust), and any in-cluster client. Set
|
||||
<code>REGISTRY_CA_FILE</code> and <code>make check</code> reports which is
|
||||
still missing. The symptom otherwise is an opaque
|
||||
<code>x509: certificate signed by unknown authority</code>.</p></div>
|
||||
|
||||
<p>Reachability also depends on where you are: if the registry is only
|
||||
routable from a managed workspace, <code>mirror</code> and <code>remote</code>
|
||||
will not resolve from a laptop at all. That is what <code>local</code> and
|
||||
<code>offline</code> are for.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="architecture">
|
||||
<h2>Architecture</h2>
|
||||
<p class="lede">The estate being modelled.</p>
|
||||
<div class="note"><p><b>TODO — placeholder.</b> The diagram below is
|
||||
illustrative only: it shows how a mocked dependency, a service under active
|
||||
work, and an unreachable remote system sit together. It is not the real
|
||||
topology. Replace <code>docs/graphs/03-architecture.dot</code> with the
|
||||
extracted platform diagrams, then run <code>make docs graphs</code>.</p></div>
|
||||
<div class="graph-container">
|
||||
<a href="viewer.html?src=graphs/03-architecture.svg"><img src="graphs/03-architecture.svg" alt="Estate topology (placeholder)"></a>
|
||||
</div>
|
||||
<div class="prose">
|
||||
<p>Each component is one of three things, and switching between them should be
|
||||
a one-line change rather than a rewrite:</p>
|
||||
<dl>
|
||||
<dt>real</dt><dd>Built from source and hot-reloaded. What you are working on — usually exactly one.</dd>
|
||||
<dt>real</dt><dd>Built from source and hot-reloaded. The thing you are actually working on — usually exactly one.</dd>
|
||||
<dt>mock</dt><dd>A generic stub with canned responses. Everything you do not care about today.</dd>
|
||||
<dt>remote</dt><dd>No pod at all: a Service of type <code>ExternalName</code> pointing at the real system. In-cluster DNS resolves identically, so callers never change.</dd>
|
||||
<dt>remote</dt><dd>No pod at all: a Service of type ExternalName pointing at the real system. In-cluster DNS resolves identically, so callers never change.</dd>
|
||||
</dl>
|
||||
<p>The intended end state is that this diagram is <b>generated from the
|
||||
running cluster</b> rather than drawn by hand — so it becomes a report of
|
||||
what exists instead of a picture of what was once intended.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" id="troubleshooting">
|
||||
<h2>Troubleshooting</h2>
|
||||
<p class="lede">The failures that are hard to diagnose from their symptoms.</p>
|
||||
<div class="prose">
|
||||
<h3>Tilt stops noticing file changes</h3>
|
||||
<p>Almost always inotify limits, and it fails silently: nothing errors, changes
|
||||
just stop being picked up. Defaults on WSL are far too low.
|
||||
<code>make check</code> reports it and prints the fix.</p>
|
||||
<p>Almost always <code>inotify</code> limits, and it fails <i>silently</i> —
|
||||
nothing errors, changes just stop being picked up. Defaults on WSL are far too
|
||||
low. <code>make check</code> reports it and prints the fix.</p>
|
||||
|
||||
<h3>Cluster creation dies halfway with a port error</h3>
|
||||
<p>Docker reports <code>failed to bind host port … address already in use</code>
|
||||
partway through creating the cluster. <code>make check</code> tests every port in
|
||||
this environment's block before anything is built.</p>
|
||||
partway through creating the cluster. Run <code>make check</code> first — it
|
||||
checks every port in this environment's block before anything is built.</p>
|
||||
|
||||
<h3>Every node stays NotReady</h3>
|
||||
<p>Usually a cluster created with the default CNI disabled and the real CNI never
|
||||
installed, after an interrupted first run. Run <code>make cluster up</code> again:
|
||||
it finishes the missing steps instead of stopping because the cluster exists.</p>
|
||||
<p>Usually a cluster created with the default CNI disabled but the real CNI
|
||||
never installed — typically an interrupted first run. Just run
|
||||
<code>make cluster up</code> again: it converges rather than exiting early, and
|
||||
will finish the missing steps.</p>
|
||||
|
||||
<h3>x509: certificate signed by unknown authority</h3>
|
||||
<p>Corporate CA trust has not reached one of the three places it needs to be. See
|
||||
<a href="#environments">Environments</a>.</p>
|
||||
<p>Corporate CA trust has not reached one of the three places it needs to be.
|
||||
See <a href="#registry">Registry</a>.</p>
|
||||
|
||||
<h3>kubectl says the context does not exist</h3>
|
||||
<p>The cluster can exist while its context does not — a reset or a switched
|
||||
|
||||
@@ -15,13 +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:
|
||||
|
||||
```
|
||||
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.
|
||||
`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.
|
||||
|
||||
## Packages
|
||||
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
# examples/starter/Dockerfile.example
|
||||
# ctrl/Dockerfile.example
|
||||
|
||||
## Naming
|
||||
|
||||
EXAMPLE — a component image. Copy, rename, replace. Named like the manifest it feeds and the resource it becomes:
|
||||
|
||||
```
|
||||
Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
|
||||
ctrl/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 relative to the build context
|
||||
## COPY paths are repo-root relative (the one that catches everyone)
|
||||
|
||||
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:
|
||||
The Tiltfile passes two paths with DIFFERENT bases, in adjacent arguments:
|
||||
|
||||
```
|
||||
context='.' the overlay folder (or a subfolder, e.g. 'repodir/api')
|
||||
dockerfile='Dockerfile.api' relative to the overlay's Tiltfile too
|
||||
context='..' the REPO ROOT (the Tiltfile is in ctrl/)
|
||||
dockerfile='Dockerfile.api' relative to the TILTFILE, so ctrl/Dockerfile.api
|
||||
```
|
||||
|
||||
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:
|
||||
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/`:
|
||||
|
||||
```
|
||||
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
|
||||
COPY ctrl/nginx.conf /etc/nginx/conf.d/default.conf # correct
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf # fails — no such file
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -39,7 +37,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.
|
||||
|
||||
@@ -13,23 +13,21 @@ 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) < <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).
|
||||
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).
|
||||
|
||||
Start with: `make check && make deps && make cluster up`
|
||||
|
||||
## FACTS
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Asked once, of ctrl/ports.sh, which resolves it through lib/config.sh:
|
||||
|
||||
```
|
||||
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
|
||||
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
||||
```
|
||||
|
||||
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.
|
||||
Read positionally, so the order is a contract; ctrl/selftest.sh pins it.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
|
||||
## Purpose and ownership
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Splitting it is what lets rig be replaced as a whole (✖ S1 in STALE.md).
|
||||
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.
|
||||
|
||||
## 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, and an overlay moved, 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, 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.
|
||||
|
||||
The manifests and the overlay arrive as absolute paths, or `-` when there is none.
|
||||
## 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.
|
||||
|
||||
## Refuse to deploy into the wrong cluster
|
||||
|
||||
@@ -24,20 +28,23 @@ 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.
|
||||
|
||||
## Namespaces
|
||||
## 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.
|
||||
|
||||
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.
|
||||
## Catalogue: build an image
|
||||
|
||||
Nothing here assumes a namespace is named after the cluster (✖ S4).
|
||||
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.
|
||||
|
||||
## Handing over to the overlay
|
||||
- `context=` the REPO ROOT — the Tiltfile is in ctrl/, so `'..'`
|
||||
- `dockerfile=` relative to THIS file — so `'Dockerfile.api'` is ctrl/Dockerfile.api
|
||||
|
||||
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`:
|
||||
Every COPY inside those Dockerfiles is therefore repo-root relative: a file sitting BESIDE the Dockerfile is still reached as `COPY ctrl/nginx.conf`.
|
||||
|
||||
```
|
||||
RIG_CLUSTER RIG_CONTEXT RIG_HTTP_PORT RIG_HTTPS_PORT RIG_TILT_PORT RIG_REGISTRY RIG_OVERLAY_DIR
|
||||
```
|
||||
## Catalogue: reload the gateway when its config changes
|
||||
|
||||
## Catalogue
|
||||
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.
|
||||
|
||||
## Catalogue: reach a service directly, bypassing the gateway
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
## addons.sh
|
||||
|
||||
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)).
|
||||
Each addon is its own idempotent script in `ctrl/addons/` — adding one is adding
|
||||
a file, not editing a dispatcher.
|
||||
|
||||
## The manifest beside the script
|
||||
## airflow.sh
|
||||
|
||||
An addon's Kubernetes objects are a plain `<name>.yaml` beside its `<name>.sh`, not a heredoc inside it: the YAML reads, diffs and lints as YAML, and the script keeps only what YAML cannot do (secrets made once, a database created, waiting for a rollout). The script renders it with `render_manifest "$ADDON_DIR/<name>.yaml"` (`lib/config.sh`) and pipes it to `kubectl apply`.
|
||||
|
||||
`render_manifest` fills `${NAME}` and `${NAME:-default}` from the loaded config or the script's own variables, and fails on a name nothing sets, so a typo is an error rather than an empty image. `$VAR` and `$(VAR)` are left alone: they are the container's, as in postgres' `pg_isready -U $POSTGRES_USER`. selftest checks that every name a manifest uses is set somewhere. `ADDON_DIR` is taken before the script's `cd` into rig's `ctrl/`: `ADDON_DIR="$(cd "$(dirname "$0")" && pwd)"`.
|
||||
|
||||
## What rig ships, and what it does not
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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,3 +47,27 @@ 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.
|
||||
|
||||
@@ -34,40 +34,11 @@ A port held by THIS environment's own cluster is not a clash; it is the thing wo
|
||||
|
||||
The ports are extracted with a second grep rather than `tr -d ':->'`: in tr, ':->' is the character RANGE ':' to '>', which does not contain '-', so the trailing dash survives and nothing ever matches.
|
||||
|
||||
## Budget first, configuration after (2026-09-23)
|
||||
|
||||
`make check` answers the question people run it for first, in the shape the adapter rig had
|
||||
(the user preferred it): a memory budget with one row per part, the node overhead, the total
|
||||
against what the machine has and has available; then a one-line verdict (`fits`, `! tight`,
|
||||
`! does not fit`, with the two opposite fixes told apart); then where things stand and the next
|
||||
command — not created, stopped (with the `docker start` line), up (with the Tilt and gateway
|
||||
URLs, and whether this environment's Tilt is already running).
|
||||
|
||||
Below that, the configuration as it is, raw: the host and toolchain (`deps.sh detect`), then
|
||||
one `key value` line each for cluster, context, overlay, profile, nodes, kind config, manifests,
|
||||
ingress, ports (named), registry, addons. `!` lines always print; `make check all` adds every
|
||||
fact (per-port detail, addon list, swap and overcommit notes). `deps.sh detect all` is the same
|
||||
switch for the host part, so the standalone `rigdeps.sh detect` is short too.
|
||||
|
||||
## MEMORY_BUDGET
|
||||
|
||||
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
|
||||
(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.
|
||||
|
||||
## Tilt: ours or another
|
||||
|
||||
A Tilt still running after `make cluster down` holds the Tilt port, and the next `make tilt`
|
||||
fails with "address already in use" (2026-09-23, the Workspace). check reads the listener with
|
||||
`ss -p` (our own processes only): a `tilt` whose working directory is this ctrl/ is this
|
||||
environment's Tilt, reported as running with its URL; another `tilt` is a `!` line with its pid,
|
||||
its folder and the `kill` that frees the port. Anything else is the ordinary port clash.
|
||||
|
||||
## 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.
|
||||
## Compact by default
|
||||
|
||||
`make check` prints one line per question — host, toolchain, and for this rig: cluster, memory,
|
||||
ports, registry, addons — and adds detail only where something needs attention (`!` lines, the
|
||||
"held elsewhere" list when memory is tight, the clashing port). `make check all` prints every fact,
|
||||
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.
|
||||
|
||||
@@ -7,8 +7,7 @@ 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> 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.d/<profile> addons, registry (OPTIONAL, examples ship as *.env.example)
|
||||
ctrl/.env machine-local values and secrets (gitignored)
|
||||
the caller's env `make cluster up PROFILE=<name>` (always wins)
|
||||
```
|
||||
@@ -21,15 +20,11 @@ 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. OVERLAY joined with overlays, for the same reason: it is chosen per machine or per call.
|
||||
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.
|
||||
|
||||
## default_cluster_name
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
## derive_port_base
|
||||
|
||||
@@ -43,15 +38,9 @@ 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 — 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`.
|
||||
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.
|
||||
|
||||
## load_config: host ports
|
||||
|
||||
@@ -59,7 +48,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, 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`.
|
||||
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.
|
||||
|
||||
## load_config: NODE_MB
|
||||
|
||||
@@ -71,7 +60,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 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.
|
||||
hostPath entries are resolved by the HOST dockerd, so HOST_WORKDIR must stay a host path even when this runs inside the installer container.
|
||||
|
||||
## What a standalone kit needs to know
|
||||
|
||||
|
||||
@@ -156,11 +156,3 @@ 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.
|
||||
|
||||
## snapshot, and the test hooks
|
||||
|
||||
`snapshot [DIR]` writes this machine as a host fixture: the files detect reads, cut down to what it needs (no environment, home or host name), plus the lines detect printed for it. It is in the kit, so the Workspace needs nothing else to take one. Detection reads through `HOST_ROOT`, `MEMINFO`, `OVERCOMMIT_FILE` and `UNAME_S`, so a fixture can stand in for a machine; with `HOST_ROOT` set, the first two default into it. How the fixtures, the clean-container run and the WSL recipe fit together: [installer-testing.md](installer-testing.md).
|
||||
|
||||
@@ -8,12 +8,6 @@ out `python -m http.server` too — a minimal Debian has no python3. What it doe
|
||||
have, by definition, is Docker: the single prerequisite rig already demands. So a
|
||||
throwaway nginx container serves a read-only bind mount.
|
||||
|
||||
It runs with a config of its own: one worker (nginx's `auto` starts one per core,
|
||||
twelve on an ordinary laptop, for a handful of static pages), no access log and a
|
||||
quiet entrypoint, so the terminal shows the URL and nothing else unless something
|
||||
fails. The pages declare an empty icon, so the browser does not ask for a
|
||||
`favicon.ico` that is not there.
|
||||
|
||||
## Committed SVGs
|
||||
|
||||
Rendered SVGs are committed alongside their `.dot` sources for the same reason:
|
||||
|
||||
@@ -12,18 +12,12 @@ 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 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:
|
||||
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:
|
||||
|
||||
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.
|
||||
@@ -42,13 +36,11 @@ 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. An activated `<name>.env` is gitignored: it is this machine's choice.
|
||||
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.
|
||||
|
||||
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.
|
||||
## env.d/client.env.example
|
||||
|
||||
## 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).
|
||||
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.
|
||||
|
||||
### Real ports (80/443)
|
||||
|
||||
@@ -56,9 +48,27 @@ 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, 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.
|
||||
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.
|
||||
|
||||
The heavier addons are left out to keep first boot viable.
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
# Testing the installer
|
||||
|
||||
## Why it needs its own tiers
|
||||
|
||||
The installer is `ctrl/deps.sh`. What reaches a machine is its generated one-file kit,
|
||||
`standalone/default/rigdeps.sh`: the AWS Workspace gets that file and nothing else. The
|
||||
machines it must work on are exactly the ones you cannot rebuild to test on:
|
||||
- the Workspace cannot be recreated;
|
||||
- redoing WSL means rebuilding the machine you work on every day;
|
||||
- a fresh install happens once per machine, so a bug there is found by the one person least
|
||||
able to diagnose it.
|
||||
|
||||
So each tier is cheaper than the one after it, and each catches what the one before cannot.
|
||||
|
||||
| tier | runs | needs | proves | cannot prove |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 host fixtures | every `make selftest` | nothing | detection reads each kind of machine right: distro, WSL, memory, overcommit, systemd, `.wslconfig`, the Git Bash refusal | that anything installs |
|
||||
| 2 clean containers | `make selftest install` | docker, network, ~1–2 min | a stock Ubuntu 22.04 / Debian refuses cleanly when bare, then installs, verifies and fetches the manifests **as a non-root user**; the offline image works with no network | WSL, the Workspace's own policies, docker itself |
|
||||
| 3a Workspace snapshot | by hand, when the Workspace changes | the kit, once, on the Workspace | tier 1 replays the real Workspace's shape from then on | its install (that is tier 2's Ubuntu 22.04 plus the real run) |
|
||||
| 3b WSL throwaway | by hand, before teaching or after a WSL change | a Windows machine | the whole path in a real WSL distro that is not your daily one | Windows-side setup (not built, see below) |
|
||||
|
||||
## Tier 1 — host fixtures
|
||||
|
||||
`tests/hosts/<name>/` is a stand-in machine:
|
||||
|
||||
```
|
||||
root/ the files detect reads: etc/os-release, proc/version, proc/meminfo,
|
||||
proc/sys/vm/overcommit_memory, etc/wsl.conf, mnt/c/Users/<u>/.wslconfig
|
||||
env optional KEY=value lines, e.g. UNAME_S=MINGW64_NT-10.0 (a fact no file carries)
|
||||
expect.txt "+ text" must appear, "- text" must not, "exit N" (default 0)
|
||||
```
|
||||
|
||||
`bash ctrl/hosttest.sh [DIR...]` runs `deps.sh detect all` with `HOST_ROOT` pointing at the
|
||||
root (which also sets `MEMINFO` and `OVERCOMMIT_FILE`) and checks the lines. Only host facts are
|
||||
asserted: docker, PATH and the filesystem belong to the machine running the test.
|
||||
|
||||
Shipped: `ubuntu-22.04` (the Workspace's shape: 7.6 GB, overcommit 1, swap in use),
|
||||
`debian-trixie`, `wsl-bare` (systemd off, no `.wslconfig`), `wsl-ready`, `git-bash`. To add a
|
||||
case, add a folder. Anything true of one real machine goes with an overlay or in `local/`,
|
||||
never here.
|
||||
|
||||
## Tier 2 — clean containers
|
||||
|
||||
`make selftest install` (`ctrl/installtest.sh [IMAGE...]`). It first refuses to run on a stale
|
||||
kit, then, for `ubuntu:22.04` and `debian:trixie-slim`:
|
||||
1. **bare:** `install` must refuse and name curl/wget — the bootstrap paradox;
|
||||
2. **after the one root step** (`apt-get install curl ca-certificates`), as a plain user:
|
||||
`install dev`, `verify dev`, `manifests --to ~/m`. The dev tier lands in `~/.local/bin`,
|
||||
the PATH advice is printed, and all three manifests are there, verified.
|
||||
|
||||
Then it builds `deps-full` and runs `install` + `manifests` with `--network none`: the
|
||||
air-gapped path. Measured 2026-09-22: 15 checks, 75 s.
|
||||
|
||||
## Tier 3a — the Workspace snapshot
|
||||
|
||||
```bash
|
||||
bash rigdeps.sh snapshot ~/rig-host-snapshot # on the Workspace; the kit is enough
|
||||
```
|
||||
|
||||
It writes the files detect reads, cut down to what detect needs:
|
||||
- the `os-release` name fields;
|
||||
- the kernel release;
|
||||
- four meminfo numbers;
|
||||
- the overcommit value;
|
||||
- `wsl.conf`'s section headers and its two keys;
|
||||
- a `.wslconfig` memory line, under the neutral user `user`;
|
||||
- `facts.txt` (arch, glibc, date);
|
||||
- `expect.txt`, the lines detect printed for it.
|
||||
|
||||
No environment, no home, no host name: selftest asserts both the file list and the absence
|
||||
of those.
|
||||
|
||||
Carry the folder back, keep it in `local/hosts/workspace/` (gitignored) or with the overlay,
|
||||
and from then on:
|
||||
|
||||
```bash
|
||||
bash ctrl/hosttest.sh local/hosts/workspace
|
||||
```
|
||||
|
||||
Add your own `+`/`-` lines to its `expect.txt` for what must stay true there. Take a new one
|
||||
when the Workspace changes (an image update, a memory change).
|
||||
|
||||
## Tier 3b — WSL, in a throwaway distro
|
||||
|
||||
Never the distro you work in: a second one, imported from a rootfs, removed afterwards. WSL
|
||||
runs every distro in one VM, so this changes nothing in yours — **except that
|
||||
`wsl --shutdown` stops both**; don't run it while you work. From PowerShell:
|
||||
|
||||
```powershell
|
||||
# a pristine Ubuntu 22.04 rootfs: from cloud-images.ubuntu.com/wsl/, or
|
||||
# `wsl --export Ubuntu-22.04 ubuntu-22.04.tar` of an untouched install kept for this
|
||||
wsl --import rig-test C:\wsl\rig-test .\ubuntu-22.04.tar
|
||||
wsl -d rig-test -u root -- bash -c "apt-get update && apt-get install -y curl ca-certificates && useradd -m t"
|
||||
copy .\rigdeps.sh \\wsl$\rig-test\home\t\
|
||||
wsl -d rig-test -u t -- bash -lc "cd ~ && bash rigdeps.sh detect all && bash rigdeps.sh install dev && bash rigdeps.sh verify dev && bash rigdeps.sh snapshot ~/snap"
|
||||
# carry \\wsl$\rig-test\home\t\snap back as a fixture, then:
|
||||
wsl --unregister rig-test
|
||||
```
|
||||
|
||||
What to look for: detect says `WSL`, and names the systemd and `.wslconfig` steps; install and
|
||||
verify pass. This is the tier to run before teaching Windows users, or after a WSL update.
|
||||
Nothing in rig automates it: a throwaway-distro harness is the multi-distro machinery that
|
||||
was removed on purpose.
|
||||
|
||||
## Not built: a Windows-host installer
|
||||
|
||||
Everything above starts inside Linux. The Windows side — enabling WSL, getting a distro, and
|
||||
the tools a Windows user needs before rig (python and the like) — is still manual, in the
|
||||
README's "Starting from plain Windows". A Windows-host installer is recorded for the future,
|
||||
for teaching; it would need tier 3b's approach, a machine that is not your daily one, to test.
|
||||
|
||||
## The hooks the tiers rely on
|
||||
|
||||
`deps.sh` reads host files through `HOST_ROOT` (`host_file()`), memory through `MEMINFO`, the
|
||||
overcommit mode through `OVERCOMMIT_FILE`, the kernel name through `UNAME_S`, and WSL through
|
||||
`host_file /proc/version`. With `HOST_ROOT` set, `MEMINFO` and `OVERCOMMIT_FILE` default into
|
||||
it. The deps image relies on `HOST_ROOT=/host` for the same reason: a container asking about
|
||||
its host.
|
||||
@@ -4,17 +4,11 @@
|
||||
|
||||
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 (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>`.
|
||||
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.
|
||||
|
||||
## Substituted variables
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Node count
|
||||
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
# 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.
|
||||
|
||||
`MEMORY_BUDGET` (and optionally `MEMORY_BUDGET_TIP`) is how an overlay tells `make check` what its
|
||||
workload costs: rig only knows the node. See check.md.
|
||||
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")/...`). Its manifest is a `<name>.yaml` beside the script,
|
||||
rendered with `render_manifest` (see [addons.md](addons.md)). 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 missing — this 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 can also live outside the project, which then holds only the overlay. The
|
||||
forwarder names it with one variable, a machine path that is never committed (the
|
||||
shell, or an ignored `.env.local`), defaulting to the vendored layout:
|
||||
|
||||
```make
|
||||
RIG_DIR ?= $(HERE)/rig
|
||||
override RIG_DIR := $(abspath $(patsubst ~/%,$(HOME)/%,$(RIG_DIR)))
|
||||
```
|
||||
|
||||
No search path: an unset `RIG_DIR` means `./rig`, and nothing else is tried. Moving rig
|
||||
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:
|
||||
|
||||
```make
|
||||
RIG_REF := 0.1.0
|
||||
RIG_AT := $(strip $(shell 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
|
||||
```
|
||||
|
||||
`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 |
|
||||
@@ -20,18 +20,12 @@ 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 OVERLAY_DIR
|
||||
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
Space-separated, so MANIFESTS_DIR must not contain spaces. Everything else in rig already assumes that of paths; kind, docker and kubectl all do.
|
||||
|
||||
`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.
|
||||
|
||||
|
||||
@@ -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]..[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.
|
||||
`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.
|
||||
|
||||
## the caller's env beats the files
|
||||
|
||||
@@ -44,21 +44,7 @@ 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. 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 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 Tiltfile hardcodes nothing
|
||||
|
||||
@@ -76,24 +62,6 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## the installer detects what each kind of machine needs
|
||||
|
||||
The machines the installer must work on — the Workspace, a WSL install — are the ones you cannot rebuild to test on, so their shapes are replayed from fixtures (`tests/hosts/`, run by `hosttest.sh`). And a snapshot is carried off a machine that may be someone else's, so it must hold only what detect reads and nothing that names the machine or the person; that, and that it replays as the machine it was taken on, is asserted on a snapshot of the machine running the test.
|
||||
|
||||
## make selftest install
|
||||
## optional: Tiltfile evaluates
|
||||
|
||||
The installer on clean machines: stock Ubuntu 22.04 and Debian containers, the generated kit, a plain user; then the offline image with no network. Docker, network and a minute or two, so it is its own verb rather than part of the cheap run. See [installer-testing.md](installer-testing.md).
|
||||
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.
|
||||
|
||||
@@ -34,21 +34,8 @@ 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. `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.
|
||||
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.
|
||||
|
||||
## Workload images are not pinned here
|
||||
## Cabinets
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="data:,">
|
||||
<title>Graph Viewer</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; }
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# `examples/data` — an overlay with a database, a scheduler and a worked pipeline
|
||||
|
||||
postgres, redis and airflow, each an upstream image run unmodified, installed as
|
||||
this overlay's own addons. rig ships the mechanism that finds and runs them; which
|
||||
services a workload needs is the workload's business, so they live here rather
|
||||
than in rig's `ctrl/addons/`. Copy what you need into your own overlay's `addons/`.
|
||||
|
||||
```bash
|
||||
OVERLAY=examples/data make cluster up # cluster `data`: postgres, then airflow
|
||||
OVERLAY=examples/data make tilt # the items-api simulator the DAG reads
|
||||
```
|
||||
|
||||
```
|
||||
rig.env ADDONS (metallb is rig's; the rest are here), namespace, identities, image pins
|
||||
addons/postgres.sh one replica on a PVC; the password generated once and kept
|
||||
addons/airflow.sh one `standalone` pod on its own `airflow` database; needs postgres
|
||||
addons/redis.sh only for switching airflow to CeleryExecutor (not in ADDONS)
|
||||
k8s/ the items-api simulator
|
||||
dags/items_to_postgres.py the worked example: API client → adapter → postgres
|
||||
Tiltfile names the simulator's resource
|
||||
```
|
||||
|
||||
Everything lands in the `data` namespace (`DATA_NAMESPACE`, and the kustomization
|
||||
names it too), so resetting an app's namespace leaves the databases alone. Costs
|
||||
roughly 2 GB with airflow, under 1 without. Airflow's first boot runs the whole
|
||||
metadata migration, so expect a few minutes before it is ready.
|
||||
|
||||
## The worked example: three links kept apart
|
||||
|
||||
- **Metadata DB (infra).** `addons/airflow.sh` creates an `airflow` database on the
|
||||
same postgres and points `SQL_ALCHEMY_CONN` there — airflow's own tables never land
|
||||
in the app's database.
|
||||
- **DAG delivery.** The addon turns this overlay's `dags/` into the `airflow-dags`
|
||||
ConfigMap, mounted at `/opt/airflow/dags`; re-run `make cluster up` after editing a
|
||||
DAG. The faster path later: a kind `extraMount` of `dags/` plus a Tilt `sync` — noted,
|
||||
not built.
|
||||
- **Data connection (operational logic).** `AIRFLOW_CONN_APP_DB`, composed each run
|
||||
from the postgres secret, gives DAGs the app's database as the `app_db` connection.
|
||||
One password reaches both URLs, with nowhere to drift.
|
||||
|
||||
The DAG itself calls the simulator with its own HTTP client (the wire, as the API
|
||||
returns it), renames the wire's fields into the app's names in `to_app_row` — **the
|
||||
adapter, which belongs to whoever owns the app's model and so lives in the overlay** —
|
||||
and upserts into `items`. Hourly, no backfill, one retry, idempotent on `item_id`.
|
||||
|
||||
```bash
|
||||
kubectl --context kind-data -n data exec deploy/airflow -- airflow dags unpause items_to_postgres
|
||||
kubectl --context kind-data -n data exec deploy/airflow -- airflow dags trigger items_to_postgres
|
||||
kubectl --context kind-data -n data exec deploy/postgres -- psql -U app -d app -c 'select * from items'
|
||||
```
|
||||
|
||||
## Addons in an overlay
|
||||
|
||||
Each one runs from rig's `ctrl/` (rig's `addons.sh` exports `RIG_CTRL`), so it
|
||||
starts with `cd "${RIG_CTRL:?...}"`, sources `./lib/config.sh` and calls
|
||||
`load_config` — and sees every key this overlay's `rig.env` sets. Run them through
|
||||
rig (`bash ctrl/addons.sh install`, or `make cluster up`), not directly.
|
||||
|
||||
## postgres — plain manifests, one replica
|
||||
|
||||
Plain manifests rather than a helm chart: a chart repo is a network dependency,
|
||||
and an offline machine needs a path with none. The image is pinned in `rig.env`
|
||||
and can be preloaded into a local registry like every other image.
|
||||
|
||||
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.
|
||||
|
||||
The password is not in `rig.env`: `addons/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.
|
||||
|
||||
## redis
|
||||
|
||||
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.
|
||||
|
||||
## airflow
|
||||
|
||||
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.
|
||||
|
||||
One pod on `standalone`: migration, admin user, scheduler and webserver in a
|
||||
single container, on LocalExecutor, which needs no broker. The official chart's
|
||||
five deployments model an installation; switching this on means wanting pipelines.
|
||||
redis is here for the day it moves to CeleryExecutor, and not before.
|
||||
|
||||
## Reaching them
|
||||
|
||||
Reach the databases with port-forward rather than binding more host ports:
|
||||
|
||||
```bash
|
||||
kubectl --context kind-data -n data port-forward svc/postgres 5432:5432
|
||||
kubectl --context kind-data -n data port-forward svc/airflow 8080:8080
|
||||
kubectl --context kind-data -n data get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d
|
||||
```
|
||||
@@ -1,6 +0,0 @@
|
||||
# The data overlay's half of the dev loop: the items-api simulator the example DAG reads.
|
||||
# rig's ctrl/Tiltfile has already applied k8s/overlays/dev; paths here are relative to
|
||||
# this folder. postgres and airflow are addons (make cluster up), not Tilt resources.
|
||||
# Notes: README.md
|
||||
|
||||
k8s_resource('items-api', labels=['simulator'])
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Apache Airflow for this overlay: one `standalone` pod (LocalExecutor — no broker).
|
||||
# Its own `airflow` database on the postgres addon; the app's data reaches DAGs as the
|
||||
# `app_db` connection; DAGs from this overlay's dags/, as a ConfigMap.
|
||||
# Requires the postgres addon; refuses to install without it.
|
||||
# Notes: ../README.md
|
||||
set -euo pipefail
|
||||
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
|
||||
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
if ! $K get deployment -n "$NS" postgres >/dev/null 2>&1; then
|
||||
echo " ! airflow needs the postgres addon, and it is not installed" >&2
|
||||
echo " add it before airflow in the overlay's ADDONS:" >&2
|
||||
echo " ADDONS=\"... postgres airflow\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── metadata DB: airflow's own tables, kept out of the app's database ──────
|
||||
# Same postgres instance, separate database, created once (idempotent).
|
||||
db_user=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_USER}' | base64 -d)
|
||||
db_pass=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)
|
||||
db_name=$($K get secret -n "$NS" postgres -o jsonpath='{.data.POSTGRES_DB}' | base64 -d)
|
||||
psql() { $K exec -n "$NS" deploy/postgres -- psql -U "$db_user" -d "$db_name" -tAc "$1"; }
|
||||
if [ "$(psql "SELECT 1 FROM pg_database WHERE datname = 'airflow'")" = 1 ]; then
|
||||
echo " database 'airflow' exists"
|
||||
else
|
||||
psql "CREATE DATABASE airflow" >/dev/null
|
||||
echo " created database 'airflow' beside '${db_name}'"
|
||||
fi
|
||||
|
||||
# ── what is generated once and kept: re-running never rotates these ────────
|
||||
if $K get secret -n "$NS" airflow >/dev/null 2>&1; then
|
||||
echo " secret exists, keeping the current admin password and fernet key"
|
||||
else
|
||||
admin_password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
|
||||
# Airflow requires a 32-byte urlsafe-base64 key; without a fixed one every
|
||||
# restart invalidates every stored connection.
|
||||
fernet_key=$(head -c 32 /dev/urandom | base64 | tr '+/' '-_')
|
||||
$K create secret generic airflow -n "$NS" \
|
||||
--from-literal=ADMIN_USER="${AIRFLOW_ADMIN_USER:-admin}" \
|
||||
--from-literal=ADMIN_PASSWORD="$admin_password" \
|
||||
--from-literal=FERNET_KEY="$fernet_key" \
|
||||
>/dev/null
|
||||
echo " generated an admin password (read it back with the command below)"
|
||||
fi
|
||||
|
||||
# ── connections: composed from what the postgres secret owns, every run ─────
|
||||
# Nothing to drift: one password reaches the metadata DB and the data connection.
|
||||
$K create secret generic airflow-connections -n "$NS" \
|
||||
--from-literal=SQL_ALCHEMY_CONN="postgresql+psycopg2://${db_user}:${db_pass}@postgres:5432/airflow" \
|
||||
--from-literal=AIRFLOW_CONN_APP_DB="postgres://${db_user}:${db_pass}@postgres:5432/${db_name}" \
|
||||
--dry-run=client -o yaml | $K apply -f - >/dev/null
|
||||
|
||||
# ── DAG delivery: this overlay's dags/ as a ConfigMap ───────────────────────
|
||||
# Edits land by re-running this addon (make cluster up). The later path — a kind
|
||||
# extraMount of dags/ plus a Tilt sync — is noted in the README, not built.
|
||||
# A ConfigMap volume is kubelet's ..data/..<timestamp> symlinks, and Airflow's DAG walker
|
||||
# follows symlinks: without the .airflowignore it stops at "Detected recursive loop".
|
||||
dags="$(_from_ctrl "$OVERLAY_DIR")/dags"
|
||||
if [ -d "$dags" ]; then
|
||||
$K create configmap airflow-dags -n "$NS" --from-file="$dags" \
|
||||
--from-literal=.airflowignore='^\.\.' \
|
||||
--dry-run=client -o yaml | $K apply -f - >/dev/null
|
||||
echo " dags: $(ls "$dags" | grep -c '\.py$') file(s) from $(basename "$(_abs_from_ctrl "$OVERLAY_DIR")")/dags"
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
render_manifest "$ADDON_DIR/airflow.yaml" | $K apply -n "$NS" -f - >/dev/null
|
||||
|
||||
echo " waiting for airflow (the first boot migrates the database, so this is slow)..."
|
||||
$K rollout status deployment/airflow -n "$NS" --timeout=600s
|
||||
|
||||
echo " in-cluster: http://airflow.${NS}.svc.cluster.local:8080"
|
||||
echo " reach it: kubectl --context ${KUBECONTEXT} -n ${NS} port-forward svc/airflow 8080:8080"
|
||||
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret airflow -o jsonpath='{.data.ADMIN_PASSWORD}' | base64 -d"
|
||||
@@ -1,69 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
selector:
|
||||
app: airflow
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: airflow
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: airflow
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: airflow
|
||||
spec:
|
||||
containers:
|
||||
- name: airflow
|
||||
image: ${AIRFLOW_IMAGE}
|
||||
args: ["standalone"]
|
||||
env:
|
||||
- name: AIRFLOW__CORE__EXECUTOR
|
||||
value: LocalExecutor
|
||||
- name: AIRFLOW__CORE__LOAD_EXAMPLES
|
||||
value: "false"
|
||||
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow-connections, key: SQL_ALCHEMY_CONN}
|
||||
- name: AIRFLOW_CONN_APP_DB
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow-connections, key: AIRFLOW_CONN_APP_DB}
|
||||
- name: AIRFLOW__CORE__FERNET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: FERNET_KEY}
|
||||
- name: _AIRFLOW_WWW_USER_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_USER}
|
||||
- name: _AIRFLOW_WWW_USER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: dags
|
||||
mountPath: /opt/airflow/dags
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
# First boot runs the whole migration before it serves anything.
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 20
|
||||
volumes:
|
||||
- name: dags
|
||||
configMap:
|
||||
name: airflow-dags
|
||||
optional: true
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL for this overlay: plain manifests, one replica on a PVC, password generated once and kept.
|
||||
# Notes: ../README.md
|
||||
set -euo pipefail
|
||||
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
|
||||
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
|
||||
|
||||
# The password is generated once and then left alone, so re-running this does
|
||||
# not rotate the credential out from under whatever is already connected.
|
||||
if $K get secret -n "$NS" postgres >/dev/null 2>&1; then
|
||||
echo " secret exists, keeping the current password"
|
||||
else
|
||||
password=$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | head -c 24)
|
||||
$K create secret generic postgres -n "$NS" \
|
||||
--from-literal=POSTGRES_DB="${POSTGRES_DB:-postgres}" \
|
||||
--from-literal=POSTGRES_USER="${POSTGRES_USER:-postgres}" \
|
||||
--from-literal=POSTGRES_PASSWORD="$password" >/dev/null
|
||||
echo " generated a password (read it back with the command printed below)"
|
||||
fi
|
||||
|
||||
echo " applying manifests"
|
||||
render_manifest "$ADDON_DIR/postgres.yaml" | $K apply -n "$NS" -f - >/dev/null
|
||||
|
||||
echo " waiting for postgres..."
|
||||
$K rollout status deployment/postgres -n "$NS" --timeout=240s
|
||||
|
||||
echo " in-cluster: postgres.${NS}.svc.cluster.local:5432"
|
||||
echo " password: kubectl --context ${KUBECONTEXT} -n ${NS} get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Redis for this overlay: cache and broker (Celery), no persistence.
|
||||
# Notes: ../README.md
|
||||
set -euo pipefail
|
||||
ADDON_DIR="$(cd "$(dirname "$0")" && pwd)" # its manifest sits beside it
|
||||
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||
|
||||
source ./lib/config.sh
|
||||
load_config
|
||||
|
||||
K="kubectl --context ${KUBECONTEXT}"
|
||||
NS="${DATA_NAMESPACE:-data}"
|
||||
|
||||
$K get namespace "$NS" >/dev/null 2>&1 || $K create namespace "$NS"
|
||||
|
||||
echo " applying manifests"
|
||||
render_manifest "$ADDON_DIR/redis.yaml" | $K apply -n "$NS" -f - >/dev/null
|
||||
|
||||
echo " waiting for redis..."
|
||||
$K rollout status deployment/redis -n "$NS" --timeout=180s
|
||||
|
||||
echo " in-cluster: redis://redis.${NS}.svc.cluster.local:6379/0"
|
||||
@@ -1,81 +0,0 @@
|
||||
"""Pull items from the items API, rename them into the app's names, upsert into postgres.
|
||||
|
||||
Three links, kept apart on purpose:
|
||||
|
||||
- the API client (`fetch_items`) talks to the wire as it is — here the overlay's
|
||||
own simulator, `items-api`, whose field names are the API's;
|
||||
- the adapter (`to_app_row`) is the one place the wire's names become the app's:
|
||||
`id` -> `item_id`, `name` -> `item_name`, `price.amount_cents` -> `price_cents`.
|
||||
It belongs to whoever owns the app's model, so it lives in the overlay, not in rig;
|
||||
- the load writes through the `app_db` connection (AIRFLOW_CONN_APP_DB, built by
|
||||
addons/airflow.sh from the postgres secret) and is idempotent: an upsert keyed
|
||||
on `item_id`, so a retry or a rerun never duplicates a row.
|
||||
|
||||
Operational logic is explicit and minimal: hourly, no backfill, one retry.
|
||||
"""
|
||||
import json
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from airflow import DAG
|
||||
from airflow.operators.python import PythonOperator
|
||||
|
||||
ITEMS_URL = "http://items-api/v1/items"
|
||||
|
||||
CREATE = """
|
||||
CREATE TABLE IF NOT EXISTS items (
|
||||
item_id text PRIMARY KEY,
|
||||
item_name text NOT NULL,
|
||||
price_cents integer NOT NULL,
|
||||
currency text NOT NULL,
|
||||
loaded_at timestamptz NOT NULL DEFAULT now()
|
||||
)
|
||||
"""
|
||||
|
||||
UPSERT = """
|
||||
INSERT INTO items (item_id, item_name, price_cents, currency)
|
||||
VALUES (%(item_id)s, %(item_name)s, %(price_cents)s, %(currency)s)
|
||||
ON CONFLICT (item_id) DO UPDATE
|
||||
SET item_name = EXCLUDED.item_name,
|
||||
price_cents = EXCLUDED.price_cents,
|
||||
currency = EXCLUDED.currency,
|
||||
loaded_at = now()
|
||||
"""
|
||||
|
||||
|
||||
def fetch_items():
|
||||
"""The API client: the wire, as the API returns it."""
|
||||
with urllib.request.urlopen(ITEMS_URL, timeout=10) as response:
|
||||
return json.load(response)["items"]
|
||||
|
||||
|
||||
def to_app_row(item):
|
||||
"""The adapter: the API's names in, the app's names out."""
|
||||
return {
|
||||
"item_id": item["id"],
|
||||
"item_name": item["name"],
|
||||
"price_cents": item["price"]["amount_cents"],
|
||||
"currency": item["price"]["currency"],
|
||||
}
|
||||
|
||||
|
||||
def load_items():
|
||||
from airflow.providers.postgres.hooks.postgres import PostgresHook
|
||||
|
||||
rows = [to_app_row(item) for item in fetch_items()]
|
||||
hook = PostgresHook(postgres_conn_id="app_db")
|
||||
hook.run(CREATE)
|
||||
for row in rows:
|
||||
hook.run(UPSERT, parameters=row)
|
||||
print(f"upserted {len(rows)} items")
|
||||
|
||||
|
||||
with DAG(
|
||||
dag_id="items_to_postgres",
|
||||
schedule="@hourly",
|
||||
start_date=datetime(2026, 1, 1),
|
||||
catchup=False,
|
||||
default_args={"retries": 1, "retry_delay": timedelta(minutes=1)},
|
||||
tags=["example"],
|
||||
) as dag:
|
||||
PythonOperator(task_id="load_items", python_callable=load_items)
|
||||
@@ -1,95 +0,0 @@
|
||||
# The simulator: a stub of the API the DAG reads, faithful to the wire (its field
|
||||
# names are the API's, not the app's). Same shape as the starter's example-mock.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: items-api-stub
|
||||
data:
|
||||
routes.json: |
|
||||
{
|
||||
"/health": {"status": 200, "body": {"status": "ok"}},
|
||||
"/v1/items": {"status": 200, "body": {"items": [
|
||||
{"id": "a-100", "name": "anvil", "price": {"amount_cents": 1999, "currency": "USD"}},
|
||||
{"id": "b-200", "name": "bucket", "price": {"amount_cents": 450, "currency": "USD"}},
|
||||
{"id": "c-300", "name": "crate", "price": {"amount_cents": 1200, "currency": "USD"}}
|
||||
]}}
|
||||
}
|
||||
serve.py: |
|
||||
import json, os
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
ROUTES = json.load(open("/etc/stub/routes.json"))
|
||||
NAME = os.environ.get("STUB_NAME", "stub")
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
r = ROUTES.get(self.path)
|
||||
if r is None:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(
|
||||
{"error": "no canned route", "stub": NAME, "path": self.path}
|
||||
).encode())
|
||||
return
|
||||
body = json.dumps(r["body"]).encode()
|
||||
self.send_response(r["status"])
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("X-Mocked-By", NAME)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print("%s %s" % (NAME, fmt % args), flush=True)
|
||||
|
||||
HTTPServer(("0.0.0.0", 8080), H).serve_forever()
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: items-api
|
||||
labels:
|
||||
app: items-api
|
||||
rig.component/impl: mock
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: items-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: items-api
|
||||
spec:
|
||||
containers:
|
||||
- name: stub
|
||||
image: python:3.12-slim
|
||||
command: ["python3", "/etc/stub/serve.py"]
|
||||
env:
|
||||
- name: STUB_NAME
|
||||
value: items-api
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: stub
|
||||
mountPath: /etc/stub
|
||||
readinessProbe:
|
||||
httpGet: { path: /health, port: 8080 }
|
||||
initialDelaySeconds: 2
|
||||
resources:
|
||||
requests: { memory: 32Mi, cpu: 10m }
|
||||
limits: { memory: 64Mi }
|
||||
volumes:
|
||||
- name: stub
|
||||
configMap:
|
||||
name: items-api-stub
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: items-api
|
||||
spec:
|
||||
selector:
|
||||
app: items-api
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# Beside the addons, in DATA_NAMESPACE — but no Namespace object: the addons created
|
||||
# it, and a Namespace Tilt owned would be deleted by `tilt down`, taking postgres and
|
||||
# airflow with it. rig's Tiltfile creates namespaces that are used and not declared.
|
||||
namespace: data
|
||||
|
||||
resources:
|
||||
- items-api.yaml
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
@@ -1,26 +0,0 @@
|
||||
# This overlay's settings: layered over rig's defaults, under ctrl/.env and the caller.
|
||||
# data — postgres, redis and airflow (upstream images, run unmodified) in their own namespace.
|
||||
# Use it: OVERLAY=examples/data make cluster up Notes: README.md
|
||||
|
||||
# Order matters: addons install in the order listed, and airflow refuses to start
|
||||
# without postgres, so postgres comes first. metallb is rig's own. Airflow runs
|
||||
# LocalExecutor and needs no broker: add redis (before airflow) only to switch to Celery.
|
||||
ADDONS="metallb postgres airflow"
|
||||
|
||||
# What each part costs in MB, for `make check`'s memory budget (rig adds the kind node).
|
||||
MEMORY_BUDGET="airflow=1800 postgres=300"
|
||||
|
||||
# Namespace for the dependency containers (k8s/base/kustomization.yaml names it too).
|
||||
DATA_NAMESPACE=data
|
||||
|
||||
# Postgres identity. The password is generated once by addons/postgres.sh and kept.
|
||||
POSTGRES_DB=app
|
||||
POSTGRES_USER=app
|
||||
POSTGRES_STORAGE=2Gi
|
||||
|
||||
AIRFLOW_ADMIN_USER=admin
|
||||
|
||||
# Upstream images, pinned by tag; bump freely, and preload them for an offline machine.
|
||||
POSTGRES_IMAGE=postgres:16-alpine
|
||||
REDIS_IMAGE=redis:7-alpine
|
||||
AIRFLOW_IMAGE=apache/airflow:2.10.4
|
||||
@@ -1,59 +0,0 @@
|
||||
# `examples/starter` — the overlay rig runs when you name none
|
||||
|
||||
An overlay is one folder, outside rig, that holds what runs: its settings, its
|
||||
manifests, its images. rig reads it and never writes into it — see
|
||||
[`docs/notes/overlay.md`](../../docs/notes/overlay.md) for the contract. This one
|
||||
ships with rig so `make tilt` has something to deploy on a fresh clone, and so a
|
||||
real overlay has a shape to be written against:
|
||||
|
||||
```
|
||||
rig.env settings layered over rig's defaults (this one sets none)
|
||||
k8s/base/ the components, as plain manifests
|
||||
k8s/overlays/dev/ how this environment differs from the base — MANIFESTS_DIR's default
|
||||
Tiltfile the workload's half of the dev loop; rig's ctrl/Tiltfile includes it
|
||||
Dockerfile.example the shape of an image you build yourself
|
||||
```
|
||||
|
||||
To start your own, copy this folder somewhere rig does not track —
|
||||
`rig/local/<name>/`, or a repo of its own — and name it:
|
||||
|
||||
```bash
|
||||
cp -r examples/starter local/myenv
|
||||
OVERLAY=local/myenv make cluster up # or OVERLAY=local/myenv in ctrl/.env
|
||||
```
|
||||
|
||||
The cluster, context and port block then follow the overlay's folder name
|
||||
(`myenv`, `kind-myenv`), so it never collides with another.
|
||||
|
||||
## The two components are examples, not the system
|
||||
|
||||
They exist so the real manifests have a shape to be written against.
|
||||
|
||||
### The three states a component can be in
|
||||
|
||||
Switching between them should be a one-line change, never a rewrite. The DNS
|
||||
name stays the same in every case, so callers never know the difference:
|
||||
|
||||
| state | what exists | when |
|
||||
| --- | --- | --- |
|
||||
| **real** | an image built from source, hot-reloaded | the one thing you are working on |
|
||||
| **mock** | a stub returning canned responses (`example-mock.yaml`) | everything else — most of the estate |
|
||||
| **remote** | no pod at all, just a Service (`example-remote.yaml`) | when the real system is reachable and you want it |
|
||||
|
||||
Most components should be **mock**. What has to be faithful is the topology —
|
||||
names, ports, dependency order, who can reach whom, how it fails. The workloads
|
||||
are noise, and mocking them is what makes several copies of a large estate fit
|
||||
on one laptop.
|
||||
|
||||
## Images
|
||||
|
||||
`Dockerfile.example` is a commented shape, not a working build. Paths in this
|
||||
folder's Tiltfile are relative to this folder, so `context='.'` is the overlay
|
||||
and every `COPY` in the Dockerfile is relative to it:
|
||||
|
||||
```
|
||||
docker_build(CLUSTER + '-api', context='.', dockerfile='Dockerfile.api')
|
||||
```
|
||||
|
||||
The name given to `docker_build` must match `image:` in the manifest — that
|
||||
string is the only thing connecting the two.
|
||||
@@ -1,69 +0,0 @@
|
||||
# The starter overlay's half of the dev loop. rig's ctrl/Tiltfile includes this once
|
||||
# it has applied k8s/overlays/dev, so every path here is relative to THIS folder.
|
||||
# Facts from rig, via os.getenv: RIG_CLUSTER RIG_CONTEXT RIG_HTTP_PORT RIG_HTTPS_PORT
|
||||
# RIG_TILT_PORT RIG_REGISTRY RIG_OVERLAY_DIR
|
||||
# Notes: rig's docs/notes/overlay.md
|
||||
|
||||
CLUSTER = os.getenv('RIG_CLUSTER')
|
||||
HTTP = os.getenv('RIG_HTTP_PORT')
|
||||
|
||||
# ── Images ─────────────────────────────────────────────────────────────────
|
||||
# (nothing yet — the examples run upstream images. Add docker_build calls here.)
|
||||
|
||||
|
||||
# ── Resources ──────────────────────────────────────────────────────────────
|
||||
# (nothing yet — add k8s_resource calls here to name and order what you deploy.)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Catalogue — paste what you need, delete the rest.
|
||||
# Commented out so this file runs as-is.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# ── build an image ─────────────────────────────────────────────────────────
|
||||
# context= and dockerfile= are both relative to this folder, so every COPY in the
|
||||
# Dockerfile is relative to the context you name here.
|
||||
#
|
||||
# docker_build(
|
||||
# CLUSTER + '-api', # must match `image:` in the manifest —
|
||||
# context='.', # that string is the only thing
|
||||
# dockerfile='Dockerfile.api', # connecting the two
|
||||
# ignore=['.git', 'rig', '.venv', 'node_modules', '__pycache__'],
|
||||
# live_update=[sync('api', '/app/api')],
|
||||
# )
|
||||
#
|
||||
# ── a shared base, built once ──────────────────────────────────────────────
|
||||
# Components that share code build FROM one base image instead of each carrying
|
||||
# a copy of it. Tilt builds the base first when a Dockerfile's FROM names it.
|
||||
#
|
||||
# docker_build(CLUSTER + '-base', context='repodir/base')
|
||||
# docker_build(CLUSTER + '-api', context='repodir/api') # its Dockerfile: FROM <cluster>-base
|
||||
#
|
||||
# ── name and order a resource ──────────────────────────────────────────────
|
||||
# k8s_resource('api', resource_deps=['postgres'], labels=['app'])
|
||||
# k8s_resource('gateway', resource_deps=['api', 'ui'], labels=['app'])
|
||||
#
|
||||
# ── reload the gateway when its config changes ─────────────────────────────
|
||||
# A hash-less configMapGenerator ConfigMap never changes name, so edits do NOT
|
||||
# roll the pod on their own.
|
||||
#
|
||||
# local_resource(
|
||||
# 'gateway-reload',
|
||||
# cmd='kubectl --context %s -n <namespace> rollout restart deployment/gateway' % os.getenv('RIG_CONTEXT'),
|
||||
# deps=['k8s/base/Caddyfile'],
|
||||
# resource_deps=['gateway'],
|
||||
# auto_init=False,
|
||||
# )
|
||||
#
|
||||
# ── manifests that need kustomize flags ────────────────────────────────────
|
||||
# rig applies MANIFESTS_DIR without flags. If a secretGenerator reads above its
|
||||
# kustomization root, set MANIFESTS_DIR=none in rig.env and apply them here instead;
|
||||
# the flag loosens a safety check for the whole build.
|
||||
#
|
||||
# k8s_yaml(kustomize('k8s/overlays/dev', flags=['--load-restrictor=LoadRestrictionsNone']))
|
||||
#
|
||||
# ── reach a service directly, bypassing the gateway ────────────────────────
|
||||
# Prefer the gateway; host ports are shared machine-wide. If you need one, take it
|
||||
# from this environment's own port block.
|
||||
#
|
||||
# k8s_resource('postgres', port_forwards=[str(int(HTTP) + 5) + ':5432'])
|
||||
@@ -1,9 +0,0 @@
|
||||
# This overlay's settings: layered over rig's defaults, under ctrl/.env and the caller.
|
||||
# The starter sets nothing, so a rig with no overlay named behaves exactly like one with
|
||||
# no overlay at all. Any key a profile could set belongs here; paths are relative to
|
||||
# this folder. PROFILE and OVERLAY are refused — they are what chooses this file.
|
||||
# Notes: rig's docs/notes/overlay.md
|
||||
#
|
||||
# ADDONS="metallb"
|
||||
# K8S_VERSION=v1_36
|
||||
# MANIFESTS_DIR=k8s/overlays/dev
|
||||
@@ -16,7 +16,7 @@ endif
|
||||
help: ## list targets
|
||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
||||
|
||||
deps: ## rigdeps.sh [detect|list|verify|fetch|install|manifest|manifests|snapshot] (default detect)
|
||||
deps: ## rigdeps.sh [detect|list|verify|fetch|install] (default detect)
|
||||
bash $(HERE)rigdeps.sh $(or $(ARGS),detect)
|
||||
|
||||
mem: ## rigmini.sh [status|push|all|backup|restore] (default status)
|
||||
|
||||
@@ -8,12 +8,7 @@
|
||||
# `make selftest` fails while this file differs from what rig generates.
|
||||
|
||||
# ── from the libraries ──
|
||||
declare -- CONFIG_OVERRIDABLE=$'PROFILE OVERLAY CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||
declare -- DEFAULT_OVERLAY="examples/starter"
|
||||
_abs_from_ctrl ()
|
||||
{
|
||||
( cd "$(_from_ctrl "$1")" 2> /dev/null && pwd )
|
||||
}
|
||||
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||
_config_restore ()
|
||||
{
|
||||
local line;
|
||||
@@ -24,25 +19,10 @@ _config_restore ()
|
||||
done <<< "$1";
|
||||
return 0
|
||||
}
|
||||
_from_ctrl ()
|
||||
{
|
||||
case "$1" in
|
||||
/*)
|
||||
echo "$1"
|
||||
;;
|
||||
*)
|
||||
echo "../$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
default_cluster_name ()
|
||||
{
|
||||
local n;
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
n=$(basename "$(_abs_from_ctrl "$OVERLAY_DIR")");
|
||||
else
|
||||
n=$(basename "$(cd .. && pwd)");
|
||||
fi;
|
||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||
echo "${n:-rig}"
|
||||
@@ -55,37 +35,8 @@ derive_port_base ()
|
||||
}
|
||||
render_kind_config ()
|
||||
{
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}" overlay_dir="";
|
||||
if [ -n "$OVERLAY_DIR" ]; then
|
||||
overlay_dir=$(_abs_from_ctrl "$OVERLAY_DIR");
|
||||
fi;
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" -e "s|\${OVERLAY_DIR}|${overlay_dir}|g" "$KIND_CONFIG"
|
||||
}
|
||||
render_manifest ()
|
||||
{
|
||||
local file="$1" text ref key def val;
|
||||
text=$(< "$file") || return 1;
|
||||
while IFS= read -r ref; do
|
||||
key=${ref#\$\{};
|
||||
key=${key%\}};
|
||||
def="";
|
||||
if [[ $key == *:-* ]]; then
|
||||
def=${key#*:-};
|
||||
key=${key%%:-*};
|
||||
fi;
|
||||
if [ -n "${!key:-}" ]; then
|
||||
val=${!key};
|
||||
else
|
||||
if [[ $ref == *:-* ]]; then
|
||||
val=$def;
|
||||
else
|
||||
echo "render_manifest: $file needs \${$key}, which nothing sets" 1>&2;
|
||||
return 1;
|
||||
fi;
|
||||
fi;
|
||||
text=${text//"$ref"/"$val"};
|
||||
done < <(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}' "$file" | sort -u);
|
||||
printf '%s\n' "$text"
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG"
|
||||
}
|
||||
|
||||
# ── configuration, frozen for profile 'default' ──
|
||||
@@ -95,8 +46,7 @@ load_config() {
|
||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||
done
|
||||
declare -g ADDONS=""
|
||||
declare -gx CERT_MANAGER_MANIFEST_SHA256="5f6a499b8c1857d57f560f536e0dcc830914b45c420899fe7ad0692c8624e408"
|
||||
declare -gx CERT_MANAGER_MANIFEST_URL="https://github.com/cert-manager/cert-manager/releases/download/v1.21.1/cert-manager.yaml"
|
||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||
declare -g CLUSTER="rig"
|
||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||
@@ -121,13 +71,8 @@ load_config() {
|
||||
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||
declare -g MANIFESTS_DIR="examples/starter/k8s/overlays/dev"
|
||||
declare -gx METALLB_MANIFEST_GIT_BLOB="7fbda334cc3ac0aaabdcb081af4f543feb3c2f9f"
|
||||
declare -gx METALLB_MANIFEST_SHA256="b0b9be2802f10aa32d45308b4457d06cde0c70544712c8d0cf5511657ffd2b69"
|
||||
declare -gx METALLB_MANIFEST_URL="https://raw.githubusercontent.com/metallb/metallb/v0.16.0/config/manifests/metallb-native.yaml"
|
||||
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||
declare -gx METALLB_VERSION="v0.16.0"
|
||||
declare -gx METRICS_SERVER_MANIFEST_SHA256="1cec29a5267809306a2c6ec74a3e449abbb705b4a8beed0c8a1963910f72c79b"
|
||||
declare -gx METRICS_SERVER_MANIFEST_URL="https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml"
|
||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||
declare -g NODES="1"
|
||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||
@@ -136,12 +81,14 @@ load_config() {
|
||||
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||
declare -g NODE_MB="800"
|
||||
declare -g OVERLAY_DIR="examples/starter"
|
||||
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||
declare -gx PROFILE="default"
|
||||
declare -g PROFILE_NAME="default"
|
||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||
declare -gx REGISTRY_IMAGE="registry:2"
|
||||
declare -g REGISTRY_MODE="local"
|
||||
declare -g REGISTRY_PORT="20313"
|
||||
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||
declare -g TILT_PORT="20312"
|
||||
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||
@@ -153,8 +100,7 @@ load_config() {
|
||||
# ── ctrl/deps.sh ──
|
||||
# Toolchain installer: detect the host, install pinned tools into $OUT_BIN, report
|
||||
# host actions it will not perform (no sudo, no apt). Usually via `make deps`.
|
||||
# Usage: deps.sh [detect [all] | list | verify [core|dev] | fetch [core|dev] [--to DIR] | install [core|dev]
|
||||
# | manifest NAME | manifests [--to DIR] | snapshot [DIR]]
|
||||
# Usage: deps.sh [detect [all] | list | verify [core|dev] | fetch [core|dev] [--to DIR] | install [core|dev]]
|
||||
# Notes: docs/notes/deps.md
|
||||
|
||||
set -euo pipefail
|
||||
@@ -178,14 +124,6 @@ abspath() {
|
||||
|
||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||
HOST_ROOT="${HOST_ROOT:-/}"
|
||||
# A host fixture (docs/notes/installer-testing.md) is a root whose kernel files stand in
|
||||
# for this machine's; UNAME_S does the same for the one fact a file cannot carry.
|
||||
if [ "$HOST_ROOT" != / ]; then
|
||||
if [ -r "$HOST_ROOT/proc/meminfo" ]; then MEMINFO="${MEMINFO:-$HOST_ROOT/proc/meminfo}"; fi
|
||||
if [ -r "$HOST_ROOT/proc/sys/vm/overcommit_memory" ]; then
|
||||
OVERCOMMIT_FILE="${OVERCOMMIT_FILE:-$HOST_ROOT/proc/sys/vm/overcommit_memory}"
|
||||
fi
|
||||
fi
|
||||
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||
@@ -297,7 +235,7 @@ docker_pkg() {
|
||||
|
||||
# Windows outside WSL (Git Bash, MSYS, Cygwin) fails confusingly; name it instead.
|
||||
require_linux() {
|
||||
case "${UNAME_S:-$(uname -s)}" in
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
cat >&2 <<'EOF'
|
||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||
@@ -316,7 +254,7 @@ EOF
|
||||
esac
|
||||
}
|
||||
|
||||
is_wsl() { grep -qi microsoft "$(host_file /proc/version)" 2>/dev/null; }
|
||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||
|
||||
detect() {
|
||||
echo "host"
|
||||
@@ -879,99 +817,6 @@ require_linux
|
||||
cmd="${1:-install}"
|
||||
[ $# -gt 0 ] && shift
|
||||
|
||||
# ── manifests rig's own addons apply ───────────────────────────────────────
|
||||
# Pinned (URL + SHA256), fetched through the same DEPS_SOURCE resolver as the
|
||||
# binaries and verified, then applied from disk: an offline machine needs no
|
||||
# network for them. Default home: vendor/manifests/ in rig's folder (gitignored).
|
||||
MANIFESTS_HOME="${MANIFESTS_HOME:-$(cd .. && pwd)/vendor/manifests}"
|
||||
BAKED_MANIFESTS="${BAKED_MANIFESTS:-/opt/rig/manifests}"
|
||||
MANIFEST_NAMES="METALLB CERT_MANAGER METRICS_SERVER"
|
||||
|
||||
manifest_path() { # NAME dir
|
||||
local v="${1}_VERSION"
|
||||
echo "$2/$(echo "$1" | tr 'A-Z_' 'a-z-')-${!v}.yaml"
|
||||
}
|
||||
|
||||
# Make one pinned manifest present and verified in dir; print only its path.
|
||||
fetch_manifest() { # NAME dir
|
||||
local name="$1" dir="$2" url_var="${1}_MANIFEST_URL" sha_var="${1}_MANIFEST_SHA256" file
|
||||
if [ -z "${!url_var:-}" ] || [ -z "${!sha_var:-}" ]; then
|
||||
echo "no pinned manifest for $name (${url_var} / ${sha_var} unset)" >&2
|
||||
exit 1
|
||||
fi
|
||||
file=$(manifest_path "$name" "$dir")
|
||||
if [ -f "$file" ] && [ "$($SHA "$file" | awk '{print $1}')" = "${!sha_var}" ]; then
|
||||
echo "$file"
|
||||
return
|
||||
fi
|
||||
mkdir -p "$dir"
|
||||
if [ "$DEPS_SOURCE" = baked ]; then
|
||||
cp "$(manifest_path "$name" "$BAKED_MANIFESTS")" "$file.tmp"
|
||||
else
|
||||
download "$(resolve_url "${!url_var}")" "$file.tmp"
|
||||
fi
|
||||
verify "$file.tmp" "${!sha_var}" "$name manifest"
|
||||
mv "$file.tmp" "$file"
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
fetch_manifests() { # [--to DIR]
|
||||
local dest="$MANIFESTS_HOME" n
|
||||
if [ "${1:-}" = --to ]; then dest="$(abspath "${2:?--to needs a directory}")"; fi
|
||||
echo "fetching the addons' manifests into $dest (source: $DEPS_SOURCE)"
|
||||
for n in $MANIFEST_NAMES; do
|
||||
echo " $n $(fetch_manifest "$n" "$dest")"
|
||||
done
|
||||
}
|
||||
|
||||
# ── snapshot: this machine as a host fixture ───────────────────────────────
|
||||
# Writes what detect reads, cut down to what it needs — never the environment, the
|
||||
# home directory or the host name — plus the lines detect prints for it. A fact of
|
||||
# one machine: keep it with an overlay or in rig's local/, never in rig itself, and
|
||||
# replay it with ctrl/hosttest.sh. Notes: docs/notes/installer-testing.md
|
||||
snapshot() {
|
||||
local dest r f
|
||||
dest="$(abspath "${1:-host-snapshot}")"
|
||||
if [ -n "$(ls -A "$dest" 2>/dev/null)" ]; then
|
||||
echo "snapshot: $dest already holds something — pick an empty directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
r="$dest/root"
|
||||
mkdir -p "$r/etc" "$r/proc/sys/vm"
|
||||
f=$(host_file /etc/os-release)
|
||||
if [ -r "$f" ]; then grep -E '^(PRETTY_NAME|NAME|VERSION_ID|ID|ID_LIKE)=' "$f" > "$r/etc/os-release"; fi
|
||||
# The kernel release says WSL or not; the full build string names build hosts.
|
||||
echo "Linux version $(awk '{print $3; exit}' "$(host_file /proc/version)" 2>/dev/null || uname -r)" > "$r/proc/version"
|
||||
grep -E '^(MemTotal|MemAvailable|SwapTotal|SwapFree):' "${MEMINFO:-/proc/meminfo}" > "$r/proc/meminfo"
|
||||
cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" > "$r/proc/sys/vm/overcommit_memory" 2>/dev/null || true
|
||||
f=$(host_file /etc/wsl.conf)
|
||||
if [ -r "$f" ]; then
|
||||
# Section headers and the two keys detect reads; a [user] default= names a person.
|
||||
grep -E '^[[:space:]]*(\[[a-z0-9]+\]|systemd[[:space:]]*=|generateResolvConf[[:space:]]*=)' "$f" > "$r/etc/wsl.conf" || true
|
||||
fi
|
||||
f=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||
if [ -n "$f" ] && grep -qE '^\s*memory\s*=' "$f"; then
|
||||
mkdir -p "$r/mnt/c/Users/user"
|
||||
{ echo "[wsl2]"; grep -E '^\s*memory\s*=' "$f"; } > "$r/mnt/c/Users/user/.wslconfig"
|
||||
fi
|
||||
{
|
||||
echo "# for the record; not replayed"
|
||||
echo "arch=$(arch)"
|
||||
echo "glibc=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || echo unknown)"
|
||||
echo "taken=$(date -u +%Y-%m-%d)"
|
||||
} > "$dest/facts.txt"
|
||||
# The lines the fixture itself decides, as detect prints them for it now.
|
||||
{
|
||||
echo "# Written by deps.sh snapshot: what detect said about this machine."
|
||||
env -u MEMINFO -u OVERCOMMIT_FILE HOST_ROOT="$r" bash "./$(basename "${BASH_SOURCE[0]}")" detect all 2>/dev/null \
|
||||
| grep -E '^ (distro|memory|overcommit|systemd|resolv\.conf|wslconfig) |^ ! systemd|^ - resolv\.conf' \
|
||||
| sed 's/^ /+ /'
|
||||
} > "$dest/expect.txt"
|
||||
echo "wrote $dest"
|
||||
(cd "$dest" && find . -type f | sort | sed 's|^\./| |')
|
||||
echo "keep it with an overlay or in rig's local/, never in rig; replay it with rig's hosttest.sh"
|
||||
}
|
||||
|
||||
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||
need_downloads() {
|
||||
require_amd64
|
||||
@@ -985,15 +830,9 @@ case "$cmd" in
|
||||
verify) verify_tools "${1:-dev}" ;;
|
||||
fetch) need_downloads; fetch "$@" ;;
|
||||
install) need_downloads; install "${1:-dev}" ;;
|
||||
manifest) need_downloads
|
||||
fetch_manifest "${1:?usage: $0 manifest <METALLB|CERT_MANAGER|METRICS_SERVER>}" "$MANIFESTS_HOME" ;;
|
||||
manifests) need_downloads; fetch_manifests "$@" ;;
|
||||
snapshot) snapshot "${1:-}" ;;
|
||||
*) echo "usage: $0 [detect [all]|list|verify|fetch|install|manifest NAME|manifests]" >&2
|
||||
*) echo "usage: $0 [detect [all]|list|verify|fetch|install]" >&2
|
||||
echo " install [core|dev] (default dev)" >&2
|
||||
echo " fetch [core|dev] [--to DIR]" >&2
|
||||
echo " manifests [--to DIR] the addons' pinned manifests, verified" >&2
|
||||
echo " snapshot [DIR] this machine as a host fixture (no secrets)" >&2
|
||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
@@ -8,12 +8,7 @@
|
||||
# `make selftest` fails while this file differs from what rig generates.
|
||||
|
||||
# ── from the libraries ──
|
||||
declare -- CONFIG_OVERRIDABLE=$'PROFILE OVERLAY CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||
declare -- DEFAULT_OVERLAY="examples/starter"
|
||||
_abs_from_ctrl ()
|
||||
{
|
||||
( cd "$(_from_ctrl "$1")" 2> /dev/null && pwd )
|
||||
}
|
||||
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||
_config_restore ()
|
||||
{
|
||||
local line;
|
||||
@@ -24,25 +19,10 @@ _config_restore ()
|
||||
done <<< "$1";
|
||||
return 0
|
||||
}
|
||||
_from_ctrl ()
|
||||
{
|
||||
case "$1" in
|
||||
/*)
|
||||
echo "$1"
|
||||
;;
|
||||
*)
|
||||
echo "../$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
default_cluster_name ()
|
||||
{
|
||||
local n;
|
||||
if [ -n "${OVERLAY:-}" ]; then
|
||||
n=$(basename "$(_abs_from_ctrl "$OVERLAY_DIR")");
|
||||
else
|
||||
n=$(basename "$(cd .. && pwd)");
|
||||
fi;
|
||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||
echo "${n:-rig}"
|
||||
@@ -55,37 +35,8 @@ derive_port_base ()
|
||||
}
|
||||
render_kind_config ()
|
||||
{
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}" overlay_dir="";
|
||||
if [ -n "$OVERLAY_DIR" ]; then
|
||||
overlay_dir=$(_abs_from_ctrl "$OVERLAY_DIR");
|
||||
fi;
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" -e "s|\${OVERLAY_DIR}|${overlay_dir}|g" "$KIND_CONFIG"
|
||||
}
|
||||
render_manifest ()
|
||||
{
|
||||
local file="$1" text ref key def val;
|
||||
text=$(< "$file") || return 1;
|
||||
while IFS= read -r ref; do
|
||||
key=${ref#\$\{};
|
||||
key=${key%\}};
|
||||
def="";
|
||||
if [[ $key == *:-* ]]; then
|
||||
def=${key#*:-};
|
||||
key=${key%%:-*};
|
||||
fi;
|
||||
if [ -n "${!key:-}" ]; then
|
||||
val=${!key};
|
||||
else
|
||||
if [[ $ref == *:-* ]]; then
|
||||
val=$def;
|
||||
else
|
||||
echo "render_manifest: $file needs \${$key}, which nothing sets" 1>&2;
|
||||
return 1;
|
||||
fi;
|
||||
fi;
|
||||
text=${text//"$ref"/"$val"};
|
||||
done < <(grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}' "$file" | sort -u);
|
||||
printf '%s\n' "$text"
|
||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG"
|
||||
}
|
||||
|
||||
# ── configuration, frozen for profile 'default' ──
|
||||
@@ -95,8 +46,7 @@ load_config() {
|
||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||
done
|
||||
declare -g ADDONS=""
|
||||
declare -gx CERT_MANAGER_MANIFEST_SHA256="5f6a499b8c1857d57f560f536e0dcc830914b45c420899fe7ad0692c8624e408"
|
||||
declare -gx CERT_MANAGER_MANIFEST_URL="https://github.com/cert-manager/cert-manager/releases/download/v1.21.1/cert-manager.yaml"
|
||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||
declare -g CLUSTER="rig"
|
||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||
@@ -121,13 +71,8 @@ load_config() {
|
||||
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||
declare -g MANIFESTS_DIR="examples/starter/k8s/overlays/dev"
|
||||
declare -gx METALLB_MANIFEST_GIT_BLOB="7fbda334cc3ac0aaabdcb081af4f543feb3c2f9f"
|
||||
declare -gx METALLB_MANIFEST_SHA256="b0b9be2802f10aa32d45308b4457d06cde0c70544712c8d0cf5511657ffd2b69"
|
||||
declare -gx METALLB_MANIFEST_URL="https://raw.githubusercontent.com/metallb/metallb/v0.16.0/config/manifests/metallb-native.yaml"
|
||||
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||
declare -gx METALLB_VERSION="v0.16.0"
|
||||
declare -gx METRICS_SERVER_MANIFEST_SHA256="1cec29a5267809306a2c6ec74a3e449abbb705b4a8beed0c8a1963910f72c79b"
|
||||
declare -gx METRICS_SERVER_MANIFEST_URL="https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.9.0/components.yaml"
|
||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||
declare -g NODES="1"
|
||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||
@@ -136,12 +81,14 @@ load_config() {
|
||||
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||
declare -g NODE_MB="800"
|
||||
declare -g OVERLAY_DIR="examples/starter"
|
||||
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||
declare -gx PROFILE="default"
|
||||
declare -g PROFILE_NAME="default"
|
||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||
declare -gx REGISTRY_IMAGE="registry:2"
|
||||
declare -g REGISTRY_MODE="local"
|
||||
declare -g REGISTRY_PORT="20313"
|
||||
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||
declare -g TILT_PORT="20312"
|
||||
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||
@@ -171,7 +118,7 @@ BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess belo
|
||||
|
||||
# Refuse Git Bash / MSYS / Cygwin and kernels without /proc, with a clear message.
|
||||
require_linux() {
|
||||
case "${UNAME_S:-$(uname -s)}" in
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
cat >&2 <<'EOF'
|
||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# A plain native Debian with memory to spare and no swap.
|
||||
+ distro Debian GNU/Linux 13 (trixie)
|
||||
+ native linux
|
||||
+ memory 15861 MB total, 11264 MB available
|
||||
+ overcommit 0 heuristic
|
||||
- in swap
|
||||
- WSL
|
||||
@@ -1,4 +0,0 @@
|
||||
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
|
||||
NAME="Debian GNU/Linux"
|
||||
VERSION_ID="13"
|
||||
ID=debian
|
||||
@@ -1,4 +0,0 @@
|
||||
MemTotal: 16241664 kB
|
||||
MemAvailable: 11534336 kB
|
||||
SwapTotal: 0 kB
|
||||
SwapFree: 0 kB
|
||||
@@ -1 +0,0 @@
|
||||
0
|
||||
@@ -1 +0,0 @@
|
||||
Linux version 6.12.48+deb13-amd64
|
||||
@@ -1 +0,0 @@
|
||||
UNAME_S=MINGW64_NT-10.0-19045
|
||||
@@ -1,5 +0,0 @@
|
||||
# Git Bash on Windows: refused before anything runs, pointing at WSL.
|
||||
exit 1
|
||||
+ This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||
+ wsl --install
|
||||
- distro
|
||||
@@ -1,7 +0,0 @@
|
||||
# A managed Ubuntu 22.04 machine: native, tight on memory, already swapping.
|
||||
+ distro Ubuntu 22.04.5 LTS
|
||||
+ native linux
|
||||
+ memory 7680 MB total, 1843 MB available, 4096 MB in swap
|
||||
+ overcommit 1 always
|
||||
- WSL
|
||||
- systemd not enabled
|
||||
@@ -1,5 +0,0 @@
|
||||
PRETTY_NAME="Ubuntu 22.04.5 LTS"
|
||||
NAME="Ubuntu"
|
||||
VERSION_ID="22.04"
|
||||
ID=ubuntu
|
||||
ID_LIKE=debian
|
||||
@@ -1,4 +0,0 @@
|
||||
MemTotal: 7864320 kB
|
||||
MemAvailable: 1887436 kB
|
||||
SwapTotal: 8388608 kB
|
||||
SwapFree: 4194304 kB
|
||||
@@ -1 +0,0 @@
|
||||
1
|
||||
@@ -1 +0,0 @@
|
||||
Linux version 6.8.0-1030-aws
|
||||
@@ -1,7 +0,0 @@
|
||||
# Fresh WSL: systemd off, resolv.conf generated, no .wslconfig — all three said.
|
||||
+ Ubuntu 22.04.5 LTS amd64, WSL
|
||||
+ ! systemd not enabled in /etc/wsl.conf
|
||||
+ - resolv.conf is WSL-generated
|
||||
+ Enable systemd — add to /etc/wsl.conf:
|
||||
+ Cap/raise the WSL VM memory
|
||||
- native linux
|
||||
@@ -1,4 +0,0 @@
|
||||
PRETTY_NAME="Ubuntu 22.04.5 LTS"
|
||||
NAME="Ubuntu"
|
||||
VERSION_ID="22.04"
|
||||
ID=ubuntu
|
||||
@@ -1,4 +0,0 @@
|
||||
MemTotal: 8055020 kB
|
||||
MemAvailable: 7340032 kB
|
||||
SwapTotal: 2097152 kB
|
||||
SwapFree: 2097152 kB
|
||||
@@ -1 +0,0 @@
|
||||
Linux version 5.15.167.4-microsoft-standard-WSL2
|
||||
@@ -1,7 +0,0 @@
|
||||
# WSL set up as BOOTSTRAP says: systemd on, resolv.conf pinned, VM memory set.
|
||||
+ amd64, WSL
|
||||
+ systemd enabled in wsl.conf
|
||||
+ resolv.conf pinned (generateResolvConf=false)
|
||||
+ wslconfig memory set: memory=12GB
|
||||
- systemd not enabled
|
||||
- Cap/raise the WSL VM memory
|
||||
@@ -1,4 +0,0 @@
|
||||
PRETTY_NAME="Ubuntu 22.04.5 LTS"
|
||||
NAME="Ubuntu"
|
||||
VERSION_ID="22.04"
|
||||
ID=ubuntu
|
||||
@@ -1,5 +0,0 @@
|
||||
[boot]
|
||||
systemd=true
|
||||
|
||||
[network]
|
||||
generateResolvConf=false
|
||||
@@ -1,2 +0,0 @@
|
||||
[wsl2]
|
||||
memory=12GB
|
||||
@@ -1,4 +0,0 @@
|
||||
MemTotal: 8055020 kB
|
||||
MemAvailable: 7340032 kB
|
||||
SwapTotal: 2097152 kB
|
||||
SwapFree: 2097152 kB
|
||||
@@ -1 +0,0 @@
|
||||
Linux version 5.15.167.4-microsoft-standard-WSL2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user