Compare commits
18 Commits
004b397b94
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b227cbc0e | |||
| 60527645f2 | |||
| dee899d86f | |||
| 8f7e409ec4 | |||
| f4b2c15a9f | |||
| 2a0a793f19 | |||
|
|
aeeb26f4e9 | ||
| 9c963514f1 | |||
| 565cecfb50 | |||
| 1752a95408 | |||
| 1dc9d38c80 | |||
| 6a005dae3b | |||
| de5b1b7ea8 | |||
| 6dbc83a449 | |||
| 730ebaff2f | |||
| dd17021402 | |||
| 19feac6d57 | |||
| 809a13eebe |
@@ -8,13 +8,13 @@
|
|||||||
#
|
#
|
||||||
# spr depends on rig, never the other way round. Building and deleting a cluster
|
# spr depends on rig, never the other way round. Building and deleting a cluster
|
||||||
# is rig's job, so up and down hand straight to rig/ctrl/cluster.sh, carrying the
|
# is rig's job, so up and down hand straight to rig/ctrl/cluster.sh, carrying the
|
||||||
# four things that make this cluster spr's rather than rig's defaults:
|
# the things that make this cluster spr's rather than rig's defaults:
|
||||||
#
|
#
|
||||||
# CLUSTER=spr rooms deploy into the kind-spr context
|
# CLUSTER=spr rooms deploy into the kind-spr context
|
||||||
# KIND_CONFIG spr's own shape, which maps the rooms' gateway NodePorts
|
# KIND_CONFIG spr's own kind config, which maps the rooms' gateway NodePorts
|
||||||
# REGISTRY_MODE=none rooms load images straight into the node
|
# REGISTRY_MODE=none rooms load images straight into the node
|
||||||
# PROFILE=minimal pinned here, so a change to rig's own ctrl/.env can never
|
# PROFILE= ADDONS= set empty here, so rig's own ctrl/.env can never quietly
|
||||||
# quietly add addons to spr's cluster
|
# OVERLAY= pick a profile, an overlay or addons for spr's cluster
|
||||||
#
|
#
|
||||||
# status stays here: it answers a question about rooms, not about the cluster.
|
# status stays here: it answers a question about rooms, not about the cluster.
|
||||||
set -e
|
set -e
|
||||||
@@ -26,7 +26,9 @@ rig() {
|
|||||||
CLUSTER=spr \
|
CLUSTER=spr \
|
||||||
KIND_CONFIG="$SCRIPT_DIR/k8s/kind-config.yaml" \
|
KIND_CONFIG="$SCRIPT_DIR/k8s/kind-config.yaml" \
|
||||||
REGISTRY_MODE=none \
|
REGISTRY_MODE=none \
|
||||||
PROFILE=minimal \
|
PROFILE= \
|
||||||
|
OVERLAY= \
|
||||||
|
ADDONS= \
|
||||||
bash "$RIG_CTRL/cluster.sh" "$@"
|
bash "$RIG_CTRL/cluster.sh" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,8 +57,7 @@ compose, the same dependency installs as a rig addon of that name:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd rig
|
cd rig
|
||||||
PROFILE=data make cluster up
|
PROFILE=data make cluster up # installs the addons too
|
||||||
PROFILE=data make addons install
|
|
||||||
|
|
||||||
kubectl -n data port-forward svc/postgres 5432:5432
|
kubectl -n data port-forward svc/postgres 5432:5432
|
||||||
kubectl -n data port-forward svc/airflow 8080:8080
|
kubectl -n data port-forward svc/airflow 8080:8080
|
||||||
|
|||||||
14
rig/.gitignore
vendored
14
rig/.gitignore
vendored
@@ -16,6 +16,14 @@ arch/*.dot
|
|||||||
# binaries pulled by `make deps-bundle` for the air-gapped installer image
|
# binaries pulled by `make deps-bundle` for the air-gapped installer image
|
||||||
vendor
|
vendor
|
||||||
|
|
||||||
# Client rigs are NOT ignored here. A copy is a SIBLING of this directory
|
# Overlays that live with no version control of their own, or clones of their own
|
||||||
# (../acme-rig), so a rule in this file cannot see it — the rules live in the
|
# repos: rig reads them and never tracks them (docs/notes/overlay.md).
|
||||||
# parent repo's .gitignore, anchored at its root, where `*-rig/` matches them.
|
/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/
|
||||||
|
|||||||
128
rig/BOOTSTRAP.md
128
rig/BOOTSTRAP.md
@@ -1,18 +1,9 @@
|
|||||||
# From a machine with nothing on it to a project you can work in
|
# From a machine with nothing on it to an environment you can work in
|
||||||
|
|
||||||
The README says the prerequisite is Docker and nothing else. This is what that
|
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 a new project running under
|
actually looks like end to end: a bare Linux box, and an overlay running under
|
||||||
Tilt at the end of it.
|
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
|
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
|
needs a logout, so knowing about them in advance is cheaper than meeting them
|
||||||
halfway through.
|
halfway through.
|
||||||
@@ -97,7 +88,7 @@ build has a Makefile target today. **On a genuinely bare machine, run it by
|
|||||||
hand:**
|
hand:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make deps-image # builds rig-deps:deps
|
make deps image # builds rig-deps:deps
|
||||||
mkdir -p ~/.local/bin
|
mkdir -p ~/.local/bin
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v /:/host:ro \
|
-v /:/host:ro \
|
||||||
@@ -108,7 +99,7 @@ docker run --rm \
|
|||||||
```
|
```
|
||||||
|
|
||||||
The image name follows the directory, like everything else here: in `rig`
|
The image name follows the directory, like everything else here: in `rig`
|
||||||
it is `rig-deps`, in a copy called `acme-rig` it is `acme-rig-deps`. The
|
it is `rig-deps`, in a copy called `other-rig` it is `other-rig-deps`. The
|
||||||
tag is `deps` (or `full`, below), not `latest`.
|
tag is `deps` (or `full`, below), not `latest`.
|
||||||
|
|
||||||
None of the four arguments are guessable, so:
|
None of the four arguments are guessable, so:
|
||||||
@@ -141,7 +132,7 @@ If something else on this machine already provides `kubectl`, the installer says
|
|||||||
by name rather than shadowing it quietly. `OUT_BIN=$PWD/def/bin` installs
|
by name rather than shadowing it quietly. `OUT_BIN=$PWD/def/bin` installs
|
||||||
somewhere private instead.
|
somewhere private instead.
|
||||||
|
|
||||||
**Two variants worth knowing before you need them.** `make deps-image full` bakes
|
**Two variants worth knowing before you need them.** `make deps image full` bakes
|
||||||
every pinned binary into the image at build time (`DEPS_SOURCE=baked`), so
|
every pinned binary into the image at build time (`DEPS_SOURCE=baked`), so
|
||||||
`docker save` gives you the entire installer as one file to carry into an
|
`docker save` gives you the entire installer as one file to carry into an
|
||||||
air-gapped network. And `DEPS_SOURCE=artifactory` with `DEPS_ARTIFACTORY_URL`
|
air-gapped network. And `DEPS_SOURCE=artifactory` with `DEPS_ARTIFACTORY_URL`
|
||||||
@@ -152,22 +143,24 @@ 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
|
every later run and every later copy of this directory. The container path is
|
||||||
the first-time path.
|
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
|
## Prove the machine before blaming the project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make setup
|
make check
|
||||||
make cluster up
|
make cluster up
|
||||||
kubectl get nodes
|
kubectl get nodes
|
||||||
```
|
```
|
||||||
|
|
||||||
`make setup` re-runs every check as a group. It is idempotent and it deliberately
|
`make check` re-runs every check — host, docker, toolchain, memory, ports — and
|
||||||
does not abort on the first failure — a setup script that dies at step two hides
|
changes nothing. Run now, it should end with nothing left to do by hand, and that
|
||||||
the fact that steps four and five were also going to fail. Run now, it should be
|
is the point: it is the scoreboard, not the installer.
|
||||||
`ok` and `done` all the way down, and that is the point: it is the scoreboard,
|
|
||||||
not the installer.
|
|
||||||
|
|
||||||
`make cluster up` builds the default `minimal` profile — one node, no addons,
|
`make cluster up` builds rig's built-in defaults — one node, no addons,
|
||||||
boots fast. You do not need it to develop anything, but you do want to know that
|
boots fast. You do not need it to develop anything, but you do want to know that
|
||||||
kind, the kubeconfig context and the derived port block work *before* a new
|
kind, the kubeconfig context and the derived port block work *before* a new
|
||||||
project has any problems of its own to confuse them with. `make cluster down`
|
project has any problems of its own to confuse them with. `make cluster down`
|
||||||
@@ -185,80 +178,30 @@ worth reading before rather than after. `make cluster free <names>` stops
|
|||||||
clusters without deleting them; `docker start` brings them back untouched.
|
clusters without deleting them; `docker start` brings them back untouched.
|
||||||
|
|
||||||
|
|
||||||
## Scaffold the project
|
## Start an overlay
|
||||||
|
|
||||||
The canonical layout is [`all/projects/templates/conventions.md`](../all/projects/templates/conventions.md).
|
What runs lives outside rig, in an overlay — see
|
||||||
Read it — it is short, opinionated, and exists precisely so nobody
|
[`docs/notes/overlay.md`](docs/notes/overlay.md). Start from rig's own:
|
||||||
reverse-engineers a layout from whichever repo they happened to open. What
|
|
||||||
follows is only the mechanical part.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SLUG=<slug> # short, lowercase, no separators
|
cp -r examples/starter local/myenv # local/ is gitignored by rig
|
||||||
cp -r ~/wdir/semester/all/projects/templates/broad ~/wdir/semester/"$SLUG"
|
echo 'OVERLAY=local/myenv' >> ctrl/.env
|
||||||
cd ~/wdir/semester/"$SLUG"
|
make check # shows the overlay, its cluster and ports
|
||||||
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,
|
The cluster takes the overlay's folder name and the context becomes
|
||||||
namespace, ConfigMap name, and the `NAME=` in `kind-up.sh` / `kind-down.sh`. One
|
`kind-<name>`, so there is nothing to edit for either. Replace the two example
|
||||||
sed does all of it.
|
components under `k8s/base/`, and add your images and resources to the
|
||||||
|
overlay's `Tiltfile`. Check the manifests before `kind` spends minutes on anything
|
||||||
The slug is the folder name, lowercase and short — `mpr`, `unt`, `nvi`. The
|
— this renders the whole tree without a cluster and catches a broken patch
|
||||||
cluster takes that name and the context becomes `kind-<slug>`, derived by the
|
immediately:
|
||||||
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
|
```bash
|
||||||
grep -h '^TILT_PORT=' ~/wdir/semester/*/ctrl/k8s/.env 2>/dev/null | sort
|
kubectl kustomize local/myenv/k8s/overlays/dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Choose a free one in `10300–10399` — the range ALL reserves in
|
If the overlay is to be versioned, make `local/myenv` a repository of its own (rig
|
||||||
`projects/index.json` under `policy` — avoiding `10350`, which is Tilt's own
|
never tracks it), or keep it anywhere else and name it by path.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
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 ctrl/k8s/overlays/dev
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## Run it
|
||||||
@@ -268,20 +211,9 @@ make kind-up # idempotent create, then selects the context
|
|||||||
make tilt-up # context + your assigned port
|
make tilt-up # context + your assigned port
|
||||||
```
|
```
|
||||||
|
|
||||||
`tilt-up` passes `--context kind-<slug>` every time, which is the point of going
|
`tilt-up` passes `--context kind-<name>` every time, which is the point of going
|
||||||
through `make` at all: tilt cannot deploy into whichever cluster you last looked
|
through `make` at all: tilt cannot deploy into whichever cluster you last looked
|
||||||
at.
|
at.
|
||||||
|
|
||||||
`make tilt-down` and `make kind-down` close the loop, and `make kind-reset` is
|
`make tilt-down` and `make kind-down` close the loop, and `make kind-reset` is
|
||||||
delete-and-recreate for when a cluster wedges.
|
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.
|
|
||||||
|
|||||||
148
rig/Makefile
148
rig/Makefile
@@ -1,37 +1,15 @@
|
|||||||
# Thin control Makefile — one target per ctrl/ script, and the subcommand is an
|
# Thin control Makefile: the subcommand is an argument (`make cluster down`); logic lives in ctrl/ scripts.
|
||||||
# argument rather than a second target: `make cluster down`, not `make cluster-down`.
|
# make check | deps | cluster up | tilt | docs (`make help` lists all)
|
||||||
#
|
# Start with: make check && make deps && make cluster up
|
||||||
# The logic lives in the scripts, never here. Each target maps to exactly one
|
# Notes: docs/notes/Makefile.md
|
||||||
# bash file, and that file holds the variants:
|
|
||||||
#
|
# Identity and ports, asked once of ctrl/ports.sh, read positionally (selftest pins the order):
|
||||||
# make cluster up -> ctrl/cluster.sh up
|
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
|
||||||
#
|
# OVERLAY/CLUSTER given as make arguments are handed over explicitly: before make 4.4,
|
||||||
# Config layers, weakest first: ctrl/versions.env (pinned toolchain) <
|
# $(shell) does not see them, and the context would follow the wrong environment.
|
||||||
# ctrl/env.d/<profile>.env (cluster shape) < ctrl/.env (local, gitignored) <
|
FACTS := $(shell $(if $(OVERLAY),OVERLAY='$(OVERLAY)') $(if $(CLUSTER),CLUSTER='$(CLUSTER)') bash ctrl/ports.sh active 2>/dev/null)
|
||||||
# the environment. So `make cluster up PROFILE=client` beats everything.
|
|
||||||
#
|
|
||||||
# Start with: make setup (then: make cluster up && make docs)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
#
|
|
||||||
# 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 here, which is a
|
|
||||||
# SECOND derivation of values lib/config.sh already owns — and the two could
|
|
||||||
# disagree about the port after `make ports 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.
|
|
||||||
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/-*$$//')
|
SLUG := $(shell echo '$(notdir $(CURDIR))' | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/^-*//; s/-*$$//')
|
||||||
# The fallback matters: ports.sh sources config.sh, and if a profile or .env is
|
# Fall back to the folder name, not empty, if ports.sh fails on a broken config.
|
||||||
# broken it exits non-zero. Losing the cluster name would send --context to the
|
|
||||||
# wrong place, so fall back to the folder rather than to empty.
|
|
||||||
CLUSTER := $(or $(word 1,$(FACTS)),$(SLUG))
|
CLUSTER := $(or $(word 1,$(FACTS)),$(SLUG))
|
||||||
KCTX := --context $(or $(word 2,$(FACTS)),kind-$(SLUG))
|
KCTX := --context $(or $(word 2,$(FACTS)),kind-$(SLUG))
|
||||||
TILT_PORT := $(word 5,$(FACTS))
|
TILT_PORT := $(word 5,$(FACTS))
|
||||||
@@ -42,100 +20,68 @@ DEPSIMG := $(SLUG)-deps
|
|||||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||||
ifneq ($(ARGS),)
|
ifneq ($(ARGS),)
|
||||||
$(eval $(ARGS):;@:)
|
$(eval $(ARGS):;@:)
|
||||||
# ...and as PHONY, because some of those words name real directories. `cfg`,
|
# ...and as PHONY, because some of those words name real directories (ctrl, docs, ...).
|
||||||
# `ctrl`, `docs`, `gen` and `init` all exist at this level, and make considers a
|
|
||||||
# target that is an existing directory already built — so `make build ctrl` ran
|
|
||||||
# the build and then printed "make: 'ctrl' is up to date". The empty rule above
|
|
||||||
# is not enough on its own; only .PHONY stops make consulting the filesystem.
|
|
||||||
.PHONY: $(ARGS)
|
.PHONY: $(ARGS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: help setup check selftest mem deps deps-image standalone cluster registry addons ports \
|
.PHONY: help check deps cluster tilt docs selftest standalone \
|
||||||
docs tilt \
|
|
||||||
kind-up kind-down kind-reset tilt-up tilt-down
|
kind-up kind-down kind-reset tilt-up tilt-down
|
||||||
|
|
||||||
help: ## list targets
|
help: ## list targets
|
||||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
||||||
|
|
||||||
# ── setup ──────────────────────────────────────────────────────────────────
|
# ── this machine ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
setup: ## prepare this machine [core] [--cluster]
|
# Everything that looks and never changes anything: host, docker, toolchain,
|
||||||
bash ctrl/setup.sh $(ARGS)
|
# config, memory, ports, registry, addons. `check mem` goes deeper on memory —
|
||||||
|
# how far it really climbs, and the WSL .wslconfig backup/restore.
|
||||||
|
check: ## is this machine ready? [all] [mem [status|push|all|backup|restore]]
|
||||||
|
bash ctrl/check.sh $(ARGS)
|
||||||
|
|
||||||
check: ## is this machine ready? reports, never fixes
|
# `deps image` is for a machine with nothing but Docker: the installer runs from
|
||||||
bash ctrl/check.sh
|
# the image instead — see BOOTSTRAP.md. `full` bakes every binary in.
|
||||||
|
deps: ## install the toolchain [core|dev] [image [full]]
|
||||||
# The counterpart to check: that one asks about the MACHINE and never fails,
|
ifeq ($(word 1,$(ARGS)),image)
|
||||||
# this one asks about RIG and exits 1, as `make standalone check` does. 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
|
|
||||||
bash ctrl/selftest.sh
|
|
||||||
|
|
||||||
mem: ## memory, its caps, what it survives [status|push|all|backup|restore]
|
|
||||||
bash ctrl/mem.sh $(or $(ARGS),status)
|
|
||||||
|
|
||||||
deps: ## install the toolchain [core|dev] (default dev)
|
|
||||||
bash ctrl/deps.sh install $(or $(ARGS),dev)
|
|
||||||
|
|
||||||
# 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;
|
|
||||||
# `check` is what selftest runs to catch a kit left behind by a change to rig.
|
|
||||||
standalone: ## generate standalone/<profile>/ kits [write|check] (default write)
|
|
||||||
bash ctrl/standalone.sh $(or $(ARGS),write)
|
|
||||||
|
|
||||||
deps-image: ## build the installer image [full]
|
|
||||||
docker build -f ctrl/Dockerfile.deps \
|
docker build -f ctrl/Dockerfile.deps \
|
||||||
--target $(if $(filter full,$(ARGS)),deps-full,deps) \
|
--target $(if $(filter full,$(ARGS)),deps-full,deps) \
|
||||||
-t $(DEPSIMG):$(if $(filter full,$(ARGS)),full,deps) .
|
-t $(DEPSIMG):$(if $(filter full,$(ARGS)),full,deps) .
|
||||||
|
else
|
||||||
|
bash ctrl/deps.sh install $(or $(ARGS),dev)
|
||||||
|
endif
|
||||||
|
|
||||||
# ── cluster ────────────────────────────────────────────────────────────────
|
# ── the cluster ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# up also starts the registry and installs the profile's addons, and the ports
|
||||||
|
# derive from the folder name — there is nothing else to run first.
|
||||||
cluster: ## this env + the machine [up|down|reset|list|free]
|
cluster: ## this env + the machine [up|down|reset|list|free]
|
||||||
bash ctrl/cluster.sh $(or $(ARGS),up)
|
bash ctrl/cluster.sh $(or $(ARGS),up)
|
||||||
|
|
||||||
registry: ## registry wiring [up|down|status] (default status)
|
# ── dev loop + docs ────────────────────────────────────────────────────────
|
||||||
bash ctrl/registry.sh $(or $(ARGS),status)
|
|
||||||
|
|
||||||
addons: ## profile addons [install|list] (default list)
|
|
||||||
bash ctrl/addons.sh $(or $(ARGS),list)
|
|
||||||
|
|
||||||
ports: ## this environment's port block [show|persist]
|
|
||||||
bash ctrl/ports.sh $(or $(ARGS),show)
|
|
||||||
|
|
||||||
# ── docs + dev loop ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
docs: ## documentation [serve|graphs] (default serve)
|
docs: ## documentation [serve|graphs] (default serve)
|
||||||
bash ctrl/docs.sh $(or $(ARGS),serve)
|
bash ctrl/docs.sh $(or $(ARGS),serve)
|
||||||
|
|
||||||
# --port is only passed when TILT_PORT resolved. It normally does, since FACTS
|
# --port only when TILT_PORT resolved; the Tiltfile asks ports.sh for the rest itself.
|
||||||
# above asks ports.sh — but ports.sh can fail on a broken profile, and without
|
|
||||||
# the guard tilt receives a bare `--port` with no value and fails on the flag
|
|
||||||
# rather than on anything real. Tilt's own default is 10350, which is the number
|
|
||||||
# every project on this machine is trying not to collide on, so falling back to
|
|
||||||
# it silently is worse than not passing the flag.
|
|
||||||
#
|
|
||||||
# The Tiltfile asks ports.sh for the rest itself — cluster, registry and where
|
|
||||||
# the manifests are — so nothing needs passing here beyond what tilt's own flags
|
|
||||||
# require.
|
|
||||||
tilt: ## dev loop [up|down] (default up)
|
tilt: ## dev loop [up|down] (default up)
|
||||||
cd ctrl && tilt $(or $(ARGS),up) $(KCTX) $(if $(filter down,$(ARGS)),,$(if $(TILT_PORT),--port $(TILT_PORT)))
|
cd ctrl && tilt $(or $(ARGS),up) $(KCTX) $(if $(filter down,$(ARGS)),,$(if $(TILT_PORT),--port $(TILT_PORT)))
|
||||||
|
|
||||||
|
# ── maintaining rig ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
# `check` is what selftest runs to catch a kit left behind by a change to rig.
|
||||||
|
standalone: ## single-file kits [write|check|export DIR] (default write)
|
||||||
|
bash ctrl/standalone.sh $(or $(ARGS),write)
|
||||||
|
|
||||||
# ── the shape every other project uses ─────────────────────────────────────
|
# ── the shape every other project uses ─────────────────────────────────────
|
||||||
# Aliases, not a second implementation: each one calls the same script the
|
# Aliases matching other projects' kind-up / tilt-up; each calls the same script.
|
||||||
# canonical target does.
|
# Nothing else reads these names: rename, delete or add freely (and update .PHONY).
|
||||||
#
|
|
||||||
# The header above argues for `make cluster down` over `make cluster-down`, and
|
|
||||||
# that still holds *within* this file. But rig is one repo among several on the
|
|
||||||
# same machine, and every other one answers to kind-up / tilt-up. Muscle memory
|
|
||||||
# spanning six projects beats internal tidiness in one, so both spellings work.
|
|
||||||
#
|
|
||||||
# `cluster list` and `cluster free` have no hyphenated twin on purpose — they
|
|
||||||
# are rig's own, with nothing to be consistent with.
|
|
||||||
#
|
|
||||||
# Nothing outside this file reads these names: the script is `ctrl/cluster.sh`
|
|
||||||
# and it takes the verb. So rename them, delete the ones you never type, or add
|
|
||||||
# the spelling your own projects use — an alias is two lines, and adding one
|
|
||||||
# costs nothing but a line in .PHONY above.
|
|
||||||
|
|
||||||
kind-up: ## alias for `cluster up`
|
kind-up: ## alias for `cluster up`
|
||||||
bash ctrl/cluster.sh up
|
bash ctrl/cluster.sh up
|
||||||
|
|||||||
152
rig/README.md
152
rig/README.md
@@ -58,30 +58,34 @@ make docs # serves on localhost, prints the URL
|
|||||||
They run before anything is installed, which matters because they are the
|
They run before anything is installed, which matters because they are the
|
||||||
instructions for everything else. No cluster and no toolchain required.
|
instructions for everything else. No cluster and no toolchain required.
|
||||||
|
|
||||||
|
Why the code is the way it is — the reasoning, measurements and gotchas — lives
|
||||||
|
in [`docs/notes/`](docs/notes/), one file per script, so the code keeps short comments.
|
||||||
|
|
||||||
## Then
|
## Then
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make check # report host and config problems; changes nothing
|
make check # is this machine ready? short; `make check all` for every detail
|
||||||
make deps # install the toolchain (add `core` on a managed machine)
|
make deps # install the toolchain (add `core` on a managed machine)
|
||||||
make cluster up # build the cluster for the active profile
|
make cluster up # cluster + registry + the profile's addons
|
||||||
```
|
```
|
||||||
|
|
||||||
`make cluster up` also starts this environment's local registry and wires it
|
That is the whole setup. `make cluster up` also starts this environment's local
|
||||||
into the node, so an image built locally is pullable by the cluster without
|
registry and wires it into the node, so an image built locally is pullable by the
|
||||||
going near docker.io:
|
cluster without going near docker.io. `make check` shows its port, among
|
||||||
|
everything else:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make registry status # prints: endpoint localhost:<port>
|
make check # ... registry localhost:<port> (running)
|
||||||
docker build -t localhost:<port>/app:1 .
|
docker build -t localhost:<port>/app:1 .
|
||||||
docker push localhost:<port>/app:1
|
docker push localhost:<port>/app:1
|
||||||
kubectl --context kind-$(basename $PWD) run app --image=localhost:<port>/app:1
|
kubectl --context kind-$(basename $PWD) run app --image=localhost:<port>/app:1
|
||||||
```
|
```
|
||||||
|
|
||||||
The port block is derived from the directory name, so two copies of rig never
|
The port block is derived from the directory name, so two copies of rig never
|
||||||
collide:
|
collide — nothing to configure. `make check all` lists it; `bash ctrl/ports.sh persist`
|
||||||
|
pins it into `ctrl/.env` if you want it fixed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make ports show # HTTP / HTTPS / TILT / REGISTRY
|
|
||||||
make cluster list # every cluster on this machine, with memory
|
make cluster list # every cluster on this machine, with memory
|
||||||
make cluster free # stop the others if memory is tight
|
make cluster free # stop the others if memory is tight
|
||||||
make cluster down # remove this cluster and its registry
|
make cluster down # remove this cluster and its registry
|
||||||
@@ -96,111 +100,101 @@ 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
|
is two lines at the bottom of the file, calling the same script the canonical
|
||||||
target does.
|
target does.
|
||||||
|
|
||||||
**`make tilt` works on a fresh copy, unedited.** rig ships `ctrl/Tiltfile`, and
|
**`make tilt` works on a fresh copy, unedited.** rig's `ctrl/Tiltfile` does
|
||||||
`k8s/base` already boots, so the dev loop comes up with the two examples running
|
rig's part — identity, context guard, registry, the manifests — and then includes
|
||||||
and nothing to configure first.
|
the overlay's own Tiltfile. With no overlay named that is `examples/starter`, so
|
||||||
|
the dev loop comes up with its two examples running and nothing to configure.
|
||||||
|
|
||||||
It hardcodes nothing. It asks `ctrl/ports.sh active` for this environment's
|
It hardcodes nothing. It asks `ctrl/ports.sh active` for this environment's
|
||||||
cluster name, kube context, ports and manifest directory — the same values every
|
cluster name, kube context, ports and paths — the same values every other rig
|
||||||
other rig script resolves through `ctrl/lib/config.sh` — so a copied and renamed
|
script resolves through `ctrl/lib/config.sh` — so a copied and renamed rig, or a
|
||||||
rig deploys into its own cluster with no edits. Every other project here writes
|
moved overlay, deploys into its own cluster with no edits.
|
||||||
its slug into the Tiltfile five or six times by hand, which is exactly the
|
|
||||||
collision `kind-config.yaml.tpl` exists to avoid.
|
|
||||||
|
|
||||||
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.
|
`make help` lists every target. `make selftest` checks rig itself, the installer's
|
||||||
|
detection included; `make selftest install` runs the installer on clean containers
|
||||||
|
([`docs/notes/installer-testing.md`](docs/notes/installer-testing.md)).
|
||||||
|
|
||||||
On a machine where Docker really is the only thing installed, `make deps` has
|
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
|
nothing to download with — see [BOOTSTRAP.md](BOOTSTRAP.md), which runs the
|
||||||
toolchain through the installer container and carries on to scaffolding and running
|
toolchain through the installer container and carries on to scaffolding and running
|
||||||
a new project.
|
a new project.
|
||||||
|
|
||||||
## One directory is one environment
|
## 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.
|
||||||
|
|
||||||
Copy this directory, rename it, run it. Cluster name, kubectl context, image
|
## One environment per folder
|
||||||
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.
|
|
||||||
|
|
||||||
A copy of this directory is a **sibling** of it, named after the environment it
|
Cluster name, kubectl context, image tags and the host port block all derive
|
||||||
models (`acme-rig`). That is why the ignore rules for copies sit in the *parent*
|
from a folder name — the overlay's when one is named, else rig's own — so copies
|
||||||
repo's `.gitignore` rather than here: a rule in this directory cannot see a
|
never collide and neither one's teardown can touch the other. Two overlays run
|
||||||
directory beside it.
|
side by side from one rig; two copies of rig do too.
|
||||||
|
|
||||||
## Profiles
|
## Profiles
|
||||||
|
|
||||||
A profile is the shape of the cluster: how many nodes, which addons, whether the
|
**rig needs no profile.** With none named it runs on built-in defaults: one node,
|
||||||
apiserver audits. They live in `ctrl/env.d/`, and the active one is `PROFILE`.
|
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):
|
||||||
|
|
||||||
| Profile | For |
|
| example | what it changes |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `minimal` | the default. One node, no addons, boots fast. |
|
| `mirror.env.example` | images through a pull-through cache of an internal registry |
|
||||||
| `client` | the regulated-estate shape — multi-node, audit on, registry mirror. |
|
| `offline.env.example` | air-gapped: everything from a preloaded local registry |
|
||||||
| `offline` | air-gapped: everything from a preloaded local registry. |
|
|
||||||
| `data` | the cabinets an environment asks for. |
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PROFILE=data make cluster up
|
cp ctrl/env.d/mirror.env.example ctrl/env.d/mirror.env
|
||||||
PROFILE=data make addons install
|
PROFILE=mirror make cluster up
|
||||||
make addons # what the active profile wants, and what exists
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A profile names a **cluster shape** — a file in `ctrl/k8s/` — rather than
|
The layers, weakest first: built-in defaults < `ctrl/versions.env` < the profile
|
||||||
restating node count and audit as variables:
|
< the overlay's `rig.env` < `ctrl/.env` < your command line.
|
||||||
|
|
||||||
| shape | nodes | audit | used by |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `kind-config.yaml.tpl` | 1 | off | `minimal`, `data` |
|
|
||||||
| `kind-config.audit.yaml.tpl` | 1 | on | `offline` |
|
|
||||||
| `kind-config.client.yaml.tpl` | 3 | on | `client` |
|
|
||||||
|
|
||||||
Both numbers are read back out of the chosen file, so the YAML is the only place
|
The **cluster itself** is one file: the overlay's `kind-config.yaml.tpl` if it has
|
||||||
that decides and there is nothing to drift. The layout under `ctrl/k8s/` is the
|
one, else rig's `ctrl/k8s/kind-config.yaml.tpl` (one node). To change it — more
|
||||||
same as every other project here — a kind config, a kustomize `base/`, an
|
nodes, other port mappings, mounts — edit it and `make cluster reset`. The node
|
||||||
`overlays/dev/` — see [`ctrl/k8s/README.md`](ctrl/k8s/README.md).
|
count is read back out of it, so there is nothing to drift.
|
||||||
|
|
||||||
## Addons
|
## Addons
|
||||||
|
|
||||||
Each addon is its own idempotent script in `ctrl/addons/`, and a profile names
|
Each addon is its own idempotent script, and `ADDONS` names the ones to install,
|
||||||
the ones it wants in `ADDONS`. Adding one is adding a file — there is no
|
in order. Adding one is adding a file — there is no dispatcher to edit. An
|
||||||
dispatcher to edit.
|
overlay's `addons/<name>.sh` is found before rig's own.
|
||||||
|
|
||||||
**There is no ingress controller, deliberately.** They pin a narrow window of
|
**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
|
Kubernetes versions, so depending on one would constrain which k8s a rig can be
|
||||||
built with — and running a trailing-edge control plane to model a legacy estate
|
built with — and running a trailing-edge control plane is often the point.
|
||||||
is the whole point. Services are reached through MetalLB and
|
Services are reached through MetalLB and `type: LoadBalancer`, which carries no
|
||||||
`type: LoadBalancer`, which carries no such constraint and is also what a real
|
such constraint and is also what a real cluster does.
|
||||||
cluster does.
|
|
||||||
|
rig's own addons make the cluster work:
|
||||||
|
|
||||||
| Addon | Does |
|
| Addon | Does |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `metallb` | gives `type: LoadBalancer` an address it can actually reach |
|
| `metallb` | gives `type: LoadBalancer` an address it can actually reach |
|
||||||
| `cert-manager` | a local CA, so TLS works offline |
|
| `cert-manager` | a local CA, so TLS works offline |
|
||||||
| `metrics-server` | makes `kubectl top` work on kind |
|
| `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.
|
|
||||||
|
|
||||||
Passwords are generated on first install and kept across re-runs, so re-running
|
What a workload needs — a database, a cache, a scheduler — belongs to its
|
||||||
an addon never rotates a credential out from under something already connected:
|
overlay. [`examples/data`](examples/data/) carries postgres, redis and airflow as
|
||||||
|
plain manifests (no helm: a chart repo is a network dependency), with passwords
|
||||||
|
generated on first install and kept across re-runs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl -n data get secret postgres -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d
|
OVERLAY=examples/data make cluster up
|
||||||
kubectl -n data port-forward svc/airflow 8080:8080
|
|
||||||
```
|
```
|
||||||
|
|||||||
68
rig/STALE.md
Normal file
68
rig/STALE.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# 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,49 +1,42 @@
|
|||||||
# Machine-local config. Copy to ctrl/.env (gitignored) and edit.
|
# Machine-local config. Copy to ctrl/.env (gitignored) and edit.
|
||||||
# Cluster SHAPE lives in ctrl/env.d/<profile>.env — not here.
|
# Cluster SHAPE: an optional profile in ctrl/env.d/. Architecture MODEL: arch/<name>.json.
|
||||||
# The architecture MODEL lives in arch/<name>.json — not here either.
|
# Notes: docs/notes/env.md
|
||||||
|
|
||||||
# Which profile in ctrl/env.d/ to build. minimal | client | offline
|
# A profile in ctrl/env.d/ to build. Empty means rig's built-in defaults, which
|
||||||
PROFILE=minimal
|
# 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>.
|
# Cluster name; the kubectl context becomes kind-<CLUSTER>.
|
||||||
# LEAVE THIS UNSET unless you need a name that differs from the directory —
|
# LEAVE UNSET: it defaults to this folder's name, which keeps the folder copyable.
|
||||||
# it defaults to this folder's name, which is what makes the folder copyable:
|
|
||||||
# copy it, rename it, and you get a separate environment with no edits.
|
|
||||||
# CLUSTER=
|
# CLUSTER=
|
||||||
|
|
||||||
# Host ports. LEAVE UNSET — they derive from the directory name so several
|
# Host ports. LEAVE UNSET — derived from the directory name (see ctrl/ports.sh).
|
||||||
# environments coexist without negotiating (see ctrl/ports.sh). `make ports`
|
# `bash ctrl/ports.sh persist` pins them here; set a value only to override.
|
||||||
# shows this environment's block; `make ports persist` writes it here so it stops
|
|
||||||
# being derived and becomes fixed. Set a value only to override.
|
|
||||||
# HTTP_PORT=
|
# HTTP_PORT=
|
||||||
# HTTPS_PORT=
|
# HTTPS_PORT=
|
||||||
# TILT_PORT=
|
# TILT_PORT=
|
||||||
# REGISTRY_PORT=
|
# REGISTRY_PORT=
|
||||||
|
|
||||||
# Where the application manifests live. The real ones are expected to be
|
# Where the manifests live. Leave unset: the overlay's k8s/overlays/dev.
|
||||||
# versioned separately from this installer — they change on a different cadence,
|
# MANIFESTS_DIR=../platform-manifests/overlays/dev
|
||||||
# by different people. Repoint this at their repo and rig stops owning them:
|
|
||||||
# MANIFESTS_DIR=../platform-manifests/overlays/dev
|
|
||||||
MANIFESTS_DIR=ctrl/k8s/overlays/dev
|
|
||||||
|
|
||||||
# Where the installer fetches the pinned binaries from.
|
# Where the installer fetches the pinned binaries from:
|
||||||
# upstream GitHub releases / dl.k8s.io (needs internet)
|
# upstream (needs internet) | artifactory (generic repo) | baked (in the image)
|
||||||
# artifactory a generic repo — what a locked-down client usually allows
|
|
||||||
# baked already inside the installer image; no network at all
|
|
||||||
DEPS_SOURCE=upstream
|
DEPS_SOURCE=upstream
|
||||||
DEPS_ARTIFACTORY_URL=
|
DEPS_ARTIFACTORY_URL=
|
||||||
|
|
||||||
# --- Registry -------------------------------------------------------------
|
# --- Registry -------------------------------------------------------------
|
||||||
# Mode comes from the profile (REGISTRY_MODE). These are the secrets it needs.
|
# Mode comes from the profile (REGISTRY_MODE). Secrets required for mirror/remote:
|
||||||
# Required for mirror/remote:
|
|
||||||
REGISTRY_REMOTE_URL=
|
REGISTRY_REMOTE_URL=
|
||||||
REGISTRY_USER=
|
REGISTRY_USER=
|
||||||
REGISTRY_PASSWORD=
|
REGISTRY_PASSWORD=
|
||||||
|
|
||||||
# Corporate root CA, if Artifactory is fronted by an internal CA (it usually is).
|
# Corporate root CA, if Artifactory is fronted by an internal CA.
|
||||||
# Trust has to reach THREE places and nothing does it for you: the host docker
|
|
||||||
# daemon, every kind node's containerd, and any in-cluster client. registry.sh
|
|
||||||
# handles the first two; check.sh reports when it's configured but not trusted.
|
|
||||||
# Symptom when missing: x509: certificate signed by unknown authority
|
# Symptom when missing: x509: certificate signed by unknown authority
|
||||||
REGISTRY_CA_FILE=
|
REGISTRY_CA_FILE=
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,18 @@
|
|||||||
# The toolchain installer image. It does NOT run the cluster — it installs a toolchain
|
# Toolchain installer image: installs the pinned toolchain onto the host; Docker is the only prerequisite.
|
||||||
# onto the host and gets out of the way.
|
|
||||||
#
|
|
||||||
# This exists to kill a bootstrap paradox: a plain bash installer needs curl, jq
|
|
||||||
# and sha256sum to already be present, and a minimal Debian has none of them.
|
|
||||||
# It carries its own toolchain, so the only host prerequisite is Docker.
|
|
||||||
#
|
|
||||||
# Two variants from one file:
|
|
||||||
# docker build -f ctrl/Dockerfile.deps --target deps -t <slug>-deps .
|
# docker build -f ctrl/Dockerfile.deps --target deps -t <slug>-deps .
|
||||||
# docker build -f ctrl/Dockerfile.deps --target deps-full -t <slug>-deps:full .
|
# docker build -f ctrl/Dockerfile.deps --target deps-full -t <slug>-deps:full .
|
||||||
#
|
# Notes: docs/notes/Dockerfile.deps.md
|
||||||
# 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.
|
|
||||||
|
|
||||||
FROM debian:trixie-slim AS deps
|
FROM debian:trixie-slim AS deps
|
||||||
|
|
||||||
# ca-certificates + curl: fetch and verify. graphviz + python3: render diagrams
|
# curl: fetch and verify; graphviz + python3: diagrams. docker-cli, NOT docker.io
|
||||||
# and validate the arch model, so the host never needs an apt package.
|
# (which lacks the `docker` binary under --no-install-recommends).
|
||||||
#
|
|
||||||
# docker-cli, NOT docker.io: we only ever talk to the host's daemon through the
|
|
||||||
# mounted socket, and under --no-install-recommends the docker.io package ships
|
|
||||||
# docker-init without the actual `docker` binary.
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates curl jq graphviz python3 docker-cli \
|
ca-certificates curl jq graphviz python3 docker-cli \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# The installer is the generated standalone kit, not deps.sh plus the files it
|
# The installer is the generated one-file standalone kit, pins frozen in.
|
||||||
# reads. A kit is one file with its pins frozen in and is proven to run with
|
ARG PROFILE=default
|
||||||
# nothing else from rig present — which is exactly what an image needs, and
|
|
||||||
# `make standalone` keeps it current. Pins are the same in every profile's kit.
|
|
||||||
ARG PROFILE=minimal
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY standalone/${PROFILE}/rigdeps.sh /work/rigdeps.sh
|
COPY standalone/${PROFILE}/rigdeps.sh /work/rigdeps.sh
|
||||||
RUN chmod +x /work/rigdeps.sh
|
RUN chmod +x /work/rigdeps.sh
|
||||||
@@ -43,8 +27,10 @@ CMD ["install"]
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# deps-full — same image, binaries baked in, works with no network at all.
|
# deps-full — same image, binaries and the addons' manifests baked in, works with no network.
|
||||||
FROM deps AS deps-full
|
FROM deps AS deps-full
|
||||||
RUN /work/rigdeps.sh fetch --to /opt/rig/bin
|
RUN /work/rigdeps.sh fetch --to /opt/rig/bin \
|
||||||
|
&& /work/rigdeps.sh manifests --to /opt/rig/manifests
|
||||||
ENV DEPS_SOURCE=baked \
|
ENV DEPS_SOURCE=baked \
|
||||||
BAKED_BIN=/opt/rig/bin
|
BAKED_BIN=/opt/rig/bin \
|
||||||
|
BAKED_MANIFESTS=/opt/rig/manifests
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
# EXAMPLE — a component image. Copy, rename, replace.
|
|
||||||
#
|
|
||||||
# Named like the manifest it feeds and the resource it becomes:
|
|
||||||
#
|
|
||||||
# ctrl/Dockerfile.api -> image <cluster>-api -> image: in k8s/base/api.yaml
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# ── the one that catches everyone ──────────────────────────────────────────
|
|
||||||
# The Tiltfile passes two paths with DIFFERENT bases, in adjacent arguments:
|
|
||||||
#
|
|
||||||
# context='..' the REPO ROOT (the Tiltfile is in ctrl/)
|
|
||||||
# dockerfile='Dockerfile.api' relative to the TILTFILE, so ctrl/Dockerfile.api
|
|
||||||
#
|
|
||||||
# So every COPY below is resolved against the repo root, NOT against this file's
|
|
||||||
# directory. A file sitting right beside this one is still reached as `ctrl/`:
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
FROM python:3.12-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
COPY api/requirements.txt ./
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Repo-root relative — see above.
|
|
||||||
COPY api/ ./api/
|
|
||||||
|
|
||||||
# Match this with the containerPort in the manifest and the target of the
|
|
||||||
# Service in front of it.
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
CMD ["python", "-m", "api"]
|
|
||||||
|
|
||||||
# ── live_update ────────────────────────────────────────────────────────────
|
|
||||||
# The sync in the Tiltfile's docker_build must land where this image expects it:
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
@@ -1,28 +1,10 @@
|
|||||||
# The dev loop. `make tilt` from the project root, or `cd ctrl && tilt up`.
|
# 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.
|
||||||
# This file ships with rig and works unedited: rig's own k8s/base already boots,
|
# The workload's half is the overlay's own Tiltfile, included at the end; edit that one.
|
||||||
# so `make tilt` comes up with a running cluster and no editing at all. What it
|
# Notes: docs/notes/Tiltfile.md
|
||||||
# deploys is two EXAMPLES — replace them, and add your own images and resources
|
|
||||||
# in the two marked sections near the bottom. The catalogue after them has the
|
|
||||||
# blocks to paste, with the parts that are easy to get wrong already commented.
|
|
||||||
#
|
|
||||||
# rig 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 below 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 ─────────────────────────────────────────
|
# ── who we are, and on which ports ─────────────────────────────────────────
|
||||||
# One question to rig, answered by ctrl/ports.sh, which resolves it through
|
# Asked of ctrl/ports.sh (via lib/config.sh), never recomputed here in Starlark.
|
||||||
# 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.
|
|
||||||
_facts = str(local('bash ports.sh active', quiet=True)).split()
|
_facts = str(local('bash ports.sh active', quiet=True)).split()
|
||||||
CLUSTER = _facts[0]
|
CLUSTER = _facts[0]
|
||||||
CTX = _facts[1]
|
CTX = _facts[1]
|
||||||
@@ -30,110 +12,56 @@ HTTP = _facts[2]
|
|||||||
HTTPS = _facts[3]
|
HTTPS = _facts[3]
|
||||||
TILT = _facts[4]
|
TILT = _facts[4]
|
||||||
REGISTRY = _facts[5]
|
REGISTRY = _facts[5]
|
||||||
|
# Absolute paths, or '-' when there is none.
|
||||||
# Where the manifests live. rig's own are the default; point MANIFESTS_DIR in
|
MANIFESTS = '' if _facts[6] == '-' else _facts[6]
|
||||||
# ctrl/.env at an overlay versioned somewhere else and rig stops owning them —
|
OVERLAY = '' if _facts[7] == '-' else _facts[7]
|
||||||
# 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. This file 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.
|
|
||||||
MANIFESTS = '../' + _facts[6]
|
|
||||||
|
|
||||||
# ── refuse to deploy into the wrong cluster ────────────────────────────────
|
# ── refuse to deploy into the wrong cluster ────────────────────────────────
|
||||||
# Tilt snapshots the kubectl context at startup, BEFORE parsing this file, so it
|
# Tilt fixes the context before parsing this file, so it can only be refused here.
|
||||||
# cannot be switched from here — only refused. `make tilt` passes --context for
|
# `make tilt` passes --context; this catches a bare `tilt up`.
|
||||||
# you; this catches a bare `tilt up` after some other project moved the global
|
|
||||||
# context.
|
|
||||||
allow_k8s_contexts(CTX)
|
allow_k8s_contexts(CTX)
|
||||||
if k8s_context() != CTX:
|
if k8s_context() != CTX:
|
||||||
fail("Wrong kubectl context: '%s'. This is %s — run: make tilt, or tilt up --context %s"
|
fail("Wrong kubectl context: '%s'. This is %s — run: make tilt, or tilt up --context %s"
|
||||||
% (k8s_context(), CLUSTER, CTX))
|
% (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 ───────────────────────────
|
# ── images go to this environment's own registry ───────────────────────────
|
||||||
# Fail closed. Tilt can usually infer the kind registry on its own, but "usually"
|
# Fail closed: name the registry rather than let Tilt infer it, or a miss pushes
|
||||||
# is an inference, and when it misses, an unqualified name like 'app' quietly
|
# an unqualified image to docker.io.
|
||||||
# means docker.io/library/app — a push to the public index instead of the
|
|
||||||
# registry two lines away. rig runs that registry; name it.
|
|
||||||
default_registry('localhost:' + REGISTRY)
|
default_registry('localhost:' + REGISTRY)
|
||||||
|
|
||||||
k8s_yaml(kustomize(MANIFESTS))
|
# ── the overlay's manifests ────────────────────────────────────────────────
|
||||||
|
# Every namespace they use must exist before anything lands in it, and kustomize
|
||||||
# ── Images ─────────────────────────────────────────────────────────────────
|
# does not order resources, so create them here (idempotent). The Namespaces they
|
||||||
# (nothing yet — rig's examples run upstream images. Add docker_build calls here.)
|
# declare are grouped as 'infra', whatever they are named.
|
||||||
|
if MANIFESTS:
|
||||||
|
_yaml = kustomize(MANIFESTS)
|
||||||
# ── Resources ──────────────────────────────────────────────────────────────
|
k8s_yaml(_yaml)
|
||||||
# (nothing yet — add k8s_resource calls here to name and order what you deploy.)
|
_declared = []
|
||||||
|
_namespaces = {}
|
||||||
|
for _o in decode_yaml_stream(_yaml):
|
||||||
# Everything with no dev loop of its own, gathered so it does not clutter the UI.
|
if not _o:
|
||||||
k8s_resource(
|
continue
|
||||||
objects=[CLUSTER + ':namespace'],
|
_md = _o.get('metadata') or {}
|
||||||
new_name='infra',
|
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')
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ── the workload's half: the overlay's Tiltfile ────────────────────────────
|
||||||
# Catalogue — paste what you need, delete the rest.
|
# 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.
|
||||||
# These are the shapes that recur across every project here, with the reasoning
|
os.putenv('RIG_CLUSTER', CLUSTER)
|
||||||
# kept next to them. They are comments so this file runs as-is.
|
os.putenv('RIG_CONTEXT', CTX)
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
os.putenv('RIG_HTTP_PORT', HTTP)
|
||||||
#
|
os.putenv('RIG_HTTPS_PORT', HTTPS)
|
||||||
# ── build an image ─────────────────────────────────────────────────────────
|
os.putenv('RIG_TILT_PORT', TILT)
|
||||||
# The one genuinely non-obvious thing in the whole corpus: `context` and
|
os.putenv('RIG_REGISTRY', 'localhost:' + REGISTRY)
|
||||||
# `dockerfile` are relative to DIFFERENT directories, in adjacent arguments,
|
os.putenv('RIG_OVERLAY_DIR', OVERLAY)
|
||||||
# and nothing warns you.
|
if OVERLAY and os.path.exists(OVERLAY + '/Tiltfile'):
|
||||||
#
|
include(OVERLAY + '/Tiltfile')
|
||||||
# context= the REPO ROOT — this file is in ctrl/, so '..'
|
|
||||||
# dockerfile= relative to THIS file — so 'Dockerfile.api' is ctrl/Dockerfile.api
|
|
||||||
#
|
|
||||||
# Every COPY inside those Dockerfiles is therefore repo-root relative: a file
|
|
||||||
# sitting BESIDE the Dockerfile is still reached as `COPY ctrl/nginx.conf`.
|
|
||||||
#
|
|
||||||
# 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 Caddyfile arriving via configMapGenerator with disableNameSuffixHash does
|
|
||||||
# NOT roll the pod — the ConfigMap name never changes, so nothing tells the
|
|
||||||
# Deployment anything happened. Without this you edit the routes and watch
|
|
||||||
# nothing take effect.
|
|
||||||
#
|
|
||||||
# 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 ────────────────────────
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# k8s_resource('postgres', port_forwards=[str(int(HTTP) + 5) + ':5432'])
|
|
||||||
|
|||||||
@@ -1,35 +1,59 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Install the addons the active profile asked for, in the order listed.
|
# Install the addons the configuration asks for (ADDONS), in the order listed.
|
||||||
# Each addon is its own idempotent script in ctrl/addons/ — adding one is adding
|
# One idempotent script per addon: the overlay's addons/<name>.sh first, then rig's ctrl/addons/.
|
||||||
# a file, not editing a dispatcher.
|
|
||||||
#
|
|
||||||
# Usage: addons.sh install | list
|
# Usage: addons.sh install | list
|
||||||
|
# Notes: docs/notes/addons.md
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
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() {
|
install() {
|
||||||
if [ -z "${ADDONS// /}" ]; then
|
if [ -z "${ADDONS// /}" ]; then
|
||||||
echo "no addons in profile '$PROFILE_NAME'"
|
echo "no addons asked for (ADDONS is empty)"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
local a
|
local a p
|
||||||
for a in $ADDONS; do
|
for a in $ADDONS; do
|
||||||
if [ ! -f "addons/${a}.sh" ]; then
|
p=$(addon_path "$a")
|
||||||
echo "no such addon: addons/${a}.sh" >&2
|
if [ -z "$p" ]; then
|
||||||
|
echo "no such addon: $a (looked in the overlay's addons/ and ctrl/addons/)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "addon: $a"
|
echo "addon: $a"
|
||||||
bash "addons/${a}.sh"
|
bash "$p"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
list() {
|
list() {
|
||||||
echo "profile '$PROFILE_NAME' wants: ${ADDONS:-none}"
|
echo "wanted: ${ADDONS:-none}"
|
||||||
echo "available:"
|
echo "available:"
|
||||||
ls addons/*.sh 2>/dev/null | xargs -n1 basename | sed 's/\.sh$//' | sed 's/^/ /'
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-list}" in
|
case "${1:-list}" in
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# cert-manager plus a self-signed cluster issuer.
|
# cert-manager plus a self-signed cluster issuer (offline local CA).
|
||||||
#
|
# Notes: docs/notes/addons.md
|
||||||
# In a regulated estate almost everything is TLS, so the interesting question
|
|
||||||
# during onboarding is "does this service present a cert my client trusts" — not
|
|
||||||
# "can I reach a public ACME server". A local CA answers that offline, which is
|
|
||||||
# also what makes the air-gapped profile usable.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
@@ -16,7 +12,9 @@ K="kubectl --context ${KUBECONTEXT}"
|
|||||||
if $K get deployment -n cert-manager cert-manager >/dev/null 2>&1; then
|
if $K get deployment -n cert-manager cert-manager >/dev/null 2>&1; then
|
||||||
echo " already installed"
|
echo " already installed"
|
||||||
else
|
else
|
||||||
$K apply -f "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml"
|
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||||
|
manifest=$(bash ./deps.sh manifest CERT_MANAGER)
|
||||||
|
$K apply -f "$manifest"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " waiting for cert-manager..."
|
echo " waiting for cert-manager..."
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# MetalLB — makes `Service type: LoadBalancer` actually get an address.
|
# MetalLB — makes `Service type: LoadBalancer` actually get an address.
|
||||||
#
|
# The pool is derived from the kind Docker network at install time.
|
||||||
# Why it matters here: real manifests use LoadBalancer, because a real cluster
|
# Notes: docs/notes/addons.md
|
||||||
# has one. On a bare kind cluster those Services sit at EXTERNAL-IP <pending>
|
|
||||||
# forever with no error anywhere — the deployment looks fine and simply is not
|
|
||||||
# reachable. Without this, every such Service has to be edited to NodePort,
|
|
||||||
# which means the local manifests stop matching the ones being modelled.
|
|
||||||
#
|
|
||||||
# The address pool is derived from the kind Docker network at install time, not
|
|
||||||
# hardcoded: Docker picks that subnet, it differs between machines, and a pool
|
|
||||||
# outside it is silently unroutable.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
@@ -54,13 +46,12 @@ echo " kind network $subnet → pool ${pool_start}-${pool_end}"
|
|||||||
if $K get deployment -n metallb-system controller >/dev/null 2>&1; then
|
if $K get deployment -n metallb-system controller >/dev/null 2>&1; then
|
||||||
echo " already installed"
|
echo " already installed"
|
||||||
else
|
else
|
||||||
$K apply -f "https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml"
|
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||||
|
manifest=$(bash ./deps.sh manifest METALLB)
|
||||||
|
$K apply -f "$manifest"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# `kubectl wait` on a selector errors out immediately when nothing matches yet,
|
# `rollout status`, not `kubectl wait`: wait errors out while the pod doesn't exist yet.
|
||||||
# and right after apply the ReplicaSet has not created the pod — so it loses a
|
|
||||||
# race it looks like it should win. `rollout status` waits for the Deployment
|
|
||||||
# itself and handles the not-yet-created case.
|
|
||||||
echo " waiting for the controller..."
|
echo " waiting for the controller..."
|
||||||
$K rollout status deployment/controller -n metallb-system --timeout=240s
|
$K rollout status deployment/controller -n metallb-system --timeout=240s
|
||||||
$K rollout status daemonset/speaker -n metallb-system --timeout=240s
|
$K rollout status daemonset/speaker -n metallb-system --timeout=240s
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# metrics-server — makes `kubectl top` work.
|
# metrics-server — makes `kubectl top` work (patched with --kubelet-insecure-tls for kind).
|
||||||
#
|
# Notes: docs/notes/addons.md
|
||||||
# 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.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "${RIG_CTRL:-$(dirname "$0")/..}"
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
@@ -14,7 +10,9 @@ load_config
|
|||||||
K="kubectl --context ${KUBECONTEXT}"
|
K="kubectl --context ${KUBECONTEXT}"
|
||||||
|
|
||||||
if ! $K get deployment -n kube-system metrics-server >/dev/null 2>&1; then
|
if ! $K get deployment -n kube-system metrics-server >/dev/null 2>&1; then
|
||||||
$K apply -f "https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml"
|
# The pinned manifest, verified on disk — never a URL applied directly.
|
||||||
|
manifest=$(bash ./deps.sh manifest METRICS_SERVER)
|
||||||
|
$K apply -f "$manifest"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
$K patch deployment metrics-server -n kube-system --type=json \
|
$K patch deployment metrics-server -n kube-system --type=json \
|
||||||
|
|||||||
@@ -1,61 +1,36 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Readiness check: is this machine ready to run rig?
|
# 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)
|
||||||
# Reports and instructs; never silently fixes anything. Everything it finds is
|
# Notes: docs/notes/check.md
|
||||||
# either already fine, or something a human has to decide on.
|
|
||||||
#
|
|
||||||
# Runs ctrl/deps.sh host detection in a container when Docker is the only thing
|
|
||||||
# installed, or directly when the toolchain is already present. Then adds the
|
|
||||||
# checks that need this repo's config: profile sanity, CA trust, port clashes.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
DEPS_IMAGE="${DEPS_IMAGE:-$(basename "$(cd .. && pwd)")-deps}"
|
# `check mem` goes deeper on memory than the summary below: how far allocation
|
||||||
|
# really climbs, and the WSL .wslconfig backup/restore.
|
||||||
|
if [ "${1:-}" = mem ]; then
|
||||||
|
shift
|
||||||
|
exec bash ./mem.sh "${@:-status}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Host detection. Prefer running it bare — it needs no dependencies beyond
|
# Compact by default: facts only with `all`; problems (!) always print.
|
||||||
# coreutils — and fall back to the container only if this shell can't.
|
VERBOSE=""
|
||||||
bash ./deps.sh detect
|
if [ "${1:-}" = all ]; then VERBOSE=1; fi
|
||||||
|
fact() { if [ -n "$VERBOSE" ]; then echo "$@"; fi; }
|
||||||
|
|
||||||
# ── repo-level checks ──────────────────────────────────────────────────────
|
bash ./deps.sh detect ${VERBOSE:+all}
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
|
|
||||||
echo
|
# A /proc/meminfo field in MB, 0 if absent. MEMINFO/OVERCOMMIT_FILE override for testing.
|
||||||
echo "config"
|
|
||||||
echo " profile ${PROFILE_NAME} (nodes=${NODES} audit=${AUDIT})"
|
|
||||||
echo " cluster ${CLUSTER} (context ${KUBECONTEXT})"
|
|
||||||
echo " registry ${REGISTRY_MODE}"
|
|
||||||
echo " ingress ${INGRESS_MODE}"
|
|
||||||
|
|
||||||
if [ ! -f ./.env ]; then
|
|
||||||
echo " ! ctrl/.env missing — copy it: cp ctrl/.env.example ctrl/.env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── memory ─────────────────────────────────────────────────────────────────
|
|
||||||
#
|
|
||||||
# A profile on a box that is already full is the most common first failure, and
|
|
||||||
# it presents as pods stuck Pending rather than anything that says "memory".
|
|
||||||
# Warns; never blocks. Whether to try anyway is the user's call.
|
|
||||||
|
|
||||||
# A /proc/meminfo field in MB, 0 if absent. MEMINFO and OVERCOMMIT_FILE exist
|
|
||||||
# only so the tight and does-not-fit branches can be exercised against another
|
|
||||||
# machine's real numbers; in normal use they are the kernel's own files.
|
|
||||||
mb_of() {
|
mb_of() {
|
||||||
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# NODE_MB — what one node costs — comes from load_config (lib/config.sh), where
|
# NODE_MB (cost of one node) comes from load_config in lib/config.sh; do not copy it here.
|
||||||
# its measurement is recorded. It lives there, not here, because the memory tool
|
|
||||||
# and every standalone kit need the same number: a copy of it is how rigmini.sh
|
|
||||||
# came to say 2 GB per node long after rig had measured 800 MB.
|
|
||||||
|
|
||||||
# Every running container's working set in MB, tagged with the kind cluster it
|
# Every running container's working set in MB, tagged with its kind cluster ('-' if none).
|
||||||
# belongs to ('-' when it is not kind). docker stats reports usage minus page
|
|
||||||
# cache, which is what actually competes — cache is handed back under pressure.
|
|
||||||
# Counting only kind would hide the usual culprit on a managed workspace, where
|
|
||||||
# the memory is held by other containers entirely.
|
|
||||||
container_mb() {
|
container_mb() {
|
||||||
docker info >/dev/null 2>&1 || return 0
|
docker info >/dev/null 2>&1 || return 0
|
||||||
awk -F'\t' '
|
awk -F'\t' '
|
||||||
@@ -81,6 +56,49 @@ container_mb() {
|
|||||||
<(docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}' 2>/dev/null)
|
<(docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}' 2>/dev/null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port_busy() {
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
ss -ltn "sport = :$1" 2>/dev/null | grep -q LISTEN && return 0 || return 1
|
||||||
|
fi
|
||||||
|
# iproute2 is absent from a minimal Debian, so fall back to procfs rather
|
||||||
|
# than silently reporting everything as free.
|
||||||
|
local hex; hex=$(printf ':%04X' "$1")
|
||||||
|
grep -qi "^ *[0-9]*: [0-9A-F]*$hex " /proc/net/tcp /proc/net/tcp6 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "rig"
|
||||||
|
echo " cluster ${CLUSTER} (${KUBECONTEXT}) profile ${PROFILE_NAME}, ${NODES} node(s), registry ${REGISTRY_MODE}"
|
||||||
|
if [ -n "${OVERLAY:-}" ]; then
|
||||||
|
echo " overlay $(basename "$(_abs_from_ctrl "$OVERLAY_DIR")") ($(_abs_from_ctrl "$OVERLAY_DIR"))"
|
||||||
|
elif [ -n "$OVERLAY_DIR" ]; then
|
||||||
|
fact " 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
|
||||||
|
fact " manifests ${MANIFESTS_DIR:-none}"
|
||||||
|
fact " kind config ${KIND_CONFIG}"
|
||||||
|
fact " 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
|
||||||
|
|
||||||
|
# ── memory: does this cluster fit right now? Warns; never blocks. ──────────
|
||||||
total_mb=$(mb_of MemTotal)
|
total_mb=$(mb_of MemTotal)
|
||||||
avail_mb=$(mb_of MemAvailable)
|
avail_mb=$(mb_of MemAvailable)
|
||||||
swap_used_mb=$(( $(mb_of SwapTotal) - $(mb_of SwapFree) ))
|
swap_used_mb=$(( $(mb_of SwapTotal) - $(mb_of SwapFree) ))
|
||||||
@@ -88,131 +106,111 @@ overcommit=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/nul
|
|||||||
need_mb=$(( NODES * NODE_MB ))
|
need_mb=$(( NODES * NODE_MB ))
|
||||||
|
|
||||||
rows=$(container_mb)
|
rows=$(container_mb)
|
||||||
# Once this environment's own cluster is running, its real footprint is already
|
# If our cluster is already up, its memory is already out of MemAvailable: need nothing more.
|
||||||
# out of MemAvailable and the per-node estimate stops being relevant. Subtracting
|
|
||||||
# the measurement from the estimate would count the same memory twice, and a
|
|
||||||
# running cluster that happens to sit under 800 MB would still "need" the gap.
|
|
||||||
ours_mb=$(awk -F'\t' -v c="$CLUSTER" '$2 == c { s += $1 } END { print s + 0 }' <<< "$rows")
|
ours_mb=$(awk -F'\t' -v c="$CLUSTER" '$2 == c { s += $1 } END { print s + 0 }' <<< "$rows")
|
||||||
still_mb=$(( ours_mb > 0 ? 0 : need_mb ))
|
still_mb=$(( ours_mb > 0 ? 0 : need_mb ))
|
||||||
|
headroom=$(( avail_mb - still_mb ))
|
||||||
|
|
||||||
echo
|
# The biggest things holding memory, other than this cluster: kind clusters summed, the rest by name.
|
||||||
echo "memory"
|
|
||||||
printf " this profile ~%d MB %s node(s) x %d MB — the cluster alone, your workload on top\n" \
|
|
||||||
"$need_mb" "$NODES" "$NODE_MB"
|
|
||||||
if [ "$ours_mb" -gt 0 ]; then
|
|
||||||
printf " already held %d MB by '%s', which is up\n" "$ours_mb" "$CLUSTER"
|
|
||||||
fi
|
|
||||||
printf " available %d MB of %d MB\n" "$avail_mb" "$total_mb"
|
|
||||||
|
|
||||||
# The biggest things holding memory right now, other than this cluster: kind
|
|
||||||
# clusters summed per cluster, everything else by container name.
|
|
||||||
others=$(awk -F'\t' -v c="$CLUSTER" '
|
others=$(awk -F'\t' -v c="$CLUSTER" '
|
||||||
$2 != c && $2 != "-" && $2 != "" { k["kind cluster \x27" $2 "\x27"] += $1 }
|
$2 != c && $2 != "-" && $2 != "" { k["kind cluster \x27" $2 "\x27"] += $1 }
|
||||||
$2 == "-" { k["container \x27" $3 "\x27"] += $1 }
|
$2 == "-" { k["container \x27" $3 "\x27"] += $1 }
|
||||||
END { for (n in k) printf "%d\t%s\n", k[n], n }' <<< "$rows" | sort -rn)
|
END { for (n in k) printf "%d\t%s\n", k[n], n }' <<< "$rows" | sort -rn)
|
||||||
if [ -n "$others" ]; then
|
|
||||||
echo " held elsewhere:"
|
|
||||||
head -6 <<< "$others" | awk -F'\t' '{ printf " %6d MB %s\n", $1, $2 }'
|
|
||||||
n_others=$(wc -l <<< "$others")
|
|
||||||
if [ "$n_others" -gt 6 ]; then
|
|
||||||
echo " ... and $((n_others - 6)) more"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
headroom=$(( avail_mb - still_mb ))
|
if [ "$still_mb" -eq 0 ] && [ "$headroom" -ge 512 ]; then
|
||||||
if [ "$still_mb" -eq 0 ]; then
|
printf " memory up, holding %d MB — %d MB headroom for what you deploy\n" "$ours_mb" "$headroom"
|
||||||
if [ "$headroom" -ge 512 ]; then
|
elif [ "$still_mb" -eq 0 ]; then
|
||||||
printf " fits — already up; %d MB headroom for what you deploy\n" "$headroom"
|
printf " ! memory up, but only %d MB headroom for anything you deploy\n" "$headroom"
|
||||||
else
|
|
||||||
printf " ! already up, but only %d MB headroom for anything you deploy\n" "$headroom"
|
|
||||||
fi
|
|
||||||
elif [ "$headroom" -ge 512 ]; then
|
elif [ "$headroom" -ge 512 ]; then
|
||||||
printf " fits — %d MB headroom for what you deploy\n" "$headroom"
|
printf " memory fits — ~%d MB for %s node(s), %d MB headroom\n" "$need_mb" "$NODES" "$headroom"
|
||||||
elif [ "$headroom" -ge 0 ]; then
|
elif [ "$headroom" -ge 0 ]; then
|
||||||
printf " ! fits, but only %d MB headroom for anything you deploy\n" "$headroom"
|
printf " ! memory fits, but only %d MB headroom (~%d MB for %s node(s))\n" "$headroom" "$need_mb" "$NODES"
|
||||||
else
|
else
|
||||||
printf " ! does not fit right now: ~%d MB needed, %d MB available\n" "$still_mb" "$avail_mb"
|
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.
|
# Two failures with opposite fixes, and telling them apart is the point.
|
||||||
if [ "$still_mb" -le "$total_mb" ]; then
|
if [ "$still_mb" -le "$total_mb" ]; then
|
||||||
echo " The machine is big enough; something else is holding memory (above)."
|
echo " something else holds it (below) — stopping that helps, a bigger VM would not."
|
||||||
echo " Stopping that is what helps — a bigger VM would not."
|
|
||||||
if grep -q 'kind cluster' <<< "$others"; then
|
if grep -q 'kind cluster' <<< "$others"; then
|
||||||
echo " 'make cluster free' stops the other kind clusters. It stops, never deletes."
|
echo " 'make cluster free' stops the other kind clusters. It stops, never deletes."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo " The machine itself is too small: ~${still_mb} MB needed, ${total_mb} MB total."
|
echo " the machine itself is too small: ${total_mb} MB total."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -n "$others" ] && { [ -n "$VERBOSE" ] || [ "$headroom" -lt 512 ]; }; then
|
||||||
|
echo " held elsewhere:"
|
||||||
|
head -6 <<< "$others" | awk -F'\t' '{ printf " %6d MB %s\n", $1, $2 }'
|
||||||
|
n_others=$(wc -l <<< "$others")
|
||||||
|
if [ "$n_others" -gt 6 ]; then
|
||||||
|
echo " ... and $((n_others - 6)) more"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$swap_used_mb" -gt 0 ]; then
|
if [ "$swap_used_mb" -gt 0 ]; then
|
||||||
printf " ! %d MB already in swap — available memory does not count it, so expect a\n" "$swap_used_mb"
|
fact " ${swap_used_mb} MB already in swap, which 'available' does not count: expect slow before failing"
|
||||||
echo " cluster here to be slow well before it fails"
|
|
||||||
fi
|
fi
|
||||||
if [ "$overcommit" = "1" ]; then
|
if [ "$overcommit" = "1" ]; then
|
||||||
echo " ! overcommit=1: allocations never fail here, so read 'fits' as a ceiling."
|
fact " overcommit=1: allocations never fail, so read 'fits' as a ceiling (OOM killer settles up)"
|
||||||
echo " A cluster that starts cleanly can still lose processes to the OOM killer."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The CA reaches three places and only one of them is ours. Report the other two.
|
# ── ports: checked before creation; docker reports a clash only halfway through. ──
|
||||||
if [ -n "${REGISTRY_CA_FILE:-}" ]; then
|
# Ports held by our own cluster are not clashes. Second grep, not `tr -d ':->'` (a tr range).
|
||||||
echo
|
|
||||||
echo "registry CA"
|
|
||||||
if [ ! -r "$REGISTRY_CA_FILE" ]; then
|
|
||||||
echo " ! REGISTRY_CA_FILE not readable: $REGISTRY_CA_FILE"
|
|
||||||
else
|
|
||||||
echo " file $REGISTRY_CA_FILE"
|
|
||||||
host="${REGISTRY_REMOTE_URL#*://}"; host="${host%%/*}"
|
|
||||||
if [ -n "$host" ] && [ ! -f "/etc/docker/certs.d/${host}/ca.crt" ]; then
|
|
||||||
echo " ! the HOST docker daemon does not trust it yet:"
|
|
||||||
echo " sudo mkdir -p /etc/docker/certs.d/${host}"
|
|
||||||
echo " sudo cp ${REGISTRY_CA_FILE} /etc/docker/certs.d/${host}/ca.crt"
|
|
||||||
echo " (kind nodes are handled by registry.sh; in-cluster clients are the workload's job)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Host ports this environment will try to bind. Checked before cluster creation
|
|
||||||
# because docker reports a clash halfway through, as an opaque
|
|
||||||
# "failed to bind host port ...: address already in use".
|
|
||||||
echo
|
|
||||||
echo "ports (block derived from the directory name — see 'make ports')"
|
|
||||||
|
|
||||||
port_busy() {
|
|
||||||
if command -v ss >/dev/null 2>&1; then
|
|
||||||
ss -ltn "sport = :$1" 2>/dev/null | grep -q LISTEN && return 0 || return 1
|
|
||||||
fi
|
|
||||||
# iproute2 is absent from a minimal Debian, so fall back to procfs rather
|
|
||||||
# than silently reporting everything as free.
|
|
||||||
local hex; hex=$(printf ':%04X' "$1")
|
|
||||||
grep -qi "^ *[0-9]*: [0-9A-F]*$hex " /proc/net/tcp /proc/net/tcp6 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# A port held by THIS environment's own cluster is not a clash — it is the thing
|
|
||||||
# working. Reporting it as a problem every time the cluster is up would train
|
|
||||||
# people to ignore this section, which is the opposite of the point.
|
|
||||||
# Extract 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.
|
|
||||||
ours=$(docker ps --filter "label=io.x-k8s.kind.cluster=${CLUSTER}" \
|
ours=$(docker ps --filter "label=io.x-k8s.kind.cluster=${CLUSTER}" \
|
||||||
--format '{{.Ports}}' 2>/dev/null | tr ',' '\n' \
|
--format '{{.Ports}}' 2>/dev/null | tr ',' '\n' \
|
||||||
| grep -oE ':[0-9]+->' | grep -oE '[0-9]+' || true)
|
| grep -oE ':[0-9]+->' | grep -oE '[0-9]+' || true)
|
||||||
|
|
||||||
clash=0
|
clash=0 list="" mine=0
|
||||||
for entry in "HTTP:${HTTP_PORT}" "HTTPS:${HTTPS_PORT}" \
|
for entry in "HTTP:${HTTP_PORT}" "HTTPS:${HTTPS_PORT}" \
|
||||||
"TILT:${TILT_PORT}" "REGISTRY:${REGISTRY_PORT}"; do
|
"TILT:${TILT_PORT}" "REGISTRY:${REGISTRY_PORT}"; do
|
||||||
name="${entry%%:*}"; p="${entry#*:}"
|
name="${entry%%:*}"; p="${entry#*:}"
|
||||||
[ -n "$p" ] || continue
|
[ -n "$p" ] || continue
|
||||||
|
list+="$p "
|
||||||
if ! port_busy "$p"; then
|
if ! port_busy "$p"; then
|
||||||
printf " %-9s %-6s free\n" "$name" "$p"
|
fact "$(printf " %-9s %-6s free" "$name" "$p")"
|
||||||
elif echo "$ours" | grep -qx "$p"; then
|
elif echo "$ours" | grep -qx "$p"; then
|
||||||
printf " %-9s %-6s in use by this environment's cluster\n" "$name" "$p"
|
mine=1
|
||||||
|
fact "$(printf " %-9s %-6s in use by this environment's cluster" "$name" "$p")"
|
||||||
else
|
else
|
||||||
printf " ! %-9s %-6s IN USE by something else\n" "$name" "$p"
|
printf " ! ports %s %s IN USE by something else\n" "$name" "$p"
|
||||||
clash=1
|
clash=1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$clash" -eq 1 ]; then
|
if [ "$clash" -eq 1 ]; then
|
||||||
echo " override the clashing one in ctrl/.env, e.g. HTTP_PORT=21080"
|
echo " override it in ctrl/.env (e.g. HTTP_PORT=21080), or rename this directory"
|
||||||
echo " (or rename this directory — the whole block follows the name)"
|
elif [ "$mine" -eq 1 ]; then
|
||||||
|
echo " ports ${list% } held by this cluster"
|
||||||
|
else
|
||||||
|
echo " ports ${list% } free"
|
||||||
|
fi
|
||||||
|
if [ -n "${OVERLAY:-}" ]; then
|
||||||
|
fact " derived from the overlay's folder name"
|
||||||
|
else
|
||||||
|
fact " derived from the directory name; pin them: bash ctrl/ports.sh persist"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── what `make cluster up` wires in beside the cluster ─────────────────────
|
||||||
|
REG_NAME="${CLUSTER}-registry"
|
||||||
|
if state=$(docker inspect -f '{{.State.Status}}' "$REG_NAME" 2>/dev/null); then
|
||||||
|
echo " registry localhost:${REGISTRY_PORT} ($state)"
|
||||||
|
else
|
||||||
|
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
|
||||||
|
|
||||||
|
# The CA reaches three places and only one of them is ours. Report the other two.
|
||||||
|
if [ -n "${REGISTRY_CA_FILE:-}" ]; then
|
||||||
|
if [ ! -r "$REGISTRY_CA_FILE" ]; then
|
||||||
|
echo " ! CA REGISTRY_CA_FILE not readable: $REGISTRY_CA_FILE"
|
||||||
|
else
|
||||||
|
fact " CA $REGISTRY_CA_FILE"
|
||||||
|
host="${REGISTRY_REMOTE_URL#*://}"; host="${host%%/*}"
|
||||||
|
if [ -n "$host" ] && [ ! -f "/etc/docker/certs.d/${host}/ca.crt" ]; then
|
||||||
|
echo " ! CA the HOST docker daemon does not trust it yet:"
|
||||||
|
echo " sudo mkdir -p /etc/docker/certs.d/${host}"
|
||||||
|
echo " sudo cp ${REGISTRY_CA_FILE} /etc/docker/certs.d/${host}/ca.crt"
|
||||||
|
echo " (kind nodes are handled by registry.sh; in-cluster clients are the workload's job)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Cluster lifecycle, plus what else is running on this machine.
|
# Cluster lifecycle (convergent, not exit-early), plus what else runs on this machine.
|
||||||
#
|
|
||||||
# `list` and `free` live here rather than in a separate script because a
|
|
||||||
# near-identical second name (cluster / clusters) is a trap — you reach for one
|
|
||||||
# and get the other. One target, one file, unambiguous subcommands.
|
|
||||||
#
|
|
||||||
# "Idempotent" here means CONVERGENT, not "exits early if the cluster exists".
|
|
||||||
# That distinction matters: an interrupted first run can leave a cluster created
|
|
||||||
# but not finished, and returning early on the re-run would strand it there.
|
|
||||||
# The create step is conditional; every step after it always runs, and each one
|
|
||||||
# is individually idempotent.
|
|
||||||
#
|
|
||||||
# Usage: cluster.sh up | down | reset | list | free
|
# Usage: cluster.sh up | down | reset | list | free
|
||||||
|
# Notes: docs/notes/cluster.md
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
@@ -23,15 +13,15 @@ up() {
|
|||||||
echo "cluster '$CLUSTER' exists — converging"
|
echo "cluster '$CLUSTER' exists — converging"
|
||||||
else
|
else
|
||||||
# Say what this profile locks in BEFORE spending minutes building it:
|
# Say what this profile locks in BEFORE spending minutes building it:
|
||||||
# the audit policy is an apiserver flag and cannot be changed later.
|
# the kind config is fixed at creation and cannot be changed later.
|
||||||
echo "creating cluster '$CLUSTER' from profile '$PROFILE_NAME'"
|
echo "creating cluster '$CLUSTER' from profile '$PROFILE_NAME'"
|
||||||
echo " shape ${KIND_CONFIG_SHOWN}"
|
echo " overlay ${OVERLAY_DIR:-none}"
|
||||||
|
echo " kind config ${KIND_CONFIG}"
|
||||||
echo " nodes $NODES"
|
echo " nodes $NODES"
|
||||||
echo " image $NODE_IMAGE"
|
echo " image $NODE_IMAGE"
|
||||||
echo " audit $AUDIT"
|
|
||||||
echo " ingress $INGRESS_MODE"
|
echo " ingress $INGRESS_MODE"
|
||||||
echo " registry $REGISTRY_MODE"
|
echo " registry $REGISTRY_MODE"
|
||||||
echo " (audit is fixed at creation — 'make cluster reset' to change it)"
|
echo " (fixed at creation — edit the kind config, then 'make cluster reset')"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
render_kind_config | kind create cluster --config -
|
render_kind_config | kind create cluster --config -
|
||||||
@@ -69,7 +59,7 @@ down() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# The escape hatch for a wedged cluster, and the only way to change a
|
# The escape hatch for a wedged cluster, and the only way to change a
|
||||||
# creation-time setting such as the audit policy.
|
# creation-time setting such as the node count or port mappings.
|
||||||
reset() {
|
reset() {
|
||||||
down
|
down
|
||||||
echo
|
echo
|
||||||
|
|||||||
390
rig/ctrl/deps.sh
390
rig/ctrl/deps.sh
@@ -1,38 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# rig:standalone rigdeps detect
|
# rig:standalone rigdeps detect
|
||||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
# Toolchain installer: detect the host, install pinned tools into $OUT_BIN, report
|
||||||
# report what it could not do.
|
# 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]
|
||||||
# It never runs the cluster, never uses sudo or apt, and writes only into
|
# | manifest NAME | manifests [--to DIR] | snapshot [DIR]]
|
||||||
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
# Notes: docs/notes/deps.md
|
||||||
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
|
||||||
# decide on, never performed. That is what makes it safe to run on a machine that
|
|
||||||
# already has a working setup.
|
|
||||||
#
|
|
||||||
# Usage (normally via `make deps`, or directly):
|
|
||||||
# deps.sh detect # report host facts only, change nothing
|
|
||||||
# deps.sh list # the pinned versions
|
|
||||||
# deps.sh verify [core|dev] # run what is installed and see if it works
|
|
||||||
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
|
||||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
|
||||||
#
|
|
||||||
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
|
||||||
# Default is dev.
|
|
||||||
#
|
|
||||||
# Runs both inside the installer container and bare on a host. Inside the
|
|
||||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
|
||||||
# falls back to /.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
# Keep the caller's cwd so a relative --to resolves there, not against ctrl/.
|
||||||
# not against ctrl/ once we've moved.
|
|
||||||
INVOKED_FROM="$PWD"
|
INVOKED_FROM="$PWD"
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
# Pins arrive through load_config like every other setting, not by sourcing
|
# Pins arrive through load_config, not by sourcing versions.env, so `make
|
||||||
# versions.env here. That is what lets `make standalone` freeze them into a
|
# standalone` can freeze them in.
|
||||||
# one-file installer: configuration has exactly one way in.
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
|
|
||||||
@@ -46,6 +27,14 @@ abspath() {
|
|||||||
|
|
||||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||||
HOST_ROOT="${HOST_ROOT:-/}"
|
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_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||||
@@ -53,12 +42,11 @@ BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
|||||||
# Collected by detect(), printed by report_manual() at the very end.
|
# Collected by detect(), printed by report_manual() at the very end.
|
||||||
MANUAL=()
|
MANUAL=()
|
||||||
|
|
||||||
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
# Facts print only with VERBOSE (`detect all`); problems (! and -) always print.
|
||||||
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
fact() { if [ -n "${VERBOSE:-}" ]; then echo "$@"; fi; }
|
||||||
# container's own view is already the host's.
|
|
||||||
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
# Host FILES are read through $HOST_ROOT; kernel facts are shared with the container.
|
||||||
# tight and does-not-fit branches can be exercised against a real machine's
|
# A /proc/meminfo field in MB, 0 if absent. MEMINFO overrides the source for testing.
|
||||||
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
|
||||||
mb_of() {
|
mb_of() {
|
||||||
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
@@ -83,9 +71,7 @@ arch() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# The pins above are amd64. Rather than download something that cannot execute
|
# Pins are amd64 only: refuse elsewhere and print how to get the right checksums.
|
||||||
# and let it fail as "cannot execute binary file: Exec format error", say so
|
|
||||||
# here and hand over the commands that produce the right checksums.
|
|
||||||
require_amd64() {
|
require_amd64() {
|
||||||
local a; a=$(arch)
|
local a; a=$(arch)
|
||||||
[ "$a" = "amd64" ] && return 0
|
[ "$a" = "amd64" ] && return 0
|
||||||
@@ -138,9 +124,7 @@ pick_sha() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── package manager, for the instructions only ─────────────────────────────
|
# ── package manager, for the instructions only ─────────────────────────────
|
||||||
# This never runs a package manager. It names one so the reported action is
|
# Never runs one; names the right one so reported actions are pasteable.
|
||||||
# something you can paste, on the distro you are actually on — an apt line on
|
|
||||||
# Amazon Linux 2 is a wrong answer dressed up as help.
|
|
||||||
|
|
||||||
pkg_install_cmd() {
|
pkg_install_cmd() {
|
||||||
local pkg="$1"
|
local pkg="$1"
|
||||||
@@ -160,11 +144,9 @@ docker_pkg() {
|
|||||||
|
|
||||||
# ── detect ─────────────────────────────────────────────────────────────────
|
# ── detect ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
# Windows outside WSL (Git Bash, MSYS, Cygwin) fails confusingly; name it instead.
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
require_linux() {
|
||||||
case "$(uname -s)" in
|
case "${UNAME_S:-$(uname -s)}" in
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
@@ -183,40 +165,34 @@ EOF
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
is_wsl() { grep -qi microsoft "$(host_file /proc/version)" 2>/dev/null; }
|
||||||
|
|
||||||
detect() {
|
detect() {
|
||||||
echo "host"
|
echo "host"
|
||||||
echo " kernel $(uname -r)"
|
fact " kernel $(uname -r)"
|
||||||
echo " arch $(arch) ($(uname -m))"
|
|
||||||
|
|
||||||
local osr; osr=$(host_file /etc/os-release)
|
local osr distro=""; osr=$(host_file /etc/os-release)
|
||||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
[ -r "$osr" ] && distro=$(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")
|
||||||
|
echo " distro ${distro:-unknown} $(arch), $(if is_wsl; then echo WSL; else echo native linux; fi)"
|
||||||
|
|
||||||
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
# In MB (whole GB rounds away too much). Facts only; check.sh judges sufficiency.
|
||||||
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
|
||||||
# that is enough depends on the profile, which check.sh knows and this does not.
|
|
||||||
local total_mb avail_mb swap_total_mb swap_used_mb om
|
local total_mb avail_mb swap_total_mb swap_used_mb om
|
||||||
total_mb=$(mb_of MemTotal)
|
total_mb=$(mb_of MemTotal)
|
||||||
avail_mb=$(mb_of MemAvailable)
|
avail_mb=$(mb_of MemAvailable)
|
||||||
swap_total_mb=$(mb_of SwapTotal)
|
swap_total_mb=$(mb_of SwapTotal)
|
||||||
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
||||||
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
printf " memory %d MB total, %d MB available%s\n" "$total_mb" "$avail_mb" \
|
||||||
if [ "$swap_total_mb" -gt 0 ]; then
|
"$(if [ "$swap_used_mb" -gt 0 ]; then echo ", $swap_used_mb MB in swap"; fi)"
|
||||||
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
# Overcommit mode: with 1 the OOM killer settles up later, after a clean start.
|
||||||
# always says yes and settles up later with the OOM killer, so a cluster that
|
|
||||||
# starts cleanly can still lose processes afterwards.
|
|
||||||
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||||
case "$om" in
|
case "$om" in
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
0) fact " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
1) fact " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||||
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
2) fact " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo " install to $OUT_BIN"
|
fact " install to $OUT_BIN"
|
||||||
detect_libc
|
detect_libc
|
||||||
detect_prereqs
|
detect_prereqs
|
||||||
detect_wsl
|
detect_wsl
|
||||||
@@ -228,18 +204,13 @@ detect() {
|
|||||||
|
|
||||||
detect_wsl() {
|
detect_wsl() {
|
||||||
if ! is_wsl; then
|
if ! is_wsl; then
|
||||||
echo " platform native linux"
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " platform WSL"
|
# systemd is off by default in WSL; enabling it needs a Windows-side restart.
|
||||||
|
|
||||||
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
|
||||||
# host service need it. Enabling it requires a Windows-side restart, which
|
|
||||||
# cannot be issued from inside the distro.
|
|
||||||
local wc; wc=$(host_file /etc/wsl.conf)
|
local wc; wc=$(host_file /etc/wsl.conf)
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||||
echo " systemd enabled in wsl.conf"
|
fact " systemd enabled in wsl.conf"
|
||||||
else
|
else
|
||||||
echo " ! systemd not enabled in /etc/wsl.conf"
|
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||||
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||||
@@ -251,27 +222,24 @@ detect_wsl() {
|
|||||||
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||||
# local DNS setup.
|
# local DNS setup.
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||||
echo " resolv.conf pinned (generateResolvConf=false)"
|
fact " resolv.conf pinned (generateResolvConf=false)"
|
||||||
else
|
else
|
||||||
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
fact " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local wcfg
|
local wcfg
|
||||||
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||||
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
fact " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||||
else
|
else
|
||||||
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||||
make mem status
|
make check mem
|
||||||
It prints the edit to make and the command to apply it.")
|
It prints the edit to make and the command to apply it.")
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
# Filesystem types that deliver no inotify events (9p, drvfs, network, fuse).
|
||||||
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
# Checks the fs type, not the path.
|
||||||
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
|
||||||
# way. None of them deliver inotify events, so anything watching files goes
|
|
||||||
# quiet without saying why.
|
|
||||||
watch_hostile_fs() {
|
watch_hostile_fs() {
|
||||||
local dir="$1" fstype
|
local dir="$1" fstype
|
||||||
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||||
@@ -292,28 +260,22 @@ detect_filesystem() {
|
|||||||
on a $fstype mount, and everything else is slower:
|
on a $fstype mount, and everything else is slower:
|
||||||
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||||
else
|
else
|
||||||
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
fact " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
# tilt needs glibc >= 2.34 (measured on Amazon Linux 2). Report the version here;
|
||||||
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
# `verify` catches the actual failure after installing.
|
||||||
#
|
|
||||||
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
|
||||||
#
|
|
||||||
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
|
||||||
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
|
||||||
# version now; `verify` catches the actual failure after installing.
|
|
||||||
detect_libc() {
|
detect_libc() {
|
||||||
local v=""
|
local v=""
|
||||||
if command -v ldd >/dev/null 2>&1; then
|
if command -v ldd >/dev/null 2>&1; then
|
||||||
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
||||||
fi
|
fi
|
||||||
if [ -z "$v" ]; then
|
if [ -z "$v" ]; then
|
||||||
echo " libc unknown (no ldd) — 'verify' is the real test"
|
fact " libc unknown (no ldd) — 'verify' is the real test"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo " libc glibc $v"
|
fact " libc glibc $v"
|
||||||
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
||||||
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
||||||
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
||||||
@@ -322,25 +284,23 @@ detect_libc() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# What this script needs to do its own job. Reported here so `detect` answers
|
# What this script itself needs, so `detect` answers "will install work?".
|
||||||
# "will install work?" instead of leaving you to find out one download in.
|
|
||||||
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
|
||||||
detect_prereqs() {
|
detect_prereqs() {
|
||||||
local missing=""
|
local missing=""
|
||||||
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
if command -v curl >/dev/null 2>&1; then fact " download curl"
|
||||||
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
elif command -v wget >/dev/null 2>&1; then fact " download wget"
|
||||||
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
||||||
echo " checksums ok"
|
fact " checksums ok"
|
||||||
else
|
else
|
||||||
echo " ! no sha256sum or shasum — downloads could not be verified"
|
echo " ! no sha256sum or shasum — downloads could not be verified"
|
||||||
missing+=" coreutils"
|
missing+=" coreutils"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
||||||
echo " archives tar + gzip"
|
fact " archives tar + gzip"
|
||||||
else
|
else
|
||||||
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
||||||
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
||||||
@@ -355,10 +315,7 @@ detect_prereqs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detect_docker() {
|
detect_docker() {
|
||||||
# Reachability of the daemon is the real question, and the CLI is only how
|
# Daemon reachability is the real question; the CLI is only how we ask.
|
||||||
# we ask it. Note that when this runs inside the installer container, Docker
|
|
||||||
# necessarily exists on the host — otherwise nothing would be executing —
|
|
||||||
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
if [ -S /var/run/docker.sock ]; then
|
if [ -S /var/run/docker.sock ]; then
|
||||||
echo " docker socket present (no cli in this context)"
|
echo " docker socket present (no cli in this context)"
|
||||||
@@ -377,12 +334,9 @@ detect_docker() {
|
|||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||||
local n
|
local n
|
||||||
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||||
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
# Must be an `if`, not `[ ] && echo`: a zero count would return 1 under set -e.
|
||||||
# function the latter returns 1 when the count is zero, and `set -e`
|
|
||||||
# then kills the caller. That is the fresh-machine case — no clusters
|
|
||||||
# yet — so the bug only ever shows up where it does most harm.
|
|
||||||
if [ "$n" -gt 0 ]; then
|
if [ "$n" -gt 0 ]; then
|
||||||
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
echo " kind $n node container(s) running — 'make cluster list'"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
@@ -397,7 +351,7 @@ detect_inotify() {
|
|||||||
local w i
|
local w i
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
echo " inotify watches=$w instances=$i"
|
fact " inotify watches=$w instances=$i"
|
||||||
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||||
@@ -457,18 +411,13 @@ fetch_tgz() {
|
|||||||
echo " fetching $name"
|
echo " fetching $name"
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
verify "$tmp" "$sha" "$name"
|
verify "$tmp" "$sha" "$name"
|
||||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
# --no-same-owner: as root, tar would restore the archive's uid/gid.
|
||||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
|
||||||
# user does not own.
|
|
||||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
chmod +x "$dest/$name"
|
chmod +x "$dest/$name"
|
||||||
}
|
}
|
||||||
|
|
||||||
# The installer runs as root so it can reach the docker socket, which means
|
# The installer runs as root; hand files in a mounted dir back to the mount point's owner.
|
||||||
# everything it writes into a mounted volume lands root-owned and unusable from
|
|
||||||
# the host. Hand it back to whoever owns the mount point (the host user created
|
|
||||||
# that directory before mounting it).
|
|
||||||
fix_ownership() {
|
fix_ownership() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
[ -d "$dir" ] || return 0
|
[ -d "$dir" ] || return 0
|
||||||
@@ -480,38 +429,14 @@ fix_ownership() {
|
|||||||
chown -R "$owner" "$dir" 2>/dev/null || true
|
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Two tiers, because not every machine should get cluster tooling.
|
# core: talk to a cluster someone else runs. dev: core plus tools that build clusters.
|
||||||
#
|
|
||||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
|
||||||
# creates one. Appropriate on a managed or corporate-issued machine
|
|
||||||
# where development tools are not wanted by default.
|
|
||||||
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
|
||||||
#
|
|
||||||
# The split exists because "install the toolchain" is not one decision: on a
|
|
||||||
# managed workspace the right answer is kubectl and nothing else.
|
|
||||||
CORE_TOOLS="kubectl jq"
|
CORE_TOOLS="kubectl jq"
|
||||||
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
# No helm (nothing uses a chart). ctlptl wires in a local registry; compose is often
|
||||||
# has ever invoked it. Add it back the day something actually needs a chart.
|
# missing from distro docker packages.
|
||||||
#
|
|
||||||
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
|
||||||
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
|
||||||
# because it is what wires a cluster to a local registry — without one, an
|
|
||||||
# unqualified image name resolves to docker.io/library/<name> and there is
|
|
||||||
# nothing structural stopping a push there.
|
|
||||||
#
|
|
||||||
# docker-compose is 'dev' for the same reason, and is here because the distro
|
|
||||||
# docker packages ship the daemon and CLI but frequently not the compose
|
|
||||||
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
|
||||||
# working Docker, and nothing about that message names the missing piece.
|
|
||||||
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
||||||
|
|
||||||
# ── what is already on this machine ───────────────────────────────────────
|
# ── what is already on this machine ───────────────────────────────────────
|
||||||
#
|
# A tool already on PATH at its pinned version is left where it is.
|
||||||
# A tool already on PATH at its pinned version is left where it is. Without
|
|
||||||
# this, install downloads a second copy into OUT_BIN and then reports the first
|
|
||||||
# one as shadowed — noise, and wrong, when both are the same version. That is
|
|
||||||
# the normal state of any machine someone set up by hand, whatever directory
|
|
||||||
# they happened to choose.
|
|
||||||
|
|
||||||
pin_of() {
|
pin_of() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -524,9 +449,7 @@ pin_of() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# The version string a binary reports. Each tool spells the question
|
# The version string a binary reports (kubectl needs --client).
|
||||||
# differently, and kubectl has to be told --client or it goes looking for a
|
|
||||||
# server to ask.
|
|
||||||
reported_version() {
|
reported_version() {
|
||||||
local tool="$1" path="$2"
|
local tool="$1" path="$2"
|
||||||
case "$tool" in
|
case "$tool" in
|
||||||
@@ -536,13 +459,8 @@ reported_version() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
# Does the binary at PATH report PIN? Whole-token match, leading v optional.
|
||||||
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
# Bash regex rather than grep, deliberately.
|
||||||
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
|
||||||
#
|
|
||||||
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
|
||||||
# on every machine — some builds reject patterns that others accept — and a
|
|
||||||
# failed grep inside a count reads exactly like a zero.
|
|
||||||
version_matches() {
|
version_matches() {
|
||||||
local tool="$1" path="$2" pin="$3" out v re
|
local tool="$1" path="$2" pin="$3" out v re
|
||||||
out=$(reported_version "$tool" "$path") || return 1
|
out=$(reported_version "$tool" "$path") || return 1
|
||||||
@@ -552,10 +470,8 @@ version_matches() {
|
|||||||
[[ $out =~ $re ]]
|
[[ $out =~ $re ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
# DEPS_ONLY narrows a fetch to the tools it names; unset means the whole tier.
|
||||||
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
# Only install() sets it.
|
||||||
# DIR" must not quietly skip something because this machine happens to have it.
|
|
||||||
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
|
||||||
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
||||||
|
|
||||||
# Every tool in the tier with its state, probed once and reported once. What
|
# Every tool in the tier with its state, probed once and reported once. What
|
||||||
@@ -564,20 +480,18 @@ TOOLCHAIN_NEED=""
|
|||||||
detect_toolchain() {
|
detect_toolchain() {
|
||||||
local tier="${TIER:-dev}" b pin path found
|
local tier="${TIER:-dev}" b pin path found
|
||||||
TOOLCHAIN_NEED=""
|
TOOLCHAIN_NEED=""
|
||||||
|
local n=0
|
||||||
echo
|
echo
|
||||||
echo "toolchain (pinned, tier '$tier')"
|
fact "toolchain (pinned, tier '$tier')"
|
||||||
for b in $(tier_tools "$tier"); do
|
for b in $(tier_tools "$tier"); do
|
||||||
|
n=$((n + 1))
|
||||||
pin=$(pin_of "$b")
|
pin=$(pin_of "$b")
|
||||||
path=$(command -v "$b" 2>/dev/null || true)
|
path=$(command -v "$b" 2>/dev/null || true)
|
||||||
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
# compose is normally a docker CLI plugin, not on PATH: ask docker instead.
|
||||||
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
|
||||||
# has no `docker-compose` to find — and probing only PATH would report it
|
|
||||||
# missing and re-download a copy that is already there. That is the exact
|
|
||||||
# noise the version-aware skip exists to prevent, so ask docker instead.
|
|
||||||
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
||||||
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
||||||
if [ "${found#v}" = "${pin#v}" ]; then
|
if [ "${found#v}" = "${pin#v}" ]; then
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
fact "$(printf " %-8s %-9s %s" "$b" "$pin" "docker cli plugin")"
|
||||||
else
|
else
|
||||||
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
||||||
"$b" "$pin" "$found"
|
"$b" "$pin" "$found"
|
||||||
@@ -590,7 +504,7 @@ detect_toolchain() {
|
|||||||
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
||||||
TOOLCHAIN_NEED+="$b "
|
TOOLCHAIN_NEED+="$b "
|
||||||
elif version_matches "$b" "$path" "$pin"; then
|
elif version_matches "$b" "$path" "$pin"; then
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
fact "$(printf " %-8s %-9s %s" "$b" "$pin" "$path")"
|
||||||
else
|
else
|
||||||
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
||||||
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
||||||
@@ -598,9 +512,10 @@ detect_toolchain() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ -z "$TOOLCHAIN_NEED" ]; then
|
if [ -z "$TOOLCHAIN_NEED" ]; then
|
||||||
echo " every pinned tool is already on PATH — nothing to fetch"
|
if [ -n "${VERBOSE:-}" ]; then echo " all $n on PATH — nothing to fetch"
|
||||||
|
else echo "toolchain all $n pinned tools on PATH (tier $tier)"; fi
|
||||||
else
|
else
|
||||||
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
echo "toolchain 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,8 +556,7 @@ fetch() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
fix_ownership "$dest"
|
fix_ownership "$dest"
|
||||||
# kind writes the kubeconfig as root too; hand that back as well when it's
|
# kind writes the kubeconfig as root too; hand that back as well.
|
||||||
# a mounted host directory rather than container-local state.
|
|
||||||
fix_ownership "${KUBE_DIR:-/out/kube}"
|
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,14 +578,8 @@ report_manual() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Installing into a directory that sits early in PATH silently replaces whatever
|
# A verified download proves the right file, not that this machine can run it
|
||||||
# the machine was already using — which on a shared or client machine can break
|
# (old glibc breaks tilt). Run each one now.
|
||||||
# unrelated work (kubectl more than one minor away from a cluster is the common
|
|
||||||
# one). Say so; never decide it for them.
|
|
||||||
# Downloading a verified binary proves it is the right file, not that this
|
|
||||||
# machine can run it. On an old distro tilt fails here, with a linker error
|
|
||||||
# about a missing symbol, and finding that out now beats finding out during a
|
|
||||||
# first cluster build.
|
|
||||||
verify_tools() {
|
verify_tools() {
|
||||||
local tier="${1:-dev}" b bin out rc broke=0
|
local tier="${1:-dev}" b bin out rc broke=0
|
||||||
echo "checking that each one actually runs"
|
echo "checking that each one actually runs"
|
||||||
@@ -681,11 +589,7 @@ verify_tools() {
|
|||||||
printf ' %-14s not installed\n' "$b"
|
printf ' %-14s not installed\n' "$b"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
# Not piped into `head`: under pipefail, SIGPIPE (141) looked like failure.
|
||||||
# than one line gets SIGPIPE when head closes the pipe, and the
|
|
||||||
# pipeline reports 141 — so a working kubectl was announced as "does
|
|
||||||
# not run here", with its own correct version string as the evidence.
|
|
||||||
# Take the first line afterwards, from the string.
|
|
||||||
rc=0
|
rc=0
|
||||||
case "$b" in
|
case "$b" in
|
||||||
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
||||||
@@ -763,18 +667,12 @@ warn_shadowing() {
|
|||||||
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||||
}
|
}
|
||||||
|
|
||||||
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
# Link the fetched docker-compose into ~/.docker/cli-plugins so `docker compose` works.
|
||||||
# retired v1 spelling; every compose file written in the last few years assumes
|
|
||||||
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
|
||||||
# So the binary is fetched like any other and then linked, in your own home —
|
|
||||||
# no root, and nothing outside it.
|
|
||||||
install_compose_plugin() {
|
install_compose_plugin() {
|
||||||
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
||||||
[ -x "$src" ] || return 0
|
[ -x "$src" ] || return 0
|
||||||
mkdir -p "$dir"
|
mkdir -p "$dir"
|
||||||
# Something else already owns that name — docker-desktop and some distro
|
# A real file there belongs to something else (docker-desktop, distro): don't overwrite.
|
||||||
# packages install a real file there. Overwriting it would take the plugin
|
|
||||||
# away from whatever put it there, so say so and let the user decide.
|
|
||||||
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
||||||
MANUAL+=("Something already installs the compose plugin at
|
MANUAL+=("Something already installs the compose plugin at
|
||||||
$dir/docker-compose
|
$dir/docker-compose
|
||||||
@@ -805,16 +703,12 @@ install() {
|
|||||||
if [ "$tier" = "core" ]; then
|
if [ "$tier" = "core" ]; then
|
||||||
echo " (no kind/tilt — 'make deps dev' adds them)"
|
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||||
fi
|
fi
|
||||||
# Only when compose was one of the things fetched: linking a binary
|
# Only when compose was fetched, never at a copy rig did not install.
|
||||||
# that is already satisfied elsewhere on PATH would point the plugin at
|
|
||||||
# a copy rig did not install.
|
|
||||||
case " $TOOLCHAIN_NEED " in
|
case " $TOOLCHAIN_NEED " in
|
||||||
*" docker-compose "*) install_compose_plugin ;;
|
*" docker-compose "*) install_compose_plugin ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Only worth saying when something actually landed in OUT_BIN. When every
|
# PATH advice only when something actually landed in OUT_BIN.
|
||||||
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
|
||||||
# telling the user to add it would be advice to fix nothing.
|
|
||||||
case ":${PATH}:" in
|
case ":${PATH}:" in
|
||||||
*":$OUT_BIN:"*) ;;
|
*":$OUT_BIN:"*) ;;
|
||||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||||
@@ -830,13 +724,103 @@ install() {
|
|||||||
|
|
||||||
require_linux
|
require_linux
|
||||||
|
|
||||||
# Read the command, THEN shift — and shift only if there is something there.
|
# Shift only if there is an argument: a bare `shift` returns 1 under set -e.
|
||||||
# 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.
|
|
||||||
cmd="${1:-install}"
|
cmd="${1:-install}"
|
||||||
[ $# -gt 0 ] && shift
|
[ $# -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.
|
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||||
need_downloads() {
|
need_downloads() {
|
||||||
require_amd64
|
require_amd64
|
||||||
@@ -845,14 +829,20 @@ need_downloads() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
detect) detect; report_manual ;;
|
detect) if [ "${1:-}" = all ]; then VERBOSE=1; fi; detect; report_manual ;;
|
||||||
list) list ;;
|
list) list ;;
|
||||||
verify) verify_tools "${1:-dev}" ;;
|
verify) verify_tools "${1:-dev}" ;;
|
||||||
fetch) need_downloads; fetch "$@" ;;
|
fetch) need_downloads; fetch "$@" ;;
|
||||||
install) need_downloads; install "${1:-dev}" ;;
|
install) need_downloads; install "${1:-dev}" ;;
|
||||||
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
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 " install [core|dev] (default dev)" >&2
|
echo " install [core|dev] (default dev)" >&2
|
||||||
echo " fetch [core|dev] [--to DIR]" >&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
|
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||||
exit 1 ;;
|
exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Documentation: render the diagrams, and serve the pages.
|
# Documentation: render the diagrams, and serve the pages from a throwaway nginx container.
|
||||||
#
|
|
||||||
# The docs are the instructions for building the cluster, so they must work
|
|
||||||
# BEFORE anything else exists. That rules out serving them from the cluster, and
|
|
||||||
# it rules out python -m http.server too — a minimal Debian has no python3. What
|
|
||||||
# it does have, by definition, is Docker: the single prerequisite rig already
|
|
||||||
# demands. So a throwaway nginx container serves a read-only bind mount.
|
|
||||||
#
|
|
||||||
# Rendered SVGs are committed alongside their .dot sources for the same reason:
|
|
||||||
# the pages have to read on a machine with no Graphviz installed.
|
|
||||||
#
|
|
||||||
# Usage: docs.sh serve | graphs
|
# Usage: docs.sh serve | graphs
|
||||||
|
# Notes: docs/notes/docs.md
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# client — the regulated-estate shape. Multi-node so taints, affinity and
|
|
||||||
# topology are real; apiserver audit on; images through a pull-through cache of
|
|
||||||
# the corporate registry.
|
|
||||||
#
|
|
||||||
# Costs roughly 4-6 GB. Check `make cluster list` before starting this alongside
|
|
||||||
# other work — see the memory note in the README.
|
|
||||||
|
|
||||||
PROFILE_NAME=client
|
|
||||||
K8S_VERSION=v1_36
|
|
||||||
KIND_CONFIG=kind-config.client.yaml.tpl
|
|
||||||
ADDONS="metallb cert-manager metrics-server"
|
|
||||||
REGISTRY_MODE=mirror
|
|
||||||
INGRESS_MODE=hostport
|
|
||||||
DNS_MODE=hosts
|
|
||||||
|
|
||||||
# Ports derive from the directory name by default (see ctrl/ports.sh), so
|
|
||||||
# several environments run side by side.
|
|
||||||
#
|
|
||||||
# Opt in to the real ports below 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.
|
|
||||||
# HTTP_PORT=80
|
|
||||||
# HTTPS_PORT=443
|
|
||||||
|
|
||||||
# Set these in ctrl/.env (gitignored), not here:
|
|
||||||
# REGISTRY_REMOTE_URL=https://artifactory.corp.example/artifactory/api/docker/docker-virtual
|
|
||||||
# REGISTRY_USER / REGISTRY_PASSWORD
|
|
||||||
# REGISTRY_CA_FILE=/path/to/corp-root-ca.crt
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# data — a cluster with the cabinets an environment asks for.
|
|
||||||
#
|
|
||||||
# A cabinet is a public service dropped in as-is — the upstream image,
|
|
||||||
# unmodified, reachable at a known address. It is declared once and installs on
|
|
||||||
# either target: a `service.yml` composes it for a laptop, and the addons below
|
|
||||||
# install the same one here. The names match deliberately — each cabinet.json
|
|
||||||
# carries a `rig_addon` field pointing at ctrl/addons/<name>.sh.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
PROFILE_NAME=data
|
|
||||||
K8S_VERSION=v1_36
|
|
||||||
KIND_CONFIG=kind-config.yaml.tpl
|
|
||||||
# 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 minimal.env: `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 not here: 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.
|
|
||||||
POSTGRES_DB=app
|
|
||||||
POSTGRES_USER=app
|
|
||||||
POSTGRES_STORAGE=2Gi
|
|
||||||
|
|
||||||
AIRFLOW_ADMIN_USER=admin
|
|
||||||
|
|
||||||
# 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
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# minimal — the default. One node, no addons, no registry.
|
|
||||||
# Assumes nothing and boots fast. Start here; move to client.env when you need
|
|
||||||
# the regulated behaviours.
|
|
||||||
#
|
|
||||||
|
|
||||||
PROFILE_NAME=minimal
|
|
||||||
K8S_VERSION=v1_36
|
|
||||||
KIND_CONFIG=kind-config.yaml.tpl
|
|
||||||
ADDONS=""
|
|
||||||
# local, not none: `none` leaves the cluster with no registry to push to, and an
|
|
||||||
# unqualified image name then means docker.io/library/<name>. In a regulated
|
|
||||||
# estate that is a disclosure risk, not a convenience trade — so the default
|
|
||||||
# carries the guard even though it costs one container.
|
|
||||||
REGISTRY_MODE=local
|
|
||||||
INGRESS_MODE=hostport
|
|
||||||
DNS_MODE=hosts
|
|
||||||
|
|
||||||
# Ports are deliberately NOT set here. They derive from the directory name so
|
|
||||||
# several environments coexist — see ctrl/ports.sh, and `make ports` to see the
|
|
||||||
# block this one gets. A fixed default here would collide with whatever else the
|
|
||||||
# machine happens to be running; 8080 in particular is rarely free.
|
|
||||||
21
rig/ctrl/env.d/mirror.env.example
Normal file
21
rig/ctrl/env.d/mirror.env.example
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# 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.
|
||||||
|
# Notes: docs/notes/env.md
|
||||||
|
|
||||||
|
PROFILE_NAME=mirror
|
||||||
|
K8S_VERSION=v1_36
|
||||||
|
ADDONS="metallb cert-manager metrics-server"
|
||||||
|
REGISTRY_MODE=mirror
|
||||||
|
INGRESS_MODE=hostport
|
||||||
|
DNS_MODE=hosts
|
||||||
|
|
||||||
|
# Ports derive from the directory name by default (see ctrl/ports.sh).
|
||||||
|
# Real ports only if this is the ONLY environment and nothing owns :80; `make check` tests it.
|
||||||
|
# HTTP_PORT=80
|
||||||
|
# HTTPS_PORT=443
|
||||||
|
|
||||||
|
# Set these in ctrl/.env (gitignored), not here:
|
||||||
|
# REGISTRY_REMOTE_URL=https://registry.internal.example/api/docker/docker-virtual
|
||||||
|
# REGISTRY_USER / REGISTRY_PASSWORD
|
||||||
|
# REGISTRY_CA_FILE=/path/to/internal-root-ca.crt
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
PROFILE_NAME=offline
|
|
||||||
K8S_VERSION=v1_36
|
|
||||||
KIND_CONFIG=kind-config.audit.yaml.tpl
|
|
||||||
ADDONS="metallb"
|
|
||||||
REGISTRY_MODE=local
|
|
||||||
INGRESS_MODE=hostport
|
|
||||||
DNS_MODE=hosts
|
|
||||||
|
|
||||||
# Derived from the directory name by default — see ctrl/ports.sh.
|
|
||||||
# Uncomment for the real ports, but only if this is the only environment.
|
|
||||||
# HTTP_PORT=80
|
|
||||||
# HTTPS_PORT=443
|
|
||||||
15
rig/ctrl/env.d/offline.env.example
Normal file
15
rig/ctrl/env.d/offline.env.example
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# EXAMPLE PROFILE (optional): copy to offline.env, then PROFILE=offline; overlays the defaults.
|
||||||
|
# offline — air-gapped: images from a preloaded local registry; pair with DEPS_SOURCE=baked.
|
||||||
|
# Notes: docs/notes/env.md
|
||||||
|
|
||||||
|
PROFILE_NAME=offline
|
||||||
|
K8S_VERSION=v1_36
|
||||||
|
ADDONS="metallb"
|
||||||
|
REGISTRY_MODE=local
|
||||||
|
INGRESS_MODE=hostport
|
||||||
|
DNS_MODE=hosts
|
||||||
|
|
||||||
|
# Derived from the directory name by default — see ctrl/ports.sh.
|
||||||
|
# Uncomment for the real ports, but only if this is the only environment.
|
||||||
|
# HTTP_PORT=80
|
||||||
|
# HTTPS_PORT=443
|
||||||
52
rig/ctrl/hosttest.sh
Executable file
52
rig/ctrl/hosttest.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/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"
|
||||||
101
rig/ctrl/installtest.sh
Executable file
101
rig/ctrl/installtest.sh
Executable file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/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"
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# `ctrl/k8s` — cluster shape, 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, audit
|
|
||||||
base/ the components, as plain manifests
|
|
||||||
overlays/dev/ how this rig differs from the base
|
|
||||||
audit-policy.yaml mounted into the apiserver by the audit shapes
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
**The chosen file is the source of truth for node count and audit.**
|
|
||||||
`lib/config.sh` reads both back out of it, so a profile names a shape and does
|
|
||||||
not restate what the YAML already says.
|
|
||||||
|
|
||||||
| file | nodes | audit | profiles |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| `kind-config.yaml.tpl` | 1 | off | `minimal`, `data` |
|
|
||||||
| `kind-config.audit.yaml.tpl` | 1 | on | `offline` |
|
|
||||||
| `kind-config.client.yaml.tpl` | 3 | on | `client` |
|
|
||||||
|
|
||||||
A profile picks one with `KIND_CONFIG` in `ctrl/env.d/<profile>.env`. Adding a
|
|
||||||
shape is adding a file — there is no dispatcher to edit.
|
|
||||||
|
|
||||||
Audit is an apiserver flag and therefore fixed at creation: changing it is
|
|
||||||
`make cluster reset`, not a re-apply.
|
|
||||||
|
|
||||||
## `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,44 +0,0 @@
|
|||||||
# Apiserver audit policy. Mounted into the control plane at creation when a
|
|
||||||
# profile sets AUDIT=on — an apiserver flag, so it cannot be added to a running
|
|
||||||
# cluster without recreating it.
|
|
||||||
#
|
|
||||||
# Deliberately modest: enough to make "who changed what, and when" answerable
|
|
||||||
# during onboarding without filling the disk. Read the log with:
|
|
||||||
# docker exec <cluster>-control-plane cat /var/log/kubernetes/audit.log
|
|
||||||
apiVersion: audit.k8s.io/v1
|
|
||||||
kind: Policy
|
|
||||||
|
|
||||||
# Never log the request body for these — they contain credentials.
|
|
||||||
omitStages:
|
|
||||||
- RequestReceived
|
|
||||||
|
|
||||||
rules:
|
|
||||||
# Secrets/configmaps: record that access happened, never the contents.
|
|
||||||
- level: Metadata
|
|
||||||
resources:
|
|
||||||
- group: ""
|
|
||||||
resources: ["secrets", "configmaps"]
|
|
||||||
|
|
||||||
# Authn/authz decisions — the part an auditor actually asks about.
|
|
||||||
- level: Metadata
|
|
||||||
nonResourceURLs:
|
|
||||||
- /apis*
|
|
||||||
- /api*
|
|
||||||
|
|
||||||
# Mutations to workloads and policy: full request, so a diff is reconstructable.
|
|
||||||
- level: Request
|
|
||||||
verbs: ["create", "update", "patch", "delete"]
|
|
||||||
resources:
|
|
||||||
- group: ""
|
|
||||||
resources: ["pods", "services", "serviceaccounts", "namespaces"]
|
|
||||||
- group: "apps"
|
|
||||||
- group: "networking.k8s.io"
|
|
||||||
- group: "rbac.authorization.k8s.io"
|
|
||||||
|
|
||||||
# Everything else that changes state: metadata only.
|
|
||||||
- level: Metadata
|
|
||||||
verbs: ["create", "update", "patch", "delete"]
|
|
||||||
|
|
||||||
# Reads are dropped entirely — otherwise controller polling drowns the log.
|
|
||||||
- level: None
|
|
||||||
verbs: ["get", "list", "watch"]
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# Cluster shape: one node, apiserver audit ON. Used by the `offline` profile.
|
|
||||||
#
|
|
||||||
# Audit is an apiserver flag, so it is fixed when the cluster is created —
|
|
||||||
# changing it means `make cluster reset`, not a re-apply. That is why it is a
|
|
||||||
# property of the cluster file rather than something switched at runtime.
|
|
||||||
#
|
|
||||||
# k8s >= 1.31 uses kubeadm v1beta4, where extraArgs is a LIST of name/value
|
|
||||||
# pairs. The older map form is silently ignored — it does not error, audit
|
|
||||||
# simply never turns on.
|
|
||||||
#
|
|
||||||
# Substituted by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
|
||||||
# (named without the ${...} braces so this line survives the substitution)
|
|
||||||
kind: Cluster
|
|
||||||
apiVersion: kind.x-k8s.io/v1alpha4
|
|
||||||
name: ${CLUSTER}
|
|
||||||
|
|
||||||
containerdConfigPatches:
|
|
||||||
- |-
|
|
||||||
[plugins."io.containerd.grpc.v1.cri".registry]
|
|
||||||
config_path = "/etc/containerd/certs.d"
|
|
||||||
|
|
||||||
kubeadmConfigPatches:
|
|
||||||
- |
|
|
||||||
kind: ClusterConfiguration
|
|
||||||
apiServer:
|
|
||||||
extraArgs:
|
|
||||||
- name: audit-policy-file
|
|
||||||
value: /etc/kubernetes/audit/policy.yaml
|
|
||||||
- name: audit-log-path
|
|
||||||
value: /var/log/kubernetes/audit.log
|
|
||||||
- name: audit-log-maxage
|
|
||||||
value: "7"
|
|
||||||
extraVolumes:
|
|
||||||
- name: audit-policy
|
|
||||||
hostPath: /etc/kubernetes/audit
|
|
||||||
mountPath: /etc/kubernetes/audit
|
|
||||||
readOnly: true
|
|
||||||
- name: audit-log
|
|
||||||
hostPath: /var/log/kubernetes
|
|
||||||
mountPath: /var/log/kubernetes
|
|
||||||
readOnly: false
|
|
||||||
|
|
||||||
nodes:
|
|
||||||
- role: control-plane
|
|
||||||
image: ${NODE_IMAGE}
|
|
||||||
# hostPath is resolved by the HOST dockerd, so this must be a host path even
|
|
||||||
# when cluster.sh runs inside the installer container. HOST_WORKDIR says where
|
|
||||||
# this rig lives on the host; bare on a host it is just the repo root.
|
|
||||||
extraMounts:
|
|
||||||
- hostPath: ${HOST_WORKDIR}/ctrl/k8s/audit-policy.yaml
|
|
||||||
containerPath: /etc/kubernetes/audit/policy.yaml
|
|
||||||
readOnly: true
|
|
||||||
extraPortMappings:
|
|
||||||
- containerPort: 30080
|
|
||||||
hostPort: ${HTTP_PORT}
|
|
||||||
listenAddress: "0.0.0.0"
|
|
||||||
protocol: TCP
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Cluster shape: three nodes, apiserver audit ON. Used by the `client` profile —
|
|
||||||
# the regulated-estate shape.
|
|
||||||
#
|
|
||||||
# Multi-node so taints, affinity and topology spread are real rather than
|
|
||||||
# vacuously satisfied by a single node. It costs roughly 4-6 GB; run
|
|
||||||
# `make cluster list` before starting this alongside other work.
|
|
||||||
#
|
|
||||||
# Substituted by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
|
||||||
# (named without the ${...} braces so this line survives the substitution)
|
|
||||||
kind: Cluster
|
|
||||||
apiVersion: kind.x-k8s.io/v1alpha4
|
|
||||||
name: ${CLUSTER}
|
|
||||||
|
|
||||||
containerdConfigPatches:
|
|
||||||
- |-
|
|
||||||
[plugins."io.containerd.grpc.v1.cri".registry]
|
|
||||||
config_path = "/etc/containerd/certs.d"
|
|
||||||
|
|
||||||
kubeadmConfigPatches:
|
|
||||||
- |
|
|
||||||
kind: ClusterConfiguration
|
|
||||||
apiServer:
|
|
||||||
extraArgs:
|
|
||||||
- name: audit-policy-file
|
|
||||||
value: /etc/kubernetes/audit/policy.yaml
|
|
||||||
- name: audit-log-path
|
|
||||||
value: /var/log/kubernetes/audit.log
|
|
||||||
- name: audit-log-maxage
|
|
||||||
value: "7"
|
|
||||||
extraVolumes:
|
|
||||||
- name: audit-policy
|
|
||||||
hostPath: /etc/kubernetes/audit
|
|
||||||
mountPath: /etc/kubernetes/audit
|
|
||||||
readOnly: true
|
|
||||||
- name: audit-log
|
|
||||||
hostPath: /var/log/kubernetes
|
|
||||||
mountPath: /var/log/kubernetes
|
|
||||||
readOnly: false
|
|
||||||
|
|
||||||
nodes:
|
|
||||||
- role: control-plane
|
|
||||||
image: ${NODE_IMAGE}
|
|
||||||
extraMounts:
|
|
||||||
- hostPath: ${HOST_WORKDIR}/ctrl/k8s/audit-policy.yaml
|
|
||||||
containerPath: /etc/kubernetes/audit/policy.yaml
|
|
||||||
readOnly: true
|
|
||||||
extraPortMappings:
|
|
||||||
- containerPort: 30080
|
|
||||||
hostPort: ${HTTP_PORT}
|
|
||||||
listenAddress: "0.0.0.0"
|
|
||||||
protocol: TCP
|
|
||||||
- role: worker
|
|
||||||
image: ${NODE_IMAGE}
|
|
||||||
- role: worker
|
|
||||||
image: ${NODE_IMAGE}
|
|
||||||
@@ -1,23 +1,12 @@
|
|||||||
# Cluster shape: one node, no audit. Used by the `minimal` and `data` profiles.
|
# 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
|
||||||
# A TEMPLATE rather than a plain kind-config.yaml because a rig is copied and
|
# lib/config.sh reads the node count back from this file.
|
||||||
# renamed to make a second environment, and both the cluster name and the host
|
# Notes: docs/notes/kind-config.md
|
||||||
# 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 by ctrl/cluster.sh: CLUSTER, NODE_IMAGE, HTTP_PORT, HOST_WORKDIR
|
|
||||||
# (named without the ${...} braces so this line survives the substitution)
|
|
||||||
# Node count and audit are READ BACK from this file by lib/config.sh, so this
|
|
||||||
# YAML is the source of truth for both — there is no second place to update.
|
|
||||||
kind: Cluster
|
kind: Cluster
|
||||||
apiVersion: kind.x-k8s.io/v1alpha4
|
apiVersion: kind.x-k8s.io/v1alpha4
|
||||||
name: ${CLUSTER}
|
name: ${CLUSTER}
|
||||||
|
|
||||||
# Point containerd at a certs.d directory. registry.sh drops per-host hosts.toml
|
# containerd reads per-host registry config from certs.d (written by registry.sh).
|
||||||
# files in there afterwards, so switching registry mode never requires
|
|
||||||
# recreating the cluster.
|
|
||||||
containerdConfigPatches:
|
containerdConfigPatches:
|
||||||
- |-
|
- |-
|
||||||
[plugins."io.containerd.grpc.v1.cri".registry]
|
[plugins."io.containerd.grpc.v1.cri".registry]
|
||||||
@@ -26,9 +15,7 @@ containerdConfigPatches:
|
|||||||
nodes:
|
nodes:
|
||||||
- role: control-plane
|
- role: control-plane
|
||||||
image: ${NODE_IMAGE}
|
image: ${NODE_IMAGE}
|
||||||
# One NodePort bridged to the host; an in-cluster gateway owns it. There is
|
# One NodePort bridged to the host, owned by an in-cluster gateway (no ingress controller).
|
||||||
# deliberately no ingress controller — they pin a narrow window of k8s
|
|
||||||
# versions, and running a trailing-edge control plane is the point.
|
|
||||||
extraPortMappings:
|
extraPortMappings:
|
||||||
- containerPort: 30080
|
- containerPort: 30080
|
||||||
hostPort: ${HTTP_PORT}
|
hostPort: ${HTTP_PORT}
|
||||||
|
|||||||
@@ -1,49 +1,40 @@
|
|||||||
# Shared config loading. Sourced, never executed.
|
# Shared config loading: how the config layers compose. Sourced, never executed.
|
||||||
#
|
# Precedence, weakest first:
|
||||||
# The ecosystem convention is that scripts are standalone with no shared log
|
# defaults < versions.env < env.d/<profile> < <overlay>/rig.env < .env < caller's env.
|
||||||
# library — that still holds. This file is not a logging lib; it is the single
|
|
||||||
# definition of how the config layers compose, which every script has to agree
|
|
||||||
# on exactly. Precedence, weakest first:
|
|
||||||
#
|
|
||||||
# ctrl/versions.env pinned toolchain + image digests (committed)
|
|
||||||
# ctrl/env.d/<profile> cluster shape (committed)
|
|
||||||
# ctrl/.env machine-local values and secrets (gitignored)
|
|
||||||
# the caller's env `make cluster up PROFILE=client` (always wins)
|
|
||||||
#
|
|
||||||
# That last rule is why this is more than a few `source` lines: .env sets
|
|
||||||
# PROFILE, so without snapshotting it would silently override the PROFILE the
|
|
||||||
# user just typed on the command line.
|
|
||||||
#
|
|
||||||
# Run from ctrl/.
|
# Run from ctrl/.
|
||||||
|
# Notes: docs/notes/config.md
|
||||||
|
|
||||||
# Values a user can reasonably override per-invocation. Anything set in the
|
# Per-invocation overrides: restored after the files are read, so the caller wins.
|
||||||
# environment when load_config runs is restored after the files are read.
|
# NODES is deliberately not here (read from the kind config).
|
||||||
# NODES and AUDIT are deliberately NOT here: they are properties of the chosen
|
CONFIG_OVERRIDABLE="PROFILE OVERLAY CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
||||||
# ctrl/k8s/kind-config*.yaml.tpl and are read back out of it below, so there is
|
|
||||||
# one place that decides the shape of the cluster rather than two that can drift.
|
|
||||||
#
|
|
||||||
# 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 — the one precedence
|
|
||||||
# rule this header states. Both are now listed; the other twelve are unchanged.
|
|
||||||
CONFIG_OVERRIDABLE="PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS
|
|
||||||
REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT
|
REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT
|
||||||
SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT
|
SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT
|
||||||
REGISTRY_PORT MANIFESTS_DIR"
|
REGISTRY_PORT MANIFESTS_DIR"
|
||||||
|
|
||||||
# The containing folder's name, reduced to something kind accepts as a cluster
|
# rig's own example, used when no overlay is named (relative to rig's root).
|
||||||
# name (a DNS label: lowercase alphanumerics and dashes). Run from ctrl/, so the
|
DEFAULT_OVERLAY=examples/starter
|
||||||
# repo root is the parent.
|
|
||||||
|
# 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.
|
||||||
default_cluster_name() {
|
default_cluster_name() {
|
||||||
local n
|
local n
|
||||||
n=$(basename "$(cd .. && pwd)")
|
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" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
|
||||||
echo "${n:-rig}"
|
echo "${n:-rig}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Base of this environment's 10-port block. cksum is used rather than $RANDOM or
|
# Base of this environment's 10-port block; cksum so it is the same on every machine.
|
||||||
# bash hashing because it is POSIX and returns the same value on every machine,
|
|
||||||
# which is what makes the block reproducible instead of merely unique.
|
|
||||||
derive_port_base() {
|
derive_port_base() {
|
||||||
local h; h=$(printf '%s' "$1" | cksum | awk '{print $1}')
|
local h; h=$(printf '%s' "$1" | cksum | awk '{print $1}')
|
||||||
echo $((20000 + (h % 200) * 10))
|
echo $((20000 + (h % 200) * 10))
|
||||||
@@ -61,51 +52,118 @@ load_config() {
|
|||||||
|
|
||||||
set -a
|
set -a
|
||||||
source ./versions.env
|
source ./versions.env
|
||||||
# RIG_PORTABLE skips the machine-local layer. config_snapshot sets it, so a
|
# RIG_PORTABLE skips the machine-local .env (set by config_snapshot for kits).
|
||||||
# generated standalone kit never carries this machine's .env — which holds
|
|
||||||
# local values and, by its own description, secrets.
|
|
||||||
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
# Re-apply overrides now so PROFILE is the caller's before we pick the file.
|
# Re-apply overrides now so PROFILE is the caller's before we pick the file.
|
||||||
_config_restore "$saved"
|
_config_restore "$saved"
|
||||||
|
|
||||||
local profile="${PROFILE:-minimal}"
|
# A profile is optional; naming one that does not exist is an error.
|
||||||
if [ ! -f "./env.d/${profile}.env" ]; then
|
local profile="${PROFILE:-}" layered=""
|
||||||
echo "no such profile: env.d/${profile}.env" >&2
|
if [ -n "$profile" ] && [ "$profile" != default ]; then
|
||||||
echo "available: $(ls env.d/*.env 2>/dev/null | xargs -n1 basename | sed 's/\.env$//' | tr '\n' ' ')" >&2
|
if [ ! -f "./env.d/${profile}.env" ]; then
|
||||||
exit 1
|
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
|
fi
|
||||||
|
|
||||||
set -a
|
# The overlay: one folder, outside rig, holding a use case (docs/notes/overlay.md).
|
||||||
source "./env.d/${profile}.env"
|
# Named ones must exist; with none named, rig's own example is used if present.
|
||||||
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
OVERLAY_DIR=""
|
||||||
set +a
|
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
|
||||||
|
|
||||||
_config_restore "$saved"
|
# 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"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The defaults a profile would otherwise have to supply. Weakest of all: a
|
||||||
|
# profile, ctrl/.env and the caller each override them.
|
||||||
|
PROFILE_NAME="${PROFILE_NAME:-default}"
|
||||||
|
ADDONS="${ADDONS-}"
|
||||||
|
# local, not none: with no registry an unqualified image name means
|
||||||
|
# docker.io/library/<name>, and a default must not make that disclosure.
|
||||||
|
REGISTRY_MODE="${REGISTRY_MODE:-local}"
|
||||||
|
INGRESS_MODE="${INGRESS_MODE:-hostport}"
|
||||||
|
DNS_MODE="${DNS_MODE:-hosts}"
|
||||||
|
# The newest node image versions.env pins, found rather than restated, so
|
||||||
|
# bumping the pins moves the default with them.
|
||||||
|
if [ -z "${K8S_VERSION:-}" ]; then
|
||||||
|
K8S_VERSION=$(compgen -v NODE_IMAGE_v | sort -V | tail -1)
|
||||||
|
K8S_VERSION="${K8S_VERSION#NODE_IMAGE_}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Identity follows the FOLDER, so copying this directory somewhere else and
|
# Identity follows the folder, so a renamed copy is a distinct environment.
|
||||||
# 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.
|
|
||||||
CLUSTER="${CLUSTER:-$(default_cluster_name)}"
|
CLUSTER="${CLUSTER:-$(default_cluster_name)}"
|
||||||
KUBECONTEXT="kind-${CLUSTER}"
|
KUBECONTEXT="kind-${CLUSTER}"
|
||||||
|
|
||||||
# Host ports are a single shared namespace, so unlike the cluster name they
|
# Host ports: fill only the gaps from the derived block; anything already set wins.
|
||||||
# cannot just follow the directory — they have to be spread out. Anything
|
|
||||||
# already set (ctrl/.env, a profile, the command line) wins; only the gaps
|
|
||||||
# are filled. See ports.sh for the reasoning.
|
|
||||||
local base; base=$(derive_port_base "$CLUSTER")
|
local base; base=$(derive_port_base "$CLUSTER")
|
||||||
HTTP_PORT="${HTTP_PORT:-$base}"
|
HTTP_PORT="${HTTP_PORT:-$base}"
|
||||||
HTTPS_PORT="${HTTPS_PORT:-$((base + 1))}"
|
HTTPS_PORT="${HTTPS_PORT:-$((base + 1))}"
|
||||||
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
TILT_PORT="${TILT_PORT:-$((base + 2))}"
|
||||||
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
REGISTRY_PORT="${REGISTRY_PORT:-$((base + 3))}"
|
||||||
|
|
||||||
# Where the workload's manifests live, repo-root relative. Defaulted here so
|
# Where the workload's manifests live, relative to rig's folder (or absolute):
|
||||||
# it is always resolved rather than sometimes-set: it is the seam that lets
|
# the overlay's k8s/overlays/dev unless something names another. `none`: rig
|
||||||
# the real manifests be versioned away from the installer, and a consumer
|
# applies none (the overlay's Tiltfile does). A named folder must exist.
|
||||||
# should not have to know whether anyone filled it in. See k8s/README.md.
|
if [ "${MANIFESTS_DIR:-}" = ctrl/k8s/overlays/dev ] && [ ! -d ../ctrl/k8s/overlays/dev ]; then
|
||||||
MANIFESTS_DIR="${MANIFESTS_DIR:-ctrl/k8s/overlays/dev}"
|
# 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
|
||||||
|
|
||||||
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
# Profiles name a k8s minor (v1_36); versions.env holds the pinned digest.
|
||||||
local var="NODE_IMAGE_${K8S_VERSION}"
|
local var="NODE_IMAGE_${K8S_VERSION}"
|
||||||
@@ -115,59 +173,39 @@ load_config() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The cluster's shape is a file in ctrl/k8s/, named by the profile. Adding a
|
# The cluster is one file: the overlay's kind-config.yaml.tpl if it has one,
|
||||||
# shape is adding a file; there is no dispatcher to edit.
|
# 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).
|
||||||
# A host that needs its own shape — extra port mappings, more nodes — passes
|
if [ -z "${KIND_CONFIG:-}" ] && [ -n "$OVERLAY_DIR" ] \
|
||||||
# an absolute path instead, and rig renders it exactly like one of its own:
|
&& [ -f "$(_from_ctrl "$OVERLAY_DIR")/kind-config.yaml.tpl" ]; then
|
||||||
# ${CLUSTER} and ${NODE_IMAGE} are substituted either way. The shape stays in
|
KIND_CONFIG="$(_from_ctrl "$OVERLAY_DIR")/kind-config.yaml.tpl"
|
||||||
# the host's tree, because what a host's cluster needs is the host's business;
|
fi
|
||||||
# rig only knows how to build whatever it is handed.
|
KIND_CONFIG="${KIND_CONFIG:-./k8s/kind-config.yaml.tpl}"
|
||||||
KIND_CONFIG="${KIND_CONFIG:-kind-config.yaml.tpl}"
|
if [ ! -f "$KIND_CONFIG" ]; then
|
||||||
case "$KIND_CONFIG" in
|
echo "no kind config at KIND_CONFIG=${KIND_CONFIG}" >&2
|
||||||
/*) KIND_CONFIG_PATH="$KIND_CONFIG"; KIND_CONFIG_SHOWN="$KIND_CONFIG" ;;
|
|
||||||
*) KIND_CONFIG_PATH="./k8s/${KIND_CONFIG}"; KIND_CONFIG_SHOWN="ctrl/k8s/${KIND_CONFIG}" ;;
|
|
||||||
esac
|
|
||||||
if [ ! -f "$KIND_CONFIG_PATH" ]; then
|
|
||||||
echo "no such cluster shape: ${KIND_CONFIG_SHOWN}" >&2
|
|
||||||
echo "rig's own: $(ls k8s/kind-config*.yaml.tpl 2>/dev/null | xargs -n1 basename | tr '\n' ' ')" >&2
|
|
||||||
echo "or pass an absolute path to a shape of your own" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Read the shape back out of the YAML rather than trusting a profile to
|
# Read the node count back out of the file rather than restating it:
|
||||||
# restate it. check.sh sizes the memory warning on NODES, and cluster.sh
|
# check.sh and the memory tool size their budget on NODES.
|
||||||
# prints AUDIT before spending minutes building something that cannot be
|
NODES=$(grep -c '^ - role:' "$KIND_CONFIG")
|
||||||
# changed afterwards — both would mislead if the numbers drifted.
|
|
||||||
NODES=$(grep -c '^ - role:' "$KIND_CONFIG_PATH")
|
|
||||||
if grep -q 'audit-policy-file' "$KIND_CONFIG_PATH"; then AUDIT=on; else AUDIT=off; fi
|
|
||||||
|
|
||||||
# What one node costs, measured rather than guessed. On 2026-09-11 a minimal
|
# Measured MB per node (cluster alone, errs high for workers); shared by
|
||||||
# control-plane node ran at 620 MiB idle and ~728 MiB with a small mock, plus
|
# check.sh, the memory tool and standalone kits.
|
||||||
# 16 MiB for the local registry — ~745 MiB of working set. 800 rounds that up,
|
|
||||||
# and agrees with the 800 MB observed independently on a larger rig. Worker
|
|
||||||
# nodes carry no etcd or apiserver and are lighter, so for a multi-node shape
|
|
||||||
# this errs high. It is the cluster alone: whatever you deploy comes on top.
|
|
||||||
#
|
|
||||||
# Here rather than in check.sh because the memory tool and every standalone
|
|
||||||
# kit need the same figure.
|
|
||||||
NODE_MB=800
|
NODE_MB=800
|
||||||
}
|
}
|
||||||
|
|
||||||
# Render a cluster shape to stdout. sed rather than envsubst: envsubst is
|
# Render the kind config to stdout with sed (not envsubst) over an explicit variable list.
|
||||||
# gettext-base, absent from a minimal Debian, and Docker is meant to be the only
|
# HOST_WORKDIR and OVERLAY_DIR must be host paths: the host dockerd resolves hostPath entries.
|
||||||
# prerequisite. The variable list is explicit so a template cannot quietly start
|
|
||||||
# depending on something the caller does not set.
|
|
||||||
#
|
|
||||||
# hostPath entries are resolved by the HOST dockerd, so HOST_WORKDIR must stay a
|
|
||||||
# host path even when this runs inside the installer container.
|
|
||||||
render_kind_config() {
|
render_kind_config() {
|
||||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}"
|
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" \
|
sed -e "s|\${CLUSTER}|${CLUSTER}|g" \
|
||||||
-e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" \
|
-e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" \
|
||||||
-e "s|\${HTTP_PORT}|${HTTP_PORT}|g" \
|
-e "s|\${HTTP_PORT}|${HTTP_PORT}|g" \
|
||||||
-e "s|\${HOST_WORKDIR}|${host_workdir}|g" \
|
-e "s|\${HOST_WORKDIR}|${host_workdir}|g" \
|
||||||
"$KIND_CONFIG_PATH"
|
-e "s|\${OVERLAY_DIR}|${overlay_dir}|g" \
|
||||||
|
"$KIND_CONFIG"
|
||||||
}
|
}
|
||||||
|
|
||||||
_config_restore() {
|
_config_restore() {
|
||||||
@@ -183,29 +221,33 @@ _config_restore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── what a standalone kit needs to know ────────────────────────────────────
|
# ── what a standalone kit needs to know ────────────────────────────────────
|
||||||
# Two questions the kit generator (ctrl/standalone.sh) asks, so that it never has
|
# The questions ctrl/standalone.sh asks, so it never knows how config is stored.
|
||||||
# to know how configuration is stored. Where profiles live, which files are
|
|
||||||
# layered and what is derived are this file's business and can change freely;
|
|
||||||
# the generator only calls these.
|
|
||||||
|
|
||||||
# Every profile rig can be run as, one per line.
|
# Every configuration rig can run as, one per line: each profile, or `default`
|
||||||
|
# when there are none. Never empty.
|
||||||
config_profiles() {
|
config_profiles() {
|
||||||
local f
|
local f found=""
|
||||||
for f in ./env.d/*.env; do
|
for f in ./env.d/*.env; do
|
||||||
[ -e "$f" ] || continue
|
[ -e "$f" ] || continue
|
||||||
f=${f##*/}; echo "${f%.env}"
|
f=${f##*/}; echo "${f%.env}"; found=1
|
||||||
done
|
done
|
||||||
|
[ -n "$found" ] || echo default
|
||||||
}
|
}
|
||||||
|
|
||||||
# The resolved configuration for one profile, as `declare -p` lines — exactly
|
# What load_config sets, minus the machine-local layer, as `declare -p` lines.
|
||||||
# what load_config leaves behind, minus the machine-local layer. A kit freezes
|
# Usage: config_snapshot <profile> | --current (found by difference, not a list)
|
||||||
# this in place of load_config, so it carries rig's decisions for that profile
|
|
||||||
# and nothing about the machine it was generated on.
|
|
||||||
#
|
|
||||||
# Found by difference, not by a list: whatever load_config sets today, it sets.
|
|
||||||
# A list here would be one more place to forget a variable.
|
|
||||||
config_snapshot() {
|
config_snapshot() {
|
||||||
local _rig_snap_profile="$1"
|
local _rig_snap_choices
|
||||||
|
if [ "$1" = --current ]; then
|
||||||
|
_rig_snap_choices=$( (
|
||||||
|
load_config >/dev/null || exit 1
|
||||||
|
for _rig_snap_n in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!_rig_snap_n+x}" ]; then printf 'export %s=%q\n' "$_rig_snap_n" "${!_rig_snap_n}"; fi
|
||||||
|
done
|
||||||
|
) ) || return 1
|
||||||
|
else
|
||||||
|
_rig_snap_choices="export PROFILE=$(printf '%q' "$1")"
|
||||||
|
fi
|
||||||
(
|
(
|
||||||
# Nothing from the caller's shell may leak into a kit.
|
# Nothing from the caller's shell may leak into a kit.
|
||||||
for _rig_snap_n in $CONFIG_OVERRIDABLE; do unset "$_rig_snap_n"; done
|
for _rig_snap_n in $CONFIG_OVERRIDABLE; do unset "$_rig_snap_n"; done
|
||||||
@@ -213,7 +255,8 @@ config_snapshot() {
|
|||||||
for _rig_snap_n in $(compgen -v); do
|
for _rig_snap_n in $(compgen -v); do
|
||||||
_rig_snap_was[$_rig_snap_n]="${!_rig_snap_n-}"
|
_rig_snap_was[$_rig_snap_n]="${!_rig_snap_n-}"
|
||||||
done
|
done
|
||||||
PROFILE="$_rig_snap_profile" RIG_PORTABLE=1 load_config >/dev/null
|
eval "$_rig_snap_choices"
|
||||||
|
RIG_PORTABLE=1 load_config >/dev/null
|
||||||
for _rig_snap_n in $(compgen -v); do
|
for _rig_snap_n in $(compgen -v); do
|
||||||
case "$_rig_snap_n" in
|
case "$_rig_snap_n" in
|
||||||
_rig_snap_*|RIG_PORTABLE|BASH*|FUNCNAME|PIPESTATUS|LINENO|RANDOM|SRANDOM|\
|
_rig_snap_*|RIG_PORTABLE|BASH*|FUNCNAME|PIPESTATUS|LINENO|RANDOM|SRANDOM|\
|
||||||
@@ -227,19 +270,23 @@ config_snapshot() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# A replacement for load_config with one profile's resolution frozen in, printed
|
# The profile this machine runs, as load_config resolves it here.
|
||||||
# as a function definition for a standalone kit to carry. The generator embeds
|
config_current_profile() { ( load_config >/dev/null && echo "$PROFILE_NAME" ); }
|
||||||
# whatever this prints and interprets none of it, so what "frozen" means stays
|
|
||||||
# rig's decision.
|
# Names (never values) of .env keys an export does not carry, e.g. credentials.
|
||||||
#
|
config_left_out() {
|
||||||
# It keeps load_config's one stated rule: the caller's env wins for anything in
|
[ -f ./.env ] || return 0
|
||||||
# CONFIG_OVERRIDABLE. A kit therefore behaves like rig — `OUT_BIN=... rigdeps.sh`
|
local k
|
||||||
# still works — rather than like a copy with everything pinned.
|
for k in $(sed -nE 's/^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=.*/\2/p' ./.env | sort -u); do
|
||||||
#
|
case " $(echo $CONFIG_OVERRIDABLE) " in
|
||||||
# What freezing does give up, knowingly: values DERIVED from an overridable one
|
*" $k "*) ;;
|
||||||
# are fixed at generation. Override CLUSTER and the ports stay the ones derived
|
*) echo "$k" ;;
|
||||||
# for the original name. Re-deriving would mean carrying the layering itself,
|
esac
|
||||||
# which is exactly what a kit exists not to need.
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print a load_config with a resolution frozen in, for a standalone kit to carry.
|
||||||
|
# The caller's env still wins; derived values (e.g. ports) stay fixed.
|
||||||
config_freeze() {
|
config_freeze() {
|
||||||
local snap
|
local snap
|
||||||
snap=$(config_snapshot "$1") || return 1
|
snap=$(config_snapshot "$1") || return 1
|
||||||
|
|||||||
123
rig/ctrl/mem.sh
123
rig/ctrl/mem.sh
@@ -1,32 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# rig:standalone rigmini status
|
# rig:standalone rigmini status
|
||||||
# How much memory this machine will actually give you before something dies —
|
# rig's memory tool (also generated as rigmini.sh): what the machine advertises vs. what it survives.
|
||||||
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
# Usage: mem.sh status | push [--to GB] [--to-oom] | all [--budget GB] | backup | restore (WSL)
|
||||||
#
|
# Notes: docs/notes/mem.md
|
||||||
# There are two numbers and they are rarely the same. `status` reports what the
|
|
||||||
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
|
||||||
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
|
||||||
# against what this profile's cluster needs.
|
|
||||||
#
|
|
||||||
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
|
||||||
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
|
||||||
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
|
||||||
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
|
||||||
# on a box that OOMs at 2.
|
|
||||||
#
|
|
||||||
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
|
||||||
# allocation that can be raised, and the commonest failure is raising it without
|
|
||||||
# restarting — so status compares what .wslconfig says with what actually booted.
|
|
||||||
#
|
|
||||||
# Reports and instructs. It never raises a limit, frees anything or installs a
|
|
||||||
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
|
||||||
# itself, so that `restore` has something to put back after a hand edit.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# mem.sh status what it has, what caps it
|
|
||||||
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
|
||||||
# mem.sh all [--budget GB] both, then the verdict
|
|
||||||
# mem.sh backup | restore .wslconfig, WSL only
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
@@ -42,11 +18,9 @@ BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess belo
|
|||||||
|
|
||||||
# ── platform ───────────────────────────────────────────────────────────────
|
# ── platform ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
# Refuse Git Bash / MSYS / Cygwin and kernels without /proc, with a clear message.
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
require_linux() {
|
||||||
case "$(uname -s)" in
|
case "${UNAME_S:-$(uname -s)}" in
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
@@ -99,9 +73,7 @@ avail_meminfo_mb() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
# This cgroup's limit/usage files, set once by find_cgroup (cheap for the poll loop).
|
||||||
# find_cgroup, because every later reading needs both and hunting for the files
|
|
||||||
# on each call would be the slow part of the poll loop.
|
|
||||||
CG_MAX_FILE=""
|
CG_MAX_FILE=""
|
||||||
CG_CUR_FILE=""
|
CG_CUR_FILE=""
|
||||||
CG_VERSION=""
|
CG_VERSION=""
|
||||||
@@ -109,10 +81,8 @@ CG_VERSION=""
|
|||||||
find_cgroup() {
|
find_cgroup() {
|
||||||
local rel
|
local rel
|
||||||
|
|
||||||
# Inside a container the cgroup namespace makes the top of the tree BE the
|
# Top of tree first (right inside a container), then this shell's own slice
|
||||||
# container's own cgroup, so the unqualified path is already the right one.
|
# from /proc/self/cgroup (right on a host).
|
||||||
# On a host it is the root cgroup, which is never limited — hence the second
|
|
||||||
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
|
||||||
if [ -r /sys/fs/cgroup/memory.max ]; then
|
if [ -r /sys/fs/cgroup/memory.max ]; then
|
||||||
CG_VERSION=v2
|
CG_VERSION=v2
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
||||||
@@ -141,10 +111,7 @@ find_cgroup() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
# The cap in MB, or "" when unlimited ("max", or any value >= MemTotal).
|
||||||
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
|
||||||
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
|
||||||
# own memory is not a limit, however it is written.
|
|
||||||
cgroup_cap_mb() {
|
cgroup_cap_mb() {
|
||||||
local raw cap
|
local raw cap
|
||||||
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
||||||
@@ -185,10 +152,7 @@ effective_ceiling_mb() {
|
|||||||
echo "$c"
|
echo "$c"
|
||||||
}
|
}
|
||||||
|
|
||||||
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
# Room left right now: cgroup cap minus usage when capped, else MemAvailable.
|
||||||
# In a capped container /proc/meminfo describes the host and is worse than
|
|
||||||
# useless for this — it would report tens of gigabytes free on a box that is one
|
|
||||||
# allocation from being killed.
|
|
||||||
headroom_mb() {
|
headroom_mb() {
|
||||||
local cap used
|
local cap used
|
||||||
cap=$(cgroup_cap_mb)
|
cap=$(cgroup_cap_mb)
|
||||||
@@ -202,9 +166,7 @@ headroom_mb() {
|
|||||||
|
|
||||||
# ── status ─────────────────────────────────────────────────────────────────
|
# ── status ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
# Ask Windows for %USERPROFILE%; fall back to whichever profile owns a .wslconfig.
|
||||||
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
|
||||||
# Windows, then fall back to whichever profile actually owns a config.
|
|
||||||
wslconfig_path() {
|
wslconfig_path() {
|
||||||
local profile winpath found
|
local profile winpath found
|
||||||
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
||||||
@@ -260,9 +222,7 @@ status() {
|
|||||||
echo " ulimit -v unlimited"
|
echo " ulimit -v unlimited"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# overcommit_memory=0 is the default heuristic: a large allocation is
|
# Overcommit mode decides whether limits show as failed mallocs or OOM kills.
|
||||||
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
|
||||||
# than as a failed malloc. It is why `push` touches every page it asks for.
|
|
||||||
local om or_
|
local om or_
|
||||||
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
||||||
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
||||||
@@ -335,9 +295,7 @@ status() {
|
|||||||
echo " ! docker cli present but the daemon is unreachable"
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
# WSL: report the .wslconfig cap and whether it was applied (needs wsl --shutdown).
|
||||||
# not usefully apply — the change costs a full VM restart. Report it, and
|
|
||||||
# report the commonest mistake, which is editing it and not restarting.
|
|
||||||
if is_wsl; then
|
if is_wsl; then
|
||||||
local cfg conf conf_mb n
|
local cfg conf conf_mb n
|
||||||
cfg=$(wslconfig_path)
|
cfg=$(wslconfig_path)
|
||||||
@@ -397,7 +355,7 @@ require_wsl() {
|
|||||||
|
|
||||||
# backup and restore act on the file, so unlike status they must not guess.
|
# backup and restore act on the file, so unlike status they must not guess.
|
||||||
wslconfig_required() {
|
wslconfig_required() {
|
||||||
local cfg; cfg=$(wslconfig_required)
|
local cfg; cfg=$(wslconfig_path)
|
||||||
if [ -z "$cfg" ]; then
|
if [ -z "$cfg" ]; then
|
||||||
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
||||||
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
||||||
@@ -429,8 +387,7 @@ backup() {
|
|||||||
local cfg dest
|
local cfg dest
|
||||||
cfg=$(wslconfig_required)
|
cfg=$(wslconfig_required)
|
||||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
# Timestamped, never overwritten.
|
||||||
# second run is not a backup.
|
|
||||||
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
||||||
cp "$cfg" "$dest"
|
cp "$cfg" "$dest"
|
||||||
echo "backed up $dest"
|
echo "backed up $dest"
|
||||||
@@ -449,9 +406,7 @@ restore() {
|
|||||||
echo " -> $cfg"
|
echo " -> $cfg"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Newest is the right default — undo the last edit — but if you backed up
|
# Restores the newest; list the others in case an older one is wanted.
|
||||||
# *after* editing, the state you want is older. Show the rest so a no-op
|
|
||||||
# restore is obviously a no-op rather than a mystery.
|
|
||||||
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
if [ "$count" -gt 1 ]; then
|
if [ "$count" -gt 1 ]; then
|
||||||
echo "$count backups exist, newest first:"
|
echo "$count backups exist, newest first:"
|
||||||
@@ -495,14 +450,9 @@ cleanup() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# The child allocates and stops itself; the parent only watches. That split is
|
# Runs as a child that may be OOM-killed; the parent survives to report.
|
||||||
# the point: under --to-oom the allocating process is expected to be killed, and
|
|
||||||
# something has to survive to say how far it got.
|
|
||||||
allocator() {
|
allocator() {
|
||||||
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
# Make this process the preferred OOM victim (raising needs no privilege).
|
||||||
# first. Raising needs no privilege (only lowering does). Without it, the
|
|
||||||
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
|
||||||
# box you are still using, that is not an acceptable coin toss.
|
|
||||||
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
||||||
|
|
||||||
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
||||||
@@ -511,16 +461,7 @@ allocator() {
|
|||||||
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
||||||
|
|
||||||
while :; do
|
while :; do
|
||||||
# Written STRAIGHT INTO the array element. The obvious spelling —
|
# Write straight into the element (one copy, not three) and touch every page.
|
||||||
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
|
||||||
# not one: the template stays resident, expanding "$chunk" makes a
|
|
||||||
# temporary word, and the append makes the element. A 128 MB step then
|
|
||||||
# needs 384 MB transiently, and on a small box it is killed on the
|
|
||||||
# first append while reporting a third of the true ceiling.
|
|
||||||
#
|
|
||||||
# printf -v into a subscript also means every page is written, so it is
|
|
||||||
# resident rather than merely promised — the only kind of allocation
|
|
||||||
# that measures anything under heuristic overcommit.
|
|
||||||
printf -v "arr[$i]" '%*s' "$bytes" ''
|
printf -v "arr[$i]" '%*s' "$bytes" ''
|
||||||
i=$((i + 1)); held=$((held + STEP_MB))
|
i=$((i + 1)); held=$((held + STEP_MB))
|
||||||
|
|
||||||
@@ -533,9 +474,7 @@ allocator() {
|
|||||||
"$held" "$rss" "$avail" "$swapped"
|
"$held" "$rss" "$avail" "$swapped"
|
||||||
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
||||||
|
|
||||||
# Worth calling out separately from the ceiling: this is where the box
|
# First swap is reported separately: slow comes before killed.
|
||||||
# stops being fast and starts being unusable, which for a scheduler is
|
|
||||||
# a different and earlier problem than being killed.
|
|
||||||
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
||||||
first_swap=$held
|
first_swap=$held
|
||||||
echo " - first swap page at ${held} MB — past here it works but crawls"
|
echo " - first swap page at ${held} MB — past here it works but crawls"
|
||||||
@@ -556,30 +495,20 @@ push() {
|
|||||||
total=$(mb MemTotal)
|
total=$(mb MemTotal)
|
||||||
ceiling=$(effective_ceiling_mb)
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
|
||||||
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
# Default step: ceiling/64, clamped to 4..256 MB.
|
||||||
# find the edge, few enough lines to read, and small enough that the
|
|
||||||
# transient cost of one allocation never dominates a small box. A fixed
|
|
||||||
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
|
||||||
if [ "$STEP_EXPLICIT" = no ]; then
|
if [ "$STEP_EXPLICIT" = no ]; then
|
||||||
STEP_MB=$(( ceiling / 64 ))
|
STEP_MB=$(( ceiling / 64 ))
|
||||||
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
||||||
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
# Stop with a cushion: 64 MB under a cgroup cap, 512 MB on a host, or 5% of ceiling if larger.
|
||||||
# depends on what it is protecting. Under a cgroup cap, running out kills
|
|
||||||
# only this container's own processes, so it need cover no more than the
|
|
||||||
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
|
||||||
# halve the answer. On a host there is everything else to protect, and the
|
|
||||||
# OOM killer does not promise to pick the process that caused the problem.
|
|
||||||
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
||||||
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
||||||
|
|
||||||
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
# INT kills the child and lets the summary below print anyway, so an
|
# Ctrl-C kills the child, frees the memory, and still prints the summary.
|
||||||
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
|
||||||
# still gives the memory back.
|
|
||||||
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
||||||
|
|
||||||
echo "push"
|
echo "push"
|
||||||
@@ -652,11 +581,7 @@ push() {
|
|||||||
fi ;;
|
fi ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# The gap between the claim and the measurement is the finding — but only
|
# Warn about claimed-vs-measured gap only when the box, not us, chose the stop.
|
||||||
# when the BOX chose where to stop. An empty $stop means the child was ended
|
|
||||||
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
|
||||||
# asked for, and flagging those as short of the ceiling would put a warning
|
|
||||||
# on every deliberately small run.
|
|
||||||
local got="${rss:-$held}"
|
local got="${rss:-$held}"
|
||||||
echo
|
echo
|
||||||
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
||||||
|
|||||||
@@ -1,25 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Give each environment its own block of host ports.
|
# Give each environment its own block of host ports, derived from the directory name.
|
||||||
#
|
# base = 20000 + (hash(slug) % 200) * 10; +0 HTTP +1 HTTPS +2 TILT +3 REGISTRY
|
||||||
# New versions of a system mean new clusters on ONE machine, not new machines.
|
|
||||||
# Cluster name, kubectl context, registry container and image tag already derive
|
|
||||||
# from the directory name, so two copies never collide there — but host ports are
|
|
||||||
# a single shared namespace and would.
|
|
||||||
#
|
|
||||||
# The block is derived from the directory name: stateless, stable, and requiring
|
|
||||||
# no coordination between copies that know nothing about each other.
|
|
||||||
#
|
|
||||||
# base = 20000 + (hash(slug) % 200) * 10
|
|
||||||
# +0 HTTP +1 HTTPS +2 TILT +3 REGISTRY (+4..9 reserved)
|
|
||||||
#
|
|
||||||
# 20000+ deliberately avoids the ports something is already likely to hold: 80,
|
|
||||||
# 443, 3000, 5432, 8000, 8080.
|
|
||||||
#
|
|
||||||
# Derivation is a default, not a decision. On first use the resolved block is
|
|
||||||
# written into ctrl/.env, so it becomes pinned, visible and editable rather than
|
|
||||||
# a number that appears from nowhere. Anything already in ctrl/.env wins.
|
|
||||||
#
|
|
||||||
# Usage: ports.sh show | active | derive | persist
|
# Usage: ports.sh show | active | derive | persist
|
||||||
|
# Notes: docs/notes/ports.md
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
@@ -38,31 +21,20 @@ derive() {
|
|||||||
DERIVED_REGISTRY=$((base + 3))
|
DERIVED_REGISTRY=$((base + 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
# The resolved facts a consumer outside bash needs, machine-readable:
|
# Resolved facts for consumers outside bash, space-separated, positional:
|
||||||
#
|
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_DIR
|
||||||
# CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR
|
# The two paths are absolute, or - when there is none. Read this, not `derive`.
|
||||||
#
|
|
||||||
# Identity and ports together, because they are one fact set — the header above
|
|
||||||
# says so: 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.
|
|
||||||
#
|
|
||||||
# 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 this file's own rule that "anything already in ctrl/.env
|
|
||||||
# wins". `active` is what anything downstream should read.
|
|
||||||
#
|
|
||||||
# Why this exists at all: the cluster name is not the bare directory name.
|
|
||||||
# default_cluster_name() lowercases it and replaces every character outside
|
|
||||||
# [a-z0-9-], because it has to be a DNS label. Re-deriving that in another
|
|
||||||
# language is how a copy in `My_Project/` ends up guarding the wrong context.
|
|
||||||
active() {
|
active() {
|
||||||
load_config
|
load_config
|
||||||
echo "$CLUSTER $KUBECONTEXT $HTTP_PORT $HTTPS_PORT $TILT_PORT $REGISTRY_PORT $MANIFESTS_DIR"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
@@ -91,6 +63,13 @@ _row() {
|
|||||||
# rewritten — an override stays an override.
|
# rewritten — an override stays an override.
|
||||||
persist() {
|
persist() {
|
||||||
derive
|
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
|
[ -f ./.env ] || cp ./.env.example ./.env
|
||||||
|
|
||||||
local wrote=0 key val
|
local wrote=0 key val
|
||||||
|
|||||||
@@ -1,28 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Registry plumbing. THIS is the seam — not a tool.
|
# Registry plumbing: REGISTRY_MODE none | local | mirror | remote. A script, not ctlptl,
|
||||||
#
|
# because ctlptl cannot express `mirror`.
|
||||||
# Four modes, selected by REGISTRY_MODE in the active profile:
|
|
||||||
#
|
|
||||||
# none Tilt builds straight into the node. No registry at all — and so no
|
|
||||||
# guard against an outward push: an unqualified image name means
|
|
||||||
# docker.io/library/<name>, and only Tilt's kind detection stands
|
|
||||||
# between that and a real push. Throwaway use only; every profile
|
|
||||||
# here now defaults to `local` instead.
|
|
||||||
# local a registry:2 container wired into the cluster.
|
|
||||||
# mirror the same container, but configured as a pull-through CACHE of the
|
|
||||||
# corporate registry. What a locked-down client actually looks like:
|
|
||||||
# images originate from corp, you don't hammer it, and you keep
|
|
||||||
# working when the VPN drops.
|
|
||||||
# remote no local container; pull straight from the corporate registry using
|
|
||||||
# an imagePullSecret.
|
|
||||||
#
|
|
||||||
# Deliberately a script rather than a tool. ctlptl collapses the `local` wiring
|
|
||||||
# into one line, but its Registry spec only accepts name/port/image/listenAddress
|
|
||||||
# — there is no way to set REGISTRY_PROXY_REMOTEURL, so it cannot express
|
|
||||||
# `mirror` at all. Keeping the seam here is what keeps the corporate registry
|
|
||||||
# swappable.
|
|
||||||
#
|
|
||||||
# Usage: registry.sh up | down | status
|
# Usage: registry.sh up | down | status
|
||||||
|
# Notes: docs/notes/registry.md
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
@@ -34,17 +14,7 @@ REG_PORT="${REGISTRY_PORT:-5005}"
|
|||||||
K="kubectl --context ${KUBECONTEXT}"
|
K="kubectl --context ${KUBECONTEXT}"
|
||||||
|
|
||||||
# ── CA trust ───────────────────────────────────────────────────────────────
|
# ── CA trust ───────────────────────────────────────────────────────────────
|
||||||
# A corporate registry is almost always fronted by an internal CA, and trust has
|
# Copy REGISTRY_CA_FILE into every kind node's trust store (nodes don't inherit host trust).
|
||||||
# to reach three separate places. Nothing does this for you, and the symptom when
|
|
||||||
# it's missing is an opaque:
|
|
||||||
# x509: certificate signed by unknown authority
|
|
||||||
#
|
|
||||||
# 1. the host docker daemon — /etc/docker/certs.d/<host>/ca.crt (needs root)
|
|
||||||
# 2. every kind node's containerd — nodes do NOT inherit host trust
|
|
||||||
# 3. anything doing HTTPS from inside the cluster, in its own trust store
|
|
||||||
#
|
|
||||||
# We handle (2) here because it's ours to handle. (1) is reported by check.sh
|
|
||||||
# since it needs root. (3) belongs to the workload.
|
|
||||||
install_ca_into_nodes() {
|
install_ca_into_nodes() {
|
||||||
[ -n "${REGISTRY_CA_FILE:-}" ] || return 0
|
[ -n "${REGISTRY_CA_FILE:-}" ] || return 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# What rig has settled, written down as assertions.
|
# What rig has settled, written down as assertions: one decision per check.
|
||||||
#
|
# No cluster, docker or network; exits 1 on failure (unlike `make check`).
|
||||||
# These are documentation that runs. Each check is ONE decision that has already
|
# Usage: make selftest [install] (or: bash ctrl/selftest.sh [install])
|
||||||
# been made, with the reason above it — not coverage, and deliberately not an
|
# install: the installer in clean containers — docker, network, minutes (installtest.sh)
|
||||||
# exhaustive sweep of use cases. rig's own index says a rule without its reason
|
# Notes: docs/notes/selftest.md
|
||||||
# gets overridden the first time it is inconvenient; a rule nobody can restate
|
|
||||||
# is worse. So the test says what was decided, and failing it should read as
|
|
||||||
# "you are about to undo this" rather than "something broke".
|
|
||||||
#
|
|
||||||
# Scope, on purpose:
|
|
||||||
# - no cluster, no docker, no network. It must be cheap enough to actually run.
|
|
||||||
# - it asserts about RIG. `make check` asserts about the MACHINE and never
|
|
||||||
# fails; this exits 1, the way `make standalone check` does.
|
|
||||||
# - what actually deploys is not testable here. `tilt ci` stays a manual step.
|
|
||||||
#
|
|
||||||
# Usage: make selftest (or: bash ctrl/selftest.sh)
|
|
||||||
set -uo pipefail # NOT -e: one failing check must not abort the rest
|
set -uo pipefail # NOT -e: one failing check must not abort the rest
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
if [ "${1:-}" = install ]; then shift; exec bash ./installtest.sh "$@"; fi
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
|
|
||||||
rc=0
|
rc=0
|
||||||
@@ -35,6 +26,17 @@ check() { # name, expected, actual
|
|||||||
|
|
||||||
note() { printf '\n%s\n' "$1"; }
|
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
|
# 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.
|
# caller's exported value is the only thing in play.
|
||||||
resolved() {
|
resolved() {
|
||||||
@@ -42,20 +44,34 @@ 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"
|
||||||
|
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" \
|
||||||
|
"$(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)"
|
||||||
|
check "a profile that does not exist is still an error" "yes" \
|
||||||
|
"$( (cd "$NP/rig/ctrl" && PROFILE=no-such-profile bash -c 'source ./lib/config.sh; load_config' >/dev/null 2>&1) && echo no || echo yes)"
|
||||||
|
rm -rf "$NP"
|
||||||
|
|
||||||
|
|
||||||
note "the ports.sh active contract"
|
note "the ports.sh active contract"
|
||||||
# ports.sh active is read POSITIONALLY by two other files — the Makefile takes
|
# ports.sh active is read positionally by the Makefile and Tiltfile: pin field count and order.
|
||||||
# $(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.
|
|
||||||
FACTS="$(bash ports.sh active)"
|
FACTS="$(bash ports.sh active)"
|
||||||
check "active: exactly 7 fields" "7" "$(printf '%s' "$FACTS" | wc -w)"
|
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 <<< "$FACTS"
|
read -r F_CLUSTER F_CTX F_HTTP F_HTTPS F_TILT F_REG F_MANIFESTS F_OVERLAY <<< "$FACTS"
|
||||||
check "active: field 2 is kind-<cluster>" "kind-$F_CLUSTER" "$F_CTX"
|
check "active: field 2 is kind-<cluster>" "kind-$F_CLUSTER" "$F_CTX"
|
||||||
check "active: fields 3-6 are numeric" "yes" \
|
check "active: fields 3-6 are numeric" "yes" \
|
||||||
"$([[ "$F_HTTP$F_HTTPS$F_TILT$F_REG" =~ ^[0-9]+$ ]] && echo yes || echo no)"
|
"$([[ "$F_HTTP$F_HTTPS$F_TILT$F_REG" =~ ^[0-9]+$ ]] && echo yes || echo no)"
|
||||||
check "active: field 7 is a path" "yes" \
|
# Absolute, or - when there is none: an empty field would shift every later one.
|
||||||
"$([ -n "$F_MANIFESTS" ] && [ "${F_MANIFESTS#-}" = "$F_MANIFESTS" ] && echo yes || echo no)"
|
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)"
|
||||||
# derive answers a different question and must keep its own shape: it reports
|
# derive answers a different question and must keep its own shape: it reports
|
||||||
# what the directory name implies, ignoring ctrl/.env, so nothing should
|
# what the directory name implies, ignoring ctrl/.env, so nothing should
|
||||||
# configure itself from it.
|
# configure itself from it.
|
||||||
@@ -63,24 +79,21 @@ check "derive: still 4 fields, not 7" "4" "$(bash ports.sh derive | wc -w)"
|
|||||||
|
|
||||||
|
|
||||||
note "the caller's env beats the files"
|
note "the caller's env beats the files"
|
||||||
# lib/config.sh states one precedence rule: versions.env < env.d/<profile> <
|
# Every key in CONFIG_OVERRIDABLE must lose to the caller's env; the loop follows the list.
|
||||||
# ctrl/.env < the caller's env. It is enforced by CONFIG_OVERRIDABLE, a
|
|
||||||
# hand-maintained list — and a key missing from it loses to the file SILENTLY.
|
|
||||||
# REGISTRY_PORT and MANIFESTS_DIR were both missing on 2026-09-13 and were found
|
|
||||||
# by accident.
|
|
||||||
#
|
|
||||||
# So this loop is generated FROM the list: add a key to CONFIG_OVERRIDABLE and
|
|
||||||
# this test starts asking about it without anyone remembering to come here.
|
|
||||||
# Three keys name something that must exist and are validated at load, so they
|
|
||||||
# get a real alternative rather than a sentinel.
|
|
||||||
test_value() {
|
test_value() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
PROFILE) echo "client" ;; # env.d/client.env exists
|
# Picked from what exists, never named: rig must not need any particular
|
||||||
K8S_VERSION) echo "v1_35" ;; # NODE_IMAGE_v1_35 is pinned
|
# profile, template or pinned version to be present for this to run.
|
||||||
KIND_CONFIG) echo "kind-config.client.yaml.tpl" ;; # the shape must exist
|
PROFILE) config_profiles | head -1 ;;
|
||||||
|
K8S_VERSION) (set -a; source ./versions.env; compgen -v NODE_IMAGE_v | sort -V | head -1 | sed 's/^NODE_IMAGE_//') ;;
|
||||||
|
# An absolute path, as a project passing its own file does. Never equal
|
||||||
|
# to the default, so the check cannot pass by accident.
|
||||||
|
KIND_CONFIG) echo "$PWD/k8s/kind-config.yaml.tpl" ;;
|
||||||
*_PORT) echo "19999" ;;
|
*_PORT) echo "19999" ;;
|
||||||
CLUSTER) echo "selftest-name" ;;
|
CLUSTER) echo "selftest-name" ;;
|
||||||
MANIFESTS_DIR) echo "../elsewhere/overlays/dev" ;;
|
# Named folders must exist, and must not be the default.
|
||||||
|
MANIFESTS_DIR) echo "examples/starter/k8s/base" ;;
|
||||||
|
OVERLAY) echo "examples/data" ;;
|
||||||
ADDONS) echo "metallb" ;;
|
ADDONS) echo "metallb" ;;
|
||||||
*) echo "selftest-sentinel" ;;
|
*) echo "selftest-sentinel" ;;
|
||||||
esac
|
esac
|
||||||
@@ -98,16 +111,8 @@ done
|
|||||||
|
|
||||||
|
|
||||||
note "one derivation, not three"
|
note "one derivation, not three"
|
||||||
# The Makefile used to compute the cluster name itself and sed TILT_PORT out of
|
# The Makefile must take context/port from ports.sh active, checked on real `make -n` output.
|
||||||
# ctrl/.env — a second derivation of values lib/config.sh already owns, which
|
# --no-print-directory + grep, not tail -1: under `make selftest` this is a recursive make.
|
||||||
# could disagree with it after `make ports persist`. It now reads ports.sh
|
|
||||||
# active. Nothing structurally prevents the sed coming back, so the agreement is
|
|
||||||
# asserted against the real `make -n` output rather than against the source.
|
|
||||||
# --no-print-directory and a grep, not `tail -1`: run from `make selftest` this
|
|
||||||
# is a RECURSIVE make, and the "Entering/Leaving directory" lines go to STDOUT.
|
|
||||||
# tail -1 then reads "make[1]: Leaving directory ..." and both checks below fail
|
|
||||||
# — but only when invoked through make, never when the script is run directly.
|
|
||||||
# A test that passes one way and fails the other is worse than no test.
|
|
||||||
MK="$(cd .. && make --no-print-directory -n tilt 2>/dev/null | grep -m1 'tilt ')"
|
MK="$(cd .. && make --no-print-directory -n tilt 2>/dev/null | grep -m1 'tilt ')"
|
||||||
check "Makefile: --context comes from active" "$F_CTX" \
|
check "Makefile: --context comes from active" "$F_CTX" \
|
||||||
"$(printf '%s' "$MK" | sed -n 's/.*--context \([^ ]*\).*/\1/p')"
|
"$(printf '%s' "$MK" | sed -n 's/.*--context \([^ ]*\).*/\1/p')"
|
||||||
@@ -116,17 +121,14 @@ check "Makefile: --port comes from active" "$F_TILT" \
|
|||||||
|
|
||||||
|
|
||||||
note "identity follows the folder, safely"
|
note "identity follows the folder, safely"
|
||||||
# The cluster name is NOT the bare directory name: kind needs a DNS label, so
|
# The cluster name is the folder name made a DNS label, derived only in lib/config.sh.
|
||||||
# default_cluster_name lowercases it and replaces everything outside [a-z0-9-].
|
|
||||||
# Re-deriving that anywhere else is how a copy ends up guarding the wrong
|
|
||||||
# context — which is exactly why the Tiltfile asks instead of computing.
|
|
||||||
TMP="$(mktemp -d)"
|
TMP="$(mktemp -d)"
|
||||||
trap 'rm -rf "$TMP"' EXIT
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
mkdir -p "$TMP/My_Proj"
|
mkdir -p "$TMP/My_Proj"
|
||||||
cp -r . "$TMP/My_Proj/ctrl"
|
cp -r . "$TMP/My_Proj/ctrl"
|
||||||
# A pinned CLUSTER in .env would be an override, not a derivation, and this
|
# A pinned CLUSTER in .env would be an override, not a derivation, and this
|
||||||
# check is about the derivation.
|
# check is about the derivation. (An OVERLAY would be another derivation.)
|
||||||
sed -i '/^CLUSTER=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null
|
sed -i '/^CLUSTER=/d; /^OVERLAY=/d' "$TMP/My_Proj/ctrl/.env" 2>/dev/null
|
||||||
COPY="$(cd "$TMP/My_Proj/ctrl" && bash ports.sh active)"
|
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 "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")"
|
check "and a context to match" "kind-my-proj" "$(awk '{print $2}' <<< "$COPY")"
|
||||||
@@ -135,52 +137,113 @@ check "a renamed copy gets a DIFFERENT block" "different" \
|
|||||||
|
|
||||||
|
|
||||||
note "ports are stable across versions"
|
note "ports are stable across versions"
|
||||||
# Not a change-detector. The block is derived, never stored, so if the
|
# Ports are derived, never stored: a changed derivation moves every existing env's ports.
|
||||||
# derivation shifts then every EXISTING environment's ports move underneath it —
|
|
||||||
# a running cluster keeps its old ports while rig starts reporting new ones, and
|
|
||||||
# `make ports` stops describing reality. Anchored to three known names.
|
|
||||||
check "derive_port_base rig" "20310" "$(derive_port_base rig)"
|
check "derive_port_base rig" "20310" "$(derive_port_base rig)"
|
||||||
check "derive_port_base foo" "21690" "$(derive_port_base foo)"
|
check "derive_port_base foo" "21690" "$(derive_port_base foo)"
|
||||||
check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)"
|
check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)"
|
||||||
|
|
||||||
|
|
||||||
note "rig stays standalone"
|
note "rig stays standalone"
|
||||||
# rig sits inside a host project's tree but must be copyable straight out of it:
|
# rig must be copyable out of its host project: no references to the host.
|
||||||
# no imports, no paths, no assumption the host is there. This grep is the whole
|
# The pattern is assembled from fragments so this file does not match itself.
|
||||||
# test of that claim, and until now it lived only in prose and in whoever
|
# The host project's word for a backing service counts too: rig described its
|
||||||
# remembered to run it.
|
# 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')"
|
||||||
# The pattern is assembled from fragments so this file does not match ITSELF.
|
|
||||||
# Writing it literally would fail forever; excluding this file instead would put
|
|
||||||
# a blind spot in the one check that guards the boundary.
|
|
||||||
HOST_PAT="$(printf '%s' 'sole' 'print' '|\b' 'sp' 'r\b')"
|
|
||||||
check "no host-project references" "0" \
|
check "no host-project references" "0" \
|
||||||
"$(cd .. && grep -rIl -iE "$HOST_PAT" . --exclude-dir=def 2>/dev/null | wc -l)"
|
"$(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)"
|
||||||
|
|
||||||
|
|
||||||
note "the Tiltfile hardcodes nothing"
|
note "the Tiltfile hardcodes nothing"
|
||||||
# Every other Tiltfile on this machine writes its slug in five or six times by
|
# The Tiltfile asks ports.sh for its context; a literal kind-<name> would undo that.
|
||||||
# hand, so a copied project deploys into the original's cluster until someone
|
|
||||||
# edits all of them. rig's asks ports.sh. A literal kind-<name> here would mean
|
|
||||||
# that has been undone.
|
|
||||||
check "no literal kind-<name>" "0" "$(grep -cE "['\"]kind-[a-z0-9]" Tiltfile)"
|
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 "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 "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"
|
note "standalone kits are generated, current, and call only real verbs"
|
||||||
# The kits under standalone/<profile>/ are rig flattened into single files, one
|
# A kit left stale by a change to rig fails here, not on another machine.
|
||||||
# per profile. A kit left behind by a change to rig is exactly the drift they
|
|
||||||
# replaced — rigmini.sh once said 2 GB per node long after rig measured 800 MB —
|
|
||||||
# so a stale kit fails here rather than waiting to be noticed on another machine.
|
|
||||||
check "every kit matches what rig generates now" "yes" \
|
check "every kit matches what rig generates now" "yes" \
|
||||||
"$(bash standalone.sh check >/dev/null 2>&1 && echo yes || echo "no — run make standalone")"
|
"$(bash standalone.sh check >/dev/null 2>&1 && echo yes || echo "no — run make standalone")"
|
||||||
|
|
||||||
# Each kit's Makefile exists so nothing wrapping these scripts has to GUESS how to
|
# Every kit Makefile target must call a verb its script's own dispatch accepts.
|
||||||
# call them. A generated Makefile once did guess: `rigmini.sh on`, not a verb,
|
|
||||||
# and a bare `rigdeps.sh` for "check and report", which installs. So every
|
|
||||||
# target's default verb must be one its script's own dispatch accepts — read
|
|
||||||
# from that dispatch, not from a list here that could drift from it.
|
|
||||||
verbs_of() {
|
verbs_of() {
|
||||||
sed -n '/^case "\$cmd" in/,/^esac/p' "$1" | grep -oE '^ [a-z]+\)' | tr -d ' )'
|
sed -n '/^case "\$cmd" in/,/^esac/p' "$1" | grep -oE '^ [a-z]+\)' | tr -d ' )'
|
||||||
}
|
}
|
||||||
@@ -200,20 +263,129 @@ for mk in ../standalone/*/Makefile; do
|
|||||||
done
|
done
|
||||||
check "there is a kit for every profile" "$(config_profiles | wc -l)" "$kits"
|
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
|
||||||
|
cat >> "$SX/rig/ctrl/.env" <<'EOF'
|
||||||
|
REGISTRY_USER=selftest-sentinel-user
|
||||||
|
REGISTRY_PASSWORD=selftest-sentinel-password
|
||||||
|
MANIFESTS_DIR=../selftest-sentinel-choice/overlays/dev
|
||||||
|
EOF
|
||||||
|
( cd "$SX/rig/ctrl" && bash standalone.sh export "$SX/out" >/dev/null 2>&1 )
|
||||||
|
count_in() { grep -rcF -- "$1" "$2" 2>/dev/null | awk -F: '{s+=$2} END{print s+0}'; }
|
||||||
|
check "export: carries this machine's choices" "yes" \
|
||||||
|
"$([ "$(count_in selftest-sentinel-choice "$SX/out")" -gt 0 ] && echo yes || echo no)"
|
||||||
|
check "export: carries no credential" "0" \
|
||||||
|
"$(( $(count_in selftest-sentinel-user "$SX/out") + $(count_in selftest-sentinel-password "$SX/out") ))"
|
||||||
|
check "per-profile kits: carry neither, whatever this machine has" "0" \
|
||||||
|
"$( (cd "$SX/rig/ctrl" && source ./lib/config.sh && for p in $(config_profiles); do config_snapshot "$p"; done) \
|
||||||
|
| grep -cE 'selftest-sentinel-(choice|user|password)')"
|
||||||
|
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/ | sed 's/\.sh$//' | sort | xargs)"
|
||||||
|
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 "optional — needs tilt and this rig's cluster"
|
note "the installer detects what each kind of machine needs"
|
||||||
# Parsing the Tiltfile for real is the only way to know it still evaluates, but
|
# Host fixtures (tests/hosts/): a stand-in root per machine, detect run against it. The
|
||||||
# Tilt snapshots a kubectl context before parsing, so it cannot run without a
|
# expensive machines — the Workspace, a WSL install — are exactly the ones you cannot
|
||||||
# cluster. Skipped rather than failed when there is none, the same way docgen
|
# rebuild to test on, so their shapes are replayed here instead.
|
||||||
# skips its graphgen section.
|
out="$(bash ./hosttest.sh 2>&1)"
|
||||||
if ! command -v tilt >/dev/null; then
|
check "every host fixture detects as expected" "$(ls -d ../tests/hosts/*/ | wc -l) host fixture(s) as expected, 0 not" \
|
||||||
printf ' skip tilt is not installed\n'
|
"$(tail -1 <<< "$out")"
|
||||||
elif ! kubectl config get-contexts -o name 2>/dev/null | grep -qx "$F_CTX"; then
|
# A snapshot is carried off a machine that may be someone else's: it must hold only the
|
||||||
printf " skip no %s context — run 'make cluster up' to include this\n" "$F_CTX"
|
# 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"
|
||||||
|
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"
|
||||||
else
|
else
|
||||||
out="$(tilt alpha tiltfile-result --context "$F_CTX" 2>&1)"
|
printf ' skip python3 is not installed\n'
|
||||||
check "Tiltfile evaluates" "yes" \
|
|
||||||
"$(printf '%s' "$out" | grep -q '"Manifests"' && echo yes || echo "no: $(printf '%s' "$out" | tail -1)")"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Prepare a machine to run rig, and say plainly what worked, what was already
|
|
||||||
# done, and what is left for a human.
|
|
||||||
#
|
|
||||||
# This is the grouped entry point: `make setup`. Every step is idempotent and
|
|
||||||
# independently checked, so running it twice is safe and running it on a
|
|
||||||
# half-configured machine finishes the job rather than starting over.
|
|
||||||
#
|
|
||||||
# It deliberately does NOT abort on the first failure. A setup script that dies
|
|
||||||
# at step 2 hides the fact that steps 4 and 5 were also going to fail — and on
|
|
||||||
# an unfamiliar machine, the full picture is the whole point. Failures are
|
|
||||||
# collected and reported together, and the exit code reflects the worst outcome.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# setup.sh # host checks + the dev toolchain
|
|
||||||
# setup.sh core # kubectl and jq only — no cluster tooling
|
|
||||||
# setup.sh --cluster # ...and bring the cluster up
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ./lib/config.sh
|
|
||||||
load_config
|
|
||||||
|
|
||||||
WITH_CLUSTER=0
|
|
||||||
# Cluster tooling is not wanted everywhere: a managed or corporate-issued
|
|
||||||
# machine may legitimately want kubectl and nothing that builds clusters.
|
|
||||||
TIER=dev
|
|
||||||
for a in "$@"; do
|
|
||||||
case "$a" in
|
|
||||||
core|dev) TIER="$a" ;;
|
|
||||||
--cluster) WITH_CLUSTER=1 ;;
|
|
||||||
*) echo "unknown option: $a" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$TIER" = "core" ] && [ "$WITH_CLUSTER" -eq 1 ]; then
|
|
||||||
echo "core tier installs no cluster tooling, so --cluster cannot work" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── step framework ─────────────────────────────────────────────────────────
|
|
||||||
# Statuses are deliberately distinct: "already" and "done" both mean success but
|
|
||||||
# tell you very different things about the machine you are on.
|
|
||||||
STEP_NAMES=()
|
|
||||||
STEP_STATUS=()
|
|
||||||
STEP_NOTE=()
|
|
||||||
WORST=0
|
|
||||||
|
|
||||||
record() {
|
|
||||||
STEP_NAMES+=("$1"); STEP_STATUS+=("$2"); STEP_NOTE+=("${3:-}")
|
|
||||||
# Only a genuine failure is a non-zero exit. "manual" means the machine is
|
|
||||||
# fine and you have something to do — reporting that as an error makes the
|
|
||||||
# whole run look broken and trains people to ignore the output.
|
|
||||||
[ "$2" = "fail" ] && WORST=1 || true
|
|
||||||
local mark
|
|
||||||
case "$2" in
|
|
||||||
already) mark=" ok " ;;
|
|
||||||
done) mark=" done " ;;
|
|
||||||
skip) mark=" skip " ;;
|
|
||||||
manual) mark="MANUAL" ;;
|
|
||||||
fail) mark=" FAIL " ;;
|
|
||||||
esac
|
|
||||||
printf "[%s] %-22s %s\n" "$mark" "$1" "${3:-}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── steps ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
step_host() {
|
|
||||||
local out
|
|
||||||
if ! out=$(bash ./deps.sh detect 2>&1); then
|
|
||||||
record host fail "detection failed"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
# Anything flagged with '!' needs a human; surface the count here
|
|
||||||
# and the detail below rather than burying it.
|
|
||||||
local warns; warns=$(echo "$out" | grep -c '^\s*!' || true)
|
|
||||||
HOST_DETAIL="$out"
|
|
||||||
if [ "$warns" -gt 0 ]; then
|
|
||||||
record host manual "$warns item(s) need attention — see below"
|
|
||||||
else
|
|
||||||
record host already "no problems detected"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
step_toolchain() {
|
|
||||||
local want="kubectl jq"
|
|
||||||
[ "$TIER" = "dev" ] && want="$want kind tilt"
|
|
||||||
|
|
||||||
local missing=""
|
|
||||||
for b in $want; do
|
|
||||||
command -v "$b" >/dev/null 2>&1 || missing="$missing $b"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$missing" ]; then
|
|
||||||
record toolchain already "$TIER: $want"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if bash ./deps.sh install "$TIER" >/tmp/rig-deps.$$ 2>&1; then
|
|
||||||
local still=""
|
|
||||||
for b in $want; do
|
|
||||||
[ -x "${OUT_BIN:-$HOME/.local/bin}/$b" ] || still="$still $b"
|
|
||||||
done
|
|
||||||
if [ -n "$still" ]; then
|
|
||||||
record toolchain fail "still missing:$still (see /tmp/rig-deps.$$)"
|
|
||||||
else
|
|
||||||
record toolchain done "$TIER, installed:$missing"
|
|
||||||
rm -f "/tmp/rig-deps.$$"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
record toolchain fail "install failed — see /tmp/rig-deps.$$"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
step_path() {
|
|
||||||
local bin="${OUT_BIN:-$HOME/.local/bin}"
|
|
||||||
case ":$PATH:" in
|
|
||||||
*":$bin:"*) ;;
|
|
||||||
*) record path manual "add to ~/.bashrc: export PATH=\"$bin:\$PATH\""; return ;;
|
|
||||||
esac
|
|
||||||
if grep -qs "$bin" "$HOME/.bashrc" "$HOME/.profile" 2>/dev/null; then
|
|
||||||
record path already "$bin on PATH and persisted"
|
|
||||||
else
|
|
||||||
record path manual "on PATH now, but not persisted in ~/.bashrc"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
step_docker() {
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
record docker fail "no docker cli — this is the one prerequisite rig cannot install"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if docker info >/dev/null 2>&1; then
|
|
||||||
record docker already "$(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
|
||||||
else
|
|
||||||
record docker fail "daemon unreachable (in the docker group? logged out and back in?)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
step_ports() {
|
|
||||||
local busy=""
|
|
||||||
for entry in "HTTP:$HTTP_PORT" "HTTPS:$HTTPS_PORT" "TILT:$TILT_PORT" "REGISTRY:$REGISTRY_PORT"; do
|
|
||||||
local p="${entry#*:}"
|
|
||||||
if command -v ss >/dev/null 2>&1 && ss -ltn "sport = :$p" 2>/dev/null | grep -q LISTEN; then
|
|
||||||
busy="$busy ${entry%%:*}($p)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -n "$busy" ]; then
|
|
||||||
record ports fail "in use:$busy — override in ctrl/.env or rename the directory"
|
|
||||||
else
|
|
||||||
record ports already "$HTTP_PORT-$REGISTRY_PORT free"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
step_cluster() {
|
|
||||||
if [ "$TIER" = "core" ]; then
|
|
||||||
record cluster skip "core tier — no cluster tooling on this machine"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$WITH_CLUSTER" -ne 1 ]; then
|
|
||||||
record cluster skip "not requested (--cluster)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then
|
|
||||||
record cluster already "'$CLUSTER' exists"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if bash ./cluster.sh up >/tmp/rig-cluster.$$ 2>&1; then
|
|
||||||
record cluster done "'$CLUSTER' created"
|
|
||||||
rm -f "/tmp/rig-cluster.$$"
|
|
||||||
else
|
|
||||||
record cluster fail "see /tmp/rig-cluster.$$"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── run ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
echo "setting up '$CLUSTER'"
|
|
||||||
echo
|
|
||||||
HOST_DETAIL=""
|
|
||||||
step_host
|
|
||||||
step_toolchain
|
|
||||||
step_path
|
|
||||||
step_docker
|
|
||||||
step_ports
|
|
||||||
step_cluster
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [ -n "$HOST_DETAIL" ]; then
|
|
||||||
echo "host detail"
|
|
||||||
echo "$HOST_DETAIL" | sed 's/^/ /'
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Repeat only what still needs action, so the tail of the output is a to-do list
|
|
||||||
# rather than a transcript.
|
|
||||||
outstanding=0
|
|
||||||
for i in "${!STEP_NAMES[@]}"; do
|
|
||||||
case "${STEP_STATUS[$i]}" in
|
|
||||||
fail|manual)
|
|
||||||
[ "$outstanding" -eq 0 ] && echo "outstanding:"
|
|
||||||
outstanding=1
|
|
||||||
printf " %-8s %-16s %s\n" "${STEP_STATUS[$i]}" "${STEP_NAMES[$i]}" "${STEP_NOTE[$i]}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$outstanding" -eq 0 ]; then
|
|
||||||
echo "ready. next: make cluster up && make docs"
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo "(nothing was aborted — every step ran so the list above is complete)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit "$WORST"
|
|
||||||
@@ -1,43 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Generate the standalone kits: single-file versions of rig's own tools, one
|
# Generate standalone kits: rig's tools flattened into single files, one folder per profile.
|
||||||
# folder per profile, for machines the full rig is not going to.
|
# Usage: standalone.sh write|check generate (or diff) standalone/<profile>/
|
||||||
#
|
# standalone.sh export DIR one kit for this machine's config, no credentials, outside the repo
|
||||||
# A kit is a pure function of rig as it is right now. It gains nothing rig lacks
|
# Notes: docs/notes/standalone.md
|
||||||
# and loses nothing rig has — improve rig, regenerate, and every kit follows.
|
|
||||||
# Nothing in standalone/<profile>/ is ever edited by hand.
|
|
||||||
#
|
|
||||||
# What this file does NOT know, on purpose: which tools rig has, what they are
|
|
||||||
# called, how its libraries are split, where configuration lives or what it
|
|
||||||
# contains. Rig will change shape — scripts get split, renamed and grow new
|
|
||||||
# libraries — and a generator that encoded today's layout would quietly produce
|
|
||||||
# a wrong kit the first time it did. So this works from a contract a script opts
|
|
||||||
# into, and from nothing else:
|
|
||||||
#
|
|
||||||
# 1. A marker comment, alone on a line near the top, declares an entry point:
|
|
||||||
# (hash) rig:standalone <kit-name> <default-verb>
|
|
||||||
# The default verb must only REPORT: it is run as a smoke test.
|
|
||||||
# 2. Every `source` an entry point makes names a .sh file by a path that
|
|
||||||
# resolves relative to the entry point. Libraries may source further
|
|
||||||
# libraries however they like — bash follows those itself.
|
|
||||||
# 3. Configuration enters through `load_config`, and the libraries provide
|
|
||||||
# `config_profiles` and `config_freeze <profile>` — the latter prints a
|
|
||||||
# replacement load_config with that profile resolved. How config is layered,
|
|
||||||
# stored, derived or frozen is rig's business; this only asks, and embeds
|
|
||||||
# the answer without interpreting it.
|
|
||||||
#
|
|
||||||
# Bash does the resolving, not a parser here. Libraries are sourced in a clean
|
|
||||||
# shell and read back with `declare -f` and `declare -p`, so any structure bash
|
|
||||||
# can load, this can flatten.
|
|
||||||
#
|
|
||||||
# And every kit is PROVEN to stand alone before it is written: no `source` left,
|
|
||||||
# no path into rig's tree in its code, `bash -n` clean, and its default verb run
|
|
||||||
# in an empty directory with nothing from rig present. A shape this has never
|
|
||||||
# seen either passes that, or generation stops and names the kit, the file, the
|
|
||||||
# line and what is wrong. It never writes a kit that only looks finished.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# standalone.sh write generate every kit into standalone/<profile>/
|
|
||||||
# standalone.sh check generate into a scratch dir and fail if any kit differs
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
@@ -47,6 +12,11 @@ OUT="$ROOT/standalone"
|
|||||||
SELF_REL="ctrl/${0##*/}"
|
SELF_REL="ctrl/${0##*/}"
|
||||||
|
|
||||||
GENERATED_TAG="GENERATED by make standalone — do not edit"
|
GENERATED_TAG="GENERATED by make standalone — do not edit"
|
||||||
|
|
||||||
|
# The contract's own functions: questions rig answers FOR this generator. They
|
||||||
|
# are never carried into a kit — load_config is replaced by the frozen one, and
|
||||||
|
# the rest mean nothing without rig's tree. The only names this file knows.
|
||||||
|
CONTRACT_FUNCS="load_config config_profiles config_snapshot config_freeze config_current_profile config_left_out"
|
||||||
FROZEN_OPEN="# ── configuration, frozen"
|
FROZEN_OPEN="# ── configuration, frozen"
|
||||||
FROZEN_CLOSE="# ── end of frozen configuration"
|
FROZEN_CLOSE="# ── end of frozen configuration"
|
||||||
|
|
||||||
@@ -54,7 +24,7 @@ refuse() { echo >&2; echo "standalone: refusing — $*" >&2; exit 1; }
|
|||||||
|
|
||||||
# A clean bash with nothing from the caller's shell in it. What the kit carries
|
# A clean bash with nothing from the caller's shell in it. What the kit carries
|
||||||
# must not depend on who ran the generator or what they had exported.
|
# must not depend on who ran the generator or what they had exported.
|
||||||
clean_bash() { env -i PATH="$PATH" HOME="$HOME" bash --noprofile --norc "$@"; }
|
clean_bash() { env -i PATH="$PATH" HOME="$HOME" CONTRACT_FUNCS="$CONTRACT_FUNCS" bash --noprofile --norc "$@"; }
|
||||||
|
|
||||||
# ── 1. entry points ────────────────────────────────────────────────────────
|
# ── 1. entry points ────────────────────────────────────────────────────────
|
||||||
entries() {
|
entries() {
|
||||||
@@ -104,7 +74,7 @@ libs_into() {
|
|||||||
lib_defs() { # entry lib... -> declare -p globals, then declare -f functions
|
lib_defs() { # entry lib... -> declare -p globals, then declare -f functions
|
||||||
local entry="$1"; shift
|
local entry="$1"; shift
|
||||||
( cd "$(dirname "$entry")" && clean_bash -c '
|
( cd "$(dirname "$entry")" && clean_bash -c '
|
||||||
skip_var() { case "$1" in BASH*|FUNCNAME|PIPESTATUS|LINENO|RANDOM|SRANDOM|SECONDS|EPOCH*|HISTCMD|COLUMNS|LINES|PWD|OLDPWD|_|SHLVL|OPTIND|OPTERR|IFS|PS4|PATH|HOME|v|f|l|before_v|before_f) return 0 ;; esac; return 1; }
|
skip_var() { case "$1" in CONTRACT_FUNCS|BASH*|FUNCNAME|PIPESTATUS|LINENO|RANDOM|SRANDOM|SECONDS|EPOCH*|HISTCMD|COLUMNS|LINES|PWD|OLDPWD|_|SHLVL|OPTIND|OPTERR|IFS|PS4|PATH|HOME|v|f|l|before_v|before_f) return 0 ;; esac; return 1; }
|
||||||
before_v=" $(compgen -v | tr "\n" " ") "
|
before_v=" $(compgen -v | tr "\n" " ") "
|
||||||
before_f=" $(compgen -A function | tr "\n" " ") "
|
before_f=" $(compgen -A function | tr "\n" " ") "
|
||||||
for l in "$@"; do source "$l" || { echo "__FAIL__ sourcing $l" ; exit 1; }; done
|
for l in "$@"; do source "$l" || { echo "__FAIL__ sourcing $l" ; exit 1; }; done
|
||||||
@@ -115,7 +85,7 @@ lib_defs() { # entry lib... -> declare -p globals, then declare -f functions
|
|||||||
done
|
done
|
||||||
for f in $(compgen -A function); do
|
for f in $(compgen -A function); do
|
||||||
case "$before_f" in *" $f "*) continue ;; esac
|
case "$before_f" in *" $f "*) continue ;; esac
|
||||||
case "$f" in skip_var|load_config|config_profiles|config_snapshot|config_freeze) continue ;; esac
|
case " skip_var $CONTRACT_FUNCS " in *" $f "*) continue ;; esac
|
||||||
declare -f "$f"
|
declare -f "$f"
|
||||||
done
|
done
|
||||||
' _ "$@" ) || refuse "$entry: its libraries could not be sourced cleanly"
|
' _ "$@" ) || refuse "$entry: its libraries could not be sourced cleanly"
|
||||||
@@ -146,7 +116,7 @@ assemble() { # entry profile out-file lib...
|
|||||||
echo '#!/usr/bin/env bash'
|
echo '#!/usr/bin/env bash'
|
||||||
echo "# $GENERATED_TAG"
|
echo "# $GENERATED_TAG"
|
||||||
echo "#"
|
echo "#"
|
||||||
echo "# $(basename "$dest") for profile '$profile', flattened from:"
|
echo "# $(basename "$dest") for ${KIT_LABEL:-profile '$profile'}, flattened from:"
|
||||||
echo "# ctrl/$entry"
|
echo "# ctrl/$entry"
|
||||||
local l; for l in ${libs[@]+"${libs[@]}"}; do echo "# ctrl/$l"; done
|
local l; for l in ${libs[@]+"${libs[@]}"}; do echo "# ctrl/$l"; done
|
||||||
echo "# Edit those and run \`make standalone\`. Changes made here are lost, and"
|
echo "# Edit those and run \`make standalone\`. Changes made here are lost, and"
|
||||||
@@ -161,9 +131,9 @@ assemble() { # entry profile out-file lib...
|
|||||||
|
|
||||||
if [ "$calls_config" = yes ]; then
|
if [ "$calls_config" = yes ]; then
|
||||||
local frozen
|
local frozen
|
||||||
frozen=$(ask "$entry" ${libs[@]+"${libs[@]}"} -- config_freeze "$profile") \
|
frozen=$(ask "$entry" ${libs[@]+"${libs[@]}"} -- config_freeze "${FREEZE_ARG:-$profile}") \
|
||||||
|| refuse "ctrl/$entry calls load_config, but its libraries do not answer config_freeze for '$profile'"
|
|| refuse "ctrl/$entry calls load_config, but its libraries do not answer config_freeze ${FREEZE_ARG:-$profile}"
|
||||||
echo "$FROZEN_OPEN for profile '$profile' ──"
|
echo "$FROZEN_OPEN for ${KIT_LABEL:-profile '$profile'} ──"
|
||||||
printf '%s\n' "$frozen"
|
printf '%s\n' "$frozen"
|
||||||
echo "$FROZEN_CLOSE ──"
|
echo "$FROZEN_CLOSE ──"
|
||||||
echo
|
echo
|
||||||
@@ -337,5 +307,44 @@ case "$cmd" in
|
|||||||
[ "$stale" -eq 0 ] || { echo "run: make standalone"; exit 1; }
|
[ "$stale" -eq 0 ] || { echo "run: make standalone"; exit 1; }
|
||||||
echo "every kit is current"
|
echo "every kit is current"
|
||||||
;;
|
;;
|
||||||
*) echo "usage: $SELF_REL [write|check]" >&2; exit 1 ;;
|
export)
|
||||||
|
dest="${1:-}"
|
||||||
|
[ -n "$dest" ] || refuse "export needs a directory, outside the repo: make standalone export ~/rig-kit"
|
||||||
|
dest=$(realpath -m "$dest")
|
||||||
|
top=$(git -C "$ROOT" rev-parse --show-toplevel 2>/dev/null || echo "$ROOT")
|
||||||
|
case "$dest/" in
|
||||||
|
"$top"/*) refuse "an export reflects this machine, so it does not go inside the repository — $dest is under $top. The committed per-profile kits are what standalone/ is for." ;;
|
||||||
|
esac
|
||||||
|
if [ -d "$dest" ] && [ -n "$(ls -A "$dest" 2>/dev/null)" ] && ! is_generated_dir "$dest"; then
|
||||||
|
refuse "$dest already holds something that is not a previous export — pick an empty directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t all_entries < <(entries)
|
||||||
|
[ ${#all_entries[@]} -gt 0 ] || refuse "no script under ctrl/ carries a '# rig:standalone <kit> <verb>' marker"
|
||||||
|
libs_into "${all_entries[0]}"
|
||||||
|
profile=$(ask "${all_entries[0]}" ${libs[@]+"${libs[@]}"} -- config_current_profile) \
|
||||||
|
|| refuse "this machine's configuration does not resolve — run make check"
|
||||||
|
left=$(ask "${all_entries[0]}" ${libs[@]+"${libs[@]}"} -- config_left_out | tr '\n' ' ')
|
||||||
|
|
||||||
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||||
|
echo "exporting the configuration this machine runs (profile '$profile')"
|
||||||
|
FREEZE_ARG=--current
|
||||||
|
KIT_LABEL="the configuration exported from $(hostname -s 2>/dev/null || echo this machine) (profile '$profile', local choices included, credentials not)"
|
||||||
|
for e in "${all_entries[@]}"; do
|
||||||
|
read -r kit verb <<< "$(marker_of "$e")"
|
||||||
|
libs_into "$e"
|
||||||
|
assemble "$e" "$profile" "$tmp/$kit.sh" ${libs[@]+"${libs[@]}"}
|
||||||
|
done
|
||||||
|
makefile "$tmp" "${all_entries[@]}"
|
||||||
|
verify_kit "$tmp" "export" "${all_entries[@]}"
|
||||||
|
|
||||||
|
rm -rf "$dest"; mkdir -p "$(dirname "$dest")"; cp -r "$tmp" "$dest"
|
||||||
|
echo " wrote $dest: $(cd "$dest" && ls | tr '\n' ' ')— verified to stand alone"
|
||||||
|
if [ -n "${left// /}" ]; then
|
||||||
|
echo
|
||||||
|
echo " NOT carried — this machine's own, set them on the target if it needs them:"
|
||||||
|
for k in $left; do echo " $k"; done
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*) echo "usage: $SELF_REL [write|check|export DIR]" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
# Pinned toolchain — the single manifest ctrl/deps.sh installs from.
|
# Pinned toolchain (linux/amd64, upstream SHA256) — the manifest ctrl/deps.sh installs from.
|
||||||
# Every entry is a single binary; none of them needs an apt repo.
|
# To bump, take the checksum from the release's own list, e.g.
|
||||||
# kubectl fully static
|
# curl -sSL https://github.com/<org>/<repo>/releases/download/<tag>/checksums.txt | grep linux.x86_64
|
||||||
# kind libc only
|
# Notes: docs/notes/versions.md
|
||||||
# tilt libc + libstdc++ + libgcc (present in base Debian)
|
|
||||||
# jq upstream static build (Debian's is linked against libjq/libonig)
|
|
||||||
#
|
|
||||||
# Checksums are the upstream-published SHA256 of the linux/amd64 artifact.
|
|
||||||
#
|
|
||||||
# To bump: change the version, then take the checksum from the release's own
|
|
||||||
# published list — never hand-edit or hand-copy one from a download you did.
|
|
||||||
# For anything hosted on GitHub releases that is:
|
|
||||||
#
|
|
||||||
# curl -sSL https://github.com/<org>/<repo>/releases/download/<tag>/checksums.txt \
|
|
||||||
# | grep linux.x86_64
|
|
||||||
#
|
|
||||||
# (kubectl publishes its own instead: <KUBECTL_URL>.sha256.)
|
|
||||||
#
|
|
||||||
# There was a `ctrl/versions-refresh.sh` named here that has never existed. If
|
|
||||||
# bumping stops being rare enough to do by hand, write it — but a comment
|
|
||||||
# pointing at a missing script is worse than no comment.
|
|
||||||
|
|
||||||
KIND_VERSION=v0.32.0
|
KIND_VERSION=v0.32.0
|
||||||
KIND_SHA256=50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54
|
KIND_SHA256=50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54
|
||||||
@@ -32,10 +15,7 @@ TILT_VERSION=0.37.6
|
|||||||
TILT_SHA256=e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6
|
TILT_SHA256=e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6
|
||||||
TILT_URL=https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.x86_64.tar.gz
|
TILT_URL=https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.x86_64.tar.gz
|
||||||
|
|
||||||
# ctlptl — creates a kind cluster WITH a local registry wired in, which is what
|
# ctlptl — kind cluster with a local registry wired in (keeps images off docker.io).
|
||||||
# keeps images off docker.io (an unqualified name means docker.io/library/<name>).
|
|
||||||
# Same publisher and same archive shape as tilt: binary at the archive root, so
|
|
||||||
# fetch_tgz handles it with strip=0 and no special case.
|
|
||||||
CTLPTL_VERSION=0.9.4
|
CTLPTL_VERSION=0.9.4
|
||||||
CTLPTL_SHA256=c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e
|
CTLPTL_SHA256=c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e
|
||||||
CTLPTL_URL=https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/ctlptl.${CTLPTL_VERSION}.linux.x86_64.tar.gz
|
CTLPTL_URL=https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/ctlptl.${CTLPTL_VERSION}.linux.x86_64.tar.gz
|
||||||
@@ -44,39 +24,34 @@ JQ_VERSION=1.8.2
|
|||||||
JQ_SHA256=b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f
|
JQ_SHA256=b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f
|
||||||
JQ_URL=https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64
|
JQ_URL=https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64
|
||||||
|
|
||||||
# docker compose — the distro docker packages ship the daemon and the CLI but
|
# docker compose — often missing from distro packages; deps.sh links it into
|
||||||
# frequently not this, so `docker compose up` fails with "unknown command" on an
|
# ~/.docker/cli-plugins.
|
||||||
# otherwise working Docker. It is a CLI plugin, found by NAME in a plugin
|
|
||||||
# directory, so a copy in the bin dir alone only gives you the retired
|
|
||||||
# `docker-compose` v1 spelling; deps.sh links it into ~/.docker/cli-plugins.
|
|
||||||
COMPOSE_VERSION=5.5.1
|
COMPOSE_VERSION=5.5.1
|
||||||
COMPOSE_SHA256=db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576
|
COMPOSE_SHA256=db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576
|
||||||
COMPOSE_URL=https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64
|
COMPOSE_URL=https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64
|
||||||
|
|
||||||
# Node images shipped with KIND_VERSION above, pinned by digest so a kind upgrade
|
# Node images for KIND_VERSION, pinned by digest; K8S_VERSION picks one (default: the newest).
|
||||||
# can never silently move the k8s version. Profiles select one via K8S_VERSION.
|
# Older entries are kept deliberately, for targets that run an older Kubernetes.
|
||||||
# Older entries are kept deliberately: running a trailing-edge control plane is
|
|
||||||
# part of simulating a legacy estate.
|
|
||||||
NODE_IMAGE_v1_36=kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5
|
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_35=kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95
|
||||||
NODE_IMAGE_v1_34=kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256
|
NODE_IMAGE_v1_34=kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256
|
||||||
NODE_IMAGE_v1_33=kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4
|
NODE_IMAGE_v1_33=kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4
|
||||||
|
|
||||||
# Images pulled at runtime (registry, mocks). Pinned by tag; the registry mode
|
# Images pulled at runtime. Pinned by tag; the registry mode decides where they are pulled FROM.
|
||||||
# decides where they are pulled FROM.
|
|
||||||
REGISTRY_IMAGE=registry:2
|
REGISTRY_IMAGE=registry:2
|
||||||
STUB_IMAGE=python:3.12-slim
|
|
||||||
|
|
||||||
# Addons, installed by ctrl/addons/<name>.sh when listed in a profile's ADDONS.
|
# rig's own addons (ctrl/addons/<name>.sh), installed when ADDONS names them.
|
||||||
CERT_MANAGER_VERSION=v1.21.1
|
CERT_MANAGER_VERSION=v1.21.1
|
||||||
METRICS_SERVER_VERSION=v0.9.0
|
METRICS_SERVER_VERSION=v0.9.0
|
||||||
METALLB_VERSION=v0.16.0
|
METALLB_VERSION=v0.16.0
|
||||||
|
|
||||||
# Cabinets — public services dropped in as-is, the upstream image unmodified.
|
# The manifests those addons apply, fetched and verified like the binaries
|
||||||
# The same declaration installs on compose or in the cluster, so a dependency is
|
# (`deps.sh manifest <NAME>`), so an offline machine needs no network for them.
|
||||||
# named once and works either way. Pinned by tag rather than
|
# Sums from the release's own asset digest; metallb publishes none, see versions.md.
|
||||||
# digest because they are ordinary upstream images with no supply chain claim
|
CERT_MANAGER_MANIFEST_URL=https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml
|
||||||
# attached — bump freely, and preload them for the offline profile.
|
CERT_MANAGER_MANIFEST_SHA256=5f6a499b8c1857d57f560f536e0dcc830914b45c420899fe7ad0692c8624e408
|
||||||
POSTGRES_IMAGE=postgres:16-alpine
|
METRICS_SERVER_MANIFEST_URL=https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml
|
||||||
REDIS_IMAGE=redis:7-alpine
|
METRICS_SERVER_MANIFEST_SHA256=1cec29a5267809306a2c6ec74a3e449abbb705b4a8beed0c8a1963910f72c79b
|
||||||
AIRFLOW_IMAGE=apache/airflow:2.10.4
|
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
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ digraph rig_environment {
|
|||||||
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0" shape=box]
|
node [fontname="Helvetica" fontsize=11 style=filled color="#1e2a4a" fontcolor="#e8eaf0" shape=box]
|
||||||
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
edge [fontname="Helvetica" fontsize=9 fontcolor="#8892a8" color="#4a5568"]
|
||||||
|
|
||||||
label="One environment per directory — copies never collide"
|
label="One environment per folder — copies never collide"
|
||||||
labelloc=t
|
labelloc=t
|
||||||
fontsize=16
|
fontsize=16
|
||||||
fontcolor="#0066ff"
|
fontcolor="#0066ff"
|
||||||
|
|
||||||
dirname [label="directory name\ne.g. acmebank/" fillcolor="#1f6feb" fontcolor="#ffffff" shape=octagon]
|
dirname [label="folder name\nthe overlay's, else rig's\ne.g. platform-v2/" fillcolor="#1f6feb" fontcolor="#ffffff" shape=octagon]
|
||||||
|
|
||||||
subgraph cluster_derived {
|
subgraph cluster_derived {
|
||||||
label="Everything below is derived from it"
|
label="Everything below is derived from it"
|
||||||
@@ -18,11 +18,11 @@ digraph rig_environment {
|
|||||||
color="#1e2a4a"
|
color="#1e2a4a"
|
||||||
fontcolor="#8892a8"
|
fontcolor="#8892a8"
|
||||||
|
|
||||||
cname [label="cluster name\nacmebank" fillcolor="#121829"]
|
cname [label="cluster name\nplatform-v2" fillcolor="#121829"]
|
||||||
ctx [label="kubectl context\nkind-acmebank" fillcolor="#121829"]
|
ctx [label="kubectl context\nkind-platform-v2" fillcolor="#121829"]
|
||||||
img [label="image tag\nacmebank-deps" fillcolor="#121829"]
|
img [label="image tag\nplatform-v2-deps" fillcolor="#121829"]
|
||||||
ports [label="port block\n21300–21309" fillcolor="#121829"]
|
ports [label="port block\n20430–20439" fillcolor="#121829"]
|
||||||
reg [label="registry container\nacmebank-registry" fillcolor="#121829"]
|
reg [label="registry container\nplatform-v2-registry" fillcolor="#121829"]
|
||||||
}
|
}
|
||||||
|
|
||||||
subgraph cluster_config {
|
subgraph cluster_config {
|
||||||
@@ -32,9 +32,10 @@ digraph rig_environment {
|
|||||||
fontcolor="#8892a8"
|
fontcolor="#8892a8"
|
||||||
|
|
||||||
versions [label="versions.env\npinned toolchain" fillcolor="#121829"]
|
versions [label="versions.env\npinned toolchain" fillcolor="#121829"]
|
||||||
profile [label="env.d/<profile>.env\nnodes · CNI · audit · addons" fillcolor="#121829"]
|
profile [label="env.d/<profile>.env\noptional: registry · mirror" fillcolor="#121829"]
|
||||||
|
overlay [label="<overlay>/rig.env\noptional: what runs" fillcolor="#121829"]
|
||||||
localenv [label="ctrl/.env\nsecrets, overrides" fillcolor="#121829"]
|
localenv [label="ctrl/.env\nsecrets, overrides" fillcolor="#121829"]
|
||||||
shell [label="the environment\nPROFILE=client make …" fillcolor="#1a3a1a" fontcolor="#00c853"]
|
shell [label="the environment\nOVERLAY=local/x make …" fillcolor="#1a3a1a" fontcolor="#00c853"]
|
||||||
}
|
}
|
||||||
|
|
||||||
dirname -> cname
|
dirname -> cname
|
||||||
@@ -44,7 +45,8 @@ digraph rig_environment {
|
|||||||
dirname -> reg
|
dirname -> reg
|
||||||
|
|
||||||
versions -> profile [label="overridden by"]
|
versions -> profile [label="overridden by"]
|
||||||
profile -> localenv [label="overridden by"]
|
profile -> overlay [label="overridden by"]
|
||||||
|
overlay -> localenv [label="overridden by"]
|
||||||
localenv -> shell [label="overridden by" color="#00c853"]
|
localenv -> shell [label="overridden by" color="#00c853"]
|
||||||
|
|
||||||
cluster [label="kind cluster" fillcolor="#1a1a3a" fontcolor="#0066ff" shape=octagon]
|
cluster [label="kind cluster" fillcolor="#1a1a3a" fontcolor="#0066ff" shape=octagon]
|
||||||
|
|||||||
@@ -4,166 +4,181 @@
|
|||||||
<!-- Generated by graphviz version 14.1.2 (0)
|
<!-- Generated by graphviz version 14.1.2 (0)
|
||||||
-->
|
-->
|
||||||
<!-- Title: rig_environment Pages: 1 -->
|
<!-- Title: rig_environment Pages: 1 -->
|
||||||
<svg width="962pt" height="481pt"
|
<svg width="981pt" height="585pt"
|
||||||
viewBox="0.00 0.00 962.00 481.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
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 476.83)">
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 580.74)">
|
||||||
<title>rig_environment</title>
|
<title>rig_environment</title>
|
||||||
<polygon fill="#0a0e17" stroke="none" points="-4,4 -4,-476.83 958,-476.83 958,4 -4,4"/>
|
<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="477" y="-453.63" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#0066ff">One environment per directory — copies never collide</text>
|
<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>
|
||||||
<g id="clust1" class="cluster">
|
<g id="clust1" class="cluster">
|
||||||
<title>cluster_derived</title>
|
<title>cluster_derived</title>
|
||||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="8,-65 8,-144.5 596,-144.5 596,-65 8,-65"/>
|
<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="302" y="-125.3" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Everything below is derived from it</text>
|
<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>
|
||||||
</g>
|
</g>
|
||||||
<g id="clust2" class="cluster">
|
<g id="clust2" class="cluster">
|
||||||
<title>cluster_config</title>
|
<title>cluster_config</title>
|
||||||
<polygon fill="#0a0e17" stroke="#1e2a4a" stroke-dasharray="5,2" points="604,-65 604,-437.33 946,-437.33 946,-65 604,-65"/>
|
<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="775" y="-418.13" font-family="Helvetica,sans-Serif" font-size="16.00" fill="#8892a8">Configuration — weakest first, later wins</text>
|
<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>
|
||||||
</g>
|
</g>
|
||||||
<!-- dirname -->
|
<!-- dirname -->
|
||||||
<g id="node1" class="node">
|
<g id="node1" class="node">
|
||||||
<title>dirname</title>
|
<title>dirname</title>
|
||||||
<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"/>
|
<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="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="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="293" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#ffffff">e.g. acmebank/</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- cname -->
|
<!-- cname -->
|
||||||
<g id="node2" class="node">
|
<g id="node2" class="node">
|
||||||
<title>cname</title>
|
<title>cname</title>
|
||||||
<polygon fill="#121829" stroke="#1e2a4a" points="104,-109 16,-109 16,-73 104,-73 104,-109"/>
|
<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="-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">acmebank</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- dirname->cname -->
|
<!-- dirname->cname -->
|
||||||
<g id="edge1" class="edge">
|
<g id="edge1" class="edge">
|
||||||
<title>dirname->cname</title>
|
<title>dirname->cname</title>
|
||||||
<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"/>
|
<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.07,-115.32 74.76,-110.26 78.88,-120.02 84.07,-115.32"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="84.65,-115.58 75.31,-110.58 79.49,-120.31 84.65,-115.58"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- ctx -->
|
<!-- ctx -->
|
||||||
<g id="node3" class="node">
|
<g id="node3" class="node">
|
||||||
<title>ctx</title>
|
<title>ctx</title>
|
||||||
<polygon fill="#121829" stroke="#1e2a4a" points="223.75,-109 122.25,-109 122.25,-73 223.75,-73 223.75,-109"/>
|
<polygon fill="#121829" stroke="#1e2a4a" points="228,-109 122,-109 122,-73 228,-73 228,-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="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="173" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">kind-acmebank</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- dirname->ctx -->
|
<!-- dirname->ctx -->
|
||||||
<g id="edge2" class="edge">
|
<g id="edge2" class="edge">
|
||||||
<title>dirname->ctx</title>
|
<title>dirname->ctx</title>
|
||||||
<path fill="none" stroke="#4a5568" d="M265.77,-181.32C245.77,-162.07 218.77,-136.06 199.05,-117.08"/>
|
<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="201.55,-114.63 191.91,-110.21 196.69,-119.67 201.55,-114.63"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="203.11,-115.13 193.56,-110.54 198.16,-120.08 203.11,-115.13"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- img -->
|
<!-- img -->
|
||||||
<g id="node4" class="node">
|
<g id="node4" class="node">
|
||||||
<title>img</title>
|
<title>img</title>
|
||||||
<polygon fill="#121829" stroke="#1e2a4a" points="344.12,-109 241.88,-109 241.88,-73 344.12,-73 344.12,-109"/>
|
<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="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="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="293" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-deps</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- dirname->img -->
|
<!-- dirname->img -->
|
||||||
<g id="edge3" class="edge">
|
<g id="edge3" class="edge">
|
||||||
<title>dirname->img</title>
|
<title>dirname->img</title>
|
||||||
<path fill="none" stroke="#4a5568" d="M293,-181.32C293,-163.19 293,-139.07 293,-120.47"/>
|
<path fill="none" stroke="#4a5568" d="M301,-181.46C301,-162.23 301,-138.96 301,-120.97"/>
|
||||||
<polygon fill="#4a5568" stroke="#4a5568" points="296.5,-120.67 293,-110.67 289.5,-120.67 296.5,-120.67"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="304.5,-120.98 301,-110.98 297.5,-120.98 304.5,-120.98"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- ports -->
|
<!-- ports -->
|
||||||
<g id="node5" class="node">
|
<g id="node5" class="node">
|
||||||
<title>ports</title>
|
<title>ports</title>
|
||||||
<polygon fill="#121829" stroke="#1e2a4a" points="451.38,-109 362.62,-109 362.62,-73 451.38,-73 451.38,-109"/>
|
<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="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="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="407" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">21300–21309</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- dirname->ports -->
|
<!-- dirname->ports -->
|
||||||
<g id="edge4" class="edge">
|
<g id="edge4" class="edge">
|
||||||
<title>dirname->ports</title>
|
<title>dirname->ports</title>
|
||||||
<path fill="none" stroke="#4a5568" d="M318.87,-181.32C337.78,-162.15 363.29,-136.3 382,-117.34"/>
|
<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="384.47,-119.81 389,-110.24 379.49,-114.9 384.47,-119.81"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="396.49,-120.29 400.73,-110.58 391.36,-115.52 396.49,-120.29"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- reg -->
|
<!-- reg -->
|
||||||
<g id="node6" class="node">
|
<g id="node6" class="node">
|
||||||
<title>reg</title>
|
<title>reg</title>
|
||||||
<polygon fill="#121829" stroke="#1e2a4a" points="588.38,-109 469.62,-109 469.62,-73 588.38,-73 588.38,-109"/>
|
<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="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="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="529" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">acmebank-registry</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- dirname->reg -->
|
<!-- dirname->reg -->
|
||||||
<g id="edge5" class="edge">
|
<g id="edge5" class="edge">
|
||||||
<title>dirname->reg</title>
|
<title>dirname->reg</title>
|
||||||
<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"/>
|
<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="502.85,-119.26 508.36,-110.21 498.41,-113.84 502.85,-119.26"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="515.41,-119.13 521.26,-110.3 511.18,-113.56 515.41,-119.13"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- cluster -->
|
<!-- cluster -->
|
||||||
<g id="node11" class="node">
|
<g id="node12" class="node">
|
||||||
<title>cluster</title>
|
<title>cluster</title>
|
||||||
<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"/>
|
<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="407" y="-14.3" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#0066ff">kind cluster</text>
|
<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>
|
||||||
</g>
|
</g>
|
||||||
<!-- cname->cluster -->
|
<!-- cname->cluster -->
|
||||||
<g id="edge9" class="edge">
|
<g id="edge10" class="edge">
|
||||||
<title>cname->cluster</title>
|
<title>cname->cluster</title>
|
||||||
<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"/>
|
<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="341.71,-27.35 351.35,-22.96 341.07,-20.38 341.71,-27.35"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="352.6,-26.85 362.28,-22.53 352.02,-19.87 352.6,-26.85"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- ports->cluster -->
|
<!-- ports->cluster -->
|
||||||
<g id="edge10" class="edge">
|
<g id="edge11" class="edge">
|
||||||
<title>ports->cluster</title>
|
<title>ports->cluster</title>
|
||||||
<path fill="none" stroke="#4a5568" d="M407,-72.81C407,-65.23 407,-56.1 407,-47.54"/>
|
<path fill="none" stroke="#4a5568" d="M418,-72.81C418,-65.23 418,-56.1 418,-47.54"/>
|
||||||
<polygon fill="#4a5568" stroke="#4a5568" points="410.5,-47.54 407,-37.54 403.5,-47.54 410.5,-47.54"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="421.5,-47.54 418,-37.54 414.5,-47.54 421.5,-47.54"/>
|
||||||
</g>
|
</g>
|
||||||
<!-- versions -->
|
<!-- versions -->
|
||||||
<g id="node7" class="node">
|
<g id="node7" class="node">
|
||||||
<title>versions</title>
|
<title>versions</title>
|
||||||
<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"/>
|
<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="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="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="697" y="-373.38" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">pinned toolchain</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- profile -->
|
<!-- profile -->
|
||||||
<g id="node8" class="node">
|
<g id="node8" class="node">
|
||||||
<title>profile</title>
|
<title>profile</title>
|
||||||
<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"/>
|
<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="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="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="697" y="-290.13" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">nodes · CNI · audit · addons</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- versions->profile -->
|
<!-- versions->profile -->
|
||||||
<g id="edge6" class="edge">
|
<g id="edge6" class="edge">
|
||||||
<title>versions->profile</title>
|
<title>versions->profile</title>
|
||||||
<path fill="none" stroke="#4a5568" d="M697,-365.59C697,-355.32 697,-342.03 697,-330.21"/>
|
<path fill="none" stroke="#4a5568" d="M711,-469.51C711,-459.24 711,-445.94 711,-434.13"/>
|
||||||
<polygon fill="#4a5568" stroke="#4a5568" points="700.5,-330.58 697,-320.58 693.5,-330.58 700.5,-330.58"/>
|
<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="728.5" y="-339.28" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
<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>
|
</g>
|
||||||
<!-- localenv -->
|
<!-- overlay -->
|
||||||
<g id="node9" class="node">
|
<g id="node9" class="node">
|
||||||
<title>localenv</title>
|
<title>overlay</title>
|
||||||
<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"/>
|
<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="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="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="695" y="-198.09" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#e8eaf0">secrets, overrides</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>
|
</g>
|
||||||
<!-- profile->localenv -->
|
<!-- profile->overlay -->
|
||||||
<g id="edge7" class="edge">
|
<g id="edge7" class="edge">
|
||||||
<title>profile->localenv</title>
|
<title>profile->overlay</title>
|
||||||
<path fill="none" stroke="#4a5568" d="M696.61,-282.22C696.34,-269.76 695.96,-252.69 695.64,-238.23"/>
|
<path fill="none" stroke="#4a5568" d="M711,-386.26C711,-375.99 711,-362.69 711,-350.88"/>
|
||||||
<polygon fill="#4a5568" stroke="#4a5568" points="699.14,-238.28 695.42,-228.36 692.14,-238.43 699.14,-238.28"/>
|
<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="727.68" y="-256.03" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
<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>
|
||||||
</g>
|
</g>
|
||||||
<!-- shell -->
|
<!-- localenv -->
|
||||||
<g id="node10" class="node">
|
<g id="node10" 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>
|
||||||
|
</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>
|
||||||
|
</g>
|
||||||
|
<!-- shell -->
|
||||||
|
<g id="node11" class="node">
|
||||||
<title>shell</title>
|
<title>shell</title>
|
||||||
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="763.38,-109 614.62,-109 614.62,-73 763.38,-73 763.38,-109"/>
|
<polygon fill="#1a3a1a" stroke="#1e2a4a" points="791,-109 631,-109 631,-73 791,-73 791,-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="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="689" y="-80.55" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#00c853">PROFILE=client make …</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>
|
||||||
</g>
|
</g>
|
||||||
<!-- localenv->shell -->
|
<!-- localenv->shell -->
|
||||||
<g id="edge8" class="edge">
|
<g id="edge9" class="edge">
|
||||||
<title>localenv->shell</title>
|
<title>localenv->shell</title>
|
||||||
<path fill="none" stroke="#00c853" d="M694.11,-190.49C693.16,-172.16 691.63,-142.72 690.49,-120.79"/>
|
<path fill="none" stroke="#00c853" d="M711,-200.63C711,-180.03 711,-145.27 711,-120.62"/>
|
||||||
<polygon fill="#00c853" stroke="#00c853" points="694,-120.81 689.99,-111.01 687.01,-121.18 694,-120.81"/>
|
<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="724.21" y="-155.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8892a8">overridden by</text>
|
<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>
|
||||||
</g>
|
</g>
|
||||||
<!-- shell->cluster -->
|
<!-- shell->cluster -->
|
||||||
<g id="edge11" class="edge">
|
<g id="edge12" class="edge">
|
||||||
<title>shell->cluster</title>
|
<title>shell->cluster</title>
|
||||||
<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"/>
|
<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="463.2,-28.18 452.67,-29.35 461.63,-35 463.2,-28.18"/>
|
<polygon fill="#4a5568" stroke="#4a5568" points="475.23,-27.87 464.71,-29.13 473.72,-34.7 475.23,-27.87"/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
@@ -240,6 +240,7 @@
|
|||||||
<a href="#steps">The steps</a>
|
<a href="#steps">The steps</a>
|
||||||
<a href="#install">Installation</a>
|
<a href="#install">Installation</a>
|
||||||
<a href="#environments">Environments</a>
|
<a href="#environments">Environments</a>
|
||||||
|
<a href="#overlays">Overlays</a>
|
||||||
<a href="#profiles">Profiles</a>
|
<a href="#profiles">Profiles</a>
|
||||||
<a href="#registry">Registry</a>
|
<a href="#registry">Registry</a>
|
||||||
<a href="#architecture">Architecture</a>
|
<a href="#architecture">Architecture</a>
|
||||||
@@ -267,7 +268,7 @@
|
|||||||
<pre><code><span class="c"># then, in the environment directory:</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 check <span class="c"># is this machine ready? reports, never fixes</span>
|
||||||
make deps <span class="c"># install the pinned toolchain</span>
|
make deps <span class="c"># install the pinned toolchain</span>
|
||||||
make cluster up <span class="c"># build the cluster for the active profile</span>
|
make cluster up <span class="c"># cluster + registry + addons; ports derive by themselves</span>
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<p>Read <code>make check</code> before <code>make deps</code>. It never changes
|
<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
|
anything — it prints what it found and, at the end, the steps it cannot perform
|
||||||
@@ -288,29 +289,24 @@ make cluster up <span class="c"># build the cluster for the active profile</spa
|
|||||||
one failure at a time.</p>
|
one failure at a time.</p>
|
||||||
<pre><code>make check</code></pre>
|
<pre><code>make check</code></pre>
|
||||||
|
|
||||||
<h3>2 · make setup</h3>
|
<h3>2 · make deps</h3>
|
||||||
<p>Does the preparation that can be automated: installs the pinned
|
<p>Installs the pinned toolchain — only what is missing — and tells you
|
||||||
toolchain if it is missing, checks PATH, Docker, and this environment's
|
if its directory is not on PATH yet. Running it twice is safe.</p>
|
||||||
ports. Every step is independently checked, so running it twice is safe and
|
<pre><code>make deps</code></pre>
|
||||||
running it half-configured finishes the job.</p>
|
|
||||||
<p>It <b>does not stop at the first failure</b>. A setup script that dies at
|
|
||||||
step two hides that steps four and five would also have failed, and on an
|
|
||||||
unfamiliar machine the complete list is the point. The tail of the output is
|
|
||||||
a to-do list of only what is outstanding.</p>
|
|
||||||
<pre><code>make setup <span class="c"># host + toolchain</span>
|
|
||||||
</code></pre>
|
|
||||||
|
|
||||||
<h3>3 · make cluster up</h3>
|
<h3>3 · make cluster up</h3>
|
||||||
<p>Builds the cluster for the active profile. It prints what the profile
|
<p>Builds the cluster, starts its registry and installs the profile's
|
||||||
locks in <i>before</i> spending the time, because the CNI and the audit
|
addons — there is nothing else to run first. It prints what the profile
|
||||||
policy are fixed at creation and cannot be changed afterwards.</p>
|
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
|
<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
|
attempt was interrupted before the CNI was installed, running it again
|
||||||
finishes the job rather than reporting "already exists" and leaving every
|
finishes the job rather than reporting "already exists" and leaving every
|
||||||
node permanently NotReady.</p>
|
node permanently NotReady.</p>
|
||||||
<pre><code>make cluster up <span class="c"># default profile</span>
|
<pre><code>make cluster up <span class="c"># built-in defaults — no profile needed</span>
|
||||||
make cluster up PROFILE=client <span class="c"># three nodes, audit on, cached registry</span>
|
make cluster up PROFILE=mirror <span class="c"># after copying env.d/mirror.env.example: cached registry</span>
|
||||||
make cluster reset <span class="c"># destroy and rebuild — the only way to change CNI or audit</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>
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
||||||
<h3>4 · make docs</h3>
|
<h3>4 · make docs</h3>
|
||||||
@@ -322,17 +318,17 @@ make cluster reset <span class="c"># destroy and rebuild — the on
|
|||||||
<h3>Checking on things</h3>
|
<h3>Checking on things</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<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 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 ports</dt><dd>This environment's port block, and whether each is derived or overridden.</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 registry</dt><dd>Which of the four registry modes is active, and where it points.</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>
|
</dl>
|
||||||
|
|
||||||
<h3>Running more than one</h3>
|
<h3>Running more than one</h3>
|
||||||
<p>Copy the directory, rename it, and repeat from step 2. Cluster name,
|
<p>Name another overlay, or copy the directory and rename it. Cluster name,
|
||||||
context, image tags and the port block all follow the directory name, so
|
context, image tags and the port block all follow the folder name — the
|
||||||
the second environment collides with nothing and neither one's teardown can
|
overlay's, or rig's — so the second environment collides with nothing and
|
||||||
reach the other.</p>
|
neither one's teardown can reach the other.</p>
|
||||||
<pre><code>cp -r rig ../platform-v2 && cd ../platform-v2
|
<pre><code>OVERLAY=local/platform-v2 make cluster up
|
||||||
make setup && make cluster up
|
cp -r rig ../platform-v3 && cd ../platform-v3 && make cluster up
|
||||||
</code></pre>
|
</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -370,65 +366,90 @@ make setup && make cluster up
|
|||||||
</table>
|
</table>
|
||||||
<pre><code>make deps core <span class="c"># kubectl and jq only — nothing that creates a cluster</span>
|
<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>
|
make deps <span class="c"># dev, the default</span>
|
||||||
make setup core <span class="c"># same distinction, via setup</span>
|
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<p>Testing <i>in situ</i> on a managed machine is still possible — install
|
<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
|
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 running setup.</p>
|
it should be a decision rather than a side effect of installing.</p>
|
||||||
<p>The documentation itself needs neither tier: <code>make docs</code>
|
<p>The documentation itself needs neither tier: <code>make docs</code>
|
||||||
wants only Docker.</p>
|
wants only Docker.</p>
|
||||||
|
|
||||||
<h3>Air-gapped</h3>
|
<h3>Air-gapped</h3>
|
||||||
<pre><code>make deps-image full <span class="c"># bakes every binary into the image</span>
|
<pre><code>make deps image full <span class="c"># bakes every binary into the image</span>
|
||||||
docker save …-deps:full | gzip > rig.tgz
|
docker save …-deps:full | gzip > rig.tgz
|
||||||
<span class="c"># carry that one file in, then:</span>
|
<span class="c"># carry that one file in, then:</span>
|
||||||
docker load < rig.tgz && make cluster up PROFILE=offline
|
docker load < rig.tgz && make cluster up PROFILE=offline <span class="c"># from env.d/offline.env.example</span>
|
||||||
</code></pre>
|
</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="environments">
|
<section class="section" id="environments">
|
||||||
<h2>Environments</h2>
|
<h2>Environments</h2>
|
||||||
<p class="lede">One directory is one environment. Copy it, rename it, run it.</p>
|
<p class="lede">One folder is one environment — an overlay's, or rig's own. Copy it, rename it, run it.</p>
|
||||||
<div class="graph-container">
|
<div class="graph-container">
|
||||||
<a href="viewer.html?src=graphs/02-environment.svg"><img src="graphs/02-environment.svg" alt="Environment derivation"></a>
|
<a href="viewer.html?src=graphs/02-environment.svg"><img src="graphs/02-environment.svg" alt="Environment derivation"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="prose">
|
<div class="prose">
|
||||||
<p>Running several versions of a system at once means several clusters on one
|
<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
|
machine, not several machines. Everything that could collide is derived from
|
||||||
the directory name:</p>
|
the folder name (the overlay's when one is named):</p>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>cluster + context</dt><dd><code>acmebank/</code> builds <code>acmebank</code> on <code>kind-acmebank</code>.</dd>
|
<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, in the 20000+ range — clear of 80, 443, 3000, 5432, 8000 and 8080.</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>
|
<dt>registry + images</dt><dd>Named after the environment, so two copies never share one.</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<p>Two copies therefore never collide, and neither one's
|
<p>Two copies therefore never collide, and neither one's
|
||||||
<code>make cluster down</code> can touch the other. <code>make ports</code>
|
<code>make cluster down</code> can touch the other. <code>make check</code>
|
||||||
shows the block; <code>make ports persist</code> freezes it into
|
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>
|
<code>ctrl/.env</code> if you want it fixed rather than derived.</p>
|
||||||
|
|
||||||
<h3>Configuration layers</h3>
|
<h3>Configuration layers</h3>
|
||||||
<p>Weakest first, later wins: pinned versions → the profile →
|
<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 environment. So
|
<code>ctrl/.env</code> → the environment. So
|
||||||
<code>make cluster up PROFILE=client</code> always beats every file.</p>
|
<code>make cluster up PROFILE=<name></code> always beats every file.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section" id="overlays">
|
||||||
|
<h2>Overlays</h2>
|
||||||
|
<p class="lede">What runs lives outside rig — rig reads it and never writes into it.</p>
|
||||||
|
<div class="prose">
|
||||||
|
<p>An overlay is one folder, kept outside rig's version control, holding a
|
||||||
|
use case. 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</td></tr>
|
||||||
|
<tr><td><code>Tiltfile</code></td><td>the workload's half of the dev loop, included by rig's</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section" id="profiles">
|
<section class="section" id="profiles">
|
||||||
<h2>Profiles</h2>
|
<h2>Profiles</h2>
|
||||||
<p class="lede">Cluster shape is declared, not baked in.</p>
|
<p class="lede">How this machine reaches the world — optional; rig needs none.</p>
|
||||||
<div class="prose">
|
<div class="prose">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>profile</th><th>nodes</th><th>audit</th><th>registry</th><th>for</th></tr>
|
<tr><th>example</th><th>registry</th><th>for</th></tr>
|
||||||
<tr><td><code>minimal</code></td><td>1</td><td>off</td><td>none</td><td>first boot; assumes nothing</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</code></td><td>3</td><td>on</td><td>mirror</td><td>the regulated shape</td></tr>
|
<tr><td><code>mirror.env.example</code></td><td>mirror</td><td>images through an internal registry</td></tr>
|
||||||
<tr><td><code>offline</code></td><td>1</td><td>on</td><td>local</td><td>air-gapped</td></tr>
|
<tr><td><code>offline.env.example</code></td><td>local</td><td>air-gapped</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="note"><p><b>The audit policy cannot be changed later.</b> It is an
|
<div class="note"><p><b>The kind config cannot be re-applied.</b> Edit
|
||||||
apiserver flag, fixed when the cluster is created. <code>cluster up</code>
|
<code>ctrl/k8s/kind-config.yaml.tpl</code> (or the overlay's own); it takes effect when the cluster is created. <code>cluster up</code> prints what it
|
||||||
prints what a profile locks in before spending the time, and
|
locks in before spending the time, and <code>make cluster reset</code> is
|
||||||
<code>make cluster reset</code> is the way out.</p></div>
|
the way out.</p></div>
|
||||||
|
|
||||||
<h3>LoadBalancer services</h3>
|
<h3>LoadBalancer services</h3>
|
||||||
<p>Real manifests use <code>type: LoadBalancer</code>, because a real
|
<p>Real manifests use <code>type: LoadBalancer</code>, because a real
|
||||||
|
|||||||
34
rig/docs/notes/Dockerfile.deps.md
Normal file
34
rig/docs/notes/Dockerfile.deps.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# ctrl/Dockerfile.deps
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
The toolchain installer image. It does NOT run the cluster — it installs a toolchain onto the host and gets out of the way.
|
||||||
|
|
||||||
|
This exists to kill a bootstrap paradox: a plain bash installer needs curl, jq and sha256sum to already be present, and a minimal Debian has none of them. It carries its own toolchain, so the only host prerequisite is Docker.
|
||||||
|
|
||||||
|
## Variants
|
||||||
|
|
||||||
|
Two variants from one file:
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
ca-certificates + curl: fetch and verify. graphviz + python3: render diagrams and validate the arch model, so the host never needs an apt package.
|
||||||
|
|
||||||
|
docker-cli, NOT docker.io: we only ever talk to the host's daemon through the mounted socket, and under `--no-install-recommends` the docker.io package ships docker-init without the actual `docker` binary.
|
||||||
|
|
||||||
|
## The installer is the standalone kit
|
||||||
|
|
||||||
|
The installer is the generated standalone kit, not deps.sh plus the files it reads. A kit is one file with its pins frozen in and is proven to run with nothing else from rig present — which is exactly what an image needs, and `make standalone` keeps it current. Pins are the same in every profile's kit.
|
||||||
45
rig/docs/notes/Dockerfile.example.md
Normal file
45
rig/docs/notes/Dockerfile.example.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# examples/starter/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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
The overlay's Tiltfile runs from the overlay's own folder (rig's ctrl/Tiltfile includes it), so both paths in `docker_build` are relative to the overlay:
|
||||||
|
|
||||||
|
```
|
||||||
|
context='.' the overlay folder (or a subfolder, e.g. 'repodir/api')
|
||||||
|
dockerfile='Dockerfile.api' relative to the overlay's Tiltfile too
|
||||||
|
```
|
||||||
|
|
||||||
|
Every COPY is resolved against the context, NOT against the Dockerfile's directory. With `context='.'` and the Dockerfile in a subfolder, a file sitting right beside it is still reached through that subfolder:
|
||||||
|
|
||||||
|
```
|
||||||
|
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf # correct, Dockerfile in docker/
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf # fails — no such file in the context
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing warns you. The build just cannot find a file that is visibly there.
|
||||||
|
|
||||||
|
Before overlays, rig's own ctrl/Tiltfile built with `context='..'` (the repository root) and the Dockerfile in ctrl/, which is the same trap one level up.
|
||||||
|
|
||||||
|
## Dependency layer
|
||||||
|
|
||||||
|
Dependencies first, in their own layer: they change far less often than the code, so a source edit does not reinstall them on every rebuild.
|
||||||
|
|
||||||
|
## live_update
|
||||||
|
|
||||||
|
The sync in the Tiltfile's `docker_build` must land where this image expects it:
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
58
rig/docs/notes/Makefile.md
Normal file
58
rig/docs/notes/Makefile.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Makefile
|
||||||
|
|
||||||
|
## Shape and config layers
|
||||||
|
|
||||||
|
Thin control Makefile: few targets, and the subcommand is an argument rather than a second target: `make cluster down`, not `make cluster-down`.
|
||||||
|
|
||||||
|
```
|
||||||
|
make check is this machine ready? (never changes anything)
|
||||||
|
make deps install the toolchain
|
||||||
|
make cluster up cluster + registry + addons (ports derive by themselves)
|
||||||
|
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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Asked once, of ctrl/ports.sh, which resolves it through lib/config.sh:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## CLUSTER / KCTX fallback
|
||||||
|
|
||||||
|
The fallback matters: ports.sh sources config.sh, and if a profile or .env is broken it exits non-zero. Losing the cluster name would send --context to the wrong place, so fall back to the folder rather than to empty.
|
||||||
|
|
||||||
|
## ARGS as .PHONY
|
||||||
|
|
||||||
|
Words after the target become the script's subcommand; each gets a no-op rule so make does not treat them as goals. They are also marked PHONY, because some of those words name real directories. `cfg`, `ctrl`, `docs`, `gen` and `init` all exist at this level, and make considers a target that is an existing directory already built, so `make build ctrl` ran the build and then printed "make: 'ctrl' is up to date". The empty rule is not enough on its own; only .PHONY stops make consulting the filesystem.
|
||||||
|
|
||||||
|
## tilt: --port guard
|
||||||
|
|
||||||
|
--port is only passed when TILT_PORT resolved. It normally does, since FACTS asks ports.sh, but ports.sh can fail on a broken profile, and without the guard tilt receives a bare `--port` with no value and fails on the flag rather than on anything real. Tilt's own default is 10350, which is the number every project on this machine is trying not to collide on, so falling back to it silently is worse than not passing the flag.
|
||||||
|
|
||||||
|
The Tiltfile asks ports.sh for the rest itself (cluster, registry and where the manifests are), so nothing needs passing here beyond what tilt's own flags require.
|
||||||
|
|
||||||
|
## Aliases (kind-up, tilt-up, ...)
|
||||||
|
|
||||||
|
Aliases, not a second implementation: each one calls the same script the canonical target does.
|
||||||
|
|
||||||
|
The header argues for `make cluster down` over `make cluster-down`, and that still holds *within* the Makefile. But rig is one repo among several on the same machine, and every other one answers to kind-up / tilt-up. Muscle memory spanning six projects beats internal tidiness in one, so both spellings work.
|
||||||
|
|
||||||
|
`cluster list` and `cluster free` have no hyphenated twin on purpose: they are rig's own, with nothing to be consistent with.
|
||||||
|
|
||||||
|
Nothing outside the Makefile reads these names: the script is `ctrl/cluster.sh` and it takes the verb. So rename them, delete the ones you never type, or add the spelling your own projects use. An alias is two lines, and adding one costs nothing but a line in .PHONY.
|
||||||
43
rig/docs/notes/Tiltfile.md
Normal file
43
rig/docs/notes/Tiltfile.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# ctrl/Tiltfile
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Splitting it is what lets rig be replaced as a whole (✖ S1 in STALE.md).
|
||||||
|
|
||||||
|
## Nothing hardcoded to this directory
|
||||||
|
|
||||||
|
Nothing in the Tiltfile is hardcoded to this directory, deliberately. Every other project here writes its slug into the Tiltfile five or six times by hand, so a copy of the project deploys into the original's cluster until someone remembers to edit all of them. A rig is meant to be copied and renamed, and an overlay moved, so it asks instead.
|
||||||
|
|
||||||
|
## Who we are, and on which ports
|
||||||
|
|
||||||
|
One question to rig, answered by ctrl/ports.sh, which resolves it through lib/config.sh — the same path every other rig script takes. That is the point: the cluster name is NOT the bare directory name (it is lowercased and reduced to a DNS label), and the ports honour anything pinned in ctrl/.env. Recomputing either of those here in Starlark is how two copies end up disagreeing about which cluster they are talking to.
|
||||||
|
|
||||||
|
The manifests and the overlay arrive as absolute paths, or `-` when there is none.
|
||||||
|
|
||||||
|
## Refuse to deploy into the wrong cluster
|
||||||
|
|
||||||
|
Tilt snapshots the kubectl context at startup, BEFORE parsing this file, so it cannot be switched from here — only refused. `make tilt` passes --context for you; the guard catches a bare `tilt up` after some other project moved the global context.
|
||||||
|
|
||||||
|
## Images go to this environment's own registry
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Nothing here assumes a namespace is named after the cluster (✖ S4).
|
||||||
|
|
||||||
|
## Handing over to the overlay
|
||||||
|
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```
|
||||||
|
RIG_CLUSTER RIG_CONTEXT RIG_HTTP_PORT RIG_HTTPS_PORT RIG_TILT_PORT RIG_REGISTRY RIG_OVERLAY_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
## Catalogue
|
||||||
|
|
||||||
|
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.
|
||||||
43
rig/docs/notes/addons.md
Normal file
43
rig/docs/notes/addons.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# ctrl/addons.sh and ctrl/addons/*.sh
|
||||||
|
|
||||||
|
## 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)).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
|
||||||
|
## cert-manager.sh
|
||||||
|
|
||||||
|
In a regulated estate almost everything is TLS, so the interesting question
|
||||||
|
during onboarding is "does this service present a cert my client trusts" — not
|
||||||
|
"can I reach a public ACME server". A local CA answers that offline, which is
|
||||||
|
also what makes the air-gapped profile usable.
|
||||||
|
|
||||||
|
## metallb.sh — why it matters
|
||||||
|
|
||||||
|
Real manifests use LoadBalancer, because a real cluster has one. On a bare kind
|
||||||
|
cluster those Services sit at `EXTERNAL-IP <pending>` forever with no error
|
||||||
|
anywhere — the deployment looks fine and simply is not reachable. Without MetalLB,
|
||||||
|
every such Service has to be edited to NodePort, which means the local manifests
|
||||||
|
stop matching the ones being modelled.
|
||||||
|
|
||||||
|
The address pool is derived from the kind Docker network at install time, not
|
||||||
|
hardcoded: Docker picks that subnet, it differs between machines, and a pool
|
||||||
|
outside it is silently unroutable.
|
||||||
|
|
||||||
|
## metallb.sh — waiting for the controller
|
||||||
|
|
||||||
|
`kubectl wait` on a selector errors out immediately when nothing matches yet, and
|
||||||
|
right after apply the ReplicaSet has not created the pod — so it loses a race it
|
||||||
|
looks like it should win. `rollout status` waits for the Deployment itself and
|
||||||
|
handles the not-yet-created case.
|
||||||
|
|
||||||
|
## metrics-server.sh
|
||||||
|
|
||||||
|
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.
|
||||||
50
rig/docs/notes/check.md
Normal file
50
rig/docs/notes/check.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# ctrl/check.sh
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Readiness check: is this machine ready to run rig?
|
||||||
|
|
||||||
|
It reports and instructs; it never silently fixes anything. Everything it finds is either already fine, or something a human has to decide on.
|
||||||
|
|
||||||
|
Runs ctrl/deps.sh host detection in a container when Docker is the only thing installed, or directly when the toolchain is already present. Then adds the checks that need this repo's config: profile sanity, CA trust, port clashes.
|
||||||
|
|
||||||
|
## memory
|
||||||
|
|
||||||
|
A profile on a box that is already full is the most common first failure, and it presents as pods stuck Pending rather than anything that says "memory". The check warns; it never blocks. Whether to try anyway is the user's call.
|
||||||
|
|
||||||
|
## mb_of
|
||||||
|
|
||||||
|
MEMINFO and OVERCOMMIT_FILE exist only so the tight and does-not-fit branches can be exercised against another machine's real numbers; in normal use they are the kernel's own files.
|
||||||
|
|
||||||
|
## NODE_MB
|
||||||
|
|
||||||
|
NODE_MB (what one node costs) comes from load_config (lib/config.sh), where its measurement is recorded. It lives there, not here, because the memory tool and every standalone kit need the same number: a copy of it is how rigmini.sh came to say 2 GB per node long after rig had measured 800 MB.
|
||||||
|
|
||||||
|
## container_mb
|
||||||
|
|
||||||
|
Every running container's working set in MB, tagged with the kind cluster it belongs to ('-' when it is not kind). docker stats reports usage minus page cache, which is what actually competes: cache is handed back under pressure. Counting only kind would hide the usual culprit on a managed workspace, where the memory is held by other containers entirely.
|
||||||
|
|
||||||
|
## ours_mb / still_mb
|
||||||
|
|
||||||
|
Once this environment's own cluster is running, its real footprint is already out of MemAvailable and the per-node estimate stops being relevant. Subtracting the measurement from the estimate would count the same memory twice, and a running cluster that happens to sit under 800 MB would still "need" the gap.
|
||||||
|
|
||||||
|
## ports: our own cluster
|
||||||
|
|
||||||
|
A port held by THIS environment's own cluster is not a clash; it is the thing working. Reporting it as a problem every time the cluster is up would train people to ignore this section, which is the opposite of the point.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
15
rig/docs/notes/cluster.md
Normal file
15
rig/docs/notes/cluster.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# ctrl/cluster.sh
|
||||||
|
|
||||||
|
## Why list and free live here
|
||||||
|
|
||||||
|
`list` and `free` live in `cluster.sh` rather than in a separate script because a
|
||||||
|
near-identical second name (cluster / clusters) is a trap — you reach for one and
|
||||||
|
get the other. One target, one file, unambiguous subcommands.
|
||||||
|
|
||||||
|
## Idempotent means convergent
|
||||||
|
|
||||||
|
"Idempotent" here means convergent, not "exits early if the cluster exists".
|
||||||
|
That distinction matters: an interrupted first run can leave a cluster created
|
||||||
|
but not finished, and returning early on the re-run would strand it there. The
|
||||||
|
create step is conditional; every step after it always runs, and each one is
|
||||||
|
individually idempotent.
|
||||||
110
rig/docs/notes/config.md
Normal file
110
rig/docs/notes/config.md
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# ctrl/lib/config.sh
|
||||||
|
|
||||||
|
## Purpose and precedence
|
||||||
|
|
||||||
|
The ecosystem convention is that scripts are standalone with no shared log library, and that still holds. This file is not a logging lib; it is the single definition of how the config layers compose, which every script has to agree on exactly. Precedence, weakest first:
|
||||||
|
|
||||||
|
```
|
||||||
|
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 machine-local values and secrets (gitignored)
|
||||||
|
the caller's env `make cluster up PROFILE=<name>` (always wins)
|
||||||
|
```
|
||||||
|
|
||||||
|
That last rule is why this is more than a few `source` lines: .env sets PROFILE, so without snapshotting it would silently override the PROFILE the user just typed on the command line.
|
||||||
|
|
||||||
|
Run from ctrl/.
|
||||||
|
|
||||||
|
## CONFIG_OVERRIDABLE
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## derive_port_base
|
||||||
|
|
||||||
|
Base of this environment's 10-port block. cksum is used rather than $RANDOM or bash hashing because it is POSIX and returns the same value on every machine, which is what makes the block reproducible instead of merely unique.
|
||||||
|
|
||||||
|
## load_config: RIG_PORTABLE
|
||||||
|
|
||||||
|
RIG_PORTABLE skips the machine-local layer. config_snapshot sets it, so a generated standalone kit never carries this machine's .env, which holds local values and, by its own description, secrets.
|
||||||
|
|
||||||
|
## load_config: profiles are optional
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## load_config: host ports
|
||||||
|
|
||||||
|
Host ports are a single shared namespace, so unlike the cluster name they cannot just follow the directory; they have to be spread out. Anything already set (ctrl/.env, a profile, the command line) wins; only the gaps are filled. See [ports.md](ports.md) for the reasoning.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
## load_config: NODE_MB
|
||||||
|
|
||||||
|
What one node costs, measured rather than guessed. On 2026-09-11 a minimal control-plane node ran at 620 MiB idle and ~728 MiB with a small mock, plus 16 MiB for the local registry: ~745 MiB of working set. 800 rounds that up, and agrees with the 800 MB observed independently on a larger rig. Worker nodes carry no etcd or apiserver and are lighter, so for a multi-node shape this errs high. It is the cluster alone: whatever you deploy comes on top.
|
||||||
|
|
||||||
|
It is set here rather than in check.sh because the memory tool and every standalone kit need the same figure.
|
||||||
|
|
||||||
|
## render_kind_config
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## What a standalone kit needs to know
|
||||||
|
|
||||||
|
The kit generator (ctrl/standalone.sh) asks these questions so that it never has to know how configuration is stored. Where profiles live, which files are layered and what is derived are config.sh's business and can change freely; the generator only calls these functions.
|
||||||
|
|
||||||
|
## config_profiles
|
||||||
|
|
||||||
|
Every configuration rig can be run as, one per line: each profile file, or, when there are none, `default`, the built-in configuration load_config uses when no profile is named. Never empty, because rig never needs a profile.
|
||||||
|
|
||||||
|
## config_snapshot
|
||||||
|
|
||||||
|
The resolved configuration, as `declare -p` lines: exactly what load_config leaves behind, minus the machine-local layer. A kit freezes this in place of load_config, so it carries rig's decisions and not this machine's secrets.
|
||||||
|
|
||||||
|
```
|
||||||
|
config_snapshot <profile> that profile, as any machine would resolve it
|
||||||
|
config_snapshot --current what THIS machine runs: every overridable key as
|
||||||
|
resolved here, handed back in as if typed on the
|
||||||
|
command line, over the same portable resolution.
|
||||||
|
Values derived from those choices follow them;
|
||||||
|
anything else the local layer set (credentials)
|
||||||
|
is not carried. config_left_out names it.
|
||||||
|
```
|
||||||
|
|
||||||
|
Found by difference, not by a list: whatever load_config sets today, it sets. A list here would be one more place to forget a variable.
|
||||||
|
|
||||||
|
## config_left_out
|
||||||
|
|
||||||
|
What an export of this machine's configuration does NOT carry, by name only: keys the machine-local layer sets that are not choices a caller may override. They are this machine's own (registry and mirror credentials, mostly), so the target has to be told to supply them. Values are never printed.
|
||||||
|
|
||||||
|
## config_freeze
|
||||||
|
|
||||||
|
A replacement for load_config with a resolution frozen in (a profile, or --current; see config_snapshot), printed as a function definition for a standalone kit to carry. The generator embeds whatever this prints and interprets none of it, so what "frozen" means stays rig's decision.
|
||||||
|
|
||||||
|
It keeps load_config's one stated rule: the caller's env wins for anything in CONFIG_OVERRIDABLE. A kit therefore behaves like rig (`OUT_BIN=... rigdeps.sh` still works) rather than like a copy with everything pinned.
|
||||||
|
|
||||||
|
What freezing does give up, knowingly: values DERIVED from an overridable one are fixed at generation. Override CLUSTER and the ports stay the ones derived for the original name. Re-deriving would mean carrying the layering itself, which is exactly what a kit exists not to need.
|
||||||
166
rig/docs/notes/deps.md
Normal file
166
rig/docs/notes/deps.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# ctrl/deps.sh
|
||||||
|
|
||||||
|
## Purpose and safety
|
||||||
|
|
||||||
|
Toolchain installer: detect the host, install a pinned toolchain onto it, then report what it could not do.
|
||||||
|
|
||||||
|
It never runs the cluster, never uses sudo or apt, and writes only into `$OUT_BIN` (default `~/.local/bin`). Everything that would touch the host proper — systemd, inotify limits, `.wslconfig`, docker group — is REPORTED for a human to decide on, never performed. That is what makes it safe to run on a machine that already has a working setup.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Normally via `make deps`, or directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
deps.sh detect # report host facts only, change nothing
|
||||||
|
deps.sh list # the pinned versions
|
||||||
|
deps.sh verify [core|dev] # run what is installed and see if it works
|
||||||
|
deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||||
|
deps.sh install [core|dev] # detect, fetch, install, report
|
||||||
|
```
|
||||||
|
|
||||||
|
Tiers: `core` is kubectl + jq (talk to a cluster); `dev` adds kind and tilt. Default is dev.
|
||||||
|
|
||||||
|
## Container vs bare host
|
||||||
|
|
||||||
|
Runs both inside the installer container and bare on a host. Inside the container, host files are read through `$HOST_ROOT` (mount `/` as `:ro`); bare, it falls back to `/`.
|
||||||
|
|
||||||
|
Host FILES (`/etc/...`, `/mnt/c/...`) must be read through the mount. Kernel-level facts (kernel version, meminfo, inotify) are shared with the container, so the container's own view is already the host's.
|
||||||
|
|
||||||
|
## INVOKED_FROM
|
||||||
|
|
||||||
|
Keep the caller's cwd so a relative `--to` resolves where the user expects, not against `ctrl/` once we've moved.
|
||||||
|
|
||||||
|
## load_config
|
||||||
|
|
||||||
|
Pins arrive through `load_config` like every other setting, not by sourcing `versions.env` here. That is what lets `make standalone` freeze them into a one-file installer: configuration has exactly one way in.
|
||||||
|
|
||||||
|
## mb_of
|
||||||
|
|
||||||
|
A `/proc/meminfo` field in MB, 0 if the field is absent. `MEMINFO` exists so the tight and does-not-fit branches can be exercised against a real machine's numbers from somewhere else; in normal use it is always `/proc/meminfo`.
|
||||||
|
|
||||||
|
## require_amd64
|
||||||
|
|
||||||
|
The pins are amd64. Rather than download something that cannot execute and let it fail as "cannot execute binary file: Exec format error", say so here and hand over the commands that produce the right checksums.
|
||||||
|
|
||||||
|
## pkg_install_cmd
|
||||||
|
|
||||||
|
This never runs a package manager. It names one so the reported action is something you can paste, on the distro you are actually on — an apt line on Amazon Linux 2 is a wrong answer dressed up as help.
|
||||||
|
|
||||||
|
## require_linux
|
||||||
|
|
||||||
|
Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and then fails in a pile of confusing ways: no /proc, no docker socket, none of the tooling. Detectable, so name it instead.
|
||||||
|
|
||||||
|
## detect: memory
|
||||||
|
|
||||||
|
In MB. Whole gigabytes lose nearly half a GB on exactly the machines where it matters: 1874 MB available used to print as "1 GB". Facts only — whether that is enough depends on the profile, which `check.sh` knows and this does not.
|
||||||
|
|
||||||
|
## detect: overcommit
|
||||||
|
|
||||||
|
How the kernel answers an allocation it cannot really satisfy. With 1 it always says yes and settles up later with the OOM killer, so a cluster that starts cleanly can still lose processes afterwards.
|
||||||
|
|
||||||
|
## detect_wsl: systemd
|
||||||
|
|
||||||
|
systemd is off by default in WSL, and the ingress/DNS paths that use a host service need it. Enabling it requires a Windows-side restart, which cannot be issued from inside the distro.
|
||||||
|
|
||||||
|
## watch_hostile_fs
|
||||||
|
|
||||||
|
Not a path check: `/mnt` is an ordinary mount point and an ext4 disk mounted there is perfectly fine. What matters is the filesystem. The Windows drives arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same way. None of them deliver inotify events, so anything watching files goes quiet without saying why.
|
||||||
|
|
||||||
|
## detect_libc
|
||||||
|
|
||||||
|
tilt is the one binary here that needs a recent glibc. MEASURED, not guessed: tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
||||||
|
|
||||||
|
```
|
||||||
|
/lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
||||||
|
```
|
||||||
|
|
||||||
|
which names a symbol rather than the problem. Amazon Linux 2 is a stock WorkSpaces bundle, so this is the likely case, not an exotic one. Report the version now; `verify` catches the actual failure after installing.
|
||||||
|
|
||||||
|
## detect_prereqs
|
||||||
|
|
||||||
|
What this script needs to do its own job. Reported here so `detect` answers "will install work?" instead of leaving you to find out one download in. Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
||||||
|
|
||||||
|
## detect_docker
|
||||||
|
|
||||||
|
Reachability of the daemon is the real question, and the CLI is only how we ask it. When this runs inside the installer container, Docker necessarily exists on the host — otherwise nothing would be executing — so a missing CLI in there is an installer packaging bug, not a host problem.
|
||||||
|
|
||||||
|
The kind-node count check must be an `if`, not `[ ] && echo`: as the last statement in the function the latter returns 1 when the count is zero, and `set -e` then kills the caller. That is the fresh-machine case — no clusters yet — so the bug only ever shows up where it does most harm.
|
||||||
|
|
||||||
|
## fetch_tgz: --no-same-owner
|
||||||
|
|
||||||
|
Extracting as root would otherwise restore the uid/gid baked into the archive (some ship as uid 1001), leaving a binary the host user does not own.
|
||||||
|
|
||||||
|
## fix_ownership
|
||||||
|
|
||||||
|
The installer runs as root so it can reach the docker socket, which means everything it writes into a mounted volume lands root-owned and unusable from the host. Hand it back to whoever owns the mount point (the host user created that directory before mounting it).
|
||||||
|
|
||||||
|
kind writes the kubeconfig as root too; `fetch` hands that back as well when it's a mounted host directory rather than container-local state.
|
||||||
|
|
||||||
|
## Tiers (CORE_TOOLS, DEV_TOOLS)
|
||||||
|
|
||||||
|
Two tiers, because not every machine should get cluster tooling.
|
||||||
|
|
||||||
|
- `core` — kubectl, jq: talk to a cluster someone else runs. Nothing that creates one. Appropriate on a managed or corporate-issued machine where development tools are not wanted by default.
|
||||||
|
- `dev` — core plus kind and tilt: build clusters and hot-reload into them.
|
||||||
|
|
||||||
|
The split exists because "install the toolchain" is not one decision: on a managed workspace the right answer is kubectl and nothing else.
|
||||||
|
|
||||||
|
No helm: every addon installs with `kubectl apply -f <url>`, so nothing here has ever invoked it. Add it back the day something actually needs a chart.
|
||||||
|
|
||||||
|
ctlptl is `dev` rather than `core` for the same reason kind is: core is "talk to a cluster someone else runs", and ctlptl builds them. It earns its place because it is what wires a cluster to a local registry — without one, an unqualified image name resolves to `docker.io/library/<name>` and there is nothing structural stopping a push there.
|
||||||
|
|
||||||
|
docker-compose is `dev` for the same reason, and is here because the distro docker packages ship the daemon and CLI but frequently not the compose plugin — so `docker compose up` fails with "unknown command" on an otherwise working Docker, and nothing about that message names the missing piece.
|
||||||
|
|
||||||
|
## What is already on this machine (pin_of)
|
||||||
|
|
||||||
|
A tool already on PATH at its pinned version is left where it is. Without this, install downloads a second copy into `OUT_BIN` and then reports the first one as shadowed — noise, and wrong, when both are the same version. That is the normal state of any machine someone set up by hand, whatever directory they happened to choose.
|
||||||
|
|
||||||
|
## reported_version
|
||||||
|
|
||||||
|
Each tool spells the version question differently, and kubectl has to be told `--client` or it goes looking for a server to ask.
|
||||||
|
|
||||||
|
## version_matches
|
||||||
|
|
||||||
|
Matched as a whole version token, so 0.37.6 never matches 10.37.60, with the leading v optional either side: kind says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
||||||
|
|
||||||
|
Bash's own regex rather than grep, deliberately. grep is not the same program on every machine — some builds reject patterns that others accept — and a failed grep inside a count reads exactly like a zero.
|
||||||
|
|
||||||
|
## want / DEPS_ONLY
|
||||||
|
|
||||||
|
`DEPS_ONLY` narrows a fetch to the tools it names. Unset means the whole tier, which is what an explicit `deps.sh fetch` always gets: "download these into DIR" must not quietly skip something because this machine happens to have it. Only `install()` sets it, to what `detect_toolchain` found missing or mismatched.
|
||||||
|
|
||||||
|
## detect_toolchain: compose
|
||||||
|
|
||||||
|
compose is the one tool that is normally NOT a binary on PATH. It is a docker CLI plugin, so a machine where `docker compose` works perfectly has no `docker-compose` to find — and probing only PATH would report it missing and re-download a copy that is already there. That is the exact noise the version-aware skip exists to prevent, so ask docker instead.
|
||||||
|
|
||||||
|
## verify_tools
|
||||||
|
|
||||||
|
Installing into a directory that sits early in PATH silently replaces whatever the machine was already using — which on a shared or client machine can break unrelated work (kubectl more than one minor away from a cluster is the common one). Say so; never decide it for them.
|
||||||
|
|
||||||
|
Downloading a verified binary proves it is the right file, not that this machine can run it. On an old distro tilt fails here, with a linker error about a missing symbol, and finding that out now beats finding out during a first cluster build.
|
||||||
|
|
||||||
|
Output is not piped into `head`. With `pipefail` set, a tool that prints more than one line gets SIGPIPE when head closes the pipe, and the pipeline reports 141 — so a working kubectl was announced as "does not run here", with its own correct version string as the evidence. The first line is taken afterwards, from the string.
|
||||||
|
|
||||||
|
## install_compose_plugin
|
||||||
|
|
||||||
|
A copy in `OUT_BIN` only gives you `docker-compose`. That hyphenated form is the retired v1 spelling; every compose file written in the last few years assumes `docker compose`, which resolves plugins BY NAME out of a plugin directory. So the binary is fetched like any other and then linked, in your own home — no root, and nothing outside it.
|
||||||
|
|
||||||
|
If something else already owns that name — docker-desktop and some distro packages install a real file there — overwriting it would take the plugin away from whatever put it there, so say so and let the user decide.
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
The plugin is linked only when compose was one of the things fetched: linking a binary that is already satisfied elsewhere on PATH would point the plugin at a copy rig did not install.
|
||||||
|
|
||||||
|
The "put OUT_BIN on PATH" advice is only worth giving when something actually landed in `OUT_BIN`. When every tool was satisfied elsewhere, `OUT_BIN` may reasonably be off PATH, and telling the user to add it would be advice to fix nothing.
|
||||||
|
|
||||||
|
## 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).
|
||||||
14
rig/docs/notes/docs.md
Normal file
14
rig/docs/notes/docs.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# ctrl/docs.sh
|
||||||
|
|
||||||
|
## Serving without the cluster or python
|
||||||
|
|
||||||
|
The docs are the instructions for building the cluster, so they must work before
|
||||||
|
anything else exists. That rules out serving them from the cluster, and it rules
|
||||||
|
out `python -m http.server` too — a minimal Debian has no python3. What it does
|
||||||
|
have, by definition, is Docker: the single prerequisite rig already demands. So a
|
||||||
|
throwaway nginx container serves a read-only bind mount.
|
||||||
|
|
||||||
|
## Committed SVGs
|
||||||
|
|
||||||
|
Rendered SVGs are committed alongside their `.dot` sources for the same reason:
|
||||||
|
the pages have to read on a machine with no Graphviz installed.
|
||||||
64
rig/docs/notes/env.md
Normal file
64
rig/docs/notes/env.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# ctrl/.env.example, ctrl/env.d/*.env.example
|
||||||
|
|
||||||
|
## ctrl/.env.example: header
|
||||||
|
|
||||||
|
Machine-local config. Copy to ctrl/.env (gitignored) and edit. The cluster SHAPE is an optional profile in ctrl/env.d/ — see the *.env.example there. The architecture MODEL lives in arch/<name>.json — not in .env either.
|
||||||
|
|
||||||
|
## ctrl/.env.example: CLUSTER
|
||||||
|
|
||||||
|
The kubectl context becomes kind-<CLUSTER>. LEAVE THIS UNSET unless you need a name that differs from the directory — it defaults to this folder's name, which is what makes the folder copyable: copy it, rename it, and you get a separate environment with no edits.
|
||||||
|
|
||||||
|
## ctrl/.env.example: host ports
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- `upstream` — GitHub releases / dl.k8s.io (needs internet)
|
||||||
|
- `artifactory` — a generic repo; what a locked-down client usually allows
|
||||||
|
- `baked` — already inside the installer image; no network at all
|
||||||
|
|
||||||
|
## ctrl/.env.example: registry secrets
|
||||||
|
|
||||||
|
The registry mode comes from the profile (REGISTRY_MODE). REGISTRY_REMOTE_URL, REGISTRY_USER and REGISTRY_PASSWORD are the secrets it needs, required for mirror/remote.
|
||||||
|
|
||||||
|
## ctrl/.env.example: REGISTRY_CA_FILE
|
||||||
|
|
||||||
|
Corporate root CA, if Artifactory is fronted by an internal CA (it usually is). Trust has to reach THREE places and nothing does it for you: the host docker daemon, every kind node's containerd, and any in-cluster client. registry.sh handles the first two; check.sh reports when it's configured but not trusted. Symptom when missing: `x509: certificate signed by unknown authority`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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/mirror.env.example
|
||||||
|
|
||||||
|
mirror — images through a pull-through cache of an internal registry, with TLS and metrics addons. More nodes or port mappings: edit the kind config (rig's, or the overlay's).
|
||||||
|
|
||||||
|
### Real ports (80/443)
|
||||||
|
|
||||||
|
Ports derive from the directory name by default (see ctrl/ports.sh), so several environments run side by side.
|
||||||
|
|
||||||
|
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/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.
|
||||||
|
|
||||||
|
The heavier addons are left out to keep first boot viable.
|
||||||
118
rig/docs/notes/installer-testing.md
Normal file
118
rig/docs/notes/installer-testing.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# 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.
|
||||||
29
rig/docs/notes/kind-config.md
Normal file
29
rig/docs/notes/kind-config.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# ctrl/k8s/kind-config.yaml.tpl
|
||||||
|
|
||||||
|
## Why a template
|
||||||
|
|
||||||
|
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>`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Node count
|
||||||
|
|
||||||
|
The node count is READ BACK from this file by lib/config.sh, so this YAML is the source of truth for it — there is no second place to update.
|
||||||
|
|
||||||
|
## containerdConfigPatches
|
||||||
|
|
||||||
|
Point containerd at a certs.d directory. registry.sh drops per-host hosts.toml files in there afterwards, so switching registry mode never requires recreating the cluster.
|
||||||
|
|
||||||
|
## extraPortMappings
|
||||||
|
|
||||||
|
One NodePort bridged to the host; an in-cluster gateway owns it. There is deliberately no ingress controller — they pin a narrow window of k8s versions, and running a trailing-edge control plane is the point.
|
||||||
96
rig/docs/notes/mem.md
Normal file
96
rig/docs/notes/mem.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# ctrl/mem.sh
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
How much memory this machine will actually give you before something dies. This is rig's memory tool, and the standalone rigmini.sh is generated from this file.
|
||||||
|
|
||||||
|
There are two numbers and they are rarely the same. `status` reports what the machine ADVERTISES and what is quietly capping it. `push` finds what it will SURVIVE, by allocating until it stops. `all` does both and weighs the result against what this profile's cluster needs.
|
||||||
|
|
||||||
|
The gap between them is the whole reason this exists. Under WSL the cap lives in .wslconfig; in a container or a managed workspace it is a cgroup limit, and there /proc/meminfo reports the HOST's memory while the kernel kills you at a fraction of it. A script that only read MemTotal would confidently report 32 GB on a box that OOMs at 2.
|
||||||
|
|
||||||
|
Runs on native Linux and under WSL. On WSL the memory you see is a VM allocation that can be raised, and the commonest failure is raising it without restarting, so status compares what .wslconfig says with what actually booted.
|
||||||
|
|
||||||
|
It reports and instructs. It never raises a limit, frees anything or installs a package. The one write it can make is `backup`, which copies .wslconfig beside itself, so that `restore` has something to put back after a hand edit.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
mem.sh status what it has, what caps it
|
||||||
|
mem.sh push [--to GB] [--to-oom] climb until it stops
|
||||||
|
mem.sh all [--budget GB] both, then the verdict
|
||||||
|
mem.sh backup | restore .wslconfig, WSL only
|
||||||
|
```
|
||||||
|
|
||||||
|
## require_linux
|
||||||
|
|
||||||
|
Windows outside WSL (Git Bash, MSYS, Cygwin) looks close enough to work and then fails in a pile of confusing ways: no /proc, no docker socket, none of the tooling. It is detectable, so name it instead.
|
||||||
|
|
||||||
|
## CG_MAX_FILE / CG_CUR_FILE
|
||||||
|
|
||||||
|
Where a cgroup records this cgroup's own limit and usage. Set once by find_cgroup, because every later reading needs both, and hunting for the files on each call would be the slow part of the poll loop.
|
||||||
|
|
||||||
|
## find_cgroup
|
||||||
|
|
||||||
|
Inside a container the cgroup namespace makes the top of the tree BE the container's own cgroup, so the unqualified path is already the right one. On a host it is the root cgroup, which is never limited; hence the second attempt via /proc/self/cgroup, which names the slice this shell is in.
|
||||||
|
|
||||||
|
## cgroup_cap_mb
|
||||||
|
|
||||||
|
Returns the cap in MB, or "" when there is none worth reporting. cgroup v2 spells unlimited "max"; v1 spells it as a number near 2^63, which is why this compares against MemTotal rather than testing for a magic value. A "limit" above the machine's own memory is not a limit, however it is written.
|
||||||
|
|
||||||
|
## headroom_mb
|
||||||
|
|
||||||
|
How much room is left RIGHT NOW, from whichever accounting actually governs. In a capped container /proc/meminfo describes the host and is worse than useless for this: it would report tens of gigabytes free on a box that is one allocation from being killed.
|
||||||
|
|
||||||
|
## wslconfig_path
|
||||||
|
|
||||||
|
/mnt/c/Users can hold several real accounts (a renamed login leaves the old directory behind), so picking the first alphabetically is a coin toss. Ask Windows, then fall back to whichever profile actually owns a config.
|
||||||
|
|
||||||
|
## status: overcommit
|
||||||
|
|
||||||
|
overcommit_memory=0 is the default heuristic: a large allocation is granted on a guess, and the reckoning arrives later as an OOM kill rather than as a failed malloc. It is why `push` touches every page it asks for.
|
||||||
|
|
||||||
|
## status: WSL
|
||||||
|
|
||||||
|
WSL keeps its cap on the Windows side, in a file this shell can read but not usefully apply: the change costs a full VM restart. Report it, and report the commonest mistake, which is editing it and not restarting.
|
||||||
|
|
||||||
|
## backup
|
||||||
|
|
||||||
|
Backups are timestamped and never overwritten: a backup that can destroy itself on a second run is not a backup.
|
||||||
|
|
||||||
|
## restore
|
||||||
|
|
||||||
|
Newest is the right default (undo the last edit), but if you backed up *after* editing, the state you want is older. The rest are shown so a no-op restore is obviously a no-op rather than a mystery.
|
||||||
|
|
||||||
|
## allocator
|
||||||
|
|
||||||
|
The child allocates and stops itself; the parent only watches. That split is the point: under --to-oom the allocating process is expected to be killed, and something has to survive to say how far it got.
|
||||||
|
|
||||||
|
### OOM score
|
||||||
|
|
||||||
|
The child raises its own OOM score to the maximum so the kernel picks THIS process first. Raising needs no privilege (only lowering does). Without it, the kernel is free to choose your shell, your ssh session or dockerd; on a box you are still using, that is not an acceptable coin toss.
|
||||||
|
|
||||||
|
### Writing straight into the array element
|
||||||
|
|
||||||
|
Each chunk is written STRAIGHT INTO the array element (`printf -v "arr[$i]"`). The obvious spelling, building one chunk and `arr+=("$chunk")`, costs three copies per step, not one: the template stays resident, expanding "$chunk" makes a temporary word, and the append makes the element. A 128 MB step then needs 384 MB transiently, and on a small box it is killed on the first append while reporting a third of the true ceiling.
|
||||||
|
|
||||||
|
printf -v into a subscript also means every page is written, so it is resident rather than merely promised: the only kind of allocation that measures anything under heuristic overcommit.
|
||||||
|
|
||||||
|
### First swap
|
||||||
|
|
||||||
|
Worth calling out separately from the ceiling: this is where the box stops being fast and starts being unusable, which for a scheduler is a different and earlier problem than being killed.
|
||||||
|
|
||||||
|
## push: step size
|
||||||
|
|
||||||
|
A step is worth about a sixty-fourth of the ceiling: enough resolution to find the edge, few enough lines to read, and small enough that the transient cost of one allocation never dominates a small box. A fixed size cannot do all three: 128 MB is fine on 16 GB and absurd on 512 MB.
|
||||||
|
|
||||||
|
## push: floor
|
||||||
|
|
||||||
|
Stop with a cushion rather than riding it to the kill. How big a cushion depends on what it is protecting. Under a cgroup cap, running out kills only this container's own processes, so it need cover no more than the shell that prints the result, and a 512 MB cushion on a 1 GB box would halve the answer. On a host there is everything else to protect, and the OOM killer does not promise to pick the process that caused the problem.
|
||||||
|
|
||||||
|
## push: Ctrl-C
|
||||||
|
|
||||||
|
INT kills the child and lets the summary print anyway, so an impatient Ctrl-C still tells you how far it got and, more importantly, still gives the memory back.
|
||||||
|
|
||||||
|
## push: claimed vs. measured
|
||||||
|
|
||||||
|
The gap between the claim and the measurement is the finding, but only when the BOX chose where to stop. An empty $stop means the child was ended rather than deciding to end; anything else (--to, the floor) is a stop we asked for, and flagging those as short of the ceiling would put a warning on every deliberately small run.
|
||||||
171
rig/docs/notes/overlay.md
Normal file
171
rig/docs/notes/overlay.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# Overlays: what runs lives outside rig
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
rig is the machine: the toolchain, the cluster, the registry, the port block, the
|
||||||
|
dev loop's plumbing. What runs on it — the services, their manifests, their
|
||||||
|
images, their settings — belongs to whoever owns that work, changes at a
|
||||||
|
different rate, and often cannot be shared at all. Keeping both in one tree meant
|
||||||
|
editing rig's own files to use it, and then carrying those edits into every copy.
|
||||||
|
|
||||||
|
So the use case is one folder, the **overlay**, kept outside rig's version
|
||||||
|
control. rig reads it; rig never writes into it and never knows what is in it.
|
||||||
|
The dependency points one way: an overlay knows about rig, rig knows about
|
||||||
|
overlays in general and about none in particular.
|
||||||
|
|
||||||
|
## Where an overlay lives
|
||||||
|
|
||||||
|
```
|
||||||
|
rig/local/<name>/ gitignored by rig: an overlay with no version control of its own,
|
||||||
|
or a clone of its own repo
|
||||||
|
anywhere/<name>/ a repo of its own, named by path
|
||||||
|
<project>/ a project folder that carries rig at <project>/rig/ (the vendored
|
||||||
|
layout, below)
|
||||||
|
```
|
||||||
|
|
||||||
|
Name it with `OVERLAY` — in `ctrl/.env` for this machine, or per call:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OVERLAY=local/myenv make cluster up
|
||||||
|
```
|
||||||
|
|
||||||
|
Relative paths are relative to rig's folder. With none named, rig uses its own
|
||||||
|
`examples/starter`, which sets nothing, so a plain rig behaves as it always did.
|
||||||
|
An overlay that is named and missing is an error; it never falls back.
|
||||||
|
|
||||||
|
## What rig reads from it
|
||||||
|
|
||||||
|
Every piece is optional.
|
||||||
|
|
||||||
|
| in the overlay | what rig does with it |
|
||||||
|
| --- | --- |
|
||||||
|
| `rig.env` | a config layer (below). Any key a profile could set. |
|
||||||
|
| `k8s/overlays/dev/` | the default `MANIFESTS_DIR`: rig's Tiltfile applies it with kustomize |
|
||||||
|
| `kind-config.yaml.tpl` | the default `KIND_CONFIG`: the cluster's shape, rendered like rig's own |
|
||||||
|
| `addons/<name>.sh` | an addon, found before rig's `ctrl/addons/<name>.sh` of the same name |
|
||||||
|
| `Tiltfile` | the workload's half of the dev loop, included by rig's `ctrl/Tiltfile` |
|
||||||
|
|
||||||
|
Anything else in the folder is the overlay's own business: Dockerfiles, DAGs,
|
||||||
|
folders of repos or data it mounts, its `.gitignore`, the secrets its kustomize
|
||||||
|
generators read. rig does not look.
|
||||||
|
|
||||||
|
## Layers
|
||||||
|
|
||||||
|
```
|
||||||
|
built-in defaults < ctrl/versions.env < ctrl/env.d/<profile>.env < <overlay>/rig.env < ctrl/.env < the caller
|
||||||
|
```
|
||||||
|
|
||||||
|
A profile says how this machine reaches the world (a registry mirror, an
|
||||||
|
air-gapped install); an overlay says what runs. `ctrl/.env` is still this
|
||||||
|
machine's, and the caller still wins over everything.
|
||||||
|
|
||||||
|
`rig.env` may not set `PROFILE` or `OVERLAY`: both are chosen before it loads.
|
||||||
|
The paths it sets (`MANIFESTS_DIR`, `KIND_CONFIG`) are relative to the overlay.
|
||||||
|
`MANIFESTS_DIR=none` means rig applies no manifests and the overlay's Tiltfile
|
||||||
|
does, e.g. when kustomize needs flags.
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
With an overlay named, the cluster, the kubectl context and the port block
|
||||||
|
follow the overlay folder's name, sanitised the same way a rig folder's name is.
|
||||||
|
One rig can therefore serve several overlays, each in its own cluster, and a
|
||||||
|
project that carries rig at `./rig` does not name every cluster `rig`.
|
||||||
|
|
||||||
|
`ports.sh persist` refuses while an overlay is set: it writes to rig's
|
||||||
|
`ctrl/.env`, and a pin there would follow every overlay.
|
||||||
|
|
||||||
|
## The Tiltfile handoff
|
||||||
|
|
||||||
|
rig's `ctrl/Tiltfile` does rig's part — the context guard, `default_registry`,
|
||||||
|
the manifests, the namespaces they use — then publishes the facts and includes
|
||||||
|
the overlay's `Tiltfile`:
|
||||||
|
|
||||||
|
```
|
||||||
|
RIG_CLUSTER RIG_CONTEXT RIG_HTTP_PORT RIG_HTTPS_PORT RIG_TILT_PORT RIG_REGISTRY RIG_OVERLAY_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
Read them with `os.getenv`. An included Tiltfile runs from its own folder, so
|
||||||
|
every relative path in it (`docker_build` contexts, `sync`, `deps`, `local`) is
|
||||||
|
relative to the overlay — it never needs a path back into rig.
|
||||||
|
|
||||||
|
## Addons
|
||||||
|
|
||||||
|
An addon is a bash script run by `ctrl/addons.sh` from rig's `ctrl/`, with
|
||||||
|
`RIG_CTRL` exported. It starts like this, sources the config and does its work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||||
|
source ./lib/config.sh
|
||||||
|
load_config
|
||||||
|
```
|
||||||
|
|
||||||
|
`OVERLAY_DIR` is set, so an addon can find files beside it
|
||||||
|
(`$(_from_ctrl "$OVERLAY_DIR")/...`). rig's own `ctrl/addons/` holds only what
|
||||||
|
makes a cluster work (metallb, cert-manager, metrics-server);
|
||||||
|
`examples/data/addons/` shows workload ones.
|
||||||
|
|
||||||
|
## The kind config
|
||||||
|
|
||||||
|
An overlay's `kind-config.yaml.tpl` replaces rig's whole file, so start from a
|
||||||
|
copy of `ctrl/k8s/kind-config.yaml.tpl` and keep its containerd `config_path`
|
||||||
|
patch: `registry.sh` needs it, and `make check` says so when it is missing.
|
||||||
|
`${OVERLAY_DIR}` renders to the overlay's absolute path, for mounts:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
extraMounts:
|
||||||
|
- hostPath: ${OVERLAY_DIR}/datadir
|
||||||
|
containerPath: /rig/datadir
|
||||||
|
```
|
||||||
|
|
||||||
|
A kind config is fixed when the cluster is created: after changing it,
|
||||||
|
`make cluster reset`.
|
||||||
|
|
||||||
|
## The vendored layout
|
||||||
|
|
||||||
|
A project folder can carry rig inside it and be the overlay itself:
|
||||||
|
|
||||||
|
```
|
||||||
|
<project>/
|
||||||
|
Makefile the forwarder below
|
||||||
|
rig.env k8s/ Tiltfile kind-config.yaml.tpl addons/ ...
|
||||||
|
rig/ rig, placed as it is; tracked or ignored by the project, its call
|
||||||
|
```
|
||||||
|
|
||||||
|
The forwarder runs rig with `OVERLAY` set to this folder. It passes `OVERLAY` in
|
||||||
|
the environment, not as a make argument, so rig's own `$(shell ...)` sees it
|
||||||
|
under make 4.3 as well:
|
||||||
|
|
||||||
|
```make
|
||||||
|
HERE := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
|
||||||
|
ifeq ($(wildcard $(HERE)/rig/Makefile),)
|
||||||
|
$(error rig/ is 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.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 |
|
||||||
38
rig/docs/notes/ports.md
Normal file
38
rig/docs/notes/ports.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# ctrl/ports.sh
|
||||||
|
|
||||||
|
## Why each environment gets a port block
|
||||||
|
|
||||||
|
New versions of a system mean new clusters on ONE machine, not new machines. Cluster name, kubectl context, registry container and image tag already derive from the directory name, so two copies never collide there, but host ports are a single shared namespace and would.
|
||||||
|
|
||||||
|
The block is derived from the directory name: stateless, stable, and requiring no coordination between copies that know nothing about each other.
|
||||||
|
|
||||||
|
```
|
||||||
|
base = 20000 + (hash(slug) % 200) * 10
|
||||||
|
+0 HTTP +1 HTTPS +2 TILT +3 REGISTRY (+4..9 reserved)
|
||||||
|
```
|
||||||
|
|
||||||
|
20000+ deliberately avoids the ports something is already likely to hold: 80, 443, 3000, 5432, 8000, 8080.
|
||||||
|
|
||||||
|
Derivation is a default, not a decision. On first use the resolved block is written into ctrl/.env, so it becomes pinned, visible and editable rather than a number that appears from nowhere. Anything already in ctrl/.env wins.
|
||||||
|
|
||||||
|
## active
|
||||||
|
|
||||||
|
The resolved facts a consumer outside bash needs, machine-readable:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLUSTER KUBECONTEXT HTTP HTTPS TILT REGISTRY MANIFESTS_DIR OVERLAY_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.
|
||||||
|
|
||||||
|
The two paths are absolute, or `-` when there is none: an empty field would shift every later one. OVERLAY_DIR was appended rather than inserted, so readers that take fields by position kept their indexes.
|
||||||
|
|
||||||
|
Space-separated, so the paths must not contain whitespace; `active` refuses rather than print a line that splits wrong. Everything else in rig already assumes that of paths; kind, docker and kubectl all do.
|
||||||
|
|
||||||
|
## persist, with an overlay
|
||||||
|
|
||||||
|
`persist` writes into ctrl/.env, which belongs to this rig, not to an overlay. With OVERLAY set, a block pinned there would follow every overlay this rig later runs, and two of them would then share ports — the collision the derivation exists to prevent. So it refuses and says so; an overlay's ports stay derived from its folder name.
|
||||||
|
|
||||||
|
`derive` answers a DIFFERENT question (what the directory name alone implies) and deliberately ignores ctrl/.env. Configuring anything from it would silently contradict the rule that "anything already in ctrl/.env wins". `active` is what anything downstream should read.
|
||||||
|
|
||||||
|
Why this exists at all: the cluster name is not the bare directory name. default_cluster_name() lowercases it and replaces every character outside [a-z0-9-], because it has to be a DNS label. Re-deriving that in another language is how a copy in `My_Project/` ends up guarding the wrong context.
|
||||||
41
rig/docs/notes/registry.md
Normal file
41
rig/docs/notes/registry.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# ctrl/registry.sh
|
||||||
|
|
||||||
|
## Registry modes
|
||||||
|
|
||||||
|
Registry plumbing. This is the seam — not a tool. Four modes, selected by
|
||||||
|
`REGISTRY_MODE` in the active profile:
|
||||||
|
|
||||||
|
- **none** — Tilt builds straight into the node. No registry at all, and so no
|
||||||
|
guard against an outward push: an unqualified image name means
|
||||||
|
`docker.io/library/<name>`, and only Tilt's kind detection stands between that
|
||||||
|
and a real push. Throwaway use only; every profile here now defaults to `local`
|
||||||
|
instead.
|
||||||
|
- **local** — a `registry:2` container wired into the cluster.
|
||||||
|
- **mirror** — the same container, but configured as a pull-through cache of the
|
||||||
|
corporate registry. This is what a locked-down client actually looks like:
|
||||||
|
images originate from corp, you don't hammer it, and you keep working when the
|
||||||
|
VPN drops.
|
||||||
|
- **remote** — no local container; pull straight from the corporate registry
|
||||||
|
using an imagePullSecret.
|
||||||
|
|
||||||
|
## Why a script rather than ctlptl
|
||||||
|
|
||||||
|
Deliberately a script rather than a tool. ctlptl collapses the `local` wiring
|
||||||
|
into one line, but its Registry spec only accepts name/port/image/listenAddress —
|
||||||
|
there is no way to set `REGISTRY_PROXY_REMOTEURL`, so it cannot express `mirror`
|
||||||
|
at all. Keeping the seam here is what keeps the corporate registry swappable.
|
||||||
|
|
||||||
|
## CA trust (install_ca_into_nodes)
|
||||||
|
|
||||||
|
A corporate registry is almost always fronted by an internal CA, and trust has to
|
||||||
|
reach three separate places. Nothing does this for you, and the symptom when it's
|
||||||
|
missing is an opaque:
|
||||||
|
|
||||||
|
x509: certificate signed by unknown authority
|
||||||
|
|
||||||
|
1. the host docker daemon — `/etc/docker/certs.d/<host>/ca.crt` (needs root)
|
||||||
|
2. every kind node's containerd — nodes do NOT inherit host trust
|
||||||
|
3. anything doing HTTPS from inside the cluster, in its own trust store
|
||||||
|
|
||||||
|
`registry.sh` handles (2) because it's ours to handle. (1) is reported by
|
||||||
|
`check.sh` since it needs root. (3) belongs to the workload.
|
||||||
99
rig/docs/notes/selftest.md
Normal file
99
rig/docs/notes/selftest.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# ctrl/selftest.sh
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
What rig has settled, written down as assertions.
|
||||||
|
|
||||||
|
These are documentation that runs. Each check is ONE decision that has already been made, with the reason above it: not coverage, and deliberately not an exhaustive sweep of use cases. rig's own index says a rule without its reason gets overridden the first time it is inconvenient; a rule nobody can restate is worse. So the test says what was decided, and failing it should read as "you are about to undo this" rather than "something broke".
|
||||||
|
|
||||||
|
Scope, on purpose:
|
||||||
|
|
||||||
|
- No cluster, no docker, no network. It must be cheap enough to actually run.
|
||||||
|
- It asserts about RIG. `make check` asserts about the MACHINE and never fails; this exits 1, the way `make standalone check` does.
|
||||||
|
- What actually deploys is not testable here. `tilt ci` stays a manual step.
|
||||||
|
|
||||||
|
## rig needs no profile
|
||||||
|
|
||||||
|
rig assumes no configuration. A profile is an overlay on built-in defaults, so a rig with no `env.d/` at all must resolve, report, and still generate a kit. Naming a profile that does not exist must still be an error, because a typo that silently fell back to the defaults would be worse than a failure.
|
||||||
|
|
||||||
|
## the ports.sh active contract
|
||||||
|
|
||||||
|
`ports.sh active` is read POSITIONALLY by two other files: the Makefile takes `$(word 2)` and `$(word 5)`, the Tiltfile takes `_facts[0]..[7]`. Insert a field in the middle and nothing errors: Tilt simply guards on the wrong context or binds the wrong port. The field count and order are the contract, so they are pinned here rather than left to whoever edits `ports.sh` next. The two paths are absolute or `-`, never empty: an empty field would shift the ones after it just the same.
|
||||||
|
|
||||||
|
## the caller's env beats the files
|
||||||
|
|
||||||
|
`lib/config.sh` states one precedence rule: `versions.env` < `env.d/<profile>` < `ctrl/.env` < the caller's env. It is enforced by `CONFIG_OVERRIDABLE`, a hand-maintained list, and a key missing from it loses to the file SILENTLY. `REGISTRY_PORT` and `MANIFESTS_DIR` were both missing on 2026-09-13 and were found by accident.
|
||||||
|
|
||||||
|
So the loop is generated FROM the list: add a key to `CONFIG_OVERRIDABLE` and the test starts asking about it without anyone remembering to come here. Three keys name something that must exist and are validated at load, so they get a real alternative rather than a sentinel.
|
||||||
|
|
||||||
|
## one derivation, not three
|
||||||
|
|
||||||
|
The Makefile used to compute the cluster name itself and sed `TILT_PORT` out of `ctrl/.env`: a second derivation of values `lib/config.sh` already owns, which could disagree with it after `ports.sh persist`. It now reads `ports.sh active`. Nothing structurally prevents the sed coming back, so the agreement is asserted against the real `make -n` output rather than against the source.
|
||||||
|
|
||||||
|
`--no-print-directory` and a grep, not `tail -1`: run from `make selftest` this is a RECURSIVE make, and the "Entering/Leaving directory" lines go to STDOUT. `tail -1` then reads "make[1]: Leaving directory ..." and both checks fail, but only when invoked through make, never when the script is run directly. A test that passes one way and fails the other is worse than no test.
|
||||||
|
|
||||||
|
## identity follows the folder, safely
|
||||||
|
|
||||||
|
The cluster name is NOT the bare directory name: kind needs a DNS label, so `default_cluster_name` lowercases it and replaces everything outside `[a-z0-9-]`. Re-deriving that anywhere else is how a copy ends up guarding the wrong context, which is exactly why the Tiltfile asks instead of computing.
|
||||||
|
|
||||||
|
## ports are stable across versions
|
||||||
|
|
||||||
|
Not a change-detector. The block is derived, never stored, so if the derivation shifts then every EXISTING environment's ports move underneath it: a running cluster keeps its old ports while rig starts reporting new ones, and `ports.sh show` stops describing reality. Anchored to three known names.
|
||||||
|
|
||||||
|
## rig stays standalone
|
||||||
|
|
||||||
|
rig sits inside a host project's tree but must be copyable straight out of it: no imports, no paths, no assumption the host is there. This grep is the whole test of that claim, and until it was added it lived only in prose and in whoever remembered to run it.
|
||||||
|
|
||||||
|
The pattern is assembled from fragments so the file does not match ITSELF. Writing it literally would fail forever; excluding the file instead would put a blind spot in the one check that guards the boundary. It includes the host project's word for a backing service, which rig's workload addons carried until they left, and skips `local/`, where overlays live and may say anything.
|
||||||
|
|
||||||
|
## scratch copies
|
||||||
|
|
||||||
|
Every check that changes something does it in a copy made by `copy_rig`: without `local/` (overlays, possibly someone else's, possibly large) and `def/`, and without this machine's `PROFILE`, `OVERLAY`, `CLUSTER` and `MANIFESTS_DIR` choices, so a check sets exactly what it tests.
|
||||||
|
|
||||||
|
## what runs is an overlay; rig only reads it
|
||||||
|
|
||||||
|
The overlay decisions (docs/notes/overlay.md), each against a throwaway overlay in a scratch copy: with nothing named, the same cluster, ports, addons, node count and kind config as before overlays existed; `rig.env` between the profile and `ctrl/.env`, the caller above all; identity from the overlay's folder; its paths relative to itself; a named overlay that does not exist, or a `rig.env` that tries to choose the profile or the overlay, is an error; an overlay's addon found before rig's own and run from rig's `ctrl/`; `persist` refusing; `make -n tilt OVERLAY=...` asking for the overlay's context (make before 4.4 would not pass it to `$(shell)`).
|
||||||
|
|
||||||
|
And the two that make an overlay safe to hold someone else's work: rig writes nothing into it (a checksum of the folder before and after `active`, `addons list`, a kind render, `standalone write` and `export`), and nothing from it — neither a value nor its path — reaches a committed kit.
|
||||||
|
|
||||||
|
## withdrawn stays withdrawn
|
||||||
|
|
||||||
|
One check per entry in STALE.md, each asserting that the withdrawn thing has not come back. The reasoning stays in STALE.md; the check is what makes it more than prose.
|
||||||
|
|
||||||
|
## the Tiltfile hardcodes nothing
|
||||||
|
|
||||||
|
Every other Tiltfile on this machine writes its slug in five or six times by hand, so a copied project deploys into the original's cluster until someone edits all of them. rig's asks `ports.sh`. A literal `kind-<name>` in it would mean that has been undone.
|
||||||
|
|
||||||
|
## standalone kits are generated and current
|
||||||
|
|
||||||
|
The kits under `standalone/<profile>/` are rig flattened into single files, one per profile. A kit left behind by a change to rig is exactly the drift they replaced (`rigmini.sh` once said 2 GB per node long after rig measured 800 MB), so a stale kit fails here rather than waiting to be noticed on another machine.
|
||||||
|
|
||||||
|
## kit Makefiles call only real verbs
|
||||||
|
|
||||||
|
Each kit's Makefile exists so nothing wrapping these scripts has to GUESS how to call them. A generated Makefile once did guess: `rigmini.sh on`, not a verb, and a bare `rigdeps.sh` for "check and report", which installs. So every target's default verb must be one its script's own dispatch accepts, read from that dispatch, not from a list that could drift from it.
|
||||||
|
|
||||||
|
## export carries choices, not credentials
|
||||||
|
|
||||||
|
An export is "take the setup I have here somewhere else", so it carries this machine's CHOICES (profile, ports, manifest dir) and never its credentials: `ctrl/.env` can hold registry and mirror logins next to those choices. The committed per-profile kits carry neither, since they must be the same on any machine. Proven with sentinel values in a scratch copy, because the real `ctrl/.env` may have those keys empty, and an empty value proves nothing.
|
||||||
|
|
||||||
|
## the dev loop parses — tilt and kubectl, no cluster
|
||||||
|
|
||||||
|
Parsing the Tiltfile for real is the only way to know it still evaluates. Tilt snapshots a kubectl context first, but it never contacts the cluster while evaluating: given a throwaway kubeconfig whose entries are kind-named (Tilt only runs `local()` freely for contexts it recognises as local) and a `kubectl` that swallows `apply`, `tilt alpha tiltfile-result` evaluates rig's Tiltfile with the starter overlay included, and reports the resources it would deploy.
|
||||||
|
|
||||||
|
It runs as `rig`, as a copy under another name — the case that used to stop at load (✖ S4) — and with the data overlay, whose namespace is used but not declared. Skipped, not failed, without tilt or kubectl.
|
||||||
|
|
||||||
|
## rig's addons apply verified files, never URLs
|
||||||
|
|
||||||
|
The offline example profile must install rig's addons with no network. Each addon therefore asks `deps.sh manifest <NAME>` for a pinned manifest, verified on disk, instead of applying a URL; the check fails if a URL comes back or a manifest is asked for without a pinned sum (versions.md). Their container images still need preloading, and nothing here pretends otherwise.
|
||||||
|
|
||||||
|
## the examples are overlays that work as shipped
|
||||||
|
|
||||||
|
`examples/` is what real overlays are copied from, so every addon there must parse, and every DAG must be valid Python. What they deploy is exercised by the parse checks above, not here.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
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).
|
||||||
31
rig/docs/notes/standalone.md
Normal file
31
rig/docs/notes/standalone.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# ctrl/standalone.sh
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Generates the standalone kits: single-file versions of rig's own tools, one folder per profile, for machines the full rig is not going to.
|
||||||
|
|
||||||
|
A kit is a pure function of rig as it is right now. It gains nothing rig lacks and loses nothing rig has: improve rig, regenerate, and every kit follows. Nothing in `standalone/<profile>/` is ever edited by hand.
|
||||||
|
|
||||||
|
## The contract
|
||||||
|
|
||||||
|
What this file does NOT know, on purpose: which tools rig has, what they are called, how its libraries are split, where configuration lives or what it contains. Rig will change shape (scripts get split, renamed and grow new libraries), and a generator that encoded today's layout would quietly produce a wrong kit the first time it did. So it works from a contract a script opts into, and from nothing else:
|
||||||
|
|
||||||
|
1. A marker comment, alone on a line near the top, declares an entry point: `(hash) rig:standalone <kit-name> <default-verb>`. The default verb must only REPORT: it is run as a smoke test.
|
||||||
|
2. Every `source` an entry point makes names a `.sh` file by a path that resolves relative to the entry point. Libraries may source further libraries however they like; bash follows those itself.
|
||||||
|
3. Configuration enters through `load_config`, and the libraries provide `config_profiles`, `config_freeze <profile|--current>` (which prints a replacement `load_config` with that resolution frozen in) and, for an export, `config_current_profile` and `config_left_out`. How config is layered, stored, derived or frozen is rig's business; the generator only asks, and embeds the answer without interpreting it.
|
||||||
|
|
||||||
|
## Bash does the resolving
|
||||||
|
|
||||||
|
Bash does the resolving, not a parser in the generator. Libraries are sourced in a clean shell and read back with `declare -f` and `declare -p`, so any structure bash can load, this can flatten.
|
||||||
|
|
||||||
|
## Every kit is proven before it is written
|
||||||
|
|
||||||
|
Every kit is PROVEN to stand alone before it is written: no `source` left, no path into rig's tree in its code, `bash -n` clean, and its default verb run in an empty directory with nothing from rig present. A shape the generator has never seen either passes that, or generation stops and names the kit, the file, the line and what is wrong. It never writes a kit that only looks finished.
|
||||||
|
|
||||||
|
## Usage: write, check, export
|
||||||
|
|
||||||
|
- `standalone.sh write`: generate every kit into `standalone/<profile>/`.
|
||||||
|
- `standalone.sh check`: generate into a scratch dir and fail if any kit differs.
|
||||||
|
- `standalone.sh export DIR`: ONE kit for the configuration this machine runs (its profile plus the choices in its local config, WITHOUT its credentials), written outside the repo.
|
||||||
|
|
||||||
|
`write` and `check` are what gets committed: one kit per profile, identical on any machine. `export` answers the other question, "take the setup I have here somewhere else", so it reflects this machine, and for exactly that reason it never lands in the repository.
|
||||||
54
rig/docs/notes/versions.md
Normal file
54
rig/docs/notes/versions.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# ctrl/versions.env
|
||||||
|
|
||||||
|
## Pinned toolchain
|
||||||
|
|
||||||
|
The single manifest `ctrl/deps.sh` installs from. Every entry is a single binary; none of them needs an apt repo.
|
||||||
|
|
||||||
|
- kubectl — fully static
|
||||||
|
- kind — libc only
|
||||||
|
- tilt — libc + libstdc++ + libgcc (present in base Debian)
|
||||||
|
- jq — upstream static build (Debian's is linked against libjq/libonig)
|
||||||
|
|
||||||
|
Checksums are the upstream-published SHA256 of the linux/amd64 artifact.
|
||||||
|
|
||||||
|
## Bumping a pin
|
||||||
|
|
||||||
|
Change the version, then take the checksum from the release's own published list — never hand-edit or hand-copy one from a download you did. For anything hosted on GitHub releases that is:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -sSL https://github.com/<org>/<repo>/releases/download/<tag>/checksums.txt \
|
||||||
|
| grep linux.x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
(kubectl publishes its own instead: `<KUBECTL_URL>.sha256`.)
|
||||||
|
|
||||||
|
There was a `ctrl/versions-refresh.sh` named here that has never existed. If bumping stops being rare enough to do by hand, write it — but a comment pointing at a missing script is worse than no comment.
|
||||||
|
|
||||||
|
## ctlptl
|
||||||
|
|
||||||
|
Creates a kind cluster WITH a local registry wired in, which is what keeps images off docker.io (an unqualified name means `docker.io/library/<name>`). Same publisher and same archive shape as tilt: binary at the archive root, so `fetch_tgz` handles it with strip=0 and no special case.
|
||||||
|
|
||||||
|
## docker compose
|
||||||
|
|
||||||
|
The distro docker packages ship the daemon and the CLI but frequently not this, so `docker compose up` fails with "unknown command" on an otherwise working Docker. It is a CLI plugin, found by NAME in a plugin directory, so a copy in the bin dir alone only gives you the retired `docker-compose` v1 spelling; deps.sh links it into `~/.docker/cli-plugins`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Workload images are not pinned here
|
||||||
|
|
||||||
|
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.
|
||||||
98
rig/examples/data/README.md
Normal file
98
rig/examples/data/README.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# `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
|
||||||
|
```
|
||||||
6
rig/examples/data/Tiltfile
Normal file
6
rig/examples/data/Tiltfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 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,15 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Apache Airflow — the cluster half of the airflow cabinet.
|
# 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
|
||||||
# Airflow needs a metadata database before it will start at all, so this refuses
|
# `app_db` connection; DAGs from this overlay's dags/, as a ConfigMap.
|
||||||
# rather than rolls a pod that will CrashLoopBackOff while the real problem
|
# Requires the postgres addon; refuses to install without it.
|
||||||
# (postgres missing from ADDONS) stays invisible in the logs.
|
# Notes: ../README.md
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
@@ -19,16 +15,25 @@ NS="${DATA_NAMESPACE:-data}"
|
|||||||
|
|
||||||
if ! $K get deployment -n "$NS" postgres >/dev/null 2>&1; then
|
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 " ! airflow needs the postgres addon, and it is not installed" >&2
|
||||||
echo " add it before airflow in the profile's ADDONS:" >&2
|
echo " add it before airflow in the overlay's ADDONS:" >&2
|
||||||
echo " ADDONS=\"... postgres airflow\"" >&2
|
echo " ADDONS=\"... postgres airflow\"" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reuse the credential postgres generated rather than storing a second copy.
|
# ── 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_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_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)
|
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
|
if $K get secret -n "$NS" airflow >/dev/null 2>&1; then
|
||||||
echo " secret exists, keeping the current admin password and fernet key"
|
echo " secret exists, keeping the current admin password and fernet key"
|
||||||
else
|
else
|
||||||
@@ -40,11 +45,30 @@ else
|
|||||||
--from-literal=ADMIN_USER="${AIRFLOW_ADMIN_USER:-admin}" \
|
--from-literal=ADMIN_USER="${AIRFLOW_ADMIN_USER:-admin}" \
|
||||||
--from-literal=ADMIN_PASSWORD="$admin_password" \
|
--from-literal=ADMIN_PASSWORD="$admin_password" \
|
||||||
--from-literal=FERNET_KEY="$fernet_key" \
|
--from-literal=FERNET_KEY="$fernet_key" \
|
||||||
--from-literal=SQL_ALCHEMY_CONN="postgresql+psycopg2://${db_user}:${db_pass}@postgres:5432/${db_name}" \
|
|
||||||
>/dev/null
|
>/dev/null
|
||||||
echo " generated an admin password (read it back with the command below)"
|
echo " generated an admin password (read it back with the command below)"
|
||||||
fi
|
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"
|
echo " applying manifests"
|
||||||
$K apply -n "$NS" -f - >/dev/null <<YAML
|
$K apply -n "$NS" -f - >/dev/null <<YAML
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@@ -85,7 +109,10 @@ spec:
|
|||||||
value: "false"
|
value: "false"
|
||||||
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef: {name: airflow, key: SQL_ALCHEMY_CONN}
|
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
|
- name: AIRFLOW__CORE__FERNET_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef: {name: airflow, key: FERNET_KEY}
|
secretKeyRef: {name: airflow, key: FERNET_KEY}
|
||||||
@@ -97,6 +124,9 @@ spec:
|
|||||||
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
|
secretKeyRef: {name: airflow, key: ADMIN_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
|
volumeMounts:
|
||||||
|
- name: dags
|
||||||
|
mountPath: /opt/airflow/dags
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
@@ -105,6 +135,11 @@ spec:
|
|||||||
initialDelaySeconds: 60
|
initialDelaySeconds: 60
|
||||||
periodSeconds: 15
|
periodSeconds: 15
|
||||||
failureThreshold: 20
|
failureThreshold: 20
|
||||||
|
volumes:
|
||||||
|
- name: dags
|
||||||
|
configMap:
|
||||||
|
name: airflow-dags
|
||||||
|
optional: true
|
||||||
YAML
|
YAML
|
||||||
|
|
||||||
echo " waiting for airflow (the first boot migrates the database, so this is slow)..."
|
echo " waiting for airflow (the first boot migrates the database, so this is slow)..."
|
||||||
@@ -1,22 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# PostgreSQL — the cluster half of the postgres cabinet.
|
# PostgreSQL for this overlay: plain manifests, one replica on a PVC, password generated once and kept.
|
||||||
#
|
# Notes: ../README.md
|
||||||
# A cabinet is a public service dropped into the environment as-is — the
|
|
||||||
# upstream image, unmodified, reachable at a known address. This 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.
|
|
||||||
#
|
|
||||||
# Plain manifests rather than a helm chart, matching the other addons: a chart
|
|
||||||
# repo is a network dependency, and the offline 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.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Redis — the cluster half of the redis cabinet.
|
# Redis for this overlay: cache and broker (Celery), no persistence.
|
||||||
#
|
# Notes: ../README.md
|
||||||
# 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.
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")/.."
|
cd "${RIG_CTRL:?run it through rig: bash ctrl/addons.sh install}"
|
||||||
|
|
||||||
source ./lib/config.sh
|
source ./lib/config.sh
|
||||||
load_config
|
load_config
|
||||||
81
rig/examples/data/dags/items_to_postgres.py
Normal file
81
rig/examples/data/dags/items_to_postgres.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
"""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)
|
||||||
95
rig/examples/data/k8s/base/items-api.yaml
Normal file
95
rig/examples/data/k8s/base/items-api.yaml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# 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
|
||||||
10
rig/examples/data/k8s/base/kustomization.yaml
Normal file
10
rig/examples/data/k8s/base/kustomization.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
5
rig/examples/data/k8s/overlays/dev/kustomization.yaml
Normal file
5
rig/examples/data/k8s/overlays/dev/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../../base
|
||||||
23
rig/examples/data/rig.env
Normal file
23
rig/examples/data/rig.env
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 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"
|
||||||
|
|
||||||
|
# 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
|
||||||
24
rig/examples/starter/Dockerfile.example
Normal file
24
rig/examples/starter/Dockerfile.example
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Dependencies first, in their own layer, so a source edit does not reinstall them.
|
||||||
|
COPY api/requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Context-relative — see above.
|
||||||
|
COPY api/ ./api/
|
||||||
|
|
||||||
|
# Match this with the containerPort in the manifest and the target of the
|
||||||
|
# Service in front of it.
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["python", "-m", "api"]
|
||||||
|
|
||||||
|
# live_update: the Tiltfile's sync('api', '/app/api') must match COPY api/ + WORKDIR /app,
|
||||||
|
# or edits silently do nothing.
|
||||||
59
rig/examples/starter/README.md
Normal file
59
rig/examples/starter/README.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# `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.
|
||||||
69
rig/examples/starter/Tiltfile
Normal file
69
rig/examples/starter/Tiltfile
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 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'])
|
||||||
9
rig/examples/starter/rig.env
Normal file
9
rig/examples/starter/rig.env
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 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
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# standalone — rig as single files, one folder per profile
|
|
||||||
|
|
||||||
**Everything in the `<profile>/` folders here is generated. Do not edit it.**
|
|
||||||
It is rig's own tools flattened into single self-contained files, with one
|
|
||||||
profile's configuration resolved in, for a machine the full rig is not going to.
|
|
||||||
|
|
||||||
```
|
|
||||||
standalone/<profile>/rigdeps.sh rig's toolchain installer (ctrl/deps.sh)
|
|
||||||
standalone/<profile>/rigmini.sh rig's memory tool (ctrl/mem.sh)
|
|
||||||
standalone/<profile>/Makefile shorthand for calling them
|
|
||||||
```
|
|
||||||
|
|
||||||
One folder per file in `ctrl/env.d/`. Pick the profile you mean to run and copy
|
|
||||||
that folder; nothing else from rig is needed. The scripts run without the
|
|
||||||
Makefile.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash rigdeps.sh detect # report the host and toolchain; changes nothing
|
|
||||||
bash rigdeps.sh install dev # download, verify, install into ~/.local/bin
|
|
||||||
bash rigmini.sh status # advertised memory and what caps it; safe
|
|
||||||
bash rigmini.sh all # measure, then weigh it against this profile
|
|
||||||
make deps / make mem # the same, via the Makefile
|
|
||||||
```
|
|
||||||
|
|
||||||
`rigmini.sh push` and `all` deliberately consume memory. Run `status` first, and
|
|
||||||
only run them somewhere other processes may be squeezed.
|
|
||||||
|
|
||||||
## Why generated
|
|
||||||
|
|
||||||
These used to be hand-kept copies, and they drifted: the standalone memory tool
|
|
||||||
said 2 GB per node long after rig had measured 800 MB. Now a kit is a pure
|
|
||||||
function of rig. It gains nothing rig lacks; improve rig and every kit follows.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make standalone # regenerate every kit
|
|
||||||
make standalone check # fail if any kit differs from what rig generates now
|
|
||||||
```
|
|
||||||
|
|
||||||
`make selftest` runs the check, so a kit left behind by a change to rig fails there
|
|
||||||
rather than on the machine it was copied to.
|
|
||||||
|
|
||||||
How the generator stays correct as rig changes shape — it knows no file, function
|
|
||||||
or variable names, only a marker, a sourcing rule and two config questions, and it
|
|
||||||
proves each kit stands alone before writing it — is in `ctrl/standalone.sh`.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# Shorthand for the scripts beside it; they run without it. Every target
|
|
||||||
# calls a verb its script accepts — read from that script's own dispatch.
|
|
||||||
|
|
||||||
HERE := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|
||||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
|
||||||
ifneq ($(ARGS),)
|
|
||||||
$(eval $(ARGS):;@:)
|
|
||||||
.PHONY: $(ARGS)
|
|
||||||
endif
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
.PHONY: help deps mem
|
|
||||||
|
|
||||||
help: ## list targets
|
|
||||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
|
||||||
|
|
||||||
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)
|
|
||||||
bash $(HERE)rigmini.sh $(or $(ARGS),status)
|
|
||||||
@@ -1,958 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# rigdeps.sh for profile 'client', flattened from:
|
|
||||||
# ctrl/deps.sh
|
|
||||||
# ctrl/lib/config.sh
|
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
|
||||||
|
|
||||||
# ── from the libraries ──
|
|
||||||
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;
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [ -n "$line" ]; then
|
|
||||||
eval "export $line";
|
|
||||||
fi;
|
|
||||||
done <<< "$1";
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
default_cluster_name ()
|
|
||||||
{
|
|
||||||
local n;
|
|
||||||
n=$(basename "$(cd .. && pwd)");
|
|
||||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
|
||||||
echo "${n:-rig}"
|
|
||||||
}
|
|
||||||
derive_port_base ()
|
|
||||||
{
|
|
||||||
local h;
|
|
||||||
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
|
||||||
echo $((20000 + (h % 200) * 10))
|
|
||||||
}
|
|
||||||
render_kind_config ()
|
|
||||||
{
|
|
||||||
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_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'client' ──
|
|
||||||
load_config() {
|
|
||||||
local k saved=""
|
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
|
||||||
done
|
|
||||||
declare -gx ADDONS="metallb cert-manager metrics-server"
|
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
|
||||||
declare -g AUDIT="on"
|
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
|
||||||
declare -g CLUSTER="rig"
|
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
|
||||||
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
|
||||||
declare -gx COMPOSE_VERSION="5.5.1"
|
|
||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
|
||||||
declare -gx DNS_MODE="hosts"
|
|
||||||
declare -g HTTPS_PORT="20311"
|
|
||||||
declare -g HTTP_PORT="20310"
|
|
||||||
declare -gx INGRESS_MODE="hostport"
|
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
|
||||||
declare -gx K8S_VERSION="v1_36"
|
|
||||||
declare -gx KIND_CONFIG="kind-config.client.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.client.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.client.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
|
||||||
declare -g KUBECONTEXT="kind-rig"
|
|
||||||
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="ctrl/k8s/overlays/dev"
|
|
||||||
declare -gx METALLB_VERSION="v0.16.0"
|
|
||||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
|
||||||
declare -g NODES="3"
|
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
|
||||||
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
|
||||||
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
|
||||||
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 -gx POSTGRES_IMAGE="postgres:16-alpine"
|
|
||||||
declare -gx PROFILE_NAME="client"
|
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
|
||||||
declare -gx REGISTRY_MODE="mirror"
|
|
||||||
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"
|
|
||||||
declare -gx TILT_VERSION="0.37.6"
|
|
||||||
_config_restore "$saved"
|
|
||||||
}
|
|
||||||
# ── end of frozen configuration ──
|
|
||||||
|
|
||||||
# ── ctrl/deps.sh ──
|
|
||||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
|
||||||
# report what it could not do.
|
|
||||||
#
|
|
||||||
# It never runs the cluster, never uses sudo or apt, and writes only into
|
|
||||||
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
|
||||||
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
|
||||||
# decide on, never performed. That is what makes it safe to run on a machine that
|
|
||||||
# already has a working setup.
|
|
||||||
#
|
|
||||||
# Usage (normally via `make deps`, or directly):
|
|
||||||
# deps.sh detect # report host facts only, change nothing
|
|
||||||
# deps.sh list # the pinned versions
|
|
||||||
# deps.sh verify [core|dev] # run what is installed and see if it works
|
|
||||||
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
|
||||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
|
||||||
#
|
|
||||||
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
|
||||||
# Default is dev.
|
|
||||||
#
|
|
||||||
# Runs both inside the installer container and bare on a host. Inside the
|
|
||||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
|
||||||
# falls back to /.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
|
||||||
# not against ctrl/ once we've moved.
|
|
||||||
INVOKED_FROM="$PWD"
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# Pins arrive through load_config like every other setting, not by sourcing
|
|
||||||
# versions.env here. That is what lets `make standalone` freeze them into a
|
|
||||||
# one-file installer: configuration has exactly one way in.
|
|
||||||
# (sourced library inlined above)
|
|
||||||
load_config
|
|
||||||
|
|
||||||
# Resolve a possibly-relative path against the caller's original directory.
|
|
||||||
abspath() {
|
|
||||||
case "$1" in
|
|
||||||
/*) echo "$1" ;;
|
|
||||||
*) echo "$INVOKED_FROM/$1" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
|
||||||
HOST_ROOT="${HOST_ROOT:-/}"
|
|
||||||
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
|
||||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
|
||||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
|
||||||
|
|
||||||
# Collected by detect(), printed by report_manual() at the very end.
|
|
||||||
MANUAL=()
|
|
||||||
|
|
||||||
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
|
||||||
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
|
||||||
# container's own view is already the host's.
|
|
||||||
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
|
||||||
# tight and does-not-fit branches can be exercised against a real machine's
|
|
||||||
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
|
||||||
mb_of() {
|
|
||||||
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
|
||||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
|
||||||
}
|
|
||||||
|
|
||||||
host_file() {
|
|
||||||
local p="${1#/}"
|
|
||||||
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
|
||||||
echo "$HOST_ROOT/$p"
|
|
||||||
else
|
|
||||||
echo "/$p"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── the tools this script itself needs ─────────────────────────────────────
|
|
||||||
|
|
||||||
arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) echo amd64 ;;
|
|
||||||
aarch64|arm64) echo arm64 ;;
|
|
||||||
*) uname -m ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The pins above are amd64. Rather than download something that cannot execute
|
|
||||||
# and let it fail as "cannot execute binary file: Exec format error", say so
|
|
||||||
# here and hand over the commands that produce the right checksums.
|
|
||||||
require_amd64() {
|
|
||||||
local a; a=$(arch)
|
|
||||||
[ "$a" = "amd64" ] && return 0
|
|
||||||
cat >&2 <<EOF
|
|
||||||
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
|
||||||
|
|
||||||
Nothing here would run, so it does not download. To make an ${a} version, the
|
|
||||||
URLs need the ${a} artifact and the checksums need to come from each project's
|
|
||||||
own published list — not from these values, and not from a download you did:
|
|
||||||
|
|
||||||
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
|
||||||
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
|
||||||
|
|
||||||
Edit the pinned block at the top of this file with what those print.
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DL=""
|
|
||||||
pick_downloader() {
|
|
||||||
if command -v curl >/dev/null 2>&1; then DL=curl
|
|
||||||
elif command -v wget >/dev/null 2>&1; then DL=wget
|
|
||||||
else
|
|
||||||
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
|
||||||
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download() {
|
|
||||||
local url="$1" out="$2"
|
|
||||||
case "$DL" in
|
|
||||||
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
|
||||||
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
SHA=""
|
|
||||||
pick_sha() {
|
|
||||||
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
|
||||||
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
|
||||||
else
|
|
||||||
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
|
||||||
echo "Refusing to install unverified binaries." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── package manager, for the instructions only ─────────────────────────────
|
|
||||||
# This never runs a package manager. It names one so the reported action is
|
|
||||||
# something you can paste, on the distro you are actually on — an apt line on
|
|
||||||
# Amazon Linux 2 is a wrong answer dressed up as help.
|
|
||||||
|
|
||||||
pkg_install_cmd() {
|
|
||||||
local pkg="$1"
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
|
||||||
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
|
||||||
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
|
||||||
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
|
||||||
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
|
||||||
else echo "install '$pkg' with this system's package manager"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
docker_pkg() {
|
|
||||||
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── detect ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
|
||||||
case "$(uname -s)" in
|
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
|
||||||
|
|
||||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
|
||||||
|
|
||||||
wsl --install
|
|
||||||
|
|
||||||
That enables Windows features and needs a reboot, so it is not something this
|
|
||||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
|
||||||
this from there.
|
|
||||||
|
|
||||||
See "Starting from plain Windows" in README.md.
|
|
||||||
EOF
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|
||||||
|
|
||||||
detect() {
|
|
||||||
echo "host"
|
|
||||||
echo " kernel $(uname -r)"
|
|
||||||
echo " arch $(arch) ($(uname -m))"
|
|
||||||
|
|
||||||
local osr; osr=$(host_file /etc/os-release)
|
|
||||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
|
||||||
|
|
||||||
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
|
||||||
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
|
||||||
# that is enough depends on the profile, which check.sh knows and this does not.
|
|
||||||
local total_mb avail_mb swap_total_mb swap_used_mb om
|
|
||||||
total_mb=$(mb_of MemTotal)
|
|
||||||
avail_mb=$(mb_of MemAvailable)
|
|
||||||
swap_total_mb=$(mb_of SwapTotal)
|
|
||||||
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
|
||||||
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
|
||||||
if [ "$swap_total_mb" -gt 0 ]; then
|
|
||||||
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
|
||||||
# always says yes and settles up later with the OOM killer, so a cluster that
|
|
||||||
# starts cleanly can still lose processes afterwards.
|
|
||||||
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
|
||||||
case "$om" in
|
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
|
||||||
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo " install to $OUT_BIN"
|
|
||||||
detect_libc
|
|
||||||
detect_prereqs
|
|
||||||
detect_wsl
|
|
||||||
detect_filesystem
|
|
||||||
detect_docker
|
|
||||||
detect_inotify
|
|
||||||
detect_toolchain
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_wsl() {
|
|
||||||
if ! is_wsl; then
|
|
||||||
echo " platform native linux"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " platform WSL"
|
|
||||||
|
|
||||||
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
|
||||||
# host service need it. Enabling it requires a Windows-side restart, which
|
|
||||||
# cannot be issued from inside the distro.
|
|
||||||
local wc; wc=$(host_file /etc/wsl.conf)
|
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
|
||||||
echo " systemd enabled in wsl.conf"
|
|
||||||
else
|
|
||||||
echo " ! systemd not enabled in /etc/wsl.conf"
|
|
||||||
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
|
||||||
[boot]
|
|
||||||
systemd=true
|
|
||||||
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
|
||||||
# local DNS setup.
|
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
|
||||||
echo " resolv.conf pinned (generateResolvConf=false)"
|
|
||||||
else
|
|
||||||
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local wcfg
|
|
||||||
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
|
||||||
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
|
||||||
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
|
||||||
else
|
|
||||||
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
|
||||||
make mem status
|
|
||||||
It prints the edit to make and the command to apply it.")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
|
||||||
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
|
||||||
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
|
||||||
# way. None of them deliver inotify events, so anything watching files goes
|
|
||||||
# quiet without saying why.
|
|
||||||
watch_hostile_fs() {
|
|
||||||
local dir="$1" fstype
|
|
||||||
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
|
||||||
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
|
||||||
case "$fstype" in
|
|
||||||
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
|
||||||
*) echo "" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_filesystem() {
|
|
||||||
local root fstype
|
|
||||||
root=$(cd .. && pwd -P)
|
|
||||||
fstype=$(watch_hostile_fs "$root")
|
|
||||||
if [ -n "$fstype" ]; then
|
|
||||||
echo " ! this directory is on $fstype — file watching will not work"
|
|
||||||
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
|
||||||
on a $fstype mount, and everything else is slower:
|
|
||||||
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
|
||||||
else
|
|
||||||
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
|
||||||
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
|
||||||
#
|
|
||||||
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
|
||||||
#
|
|
||||||
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
|
||||||
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
|
||||||
# version now; `verify` catches the actual failure after installing.
|
|
||||||
detect_libc() {
|
|
||||||
local v=""
|
|
||||||
if command -v ldd >/dev/null 2>&1; then
|
|
||||||
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
|
||||||
fi
|
|
||||||
if [ -z "$v" ]; then
|
|
||||||
echo " libc unknown (no ldd) — 'verify' is the real test"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo " libc glibc $v"
|
|
||||||
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
|
||||||
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
|
||||||
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
|
||||||
echo " Install the core tier, or run tilt from a container."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# What this script needs to do its own job. Reported here so `detect` answers
|
|
||||||
# "will install work?" instead of leaving you to find out one download in.
|
|
||||||
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
|
||||||
detect_prereqs() {
|
|
||||||
local missing=""
|
|
||||||
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
|
||||||
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
|
||||||
echo " checksums ok"
|
|
||||||
else
|
|
||||||
echo " ! no sha256sum or shasum — downloads could not be verified"
|
|
||||||
missing+=" coreutils"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
|
||||||
echo " archives tar + gzip"
|
|
||||||
else
|
|
||||||
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
|
||||||
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
|
||||||
missing+=" tar gzip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$missing" ]; then
|
|
||||||
MANUAL+=("Install what this script needs to run at all:
|
|
||||||
$(pkg_install_cmd "${missing# }")")
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_docker() {
|
|
||||||
# Reachability of the daemon is the real question, and the CLI is only how
|
|
||||||
# we ask it. Note that when this runs inside the installer container, Docker
|
|
||||||
# necessarily exists on the host — otherwise nothing would be executing —
|
|
||||||
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
echo " docker socket present (no cli in this context)"
|
|
||||||
else
|
|
||||||
echo " ! docker not found and no socket at /var/run/docker.sock"
|
|
||||||
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
|
||||||
that needs root:
|
|
||||||
$(pkg_install_cmd "$(docker_pkg)")
|
|
||||||
sudo systemctl enable --now docker
|
|
||||||
sudo usermod -aG docker \"\$USER\"
|
|
||||||
then log out and back in, so the new group applies to your shell.")
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if docker info >/dev/null 2>&1; then
|
|
||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
|
||||||
local n
|
|
||||||
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
|
||||||
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
|
||||||
# function the latter returns 1 when the count is zero, and `set -e`
|
|
||||||
# then kills the caller. That is the fresh-machine case — no clusters
|
|
||||||
# yet — so the bug only ever shows up where it does most harm.
|
|
||||||
if [ "$n" -gt 0 ]; then
|
|
||||||
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
|
||||||
MANUAL+=("Start Docker, or add yourself to the docker group:
|
|
||||||
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
|
||||||
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
|
||||||
detect_inotify() {
|
|
||||||
local w i
|
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
|
||||||
echo " inotify watches=$w instances=$i"
|
|
||||||
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
|
||||||
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
|
||||||
MANUAL+=("Raise inotify limits (needs root on the host):
|
|
||||||
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
|
||||||
| sudo tee /etc/sysctl.d/99-rig.conf
|
|
||||||
sudo sysctl --system")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── fetch ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
|
||||||
resolve_url() {
|
|
||||||
local upstream="$1"
|
|
||||||
case "$DEPS_SOURCE" in
|
|
||||||
upstream) echo "$upstream" ;;
|
|
||||||
artifactory)
|
|
||||||
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
|
||||||
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
|
||||||
;;
|
|
||||||
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
verify() {
|
|
||||||
local file="$1" want="$2" name="$3" got
|
|
||||||
got=$($SHA "$file" | awk '{print $1}')
|
|
||||||
if [ "$got" != "$want" ]; then
|
|
||||||
echo "checksum mismatch for $name" >&2
|
|
||||||
echo " expected $want" >&2
|
|
||||||
echo " got $got" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
|
||||||
fetch_bin() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4"
|
|
||||||
local tmp="$dest/.$name.tmp"
|
|
||||||
echo " fetching $name"
|
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
|
||||||
verify "$tmp" "$sha" "$name"
|
|
||||||
mv "$tmp" "$dest/$name"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
|
||||||
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
|
||||||
# others nest it a directory down — so the caller says which.
|
|
||||||
fetch_tgz() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
|
||||||
local tmp="$dest/.$name.tgz"
|
|
||||||
echo " fetching $name"
|
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
|
||||||
verify "$tmp" "$sha" "$name"
|
|
||||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
|
||||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
|
||||||
# user does not own.
|
|
||||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
|
||||||
rm -f "$tmp"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# The installer runs as root so it can reach the docker socket, which means
|
|
||||||
# everything it writes into a mounted volume lands root-owned and unusable from
|
|
||||||
# the host. Hand it back to whoever owns the mount point (the host user created
|
|
||||||
# that directory before mounting it).
|
|
||||||
fix_ownership() {
|
|
||||||
local dir="$1"
|
|
||||||
[ -d "$dir" ] || return 0
|
|
||||||
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
|
||||||
if [ "$owner" = ":" ]; then
|
|
||||||
owner=$(stat -c '%u:%g' "$dir")
|
|
||||||
fi
|
|
||||||
[ "$owner" = "0:0" ] && return 0
|
|
||||||
chown -R "$owner" "$dir" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Two tiers, because not every machine should get cluster tooling.
|
|
||||||
#
|
|
||||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
|
||||||
# creates one. Appropriate on a managed or corporate-issued machine
|
|
||||||
# where development tools are not wanted by default.
|
|
||||||
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
|
||||||
#
|
|
||||||
# The split exists because "install the toolchain" is not one decision: on a
|
|
||||||
# managed workspace the right answer is kubectl and nothing else.
|
|
||||||
CORE_TOOLS="kubectl jq"
|
|
||||||
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
|
||||||
# has ever invoked it. Add it back the day something actually needs a chart.
|
|
||||||
#
|
|
||||||
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
|
||||||
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
|
||||||
# because it is what wires a cluster to a local registry — without one, an
|
|
||||||
# unqualified image name resolves to docker.io/library/<name> and there is
|
|
||||||
# nothing structural stopping a push there.
|
|
||||||
#
|
|
||||||
# docker-compose is 'dev' for the same reason, and is here because the distro
|
|
||||||
# docker packages ship the daemon and CLI but frequently not the compose
|
|
||||||
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
|
||||||
# working Docker, and nothing about that message names the missing piece.
|
|
||||||
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
|
||||||
|
|
||||||
# ── what is already on this machine ───────────────────────────────────────
|
|
||||||
#
|
|
||||||
# A tool already on PATH at its pinned version is left where it is. Without
|
|
||||||
# this, install downloads a second copy into OUT_BIN and then reports the first
|
|
||||||
# one as shadowed — noise, and wrong, when both are the same version. That is
|
|
||||||
# the normal state of any machine someone set up by hand, whatever directory
|
|
||||||
# they happened to choose.
|
|
||||||
|
|
||||||
pin_of() {
|
|
||||||
case "$1" in
|
|
||||||
kubectl) echo "$KUBECTL_VERSION" ;;
|
|
||||||
jq) echo "$JQ_VERSION" ;;
|
|
||||||
kind) echo "$KIND_VERSION" ;;
|
|
||||||
tilt) echo "$TILT_VERSION" ;;
|
|
||||||
ctlptl) echo "$CTLPTL_VERSION" ;;
|
|
||||||
docker-compose) echo "$COMPOSE_VERSION" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The version string a binary reports. Each tool spells the question
|
|
||||||
# differently, and kubectl has to be told --client or it goes looking for a
|
|
||||||
# server to ask.
|
|
||||||
reported_version() {
|
|
||||||
local tool="$1" path="$2"
|
|
||||||
case "$tool" in
|
|
||||||
kubectl) "$path" version --client 2>/dev/null ;;
|
|
||||||
jq) "$path" --version 2>/dev/null ;;
|
|
||||||
*) "$path" version 2>/dev/null ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
|
||||||
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
|
||||||
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
|
||||||
#
|
|
||||||
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
|
||||||
# on every machine — some builds reject patterns that others accept — and a
|
|
||||||
# failed grep inside a count reads exactly like a zero.
|
|
||||||
version_matches() {
|
|
||||||
local tool="$1" path="$2" pin="$3" out v re
|
|
||||||
out=$(reported_version "$tool" "$path") || return 1
|
|
||||||
v="${pin#v}"
|
|
||||||
v="${v//./\\.}"
|
|
||||||
re="(^|[^0-9.])v?${v}([^0-9.]|\$)"
|
|
||||||
[[ $out =~ $re ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
|
||||||
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
|
||||||
# DIR" must not quietly skip something because this machine happens to have it.
|
|
||||||
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
|
||||||
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
|
||||||
|
|
||||||
# Every tool in the tier with its state, probed once and reported once. What
|
|
||||||
# still needs fetching is left in TOOLCHAIN_NEED for install() to act on.
|
|
||||||
TOOLCHAIN_NEED=""
|
|
||||||
detect_toolchain() {
|
|
||||||
local tier="${TIER:-dev}" b pin path found
|
|
||||||
TOOLCHAIN_NEED=""
|
|
||||||
echo
|
|
||||||
echo "toolchain (pinned, tier '$tier')"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
pin=$(pin_of "$b")
|
|
||||||
path=$(command -v "$b" 2>/dev/null || true)
|
|
||||||
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
|
||||||
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
|
||||||
# has no `docker-compose` to find — and probing only PATH would report it
|
|
||||||
# missing and re-download a copy that is already there. That is the exact
|
|
||||||
# noise the version-aware skip exists to prevent, so ask docker instead.
|
|
||||||
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
|
||||||
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
|
||||||
if [ "${found#v}" = "${pin#v}" ]; then
|
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
|
||||||
else
|
|
||||||
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
|
||||||
"$b" "$pin" "$found"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
fi
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ -z "$path" ]; then
|
|
||||||
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
elif version_matches "$b" "$path" "$pin"; then
|
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
|
||||||
else
|
|
||||||
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
|
||||||
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -z "$TOOLCHAIN_NEED" ]; then
|
|
||||||
echo " every pinned tool is already on PATH — nothing to fetch"
|
|
||||||
else
|
|
||||||
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch() {
|
|
||||||
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--to) dest="$2"; shift 2 ;;
|
|
||||||
core|dev) tier="$1"; shift ;;
|
|
||||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
dest="$(abspath "$dest")"
|
|
||||||
mkdir -p "$dest"
|
|
||||||
TIER="$tier"
|
|
||||||
|
|
||||||
if [ "$DEPS_SOURCE" = "baked" ]; then
|
|
||||||
echo "installing baked binaries from $BAKED_BIN"
|
|
||||||
cp -a "$BAKED_BIN"/. "$dest"/
|
|
||||||
fix_ownership "$dest"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${DEPS_ONLY:-}" ]; then
|
|
||||||
echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)"
|
|
||||||
else
|
|
||||||
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
|
||||||
fi
|
|
||||||
if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi
|
|
||||||
if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi
|
|
||||||
if [ "$tier" = "dev" ]; then
|
|
||||||
if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi
|
|
||||||
if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi
|
|
||||||
if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi
|
|
||||||
if want docker-compose; then
|
|
||||||
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
fix_ownership "$dest"
|
|
||||||
# kind writes the kubeconfig as root too; hand that back as well when it's
|
|
||||||
# a mounted host directory rather than container-local state.
|
|
||||||
fix_ownership "${KUBE_DIR:-/out/kube}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── install ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
report_manual() {
|
|
||||||
echo
|
|
||||||
if [ ${#MANUAL[@]} -eq 0 ]; then
|
|
||||||
echo "nothing left to do by hand."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
|
||||||
echo
|
|
||||||
local n=1
|
|
||||||
for m in "${MANUAL[@]}"; do
|
|
||||||
echo " $n. $m"
|
|
||||||
echo
|
|
||||||
n=$((n + 1))
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installing into a directory that sits early in PATH silently replaces whatever
|
|
||||||
# the machine was already using — which on a shared or client machine can break
|
|
||||||
# unrelated work (kubectl more than one minor away from a cluster is the common
|
|
||||||
# one). Say so; never decide it for them.
|
|
||||||
# Downloading a verified binary proves it is the right file, not that this
|
|
||||||
# machine can run it. On an old distro tilt fails here, with a linker error
|
|
||||||
# about a missing symbol, and finding that out now beats finding out during a
|
|
||||||
# first cluster build.
|
|
||||||
verify_tools() {
|
|
||||||
local tier="${1:-dev}" b bin out rc broke=0
|
|
||||||
echo "checking that each one actually runs"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
bin="$OUT_BIN/$b"
|
|
||||||
if [ ! -x "$bin" ]; then
|
|
||||||
printf ' %-14s not installed\n' "$b"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
|
||||||
# than one line gets SIGPIPE when head closes the pipe, and the
|
|
||||||
# pipeline reports 141 — so a working kubectl was announced as "does
|
|
||||||
# not run here", with its own correct version string as the evidence.
|
|
||||||
# Take the first line afterwards, from the string.
|
|
||||||
rc=0
|
|
||||||
case "$b" in
|
|
||||||
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
|
||||||
jq) out=$("$bin" --version 2>&1) || rc=$? ;;
|
|
||||||
*) out=$("$bin" version 2>&1) || rc=$? ;;
|
|
||||||
esac
|
|
||||||
out=${out%%$'\n'*}
|
|
||||||
if [ "$rc" -eq 0 ]; then
|
|
||||||
printf ' %-14s %s\n' "$b" "$out"
|
|
||||||
else
|
|
||||||
printf ' ! %-12s does not run here: %s\n' "$b" "$out"
|
|
||||||
broke=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$broke" -eq 1 ]; then
|
|
||||||
echo
|
|
||||||
echo " A binary that downloads and verifies but will not start is almost"
|
|
||||||
echo " always this distro's libc being older than the release needs."
|
|
||||||
echo " 'detect' prints the glibc version. The core tier (kubectl + jq)"
|
|
||||||
echo " has no such dependency and will work regardless."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
list() {
|
|
||||||
echo "pinned, linux/amd64 only:"
|
|
||||||
printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION"
|
|
||||||
printf ' %-14s %s\n' jq "$JQ_VERSION"
|
|
||||||
printf ' %-14s %s\n' kind "$KIND_VERSION"
|
|
||||||
printf ' %-14s %s\n' tilt "$TILT_VERSION"
|
|
||||||
printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION"
|
|
||||||
printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION"
|
|
||||||
echo
|
|
||||||
echo " core = $CORE_TOOLS"
|
|
||||||
echo " dev = $CORE_TOOLS $DEV_TOOLS"
|
|
||||||
echo
|
|
||||||
echo "Checksums are pinned in the block at the top of this file. To bump one,"
|
|
||||||
echo "take the new checksum from the publisher's own release list — the header"
|
|
||||||
echo "comment has the exact commands."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
|
||||||
|
|
||||||
warn_shadowing() {
|
|
||||||
local b existing shadowed="" tier="${1:-dev}"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
[ -x "$OUT_BIN/$b" ] || continue
|
|
||||||
# Where would this resolve if OUT_BIN weren't in the way?
|
|
||||||
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
|
||||||
command -v "$b" 2>/dev/null || true)
|
|
||||||
[ -n "$existing" ] || continue
|
|
||||||
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
|
||||||
# The same version in both places is not a conflict: nothing changes for
|
|
||||||
# any other project whichever copy PATH happens to find first.
|
|
||||||
if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi
|
|
||||||
shadowed+=" $b $existing"$'\n'
|
|
||||||
done
|
|
||||||
|
|
||||||
[ -n "$shadowed" ] || return 0
|
|
||||||
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
|
||||||
printf '%s' "$shadowed"
|
|
||||||
echo " Other projects on this machine will pick up the new versions."
|
|
||||||
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
|
||||||
was just installed:
|
|
||||||
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
|
||||||
Or install somewhere private instead:
|
|
||||||
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
|
||||||
}
|
|
||||||
|
|
||||||
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
|
||||||
# retired v1 spelling; every compose file written in the last few years assumes
|
|
||||||
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
|
||||||
# So the binary is fetched like any other and then linked, in your own home —
|
|
||||||
# no root, and nothing outside it.
|
|
||||||
install_compose_plugin() {
|
|
||||||
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
|
||||||
[ -x "$src" ] || return 0
|
|
||||||
mkdir -p "$dir"
|
|
||||||
# Something else already owns that name — docker-desktop and some distro
|
|
||||||
# packages install a real file there. Overwriting it would take the plugin
|
|
||||||
# away from whatever put it there, so say so and let the user decide.
|
|
||||||
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
|
||||||
MANUAL+=("Something already installs the compose plugin at
|
|
||||||
$dir/docker-compose
|
|
||||||
To use rig's pinned build instead:
|
|
||||||
ln -sf $src $dir/docker-compose")
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
ln -sfn "$src" "$dir/docker-compose"
|
|
||||||
echo " compose plugin -> $dir/docker-compose"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
install() {
|
|
||||||
local tier="${1:-dev}" b
|
|
||||||
TIER="$tier"
|
|
||||||
detect
|
|
||||||
|
|
||||||
# detect_toolchain has already probed PATH. Fetch only what it found missing
|
|
||||||
# or at the wrong version; a tool already present at its pin stays where it is.
|
|
||||||
if [ -n "$TOOLCHAIN_NEED" ]; then
|
|
||||||
echo
|
|
||||||
DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier"
|
|
||||||
echo
|
|
||||||
echo "installed to $OUT_BIN ($tier):"
|
|
||||||
for b in $TOOLCHAIN_NEED; do
|
|
||||||
if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi
|
|
||||||
done
|
|
||||||
if [ "$tier" = "core" ]; then
|
|
||||||
echo " (no kind/tilt — 'make deps dev' adds them)"
|
|
||||||
fi
|
|
||||||
# Only when compose was one of the things fetched: linking a binary
|
|
||||||
# that is already satisfied elsewhere on PATH would point the plugin at
|
|
||||||
# a copy rig did not install.
|
|
||||||
case " $TOOLCHAIN_NEED " in
|
|
||||||
*" docker-compose "*) install_compose_plugin ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Only worth saying when something actually landed in OUT_BIN. When every
|
|
||||||
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
|
||||||
# telling the user to add it would be advice to fix nothing.
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
|
||||||
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
warn_shadowing "$tier"
|
|
||||||
|
|
||||||
report_manual
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
require_linux
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
cmd="${1:-install}"
|
|
||||||
[ $# -gt 0 ] && shift
|
|
||||||
|
|
||||||
# Baked mode copies binaries already in the image, so it needs no downloader.
|
|
||||||
need_downloads() {
|
|
||||||
require_amd64
|
|
||||||
if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi
|
|
||||||
pick_sha
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$cmd" in
|
|
||||||
detect) detect; report_manual ;;
|
|
||||||
list) list ;;
|
|
||||||
verify) verify_tools "${1:-dev}" ;;
|
|
||||||
fetch) need_downloads; fetch "$@" ;;
|
|
||||||
install) need_downloads; install "${1:-dev}" ;;
|
|
||||||
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
|
||||||
echo " install [core|dev] (default dev)" >&2
|
|
||||||
echo " fetch [core|dev] [--to DIR]" >&2
|
|
||||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,859 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# rigmini.sh for profile 'client', flattened from:
|
|
||||||
# ctrl/mem.sh
|
|
||||||
# ctrl/lib/config.sh
|
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
|
||||||
|
|
||||||
# ── from the libraries ──
|
|
||||||
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;
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [ -n "$line" ]; then
|
|
||||||
eval "export $line";
|
|
||||||
fi;
|
|
||||||
done <<< "$1";
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
default_cluster_name ()
|
|
||||||
{
|
|
||||||
local n;
|
|
||||||
n=$(basename "$(cd .. && pwd)");
|
|
||||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
|
||||||
echo "${n:-rig}"
|
|
||||||
}
|
|
||||||
derive_port_base ()
|
|
||||||
{
|
|
||||||
local h;
|
|
||||||
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
|
||||||
echo $((20000 + (h % 200) * 10))
|
|
||||||
}
|
|
||||||
render_kind_config ()
|
|
||||||
{
|
|
||||||
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_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'client' ──
|
|
||||||
load_config() {
|
|
||||||
local k saved=""
|
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
|
||||||
done
|
|
||||||
declare -gx ADDONS="metallb cert-manager metrics-server"
|
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
|
||||||
declare -g AUDIT="on"
|
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
|
||||||
declare -g CLUSTER="rig"
|
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
|
||||||
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
|
||||||
declare -gx COMPOSE_VERSION="5.5.1"
|
|
||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
|
||||||
declare -gx DNS_MODE="hosts"
|
|
||||||
declare -g HTTPS_PORT="20311"
|
|
||||||
declare -g HTTP_PORT="20310"
|
|
||||||
declare -gx INGRESS_MODE="hostport"
|
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
|
||||||
declare -gx K8S_VERSION="v1_36"
|
|
||||||
declare -gx KIND_CONFIG="kind-config.client.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.client.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.client.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
|
||||||
declare -g KUBECONTEXT="kind-rig"
|
|
||||||
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="ctrl/k8s/overlays/dev"
|
|
||||||
declare -gx METALLB_VERSION="v0.16.0"
|
|
||||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
|
||||||
declare -g NODES="3"
|
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
|
||||||
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
|
||||||
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
|
||||||
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 -gx POSTGRES_IMAGE="postgres:16-alpine"
|
|
||||||
declare -gx PROFILE_NAME="client"
|
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
|
||||||
declare -gx REGISTRY_MODE="mirror"
|
|
||||||
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"
|
|
||||||
declare -gx TILT_VERSION="0.37.6"
|
|
||||||
_config_restore "$saved"
|
|
||||||
}
|
|
||||||
# ── end of frozen configuration ──
|
|
||||||
|
|
||||||
# ── ctrl/mem.sh ──
|
|
||||||
# How much memory this machine will actually give you before something dies —
|
|
||||||
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
|
||||||
#
|
|
||||||
# There are two numbers and they are rarely the same. `status` reports what the
|
|
||||||
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
|
||||||
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
|
||||||
# against what this profile's cluster needs.
|
|
||||||
#
|
|
||||||
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
|
||||||
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
|
||||||
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
|
||||||
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
|
||||||
# on a box that OOMs at 2.
|
|
||||||
#
|
|
||||||
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
|
||||||
# allocation that can be raised, and the commonest failure is raising it without
|
|
||||||
# restarting — so status compares what .wslconfig says with what actually booted.
|
|
||||||
#
|
|
||||||
# Reports and instructs. It never raises a limit, frees anything or installs a
|
|
||||||
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
|
||||||
# itself, so that `restore` has something to put back after a hand edit.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# mem.sh status what it has, what caps it
|
|
||||||
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
|
||||||
# mem.sh all [--budget GB] both, then the verdict
|
|
||||||
# mem.sh backup | restore .wslconfig, WSL only
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
# (sourced library inlined above)
|
|
||||||
|
|
||||||
# ── defaults ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
STEP_MB=0 # per allocation; 0 means scale it to the ceiling. See push().
|
|
||||||
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
|
||||||
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
|
||||||
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
|
||||||
BUDGET_GB="" # --budget; empty means what this profile's cluster needs, from rig.
|
|
||||||
BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess below.
|
|
||||||
|
|
||||||
# ── platform ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
|
||||||
case "$(uname -s)" in
|
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
|
||||||
|
|
||||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
|
||||||
|
|
||||||
wsl --install
|
|
||||||
|
|
||||||
That enables Windows features and needs a reboot, so it is not something this
|
|
||||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
|
||||||
this from there.
|
|
||||||
EOF
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Everything below reads /proc. Without it there is nothing to measure, and
|
|
||||||
# failing here beats printing a page of empty fields.
|
|
||||||
if [ ! -r /proc/meminfo ]; then
|
|
||||||
echo "no readable /proc/meminfo — this needs a Linux kernel." >&2
|
|
||||||
echo "On macOS or a BSD none of the numbers below exist." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|
||||||
|
|
||||||
is_container() {
|
|
||||||
[ -f /.dockerenv ] && return 0
|
|
||||||
grep -qE '(docker|containerd|kubepods|lxc|podman)' /proc/1/cgroup 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
platform() {
|
|
||||||
if is_wsl; then echo WSL
|
|
||||||
elif is_container; then echo container
|
|
||||||
else echo "native linux"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── reading memory ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
|
||||||
|
|
||||||
# MemAvailable arrived in kernel 3.14. Older kernels — and they turn up on
|
|
||||||
# corporate images — need the estimate it replaced, which is worse but not wrong.
|
|
||||||
avail_meminfo_mb() {
|
|
||||||
if grep -q '^MemAvailable:' /proc/meminfo; then
|
|
||||||
mb MemAvailable
|
|
||||||
else
|
|
||||||
awk '/^(MemFree|Buffers|Cached):/{t+=$2} END{print int(t/1024)}' /proc/meminfo
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
|
||||||
# find_cgroup, because every later reading needs both and hunting for the files
|
|
||||||
# on each call would be the slow part of the poll loop.
|
|
||||||
CG_MAX_FILE=""
|
|
||||||
CG_CUR_FILE=""
|
|
||||||
CG_VERSION=""
|
|
||||||
|
|
||||||
find_cgroup() {
|
|
||||||
local rel
|
|
||||||
|
|
||||||
# Inside a container the cgroup namespace makes the top of the tree BE the
|
|
||||||
# container's own cgroup, so the unqualified path is already the right one.
|
|
||||||
# On a host it is the root cgroup, which is never limited — hence the second
|
|
||||||
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
|
||||||
if [ -r /sys/fs/cgroup/memory.max ]; then
|
|
||||||
CG_VERSION=v2
|
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
|
||||||
CG_CUR_FILE=/sys/fs/cgroup/memory.current
|
|
||||||
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
|
|
||||||
CG_VERSION=v1
|
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory/memory.limit_in_bytes
|
|
||||||
CG_CUR_FILE=/sys/fs/cgroup/memory/memory.usage_in_bytes
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel=$(awk -F: '$1=="0"{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
|
||||||
if [ -n "$rel" ] && [ "$rel" != "/" ] && [ -r "/sys/fs/cgroup${rel}/memory.max" ]; then
|
|
||||||
CG_VERSION=v2
|
|
||||||
CG_MAX_FILE="/sys/fs/cgroup${rel}/memory.max"
|
|
||||||
CG_CUR_FILE="/sys/fs/cgroup${rel}/memory.current"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel=$(awk -F: '$2 ~ /(^|,)memory(,|$)/{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
|
||||||
if [ -n "$rel" ] && [ "$rel" != "/" ] \
|
|
||||||
&& [ -r "/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes" ]; then
|
|
||||||
CG_VERSION=v1
|
|
||||||
CG_MAX_FILE="/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes"
|
|
||||||
CG_CUR_FILE="/sys/fs/cgroup/memory${rel}/memory.usage_in_bytes"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
|
||||||
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
|
||||||
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
|
||||||
# own memory is not a limit, however it is written.
|
|
||||||
cgroup_cap_mb() {
|
|
||||||
local raw cap
|
|
||||||
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
|
||||||
raw=$(cat "$CG_MAX_FILE" 2>/dev/null || echo max)
|
|
||||||
[ "$raw" = "max" ] && { echo ""; return 0; }
|
|
||||||
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
cap=$((raw / 1024 / 1024))
|
|
||||||
[ "$cap" -ge "$(mb MemTotal)" ] && { echo ""; return 0; }
|
|
||||||
echo "$cap"
|
|
||||||
}
|
|
||||||
|
|
||||||
cgroup_used_mb() {
|
|
||||||
local raw
|
|
||||||
[ -n "$CG_CUR_FILE" ] && [ -r "$CG_CUR_FILE" ] || { echo ""; return 0; }
|
|
||||||
raw=$(cat "$CG_CUR_FILE" 2>/dev/null || echo "")
|
|
||||||
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
echo $((raw / 1024 / 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
# ulimit -v is a per-process address-space cap. It stops YOU long before the box
|
|
||||||
# does, and because it is inherited from a login shell it is easy to hit without
|
|
||||||
# knowing it is set.
|
|
||||||
ulimit_v_mb() {
|
|
||||||
local v; v=$(ulimit -v 2>/dev/null || echo unlimited)
|
|
||||||
[ "$v" = "unlimited" ] && { echo ""; return 0; }
|
|
||||||
case "$v" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
echo $((v / 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
# The number everything else is about: the lowest of the things that can stop
|
|
||||||
# you. Printed at the end of `status` and used as the sanity bound in `push`.
|
|
||||||
effective_ceiling_mb() {
|
|
||||||
local c; c=$(mb MemTotal)
|
|
||||||
local cap; cap=$(cgroup_cap_mb)
|
|
||||||
local ul; ul=$(ulimit_v_mb)
|
|
||||||
[ -n "$cap" ] && [ "$cap" -lt "$c" ] && c="$cap"
|
|
||||||
[ -n "$ul" ] && [ "$ul" -lt "$c" ] && c="$ul"
|
|
||||||
echo "$c"
|
|
||||||
}
|
|
||||||
|
|
||||||
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
|
||||||
# In a capped container /proc/meminfo describes the host and is worse than
|
|
||||||
# useless for this — it would report tens of gigabytes free on a box that is one
|
|
||||||
# allocation from being killed.
|
|
||||||
headroom_mb() {
|
|
||||||
local cap used
|
|
||||||
cap=$(cgroup_cap_mb)
|
|
||||||
used=$(cgroup_used_mb)
|
|
||||||
if [ -n "$cap" ] && [ -n "$used" ]; then
|
|
||||||
echo $(( cap - used ))
|
|
||||||
else
|
|
||||||
avail_meminfo_mb
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── status ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
|
||||||
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
|
||||||
# Windows, then fall back to whichever profile actually owns a config.
|
|
||||||
wslconfig_path() {
|
|
||||||
local profile winpath found
|
|
||||||
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
|
||||||
case "$profile" in
|
|
||||||
""|*%*) ;;
|
|
||||||
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
|
||||||
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
|
||||||
echo "$winpath/.wslconfig"; return 0
|
|
||||||
fi ;;
|
|
||||||
esac
|
|
||||||
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
|
||||||
[ -n "$found" ] && echo "$found"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
hogs() {
|
|
||||||
echo " holding the most:"
|
|
||||||
ps -eo rss,comm --sort=-rss 2>/dev/null \
|
|
||||||
| awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
status() {
|
|
||||||
local total avail swap_total swap_free cap ul cur
|
|
||||||
|
|
||||||
echo "host"
|
|
||||||
echo " platform $(platform)"
|
|
||||||
echo " kernel $(uname -r)"
|
|
||||||
[ -r /etc/os-release ] && \
|
|
||||||
echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' /etc/os-release)"
|
|
||||||
echo " cpu $(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '?') online, load $(cut -d' ' -f1-3 /proc/loadavg)"
|
|
||||||
|
|
||||||
# ── the caps first, because they decide what the totals below are worth ──
|
|
||||||
echo
|
|
||||||
echo "caps"
|
|
||||||
cap=$(cgroup_cap_mb)
|
|
||||||
if [ -n "$cap" ]; then
|
|
||||||
cur=$(cgroup_used_mb)
|
|
||||||
echo " cgroup ${cap} MB (${CG_VERSION}, ${CG_CUR_FILE##*/} says ${cur:-?} MB used)"
|
|
||||||
echo " ! /proc/meminfo below describes the HOST, not this cgroup."
|
|
||||||
echo " $(mb MemTotal) MB total is not yours; ${cap} MB is."
|
|
||||||
elif [ -n "$CG_VERSION" ]; then
|
|
||||||
echo " cgroup none (${CG_VERSION} present, no memory limit set)"
|
|
||||||
else
|
|
||||||
echo " cgroup no memory controller found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ul=$(ulimit_v_mb)
|
|
||||||
if [ -n "$ul" ]; then
|
|
||||||
echo " ! ulimit -v ${ul} MB — a per-process cap, inherited from your shell"
|
|
||||||
echo " it stops this process long before the machine runs out"
|
|
||||||
else
|
|
||||||
echo " ulimit -v unlimited"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# overcommit_memory=0 is the default heuristic: a large allocation is
|
|
||||||
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
|
||||||
# than as a failed malloc. It is why `push` touches every page it asks for.
|
|
||||||
local om or_
|
|
||||||
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
|
||||||
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
|
||||||
case "$om" in
|
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess," ;;
|
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit," ;;
|
|
||||||
2) echo " overcommit 2 strict (ratio ${or_}%) — allocation fails honestly instead of killing later," ;;
|
|
||||||
*) echo " overcommit ${om}" ;;
|
|
||||||
esac
|
|
||||||
[ "$om" != "?" ] && echo " so RSS is the number to trust, not what a process asked for"
|
|
||||||
|
|
||||||
# ── what it says it has ──
|
|
||||||
total=$(mb MemTotal); avail=$(avail_meminfo_mb)
|
|
||||||
swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree)
|
|
||||||
echo
|
|
||||||
echo "memory"
|
|
||||||
echo " total ${total} MB"
|
|
||||||
echo " available ${avail} MB"
|
|
||||||
echo " swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
|
||||||
if [ "$swap_total" -eq 0 ]; then
|
|
||||||
echo " - no swap: this box has no cushion. It goes from fine to OOM-killed"
|
|
||||||
echo " with nothing in between, which is the abrupt failure you get in a VM."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# postgres puts its shared buffers in /dev/shm. Docker's default is 64 MB,
|
|
||||||
# and the resulting failure names neither shm nor the size.
|
|
||||||
if [ -d /dev/shm ]; then
|
|
||||||
local shm; shm=$(df -Pm /dev/shm 2>/dev/null | awk 'NR==2{print $2}')
|
|
||||||
if [ -n "$shm" ]; then
|
|
||||||
if [ "$shm" -le 64 ]; then
|
|
||||||
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
|
||||||
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
|
||||||
else
|
|
||||||
echo " /dev/shm ${shm} MB"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "disk"
|
|
||||||
local d
|
|
||||||
for d in / /tmp /var/lib/docker; do
|
|
||||||
[ -d "$d" ] || continue
|
|
||||||
df -Pm "$d" 2>/dev/null | awk -v p="$d" 'NR==2{printf " %-12s %s MB free of %s MB\n", p, $4, $2}'
|
|
||||||
done
|
|
||||||
|
|
||||||
# kind and Tilt both watch large trees, and the failure mode is silent:
|
|
||||||
# they simply stop noticing file changes. Cheap to report while we are here.
|
|
||||||
local w i
|
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
|
||||||
echo
|
|
||||||
echo "tooling"
|
|
||||||
echo " inotify watches=$w instances=$i"
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
|
||||||
echo " ! low — anything watching files will silently stop seeing changes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
echo " docker socket present, no cli"
|
|
||||||
else
|
|
||||||
echo " docker not installed"
|
|
||||||
fi
|
|
||||||
elif docker info >/dev/null 2>&1; then
|
|
||||||
local n
|
|
||||||
n=$(docker ps -q 2>/dev/null | wc -l)
|
|
||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null), ${n} container(s) running"
|
|
||||||
else
|
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
|
||||||
# not usefully apply — the change costs a full VM restart. Report it, and
|
|
||||||
# report the commonest mistake, which is editing it and not restarting.
|
|
||||||
if is_wsl; then
|
|
||||||
local cfg conf conf_mb n
|
|
||||||
cfg=$(wslconfig_path)
|
|
||||||
echo
|
|
||||||
echo "wsl"
|
|
||||||
if [ -z "$cfg" ]; then
|
|
||||||
echo " ! cannot tell which Windows profile owns .wslconfig"
|
|
||||||
else
|
|
||||||
echo " config $cfg"
|
|
||||||
conf=$(configured_memory "$cfg")
|
|
||||||
if [ -n "$conf" ]; then
|
|
||||||
conf_mb=$(to_mb "$conf")
|
|
||||||
echo " configured $conf (${conf_mb} MB), booted ${total} MB"
|
|
||||||
# The VM reports a little less than allocated; 15% covers the
|
|
||||||
# kernel without calling every healthy machine a mismatch.
|
|
||||||
if [ -n "$conf_mb" ] && [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
|
||||||
echo " ! configured ${conf_mb} MB but booted ${total} MB — not applied yet."
|
|
||||||
echo " From a WINDOWS terminal: wsl --shutdown then start the distro again."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " configured no memory= set (WSL defaults to 50% of host RAM, or 8 GB,"
|
|
||||||
echo " whichever is less). To raise it, add on the Windows side:"
|
|
||||||
echo " [wsl2]"
|
|
||||||
echo " memory=8GB"
|
|
||||||
echo " then from a WINDOWS terminal: wsl --shutdown"
|
|
||||||
fi
|
|
||||||
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
|
||||||
if [ "$n" -gt 0 ]; then
|
|
||||||
echo " backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo " - native linux: no VM allocation to raise. If memory is tight the levers"
|
|
||||||
echo " are freeing something or adding swap."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "effective ceiling $(effective_ceiling_mb) MB"
|
|
||||||
echo " the lowest of MemTotal, the cgroup cap and ulimit -v. What the box"
|
|
||||||
echo " claims. 'push' measures what it will actually hand over."
|
|
||||||
|
|
||||||
[ "$avail" -lt $(( total / 5 )) ] && { echo; hogs; }
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── .wslconfig ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
require_wsl() {
|
|
||||||
if ! is_wsl; then
|
|
||||||
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
|
||||||
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
|
||||||
echo "Use 'status' to see what the machine actually has." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# backup and restore act on the file, so unlike status they must not guess.
|
|
||||||
wslconfig_required() {
|
|
||||||
local cfg; cfg=$(wslconfig_required)
|
|
||||||
if [ -z "$cfg" ]; then
|
|
||||||
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
|
||||||
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
|
||||||
| grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "$cfg"
|
|
||||||
}
|
|
||||||
|
|
||||||
configured_memory() {
|
|
||||||
[ -r "$1" ] || { echo ""; return; }
|
|
||||||
sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
# "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo.
|
|
||||||
to_mb() {
|
|
||||||
local v="${1^^}" n
|
|
||||||
n=$(echo "$v" | tr -dc '0-9')
|
|
||||||
[ -n "$n" ] || { echo ""; return; }
|
|
||||||
case "$v" in
|
|
||||||
*GB|*G) echo $(( n * 1024 )) ;;
|
|
||||||
*MB|*M) echo "$n" ;;
|
|
||||||
*) echo $(( n / 1024 / 1024 )) ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
backup() {
|
|
||||||
require_wsl backup
|
|
||||||
local cfg dest
|
|
||||||
cfg=$(wslconfig_required)
|
|
||||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
|
||||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
|
||||||
# second run is not a backup.
|
|
||||||
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
|
||||||
cp "$cfg" "$dest"
|
|
||||||
echo "backed up $dest"
|
|
||||||
echo
|
|
||||||
echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
require_wsl restore
|
|
||||||
local cfg newest count
|
|
||||||
cfg=$(wslconfig_required)
|
|
||||||
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
|
||||||
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
|
||||||
|
|
||||||
echo "restoring $newest"
|
|
||||||
echo " -> $cfg"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Newest is the right default — undo the last edit — but if you backed up
|
|
||||||
# *after* editing, the state you want is older. Show the rest so a no-op
|
|
||||||
# restore is obviously a no-op rather than a mystery.
|
|
||||||
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
|
||||||
if [ "$count" -gt 1 ]; then
|
|
||||||
echo "$count backups exist, newest first:"
|
|
||||||
ls -t "$cfg".*.bak | sed 's/^/ /'
|
|
||||||
echo " (restoring the newest; copy another by hand to pick an older one)"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -r "$cfg" ]; then
|
|
||||||
echo "what changes:"
|
|
||||||
if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then
|
|
||||||
echo " nothing — that backup is identical to the current config"
|
|
||||||
else
|
|
||||||
sed 's/^/ /' /tmp/mem.diff
|
|
||||||
fi
|
|
||||||
rm -f /tmp/mem.diff
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "proceed? [y/N] "
|
|
||||||
read -r reply
|
|
||||||
case "$reply" in
|
|
||||||
y|Y|yes|Yes) ;;
|
|
||||||
*) echo "left alone"; return 0 ;;
|
|
||||||
esac
|
|
||||||
cp "$newest" "$cfg"
|
|
||||||
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── push ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
STATE=""
|
|
||||||
CHILD=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [ -n "$CHILD" ] && kill -0 "$CHILD" 2>/dev/null; then
|
|
||||||
kill -KILL "$CHILD" 2>/dev/null || true
|
|
||||||
wait "$CHILD" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
[ -n "$STATE" ] && rm -f "$STATE"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# The child allocates and stops itself; the parent only watches. That split is
|
|
||||||
# the point: under --to-oom the allocating process is expected to be killed, and
|
|
||||||
# something has to survive to say how far it got.
|
|
||||||
allocator() {
|
|
||||||
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
|
||||||
# first. Raising needs no privilege (only lowering does). Without it, the
|
|
||||||
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
|
||||||
# box you are still using, that is not an acceptable coin toss.
|
|
||||||
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
|
||||||
|
|
||||||
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
|
||||||
local bytes=$((STEP_MB * 1024 * 1024))
|
|
||||||
local swap_used_start
|
|
||||||
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
|
||||||
|
|
||||||
while :; do
|
|
||||||
# Written STRAIGHT INTO the array element. The obvious spelling —
|
|
||||||
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
|
||||||
# not one: the template stays resident, expanding "$chunk" makes a
|
|
||||||
# temporary word, and the append makes the element. A 128 MB step then
|
|
||||||
# needs 384 MB transiently, and on a small box it is killed on the
|
|
||||||
# first append while reporting a third of the true ceiling.
|
|
||||||
#
|
|
||||||
# printf -v into a subscript also means every page is written, so it is
|
|
||||||
# resident rather than merely promised — the only kind of allocation
|
|
||||||
# that measures anything under heuristic overcommit.
|
|
||||||
printf -v "arr[$i]" '%*s' "$bytes" ''
|
|
||||||
i=$((i + 1)); held=$((held + STEP_MB))
|
|
||||||
|
|
||||||
rss=$(awk '/^VmRSS:/{print int($2/1024)}' "/proc/$BASHPID/status" 2>/dev/null || echo 0)
|
|
||||||
avail=$(headroom_mb)
|
|
||||||
swapped=$(( $(mb SwapTotal) - $(mb SwapFree) - swap_used_start ))
|
|
||||||
[ "$swapped" -lt 0 ] && swapped=0
|
|
||||||
|
|
||||||
printf '%8s MB held rss %7s MB headroom %7s MB swap +%s MB\n' \
|
|
||||||
"$held" "$rss" "$avail" "$swapped"
|
|
||||||
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
|
||||||
|
|
||||||
# Worth calling out separately from the ceiling: this is where the box
|
|
||||||
# stops being fast and starts being unusable, which for a scheduler is
|
|
||||||
# a different and earlier problem than being killed.
|
|
||||||
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
|
||||||
first_swap=$held
|
|
||||||
echo " - first swap page at ${held} MB — past here it works but crawls"
|
|
||||||
echo "swapat $held" >> "$STATE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$TO_MB" ] && [ "$held" -ge "$TO_MB" ]; then
|
|
||||||
echo "stop reached-the-cap" >> "$STATE"; return 0
|
|
||||||
fi
|
|
||||||
if [ "$TO_OOM" = no ] && [ "$avail" -lt "$FLOOR_MB" ]; then
|
|
||||||
echo "stop floor" >> "$STATE"; return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
push() {
|
|
||||||
local total ceiling rc=0 last held rss swapat stop
|
|
||||||
total=$(mb MemTotal)
|
|
||||||
ceiling=$(effective_ceiling_mb)
|
|
||||||
|
|
||||||
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
|
||||||
# find the edge, few enough lines to read, and small enough that the
|
|
||||||
# transient cost of one allocation never dominates a small box. A fixed
|
|
||||||
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
|
||||||
if [ "$STEP_EXPLICIT" = no ]; then
|
|
||||||
STEP_MB=$(( ceiling / 64 ))
|
|
||||||
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
|
||||||
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
|
||||||
# depends on what it is protecting. Under a cgroup cap, running out kills
|
|
||||||
# only this container's own processes, so it need cover no more than the
|
|
||||||
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
|
||||||
# halve the answer. On a host there is everything else to protect, and the
|
|
||||||
# OOM killer does not promise to pick the process that caused the problem.
|
|
||||||
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
|
||||||
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
|
||||||
|
|
||||||
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
|
||||||
trap cleanup EXIT
|
|
||||||
# INT kills the child and lets the summary below print anyway, so an
|
|
||||||
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
|
||||||
# still gives the memory back.
|
|
||||||
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
|
||||||
|
|
||||||
echo "push"
|
|
||||||
echo " step ${STEP_MB} MB per allocation, every page touched"
|
|
||||||
echo " ceiling ${ceiling} MB claimed"
|
|
||||||
if [ -n "$TO_MB" ]; then
|
|
||||||
echo " stopping at ${TO_MB} MB (--to)"
|
|
||||||
elif [ "$TO_OOM" = yes ]; then
|
|
||||||
echo " ! stopping only when the kernel stops it (--to-oom)"
|
|
||||||
echo " the allocating child is marked as the preferred OOM victim,"
|
|
||||||
echo " but nothing about an OOM kill is entirely polite. Not on a box"
|
|
||||||
echo " running anything you mind losing."
|
|
||||||
else
|
|
||||||
echo " stopping when headroom drops below ${FLOOR_MB} MB"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
allocator &
|
|
||||||
CHILD=$!
|
|
||||||
wait "$CHILD" || rc=$?
|
|
||||||
CHILD=""
|
|
||||||
trap - INT
|
|
||||||
|
|
||||||
last=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 || true)
|
|
||||||
held=$(echo "$last" | awk '{print $1}')
|
|
||||||
rss=$(echo "$last" | awk '{print $2}')
|
|
||||||
swapat=$(awk '/^swapat/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
|
||||||
stop=$(awk '/^stop/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [ -z "$held" ]; then
|
|
||||||
echo " ! nothing was allocated. Even one ${STEP_MB} MB chunk failed —"
|
|
||||||
echo " try a smaller --step, or check ulimit -v in 'status'."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " reached ${rss:-$held} MB resident"
|
|
||||||
[ -n "$swapat" ] && echo " swapping from ${swapat} MB"
|
|
||||||
|
|
||||||
case "$stop" in
|
|
||||||
reached-the-cap)
|
|
||||||
echo " outcome stopped at the --to cap, not at a limit."
|
|
||||||
echo " The box held ${TO_MB} MB without complaint; there is more." ;;
|
|
||||||
floor)
|
|
||||||
echo " outcome stopped with a cushion intact, by choice."
|
|
||||||
echo " The real ceiling is higher — --to-oom finds it, at the"
|
|
||||||
echo " cost of an actual OOM kill." ;;
|
|
||||||
interrupted)
|
|
||||||
echo " outcome interrupted at ${rss:-$held} MB — where you stopped it,"
|
|
||||||
echo " not where the box did." ;;
|
|
||||||
*)
|
|
||||||
# No stop line means the child did not decide to stop: it was ended.
|
|
||||||
if [ "$rc" -ge 128 ]; then
|
|
||||||
echo " outcome the child was killed (signal $((rc - 128))) at ${rss:-$held} MB."
|
|
||||||
elif [ "$rc" -ne 0 ]; then
|
|
||||||
echo " outcome the allocation failed at ${rss:-$held} MB (exit ${rc})."
|
|
||||||
echo " bash could not get the next chunk — an honest malloc"
|
|
||||||
echo " failure rather than a kill. That is the strict-overcommit"
|
|
||||||
echo " or ulimit path."
|
|
||||||
else
|
|
||||||
echo " outcome ended at ${rss:-$held} MB."
|
|
||||||
fi
|
|
||||||
local ev
|
|
||||||
ev=$(dmesg 2>/dev/null | tail -80 | grep -iE 'oom-kill|killed process' | tail -1 || true)
|
|
||||||
if [ -n "$ev" ]; then
|
|
||||||
echo " kernel ${ev#*] }"
|
|
||||||
else
|
|
||||||
echo " - dmesg is unreadable here (dmesg_restrict, or no privilege),"
|
|
||||||
echo " so the kill cannot be confirmed from this side. The number stands."
|
|
||||||
fi ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# The gap between the claim and the measurement is the finding — but only
|
|
||||||
# when the BOX chose where to stop. An empty $stop means the child was ended
|
|
||||||
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
|
||||||
# asked for, and flagging those as short of the ceiling would put a warning
|
|
||||||
# on every deliberately small run.
|
|
||||||
local got="${rss:-$held}"
|
|
||||||
echo
|
|
||||||
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
|
||||||
echo " ! claimed ${ceiling} MB, gave up ${got} MB — under 70% of it."
|
|
||||||
echo " Something is taking the difference. 'status' names the candidates:"
|
|
||||||
echo " a cgroup cap, ulimit -v, or memory already resident."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── all ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
all() {
|
|
||||||
status
|
|
||||||
echo
|
|
||||||
echo "────────────────────────────────────────────────────────────"
|
|
||||||
echo
|
|
||||||
push
|
|
||||||
|
|
||||||
local got budget_mb ceiling
|
|
||||||
load_config
|
|
||||||
if [ -n "$BUDGET_GB" ]; then
|
|
||||||
budget_mb=$(( BUDGET_GB * 1024 ))
|
|
||||||
else
|
|
||||||
budget_mb=$(( NODES * NODE_MB ))
|
|
||||||
fi
|
|
||||||
ceiling=$(effective_ceiling_mb)
|
|
||||||
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
|
||||||
[ -n "$got" ] || got=0
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "verdict"
|
|
||||||
if [ -n "$BUDGET_GB" ]; then
|
|
||||||
echo " budget ${budget_mb} MB (--budget)"
|
|
||||||
else
|
|
||||||
# rig's own figure for this profile: nodes times what one node costs.
|
|
||||||
# Addons carry no memory figure in rig yet, so this is the cluster alone
|
|
||||||
# and whatever you deploy comes on top. --budget once you know that too.
|
|
||||||
echo " budget ${budget_mb} MB — profile ${PROFILE_NAME}: ${NODES} node(s) x ${NODE_MB} MB,"
|
|
||||||
echo " the cluster alone; your workload comes on top (--budget GB)"
|
|
||||||
fi
|
|
||||||
echo " measured ${got} MB handed over"
|
|
||||||
|
|
||||||
if [ "$got" -ge "$budget_mb" ]; then
|
|
||||||
echo " fits, with $(( got - budget_mb )) MB spare."
|
|
||||||
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
|
||||||
echo " - under 30% spare is thin once a workload runs on top: memory use"
|
|
||||||
echo " is spiky, and the spikes are what get killed."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! short by $(( budget_mb - got )) MB."
|
|
||||||
if [ "$ceiling" -ge "$budget_mb" ]; then
|
|
||||||
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
|
||||||
echo " Free something, or read the caps section again."
|
|
||||||
else
|
|
||||||
echo " The box does not have it to give. A bigger machine, or a profile"
|
|
||||||
echo " with fewer nodes."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
parse_flags() {
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--to) TO_MB=$(( ${2:?--to needs a value in GB} * 1024 )); shift 2 ;;
|
|
||||||
--to-mb) TO_MB="${2:?--to-mb needs a value in MB}"; shift 2 ;;
|
|
||||||
--step) STEP_MB="${2:?--step needs a value in MB}"; STEP_EXPLICIT=yes; shift 2 ;;
|
|
||||||
--to-oom) TO_OOM=yes; shift ;;
|
|
||||||
--budget) BUDGET_GB="${2:?--budget needs a value in GB}"; BUDGET_EXPLICIT=yes; shift 2 ;;
|
|
||||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if [ "$TO_OOM" = yes ] && [ -n "$TO_MB" ]; then
|
|
||||||
echo "--to and --to-oom contradict each other: one stops early, the other" >&2
|
|
||||||
echo "refuses to stop at all. Pick one." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
require_linux
|
|
||||||
find_cgroup
|
|
||||||
|
|
||||||
cmd="${1:-status}"
|
|
||||||
[ $# -gt 0 ] && shift
|
|
||||||
|
|
||||||
case "$cmd" in
|
|
||||||
status) parse_flags "$@"; status ;;
|
|
||||||
push) parse_flags "$@"; push ;;
|
|
||||||
all) parse_flags "$@"; all ;;
|
|
||||||
backup) backup ;;
|
|
||||||
restore) restore ;;
|
|
||||||
*) echo "usage: $0 [status|push|all|backup|restore]" >&2
|
|
||||||
echo " push [--to GB] [--to-mb MB] [--step MB] [--to-oom]" >&2
|
|
||||||
echo " all [--budget GB]" >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,963 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# rigdeps.sh for profile 'data', flattened from:
|
|
||||||
# ctrl/deps.sh
|
|
||||||
# ctrl/lib/config.sh
|
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
|
||||||
|
|
||||||
# ── from the libraries ──
|
|
||||||
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;
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [ -n "$line" ]; then
|
|
||||||
eval "export $line";
|
|
||||||
fi;
|
|
||||||
done <<< "$1";
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
default_cluster_name ()
|
|
||||||
{
|
|
||||||
local n;
|
|
||||||
n=$(basename "$(cd .. && pwd)");
|
|
||||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
|
||||||
echo "${n:-rig}"
|
|
||||||
}
|
|
||||||
derive_port_base ()
|
|
||||||
{
|
|
||||||
local h;
|
|
||||||
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
|
||||||
echo $((20000 + (h % 200) * 10))
|
|
||||||
}
|
|
||||||
render_kind_config ()
|
|
||||||
{
|
|
||||||
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_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'data' ──
|
|
||||||
load_config() {
|
|
||||||
local k saved=""
|
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
|
||||||
done
|
|
||||||
declare -gx ADDONS="metallb postgres redis airflow"
|
|
||||||
declare -gx AIRFLOW_ADMIN_USER="admin"
|
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
|
||||||
declare -g AUDIT="off"
|
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
|
||||||
declare -g CLUSTER="rig"
|
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
|
||||||
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
|
||||||
declare -gx COMPOSE_VERSION="5.5.1"
|
|
||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
|
||||||
declare -gx DATA_NAMESPACE="data"
|
|
||||||
declare -gx DNS_MODE="hosts"
|
|
||||||
declare -g HTTPS_PORT="20311"
|
|
||||||
declare -g HTTP_PORT="20310"
|
|
||||||
declare -gx INGRESS_MODE="hostport"
|
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
|
||||||
declare -gx K8S_VERSION="v1_36"
|
|
||||||
declare -gx KIND_CONFIG="kind-config.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
|
||||||
declare -g KUBECONTEXT="kind-rig"
|
|
||||||
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="ctrl/k8s/overlays/dev"
|
|
||||||
declare -gx METALLB_VERSION="v0.16.0"
|
|
||||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
|
||||||
declare -g NODES="1"
|
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
|
||||||
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
|
||||||
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
|
||||||
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 -gx POSTGRES_DB="app"
|
|
||||||
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
|
||||||
declare -gx POSTGRES_STORAGE="2Gi"
|
|
||||||
declare -gx POSTGRES_USER="app"
|
|
||||||
declare -gx PROFILE_NAME="data"
|
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
|
||||||
declare -gx 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"
|
|
||||||
declare -gx TILT_VERSION="0.37.6"
|
|
||||||
_config_restore "$saved"
|
|
||||||
}
|
|
||||||
# ── end of frozen configuration ──
|
|
||||||
|
|
||||||
# ── ctrl/deps.sh ──
|
|
||||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
|
||||||
# report what it could not do.
|
|
||||||
#
|
|
||||||
# It never runs the cluster, never uses sudo or apt, and writes only into
|
|
||||||
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
|
||||||
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
|
||||||
# decide on, never performed. That is what makes it safe to run on a machine that
|
|
||||||
# already has a working setup.
|
|
||||||
#
|
|
||||||
# Usage (normally via `make deps`, or directly):
|
|
||||||
# deps.sh detect # report host facts only, change nothing
|
|
||||||
# deps.sh list # the pinned versions
|
|
||||||
# deps.sh verify [core|dev] # run what is installed and see if it works
|
|
||||||
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
|
||||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
|
||||||
#
|
|
||||||
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
|
||||||
# Default is dev.
|
|
||||||
#
|
|
||||||
# Runs both inside the installer container and bare on a host. Inside the
|
|
||||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
|
||||||
# falls back to /.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
|
||||||
# not against ctrl/ once we've moved.
|
|
||||||
INVOKED_FROM="$PWD"
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# Pins arrive through load_config like every other setting, not by sourcing
|
|
||||||
# versions.env here. That is what lets `make standalone` freeze them into a
|
|
||||||
# one-file installer: configuration has exactly one way in.
|
|
||||||
# (sourced library inlined above)
|
|
||||||
load_config
|
|
||||||
|
|
||||||
# Resolve a possibly-relative path against the caller's original directory.
|
|
||||||
abspath() {
|
|
||||||
case "$1" in
|
|
||||||
/*) echo "$1" ;;
|
|
||||||
*) echo "$INVOKED_FROM/$1" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
|
||||||
HOST_ROOT="${HOST_ROOT:-/}"
|
|
||||||
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
|
||||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
|
||||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
|
||||||
|
|
||||||
# Collected by detect(), printed by report_manual() at the very end.
|
|
||||||
MANUAL=()
|
|
||||||
|
|
||||||
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
|
||||||
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
|
||||||
# container's own view is already the host's.
|
|
||||||
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
|
||||||
# tight and does-not-fit branches can be exercised against a real machine's
|
|
||||||
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
|
||||||
mb_of() {
|
|
||||||
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
|
||||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
|
||||||
}
|
|
||||||
|
|
||||||
host_file() {
|
|
||||||
local p="${1#/}"
|
|
||||||
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
|
||||||
echo "$HOST_ROOT/$p"
|
|
||||||
else
|
|
||||||
echo "/$p"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── the tools this script itself needs ─────────────────────────────────────
|
|
||||||
|
|
||||||
arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) echo amd64 ;;
|
|
||||||
aarch64|arm64) echo arm64 ;;
|
|
||||||
*) uname -m ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The pins above are amd64. Rather than download something that cannot execute
|
|
||||||
# and let it fail as "cannot execute binary file: Exec format error", say so
|
|
||||||
# here and hand over the commands that produce the right checksums.
|
|
||||||
require_amd64() {
|
|
||||||
local a; a=$(arch)
|
|
||||||
[ "$a" = "amd64" ] && return 0
|
|
||||||
cat >&2 <<EOF
|
|
||||||
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
|
||||||
|
|
||||||
Nothing here would run, so it does not download. To make an ${a} version, the
|
|
||||||
URLs need the ${a} artifact and the checksums need to come from each project's
|
|
||||||
own published list — not from these values, and not from a download you did:
|
|
||||||
|
|
||||||
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
|
||||||
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
|
||||||
|
|
||||||
Edit the pinned block at the top of this file with what those print.
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DL=""
|
|
||||||
pick_downloader() {
|
|
||||||
if command -v curl >/dev/null 2>&1; then DL=curl
|
|
||||||
elif command -v wget >/dev/null 2>&1; then DL=wget
|
|
||||||
else
|
|
||||||
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
|
||||||
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download() {
|
|
||||||
local url="$1" out="$2"
|
|
||||||
case "$DL" in
|
|
||||||
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
|
||||||
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
SHA=""
|
|
||||||
pick_sha() {
|
|
||||||
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
|
||||||
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
|
||||||
else
|
|
||||||
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
|
||||||
echo "Refusing to install unverified binaries." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── package manager, for the instructions only ─────────────────────────────
|
|
||||||
# This never runs a package manager. It names one so the reported action is
|
|
||||||
# something you can paste, on the distro you are actually on — an apt line on
|
|
||||||
# Amazon Linux 2 is a wrong answer dressed up as help.
|
|
||||||
|
|
||||||
pkg_install_cmd() {
|
|
||||||
local pkg="$1"
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
|
||||||
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
|
||||||
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
|
||||||
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
|
||||||
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
|
||||||
else echo "install '$pkg' with this system's package manager"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
docker_pkg() {
|
|
||||||
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── detect ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
|
||||||
case "$(uname -s)" in
|
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
|
||||||
|
|
||||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
|
||||||
|
|
||||||
wsl --install
|
|
||||||
|
|
||||||
That enables Windows features and needs a reboot, so it is not something this
|
|
||||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
|
||||||
this from there.
|
|
||||||
|
|
||||||
See "Starting from plain Windows" in README.md.
|
|
||||||
EOF
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|
||||||
|
|
||||||
detect() {
|
|
||||||
echo "host"
|
|
||||||
echo " kernel $(uname -r)"
|
|
||||||
echo " arch $(arch) ($(uname -m))"
|
|
||||||
|
|
||||||
local osr; osr=$(host_file /etc/os-release)
|
|
||||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
|
||||||
|
|
||||||
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
|
||||||
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
|
||||||
# that is enough depends on the profile, which check.sh knows and this does not.
|
|
||||||
local total_mb avail_mb swap_total_mb swap_used_mb om
|
|
||||||
total_mb=$(mb_of MemTotal)
|
|
||||||
avail_mb=$(mb_of MemAvailable)
|
|
||||||
swap_total_mb=$(mb_of SwapTotal)
|
|
||||||
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
|
||||||
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
|
||||||
if [ "$swap_total_mb" -gt 0 ]; then
|
|
||||||
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
|
||||||
# always says yes and settles up later with the OOM killer, so a cluster that
|
|
||||||
# starts cleanly can still lose processes afterwards.
|
|
||||||
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
|
||||||
case "$om" in
|
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
|
||||||
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo " install to $OUT_BIN"
|
|
||||||
detect_libc
|
|
||||||
detect_prereqs
|
|
||||||
detect_wsl
|
|
||||||
detect_filesystem
|
|
||||||
detect_docker
|
|
||||||
detect_inotify
|
|
||||||
detect_toolchain
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_wsl() {
|
|
||||||
if ! is_wsl; then
|
|
||||||
echo " platform native linux"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " platform WSL"
|
|
||||||
|
|
||||||
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
|
||||||
# host service need it. Enabling it requires a Windows-side restart, which
|
|
||||||
# cannot be issued from inside the distro.
|
|
||||||
local wc; wc=$(host_file /etc/wsl.conf)
|
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
|
||||||
echo " systemd enabled in wsl.conf"
|
|
||||||
else
|
|
||||||
echo " ! systemd not enabled in /etc/wsl.conf"
|
|
||||||
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
|
||||||
[boot]
|
|
||||||
systemd=true
|
|
||||||
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
|
||||||
# local DNS setup.
|
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
|
||||||
echo " resolv.conf pinned (generateResolvConf=false)"
|
|
||||||
else
|
|
||||||
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local wcfg
|
|
||||||
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
|
||||||
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
|
||||||
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
|
||||||
else
|
|
||||||
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
|
||||||
make mem status
|
|
||||||
It prints the edit to make and the command to apply it.")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
|
||||||
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
|
||||||
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
|
||||||
# way. None of them deliver inotify events, so anything watching files goes
|
|
||||||
# quiet without saying why.
|
|
||||||
watch_hostile_fs() {
|
|
||||||
local dir="$1" fstype
|
|
||||||
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
|
||||||
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
|
||||||
case "$fstype" in
|
|
||||||
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
|
||||||
*) echo "" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_filesystem() {
|
|
||||||
local root fstype
|
|
||||||
root=$(cd .. && pwd -P)
|
|
||||||
fstype=$(watch_hostile_fs "$root")
|
|
||||||
if [ -n "$fstype" ]; then
|
|
||||||
echo " ! this directory is on $fstype — file watching will not work"
|
|
||||||
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
|
||||||
on a $fstype mount, and everything else is slower:
|
|
||||||
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
|
||||||
else
|
|
||||||
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
|
||||||
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
|
||||||
#
|
|
||||||
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
|
||||||
#
|
|
||||||
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
|
||||||
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
|
||||||
# version now; `verify` catches the actual failure after installing.
|
|
||||||
detect_libc() {
|
|
||||||
local v=""
|
|
||||||
if command -v ldd >/dev/null 2>&1; then
|
|
||||||
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
|
||||||
fi
|
|
||||||
if [ -z "$v" ]; then
|
|
||||||
echo " libc unknown (no ldd) — 'verify' is the real test"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo " libc glibc $v"
|
|
||||||
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
|
||||||
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
|
||||||
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
|
||||||
echo " Install the core tier, or run tilt from a container."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# What this script needs to do its own job. Reported here so `detect` answers
|
|
||||||
# "will install work?" instead of leaving you to find out one download in.
|
|
||||||
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
|
||||||
detect_prereqs() {
|
|
||||||
local missing=""
|
|
||||||
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
|
||||||
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
|
||||||
echo " checksums ok"
|
|
||||||
else
|
|
||||||
echo " ! no sha256sum or shasum — downloads could not be verified"
|
|
||||||
missing+=" coreutils"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
|
||||||
echo " archives tar + gzip"
|
|
||||||
else
|
|
||||||
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
|
||||||
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
|
||||||
missing+=" tar gzip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$missing" ]; then
|
|
||||||
MANUAL+=("Install what this script needs to run at all:
|
|
||||||
$(pkg_install_cmd "${missing# }")")
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_docker() {
|
|
||||||
# Reachability of the daemon is the real question, and the CLI is only how
|
|
||||||
# we ask it. Note that when this runs inside the installer container, Docker
|
|
||||||
# necessarily exists on the host — otherwise nothing would be executing —
|
|
||||||
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
echo " docker socket present (no cli in this context)"
|
|
||||||
else
|
|
||||||
echo " ! docker not found and no socket at /var/run/docker.sock"
|
|
||||||
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
|
||||||
that needs root:
|
|
||||||
$(pkg_install_cmd "$(docker_pkg)")
|
|
||||||
sudo systemctl enable --now docker
|
|
||||||
sudo usermod -aG docker \"\$USER\"
|
|
||||||
then log out and back in, so the new group applies to your shell.")
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if docker info >/dev/null 2>&1; then
|
|
||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
|
||||||
local n
|
|
||||||
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
|
||||||
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
|
||||||
# function the latter returns 1 when the count is zero, and `set -e`
|
|
||||||
# then kills the caller. That is the fresh-machine case — no clusters
|
|
||||||
# yet — so the bug only ever shows up where it does most harm.
|
|
||||||
if [ "$n" -gt 0 ]; then
|
|
||||||
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
|
||||||
MANUAL+=("Start Docker, or add yourself to the docker group:
|
|
||||||
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
|
||||||
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
|
||||||
detect_inotify() {
|
|
||||||
local w i
|
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
|
||||||
echo " inotify watches=$w instances=$i"
|
|
||||||
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
|
||||||
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
|
||||||
MANUAL+=("Raise inotify limits (needs root on the host):
|
|
||||||
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
|
||||||
| sudo tee /etc/sysctl.d/99-rig.conf
|
|
||||||
sudo sysctl --system")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── fetch ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
|
||||||
resolve_url() {
|
|
||||||
local upstream="$1"
|
|
||||||
case "$DEPS_SOURCE" in
|
|
||||||
upstream) echo "$upstream" ;;
|
|
||||||
artifactory)
|
|
||||||
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
|
||||||
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
|
||||||
;;
|
|
||||||
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
verify() {
|
|
||||||
local file="$1" want="$2" name="$3" got
|
|
||||||
got=$($SHA "$file" | awk '{print $1}')
|
|
||||||
if [ "$got" != "$want" ]; then
|
|
||||||
echo "checksum mismatch for $name" >&2
|
|
||||||
echo " expected $want" >&2
|
|
||||||
echo " got $got" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
|
||||||
fetch_bin() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4"
|
|
||||||
local tmp="$dest/.$name.tmp"
|
|
||||||
echo " fetching $name"
|
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
|
||||||
verify "$tmp" "$sha" "$name"
|
|
||||||
mv "$tmp" "$dest/$name"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
|
||||||
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
|
||||||
# others nest it a directory down — so the caller says which.
|
|
||||||
fetch_tgz() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
|
||||||
local tmp="$dest/.$name.tgz"
|
|
||||||
echo " fetching $name"
|
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
|
||||||
verify "$tmp" "$sha" "$name"
|
|
||||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
|
||||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
|
||||||
# user does not own.
|
|
||||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
|
||||||
rm -f "$tmp"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# The installer runs as root so it can reach the docker socket, which means
|
|
||||||
# everything it writes into a mounted volume lands root-owned and unusable from
|
|
||||||
# the host. Hand it back to whoever owns the mount point (the host user created
|
|
||||||
# that directory before mounting it).
|
|
||||||
fix_ownership() {
|
|
||||||
local dir="$1"
|
|
||||||
[ -d "$dir" ] || return 0
|
|
||||||
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
|
||||||
if [ "$owner" = ":" ]; then
|
|
||||||
owner=$(stat -c '%u:%g' "$dir")
|
|
||||||
fi
|
|
||||||
[ "$owner" = "0:0" ] && return 0
|
|
||||||
chown -R "$owner" "$dir" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Two tiers, because not every machine should get cluster tooling.
|
|
||||||
#
|
|
||||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
|
||||||
# creates one. Appropriate on a managed or corporate-issued machine
|
|
||||||
# where development tools are not wanted by default.
|
|
||||||
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
|
||||||
#
|
|
||||||
# The split exists because "install the toolchain" is not one decision: on a
|
|
||||||
# managed workspace the right answer is kubectl and nothing else.
|
|
||||||
CORE_TOOLS="kubectl jq"
|
|
||||||
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
|
||||||
# has ever invoked it. Add it back the day something actually needs a chart.
|
|
||||||
#
|
|
||||||
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
|
||||||
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
|
||||||
# because it is what wires a cluster to a local registry — without one, an
|
|
||||||
# unqualified image name resolves to docker.io/library/<name> and there is
|
|
||||||
# nothing structural stopping a push there.
|
|
||||||
#
|
|
||||||
# docker-compose is 'dev' for the same reason, and is here because the distro
|
|
||||||
# docker packages ship the daemon and CLI but frequently not the compose
|
|
||||||
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
|
||||||
# working Docker, and nothing about that message names the missing piece.
|
|
||||||
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
|
||||||
|
|
||||||
# ── what is already on this machine ───────────────────────────────────────
|
|
||||||
#
|
|
||||||
# A tool already on PATH at its pinned version is left where it is. Without
|
|
||||||
# this, install downloads a second copy into OUT_BIN and then reports the first
|
|
||||||
# one as shadowed — noise, and wrong, when both are the same version. That is
|
|
||||||
# the normal state of any machine someone set up by hand, whatever directory
|
|
||||||
# they happened to choose.
|
|
||||||
|
|
||||||
pin_of() {
|
|
||||||
case "$1" in
|
|
||||||
kubectl) echo "$KUBECTL_VERSION" ;;
|
|
||||||
jq) echo "$JQ_VERSION" ;;
|
|
||||||
kind) echo "$KIND_VERSION" ;;
|
|
||||||
tilt) echo "$TILT_VERSION" ;;
|
|
||||||
ctlptl) echo "$CTLPTL_VERSION" ;;
|
|
||||||
docker-compose) echo "$COMPOSE_VERSION" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The version string a binary reports. Each tool spells the question
|
|
||||||
# differently, and kubectl has to be told --client or it goes looking for a
|
|
||||||
# server to ask.
|
|
||||||
reported_version() {
|
|
||||||
local tool="$1" path="$2"
|
|
||||||
case "$tool" in
|
|
||||||
kubectl) "$path" version --client 2>/dev/null ;;
|
|
||||||
jq) "$path" --version 2>/dev/null ;;
|
|
||||||
*) "$path" version 2>/dev/null ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
|
||||||
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
|
||||||
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
|
||||||
#
|
|
||||||
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
|
||||||
# on every machine — some builds reject patterns that others accept — and a
|
|
||||||
# failed grep inside a count reads exactly like a zero.
|
|
||||||
version_matches() {
|
|
||||||
local tool="$1" path="$2" pin="$3" out v re
|
|
||||||
out=$(reported_version "$tool" "$path") || return 1
|
|
||||||
v="${pin#v}"
|
|
||||||
v="${v//./\\.}"
|
|
||||||
re="(^|[^0-9.])v?${v}([^0-9.]|\$)"
|
|
||||||
[[ $out =~ $re ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
|
||||||
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
|
||||||
# DIR" must not quietly skip something because this machine happens to have it.
|
|
||||||
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
|
||||||
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
|
||||||
|
|
||||||
# Every tool in the tier with its state, probed once and reported once. What
|
|
||||||
# still needs fetching is left in TOOLCHAIN_NEED for install() to act on.
|
|
||||||
TOOLCHAIN_NEED=""
|
|
||||||
detect_toolchain() {
|
|
||||||
local tier="${TIER:-dev}" b pin path found
|
|
||||||
TOOLCHAIN_NEED=""
|
|
||||||
echo
|
|
||||||
echo "toolchain (pinned, tier '$tier')"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
pin=$(pin_of "$b")
|
|
||||||
path=$(command -v "$b" 2>/dev/null || true)
|
|
||||||
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
|
||||||
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
|
||||||
# has no `docker-compose` to find — and probing only PATH would report it
|
|
||||||
# missing and re-download a copy that is already there. That is the exact
|
|
||||||
# noise the version-aware skip exists to prevent, so ask docker instead.
|
|
||||||
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
|
||||||
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
|
||||||
if [ "${found#v}" = "${pin#v}" ]; then
|
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
|
||||||
else
|
|
||||||
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
|
||||||
"$b" "$pin" "$found"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
fi
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ -z "$path" ]; then
|
|
||||||
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
elif version_matches "$b" "$path" "$pin"; then
|
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
|
||||||
else
|
|
||||||
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
|
||||||
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -z "$TOOLCHAIN_NEED" ]; then
|
|
||||||
echo " every pinned tool is already on PATH — nothing to fetch"
|
|
||||||
else
|
|
||||||
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch() {
|
|
||||||
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--to) dest="$2"; shift 2 ;;
|
|
||||||
core|dev) tier="$1"; shift ;;
|
|
||||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
dest="$(abspath "$dest")"
|
|
||||||
mkdir -p "$dest"
|
|
||||||
TIER="$tier"
|
|
||||||
|
|
||||||
if [ "$DEPS_SOURCE" = "baked" ]; then
|
|
||||||
echo "installing baked binaries from $BAKED_BIN"
|
|
||||||
cp -a "$BAKED_BIN"/. "$dest"/
|
|
||||||
fix_ownership "$dest"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${DEPS_ONLY:-}" ]; then
|
|
||||||
echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)"
|
|
||||||
else
|
|
||||||
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
|
||||||
fi
|
|
||||||
if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi
|
|
||||||
if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi
|
|
||||||
if [ "$tier" = "dev" ]; then
|
|
||||||
if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi
|
|
||||||
if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi
|
|
||||||
if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi
|
|
||||||
if want docker-compose; then
|
|
||||||
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
fix_ownership "$dest"
|
|
||||||
# kind writes the kubeconfig as root too; hand that back as well when it's
|
|
||||||
# a mounted host directory rather than container-local state.
|
|
||||||
fix_ownership "${KUBE_DIR:-/out/kube}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── install ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
report_manual() {
|
|
||||||
echo
|
|
||||||
if [ ${#MANUAL[@]} -eq 0 ]; then
|
|
||||||
echo "nothing left to do by hand."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
|
||||||
echo
|
|
||||||
local n=1
|
|
||||||
for m in "${MANUAL[@]}"; do
|
|
||||||
echo " $n. $m"
|
|
||||||
echo
|
|
||||||
n=$((n + 1))
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installing into a directory that sits early in PATH silently replaces whatever
|
|
||||||
# the machine was already using — which on a shared or client machine can break
|
|
||||||
# unrelated work (kubectl more than one minor away from a cluster is the common
|
|
||||||
# one). Say so; never decide it for them.
|
|
||||||
# Downloading a verified binary proves it is the right file, not that this
|
|
||||||
# machine can run it. On an old distro tilt fails here, with a linker error
|
|
||||||
# about a missing symbol, and finding that out now beats finding out during a
|
|
||||||
# first cluster build.
|
|
||||||
verify_tools() {
|
|
||||||
local tier="${1:-dev}" b bin out rc broke=0
|
|
||||||
echo "checking that each one actually runs"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
bin="$OUT_BIN/$b"
|
|
||||||
if [ ! -x "$bin" ]; then
|
|
||||||
printf ' %-14s not installed\n' "$b"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
|
||||||
# than one line gets SIGPIPE when head closes the pipe, and the
|
|
||||||
# pipeline reports 141 — so a working kubectl was announced as "does
|
|
||||||
# not run here", with its own correct version string as the evidence.
|
|
||||||
# Take the first line afterwards, from the string.
|
|
||||||
rc=0
|
|
||||||
case "$b" in
|
|
||||||
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
|
||||||
jq) out=$("$bin" --version 2>&1) || rc=$? ;;
|
|
||||||
*) out=$("$bin" version 2>&1) || rc=$? ;;
|
|
||||||
esac
|
|
||||||
out=${out%%$'\n'*}
|
|
||||||
if [ "$rc" -eq 0 ]; then
|
|
||||||
printf ' %-14s %s\n' "$b" "$out"
|
|
||||||
else
|
|
||||||
printf ' ! %-12s does not run here: %s\n' "$b" "$out"
|
|
||||||
broke=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$broke" -eq 1 ]; then
|
|
||||||
echo
|
|
||||||
echo " A binary that downloads and verifies but will not start is almost"
|
|
||||||
echo " always this distro's libc being older than the release needs."
|
|
||||||
echo " 'detect' prints the glibc version. The core tier (kubectl + jq)"
|
|
||||||
echo " has no such dependency and will work regardless."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
list() {
|
|
||||||
echo "pinned, linux/amd64 only:"
|
|
||||||
printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION"
|
|
||||||
printf ' %-14s %s\n' jq "$JQ_VERSION"
|
|
||||||
printf ' %-14s %s\n' kind "$KIND_VERSION"
|
|
||||||
printf ' %-14s %s\n' tilt "$TILT_VERSION"
|
|
||||||
printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION"
|
|
||||||
printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION"
|
|
||||||
echo
|
|
||||||
echo " core = $CORE_TOOLS"
|
|
||||||
echo " dev = $CORE_TOOLS $DEV_TOOLS"
|
|
||||||
echo
|
|
||||||
echo "Checksums are pinned in the block at the top of this file. To bump one,"
|
|
||||||
echo "take the new checksum from the publisher's own release list — the header"
|
|
||||||
echo "comment has the exact commands."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
|
||||||
|
|
||||||
warn_shadowing() {
|
|
||||||
local b existing shadowed="" tier="${1:-dev}"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
[ -x "$OUT_BIN/$b" ] || continue
|
|
||||||
# Where would this resolve if OUT_BIN weren't in the way?
|
|
||||||
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
|
||||||
command -v "$b" 2>/dev/null || true)
|
|
||||||
[ -n "$existing" ] || continue
|
|
||||||
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
|
||||||
# The same version in both places is not a conflict: nothing changes for
|
|
||||||
# any other project whichever copy PATH happens to find first.
|
|
||||||
if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi
|
|
||||||
shadowed+=" $b $existing"$'\n'
|
|
||||||
done
|
|
||||||
|
|
||||||
[ -n "$shadowed" ] || return 0
|
|
||||||
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
|
||||||
printf '%s' "$shadowed"
|
|
||||||
echo " Other projects on this machine will pick up the new versions."
|
|
||||||
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
|
||||||
was just installed:
|
|
||||||
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
|
||||||
Or install somewhere private instead:
|
|
||||||
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
|
||||||
}
|
|
||||||
|
|
||||||
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
|
||||||
# retired v1 spelling; every compose file written in the last few years assumes
|
|
||||||
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
|
||||||
# So the binary is fetched like any other and then linked, in your own home —
|
|
||||||
# no root, and nothing outside it.
|
|
||||||
install_compose_plugin() {
|
|
||||||
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
|
||||||
[ -x "$src" ] || return 0
|
|
||||||
mkdir -p "$dir"
|
|
||||||
# Something else already owns that name — docker-desktop and some distro
|
|
||||||
# packages install a real file there. Overwriting it would take the plugin
|
|
||||||
# away from whatever put it there, so say so and let the user decide.
|
|
||||||
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
|
||||||
MANUAL+=("Something already installs the compose plugin at
|
|
||||||
$dir/docker-compose
|
|
||||||
To use rig's pinned build instead:
|
|
||||||
ln -sf $src $dir/docker-compose")
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
ln -sfn "$src" "$dir/docker-compose"
|
|
||||||
echo " compose plugin -> $dir/docker-compose"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
install() {
|
|
||||||
local tier="${1:-dev}" b
|
|
||||||
TIER="$tier"
|
|
||||||
detect
|
|
||||||
|
|
||||||
# detect_toolchain has already probed PATH. Fetch only what it found missing
|
|
||||||
# or at the wrong version; a tool already present at its pin stays where it is.
|
|
||||||
if [ -n "$TOOLCHAIN_NEED" ]; then
|
|
||||||
echo
|
|
||||||
DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier"
|
|
||||||
echo
|
|
||||||
echo "installed to $OUT_BIN ($tier):"
|
|
||||||
for b in $TOOLCHAIN_NEED; do
|
|
||||||
if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi
|
|
||||||
done
|
|
||||||
if [ "$tier" = "core" ]; then
|
|
||||||
echo " (no kind/tilt — 'make deps dev' adds them)"
|
|
||||||
fi
|
|
||||||
# Only when compose was one of the things fetched: linking a binary
|
|
||||||
# that is already satisfied elsewhere on PATH would point the plugin at
|
|
||||||
# a copy rig did not install.
|
|
||||||
case " $TOOLCHAIN_NEED " in
|
|
||||||
*" docker-compose "*) install_compose_plugin ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Only worth saying when something actually landed in OUT_BIN. When every
|
|
||||||
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
|
||||||
# telling the user to add it would be advice to fix nothing.
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
|
||||||
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
warn_shadowing "$tier"
|
|
||||||
|
|
||||||
report_manual
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
require_linux
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
cmd="${1:-install}"
|
|
||||||
[ $# -gt 0 ] && shift
|
|
||||||
|
|
||||||
# Baked mode copies binaries already in the image, so it needs no downloader.
|
|
||||||
need_downloads() {
|
|
||||||
require_amd64
|
|
||||||
if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi
|
|
||||||
pick_sha
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$cmd" in
|
|
||||||
detect) detect; report_manual ;;
|
|
||||||
list) list ;;
|
|
||||||
verify) verify_tools "${1:-dev}" ;;
|
|
||||||
fetch) need_downloads; fetch "$@" ;;
|
|
||||||
install) need_downloads; install "${1:-dev}" ;;
|
|
||||||
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
|
||||||
echo " install [core|dev] (default dev)" >&2
|
|
||||||
echo " fetch [core|dev] [--to DIR]" >&2
|
|
||||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,864 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# rigmini.sh for profile 'data', flattened from:
|
|
||||||
# ctrl/mem.sh
|
|
||||||
# ctrl/lib/config.sh
|
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
|
||||||
|
|
||||||
# ── from the libraries ──
|
|
||||||
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;
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [ -n "$line" ]; then
|
|
||||||
eval "export $line";
|
|
||||||
fi;
|
|
||||||
done <<< "$1";
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
default_cluster_name ()
|
|
||||||
{
|
|
||||||
local n;
|
|
||||||
n=$(basename "$(cd .. && pwd)");
|
|
||||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
|
||||||
echo "${n:-rig}"
|
|
||||||
}
|
|
||||||
derive_port_base ()
|
|
||||||
{
|
|
||||||
local h;
|
|
||||||
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
|
||||||
echo $((20000 + (h % 200) * 10))
|
|
||||||
}
|
|
||||||
render_kind_config ()
|
|
||||||
{
|
|
||||||
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_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'data' ──
|
|
||||||
load_config() {
|
|
||||||
local k saved=""
|
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
|
||||||
done
|
|
||||||
declare -gx ADDONS="metallb postgres redis airflow"
|
|
||||||
declare -gx AIRFLOW_ADMIN_USER="admin"
|
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
|
||||||
declare -g AUDIT="off"
|
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
|
||||||
declare -g CLUSTER="rig"
|
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
|
||||||
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
|
||||||
declare -gx COMPOSE_VERSION="5.5.1"
|
|
||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
|
||||||
declare -gx DATA_NAMESPACE="data"
|
|
||||||
declare -gx DNS_MODE="hosts"
|
|
||||||
declare -g HTTPS_PORT="20311"
|
|
||||||
declare -g HTTP_PORT="20310"
|
|
||||||
declare -gx INGRESS_MODE="hostport"
|
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
|
||||||
declare -gx K8S_VERSION="v1_36"
|
|
||||||
declare -gx KIND_CONFIG="kind-config.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
|
||||||
declare -g KUBECONTEXT="kind-rig"
|
|
||||||
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="ctrl/k8s/overlays/dev"
|
|
||||||
declare -gx METALLB_VERSION="v0.16.0"
|
|
||||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
|
||||||
declare -g NODES="1"
|
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
|
||||||
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
|
||||||
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
|
||||||
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 -gx POSTGRES_DB="app"
|
|
||||||
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
|
||||||
declare -gx POSTGRES_STORAGE="2Gi"
|
|
||||||
declare -gx POSTGRES_USER="app"
|
|
||||||
declare -gx PROFILE_NAME="data"
|
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
|
||||||
declare -gx 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"
|
|
||||||
declare -gx TILT_VERSION="0.37.6"
|
|
||||||
_config_restore "$saved"
|
|
||||||
}
|
|
||||||
# ── end of frozen configuration ──
|
|
||||||
|
|
||||||
# ── ctrl/mem.sh ──
|
|
||||||
# How much memory this machine will actually give you before something dies —
|
|
||||||
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
|
||||||
#
|
|
||||||
# There are two numbers and they are rarely the same. `status` reports what the
|
|
||||||
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
|
||||||
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
|
||||||
# against what this profile's cluster needs.
|
|
||||||
#
|
|
||||||
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
|
||||||
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
|
||||||
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
|
||||||
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
|
||||||
# on a box that OOMs at 2.
|
|
||||||
#
|
|
||||||
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
|
||||||
# allocation that can be raised, and the commonest failure is raising it without
|
|
||||||
# restarting — so status compares what .wslconfig says with what actually booted.
|
|
||||||
#
|
|
||||||
# Reports and instructs. It never raises a limit, frees anything or installs a
|
|
||||||
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
|
||||||
# itself, so that `restore` has something to put back after a hand edit.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# mem.sh status what it has, what caps it
|
|
||||||
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
|
||||||
# mem.sh all [--budget GB] both, then the verdict
|
|
||||||
# mem.sh backup | restore .wslconfig, WSL only
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
# (sourced library inlined above)
|
|
||||||
|
|
||||||
# ── defaults ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
STEP_MB=0 # per allocation; 0 means scale it to the ceiling. See push().
|
|
||||||
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
|
||||||
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
|
||||||
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
|
||||||
BUDGET_GB="" # --budget; empty means what this profile's cluster needs, from rig.
|
|
||||||
BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess below.
|
|
||||||
|
|
||||||
# ── platform ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
|
||||||
case "$(uname -s)" in
|
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
|
||||||
|
|
||||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
|
||||||
|
|
||||||
wsl --install
|
|
||||||
|
|
||||||
That enables Windows features and needs a reboot, so it is not something this
|
|
||||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
|
||||||
this from there.
|
|
||||||
EOF
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Everything below reads /proc. Without it there is nothing to measure, and
|
|
||||||
# failing here beats printing a page of empty fields.
|
|
||||||
if [ ! -r /proc/meminfo ]; then
|
|
||||||
echo "no readable /proc/meminfo — this needs a Linux kernel." >&2
|
|
||||||
echo "On macOS or a BSD none of the numbers below exist." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|
||||||
|
|
||||||
is_container() {
|
|
||||||
[ -f /.dockerenv ] && return 0
|
|
||||||
grep -qE '(docker|containerd|kubepods|lxc|podman)' /proc/1/cgroup 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
platform() {
|
|
||||||
if is_wsl; then echo WSL
|
|
||||||
elif is_container; then echo container
|
|
||||||
else echo "native linux"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── reading memory ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
|
||||||
|
|
||||||
# MemAvailable arrived in kernel 3.14. Older kernels — and they turn up on
|
|
||||||
# corporate images — need the estimate it replaced, which is worse but not wrong.
|
|
||||||
avail_meminfo_mb() {
|
|
||||||
if grep -q '^MemAvailable:' /proc/meminfo; then
|
|
||||||
mb MemAvailable
|
|
||||||
else
|
|
||||||
awk '/^(MemFree|Buffers|Cached):/{t+=$2} END{print int(t/1024)}' /proc/meminfo
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
|
||||||
# find_cgroup, because every later reading needs both and hunting for the files
|
|
||||||
# on each call would be the slow part of the poll loop.
|
|
||||||
CG_MAX_FILE=""
|
|
||||||
CG_CUR_FILE=""
|
|
||||||
CG_VERSION=""
|
|
||||||
|
|
||||||
find_cgroup() {
|
|
||||||
local rel
|
|
||||||
|
|
||||||
# Inside a container the cgroup namespace makes the top of the tree BE the
|
|
||||||
# container's own cgroup, so the unqualified path is already the right one.
|
|
||||||
# On a host it is the root cgroup, which is never limited — hence the second
|
|
||||||
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
|
||||||
if [ -r /sys/fs/cgroup/memory.max ]; then
|
|
||||||
CG_VERSION=v2
|
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
|
||||||
CG_CUR_FILE=/sys/fs/cgroup/memory.current
|
|
||||||
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
|
|
||||||
CG_VERSION=v1
|
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory/memory.limit_in_bytes
|
|
||||||
CG_CUR_FILE=/sys/fs/cgroup/memory/memory.usage_in_bytes
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel=$(awk -F: '$1=="0"{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
|
||||||
if [ -n "$rel" ] && [ "$rel" != "/" ] && [ -r "/sys/fs/cgroup${rel}/memory.max" ]; then
|
|
||||||
CG_VERSION=v2
|
|
||||||
CG_MAX_FILE="/sys/fs/cgroup${rel}/memory.max"
|
|
||||||
CG_CUR_FILE="/sys/fs/cgroup${rel}/memory.current"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel=$(awk -F: '$2 ~ /(^|,)memory(,|$)/{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
|
||||||
if [ -n "$rel" ] && [ "$rel" != "/" ] \
|
|
||||||
&& [ -r "/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes" ]; then
|
|
||||||
CG_VERSION=v1
|
|
||||||
CG_MAX_FILE="/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes"
|
|
||||||
CG_CUR_FILE="/sys/fs/cgroup/memory${rel}/memory.usage_in_bytes"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
|
||||||
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
|
||||||
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
|
||||||
# own memory is not a limit, however it is written.
|
|
||||||
cgroup_cap_mb() {
|
|
||||||
local raw cap
|
|
||||||
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
|
||||||
raw=$(cat "$CG_MAX_FILE" 2>/dev/null || echo max)
|
|
||||||
[ "$raw" = "max" ] && { echo ""; return 0; }
|
|
||||||
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
cap=$((raw / 1024 / 1024))
|
|
||||||
[ "$cap" -ge "$(mb MemTotal)" ] && { echo ""; return 0; }
|
|
||||||
echo "$cap"
|
|
||||||
}
|
|
||||||
|
|
||||||
cgroup_used_mb() {
|
|
||||||
local raw
|
|
||||||
[ -n "$CG_CUR_FILE" ] && [ -r "$CG_CUR_FILE" ] || { echo ""; return 0; }
|
|
||||||
raw=$(cat "$CG_CUR_FILE" 2>/dev/null || echo "")
|
|
||||||
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
echo $((raw / 1024 / 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
# ulimit -v is a per-process address-space cap. It stops YOU long before the box
|
|
||||||
# does, and because it is inherited from a login shell it is easy to hit without
|
|
||||||
# knowing it is set.
|
|
||||||
ulimit_v_mb() {
|
|
||||||
local v; v=$(ulimit -v 2>/dev/null || echo unlimited)
|
|
||||||
[ "$v" = "unlimited" ] && { echo ""; return 0; }
|
|
||||||
case "$v" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
echo $((v / 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
# The number everything else is about: the lowest of the things that can stop
|
|
||||||
# you. Printed at the end of `status` and used as the sanity bound in `push`.
|
|
||||||
effective_ceiling_mb() {
|
|
||||||
local c; c=$(mb MemTotal)
|
|
||||||
local cap; cap=$(cgroup_cap_mb)
|
|
||||||
local ul; ul=$(ulimit_v_mb)
|
|
||||||
[ -n "$cap" ] && [ "$cap" -lt "$c" ] && c="$cap"
|
|
||||||
[ -n "$ul" ] && [ "$ul" -lt "$c" ] && c="$ul"
|
|
||||||
echo "$c"
|
|
||||||
}
|
|
||||||
|
|
||||||
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
|
||||||
# In a capped container /proc/meminfo describes the host and is worse than
|
|
||||||
# useless for this — it would report tens of gigabytes free on a box that is one
|
|
||||||
# allocation from being killed.
|
|
||||||
headroom_mb() {
|
|
||||||
local cap used
|
|
||||||
cap=$(cgroup_cap_mb)
|
|
||||||
used=$(cgroup_used_mb)
|
|
||||||
if [ -n "$cap" ] && [ -n "$used" ]; then
|
|
||||||
echo $(( cap - used ))
|
|
||||||
else
|
|
||||||
avail_meminfo_mb
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── status ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
|
||||||
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
|
||||||
# Windows, then fall back to whichever profile actually owns a config.
|
|
||||||
wslconfig_path() {
|
|
||||||
local profile winpath found
|
|
||||||
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
|
||||||
case "$profile" in
|
|
||||||
""|*%*) ;;
|
|
||||||
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
|
||||||
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
|
||||||
echo "$winpath/.wslconfig"; return 0
|
|
||||||
fi ;;
|
|
||||||
esac
|
|
||||||
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
|
||||||
[ -n "$found" ] && echo "$found"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
hogs() {
|
|
||||||
echo " holding the most:"
|
|
||||||
ps -eo rss,comm --sort=-rss 2>/dev/null \
|
|
||||||
| awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
status() {
|
|
||||||
local total avail swap_total swap_free cap ul cur
|
|
||||||
|
|
||||||
echo "host"
|
|
||||||
echo " platform $(platform)"
|
|
||||||
echo " kernel $(uname -r)"
|
|
||||||
[ -r /etc/os-release ] && \
|
|
||||||
echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' /etc/os-release)"
|
|
||||||
echo " cpu $(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '?') online, load $(cut -d' ' -f1-3 /proc/loadavg)"
|
|
||||||
|
|
||||||
# ── the caps first, because they decide what the totals below are worth ──
|
|
||||||
echo
|
|
||||||
echo "caps"
|
|
||||||
cap=$(cgroup_cap_mb)
|
|
||||||
if [ -n "$cap" ]; then
|
|
||||||
cur=$(cgroup_used_mb)
|
|
||||||
echo " cgroup ${cap} MB (${CG_VERSION}, ${CG_CUR_FILE##*/} says ${cur:-?} MB used)"
|
|
||||||
echo " ! /proc/meminfo below describes the HOST, not this cgroup."
|
|
||||||
echo " $(mb MemTotal) MB total is not yours; ${cap} MB is."
|
|
||||||
elif [ -n "$CG_VERSION" ]; then
|
|
||||||
echo " cgroup none (${CG_VERSION} present, no memory limit set)"
|
|
||||||
else
|
|
||||||
echo " cgroup no memory controller found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ul=$(ulimit_v_mb)
|
|
||||||
if [ -n "$ul" ]; then
|
|
||||||
echo " ! ulimit -v ${ul} MB — a per-process cap, inherited from your shell"
|
|
||||||
echo " it stops this process long before the machine runs out"
|
|
||||||
else
|
|
||||||
echo " ulimit -v unlimited"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# overcommit_memory=0 is the default heuristic: a large allocation is
|
|
||||||
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
|
||||||
# than as a failed malloc. It is why `push` touches every page it asks for.
|
|
||||||
local om or_
|
|
||||||
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
|
||||||
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
|
||||||
case "$om" in
|
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess," ;;
|
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit," ;;
|
|
||||||
2) echo " overcommit 2 strict (ratio ${or_}%) — allocation fails honestly instead of killing later," ;;
|
|
||||||
*) echo " overcommit ${om}" ;;
|
|
||||||
esac
|
|
||||||
[ "$om" != "?" ] && echo " so RSS is the number to trust, not what a process asked for"
|
|
||||||
|
|
||||||
# ── what it says it has ──
|
|
||||||
total=$(mb MemTotal); avail=$(avail_meminfo_mb)
|
|
||||||
swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree)
|
|
||||||
echo
|
|
||||||
echo "memory"
|
|
||||||
echo " total ${total} MB"
|
|
||||||
echo " available ${avail} MB"
|
|
||||||
echo " swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
|
||||||
if [ "$swap_total" -eq 0 ]; then
|
|
||||||
echo " - no swap: this box has no cushion. It goes from fine to OOM-killed"
|
|
||||||
echo " with nothing in between, which is the abrupt failure you get in a VM."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# postgres puts its shared buffers in /dev/shm. Docker's default is 64 MB,
|
|
||||||
# and the resulting failure names neither shm nor the size.
|
|
||||||
if [ -d /dev/shm ]; then
|
|
||||||
local shm; shm=$(df -Pm /dev/shm 2>/dev/null | awk 'NR==2{print $2}')
|
|
||||||
if [ -n "$shm" ]; then
|
|
||||||
if [ "$shm" -le 64 ]; then
|
|
||||||
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
|
||||||
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
|
||||||
else
|
|
||||||
echo " /dev/shm ${shm} MB"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "disk"
|
|
||||||
local d
|
|
||||||
for d in / /tmp /var/lib/docker; do
|
|
||||||
[ -d "$d" ] || continue
|
|
||||||
df -Pm "$d" 2>/dev/null | awk -v p="$d" 'NR==2{printf " %-12s %s MB free of %s MB\n", p, $4, $2}'
|
|
||||||
done
|
|
||||||
|
|
||||||
# kind and Tilt both watch large trees, and the failure mode is silent:
|
|
||||||
# they simply stop noticing file changes. Cheap to report while we are here.
|
|
||||||
local w i
|
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
|
||||||
echo
|
|
||||||
echo "tooling"
|
|
||||||
echo " inotify watches=$w instances=$i"
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
|
||||||
echo " ! low — anything watching files will silently stop seeing changes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
echo " docker socket present, no cli"
|
|
||||||
else
|
|
||||||
echo " docker not installed"
|
|
||||||
fi
|
|
||||||
elif docker info >/dev/null 2>&1; then
|
|
||||||
local n
|
|
||||||
n=$(docker ps -q 2>/dev/null | wc -l)
|
|
||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null), ${n} container(s) running"
|
|
||||||
else
|
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
|
||||||
# not usefully apply — the change costs a full VM restart. Report it, and
|
|
||||||
# report the commonest mistake, which is editing it and not restarting.
|
|
||||||
if is_wsl; then
|
|
||||||
local cfg conf conf_mb n
|
|
||||||
cfg=$(wslconfig_path)
|
|
||||||
echo
|
|
||||||
echo "wsl"
|
|
||||||
if [ -z "$cfg" ]; then
|
|
||||||
echo " ! cannot tell which Windows profile owns .wslconfig"
|
|
||||||
else
|
|
||||||
echo " config $cfg"
|
|
||||||
conf=$(configured_memory "$cfg")
|
|
||||||
if [ -n "$conf" ]; then
|
|
||||||
conf_mb=$(to_mb "$conf")
|
|
||||||
echo " configured $conf (${conf_mb} MB), booted ${total} MB"
|
|
||||||
# The VM reports a little less than allocated; 15% covers the
|
|
||||||
# kernel without calling every healthy machine a mismatch.
|
|
||||||
if [ -n "$conf_mb" ] && [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
|
||||||
echo " ! configured ${conf_mb} MB but booted ${total} MB — not applied yet."
|
|
||||||
echo " From a WINDOWS terminal: wsl --shutdown then start the distro again."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " configured no memory= set (WSL defaults to 50% of host RAM, or 8 GB,"
|
|
||||||
echo " whichever is less). To raise it, add on the Windows side:"
|
|
||||||
echo " [wsl2]"
|
|
||||||
echo " memory=8GB"
|
|
||||||
echo " then from a WINDOWS terminal: wsl --shutdown"
|
|
||||||
fi
|
|
||||||
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
|
||||||
if [ "$n" -gt 0 ]; then
|
|
||||||
echo " backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo " - native linux: no VM allocation to raise. If memory is tight the levers"
|
|
||||||
echo " are freeing something or adding swap."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "effective ceiling $(effective_ceiling_mb) MB"
|
|
||||||
echo " the lowest of MemTotal, the cgroup cap and ulimit -v. What the box"
|
|
||||||
echo " claims. 'push' measures what it will actually hand over."
|
|
||||||
|
|
||||||
[ "$avail" -lt $(( total / 5 )) ] && { echo; hogs; }
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── .wslconfig ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
require_wsl() {
|
|
||||||
if ! is_wsl; then
|
|
||||||
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
|
||||||
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
|
||||||
echo "Use 'status' to see what the machine actually has." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# backup and restore act on the file, so unlike status they must not guess.
|
|
||||||
wslconfig_required() {
|
|
||||||
local cfg; cfg=$(wslconfig_required)
|
|
||||||
if [ -z "$cfg" ]; then
|
|
||||||
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
|
||||||
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
|
||||||
| grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "$cfg"
|
|
||||||
}
|
|
||||||
|
|
||||||
configured_memory() {
|
|
||||||
[ -r "$1" ] || { echo ""; return; }
|
|
||||||
sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
# "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo.
|
|
||||||
to_mb() {
|
|
||||||
local v="${1^^}" n
|
|
||||||
n=$(echo "$v" | tr -dc '0-9')
|
|
||||||
[ -n "$n" ] || { echo ""; return; }
|
|
||||||
case "$v" in
|
|
||||||
*GB|*G) echo $(( n * 1024 )) ;;
|
|
||||||
*MB|*M) echo "$n" ;;
|
|
||||||
*) echo $(( n / 1024 / 1024 )) ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
backup() {
|
|
||||||
require_wsl backup
|
|
||||||
local cfg dest
|
|
||||||
cfg=$(wslconfig_required)
|
|
||||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
|
||||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
|
||||||
# second run is not a backup.
|
|
||||||
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
|
||||||
cp "$cfg" "$dest"
|
|
||||||
echo "backed up $dest"
|
|
||||||
echo
|
|
||||||
echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
require_wsl restore
|
|
||||||
local cfg newest count
|
|
||||||
cfg=$(wslconfig_required)
|
|
||||||
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
|
||||||
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
|
||||||
|
|
||||||
echo "restoring $newest"
|
|
||||||
echo " -> $cfg"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Newest is the right default — undo the last edit — but if you backed up
|
|
||||||
# *after* editing, the state you want is older. Show the rest so a no-op
|
|
||||||
# restore is obviously a no-op rather than a mystery.
|
|
||||||
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
|
||||||
if [ "$count" -gt 1 ]; then
|
|
||||||
echo "$count backups exist, newest first:"
|
|
||||||
ls -t "$cfg".*.bak | sed 's/^/ /'
|
|
||||||
echo " (restoring the newest; copy another by hand to pick an older one)"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -r "$cfg" ]; then
|
|
||||||
echo "what changes:"
|
|
||||||
if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then
|
|
||||||
echo " nothing — that backup is identical to the current config"
|
|
||||||
else
|
|
||||||
sed 's/^/ /' /tmp/mem.diff
|
|
||||||
fi
|
|
||||||
rm -f /tmp/mem.diff
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "proceed? [y/N] "
|
|
||||||
read -r reply
|
|
||||||
case "$reply" in
|
|
||||||
y|Y|yes|Yes) ;;
|
|
||||||
*) echo "left alone"; return 0 ;;
|
|
||||||
esac
|
|
||||||
cp "$newest" "$cfg"
|
|
||||||
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── push ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
STATE=""
|
|
||||||
CHILD=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [ -n "$CHILD" ] && kill -0 "$CHILD" 2>/dev/null; then
|
|
||||||
kill -KILL "$CHILD" 2>/dev/null || true
|
|
||||||
wait "$CHILD" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
[ -n "$STATE" ] && rm -f "$STATE"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# The child allocates and stops itself; the parent only watches. That split is
|
|
||||||
# the point: under --to-oom the allocating process is expected to be killed, and
|
|
||||||
# something has to survive to say how far it got.
|
|
||||||
allocator() {
|
|
||||||
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
|
||||||
# first. Raising needs no privilege (only lowering does). Without it, the
|
|
||||||
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
|
||||||
# box you are still using, that is not an acceptable coin toss.
|
|
||||||
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
|
||||||
|
|
||||||
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
|
||||||
local bytes=$((STEP_MB * 1024 * 1024))
|
|
||||||
local swap_used_start
|
|
||||||
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
|
||||||
|
|
||||||
while :; do
|
|
||||||
# Written STRAIGHT INTO the array element. The obvious spelling —
|
|
||||||
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
|
||||||
# not one: the template stays resident, expanding "$chunk" makes a
|
|
||||||
# temporary word, and the append makes the element. A 128 MB step then
|
|
||||||
# needs 384 MB transiently, and on a small box it is killed on the
|
|
||||||
# first append while reporting a third of the true ceiling.
|
|
||||||
#
|
|
||||||
# printf -v into a subscript also means every page is written, so it is
|
|
||||||
# resident rather than merely promised — the only kind of allocation
|
|
||||||
# that measures anything under heuristic overcommit.
|
|
||||||
printf -v "arr[$i]" '%*s' "$bytes" ''
|
|
||||||
i=$((i + 1)); held=$((held + STEP_MB))
|
|
||||||
|
|
||||||
rss=$(awk '/^VmRSS:/{print int($2/1024)}' "/proc/$BASHPID/status" 2>/dev/null || echo 0)
|
|
||||||
avail=$(headroom_mb)
|
|
||||||
swapped=$(( $(mb SwapTotal) - $(mb SwapFree) - swap_used_start ))
|
|
||||||
[ "$swapped" -lt 0 ] && swapped=0
|
|
||||||
|
|
||||||
printf '%8s MB held rss %7s MB headroom %7s MB swap +%s MB\n' \
|
|
||||||
"$held" "$rss" "$avail" "$swapped"
|
|
||||||
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
|
||||||
|
|
||||||
# Worth calling out separately from the ceiling: this is where the box
|
|
||||||
# stops being fast and starts being unusable, which for a scheduler is
|
|
||||||
# a different and earlier problem than being killed.
|
|
||||||
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
|
||||||
first_swap=$held
|
|
||||||
echo " - first swap page at ${held} MB — past here it works but crawls"
|
|
||||||
echo "swapat $held" >> "$STATE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$TO_MB" ] && [ "$held" -ge "$TO_MB" ]; then
|
|
||||||
echo "stop reached-the-cap" >> "$STATE"; return 0
|
|
||||||
fi
|
|
||||||
if [ "$TO_OOM" = no ] && [ "$avail" -lt "$FLOOR_MB" ]; then
|
|
||||||
echo "stop floor" >> "$STATE"; return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
push() {
|
|
||||||
local total ceiling rc=0 last held rss swapat stop
|
|
||||||
total=$(mb MemTotal)
|
|
||||||
ceiling=$(effective_ceiling_mb)
|
|
||||||
|
|
||||||
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
|
||||||
# find the edge, few enough lines to read, and small enough that the
|
|
||||||
# transient cost of one allocation never dominates a small box. A fixed
|
|
||||||
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
|
||||||
if [ "$STEP_EXPLICIT" = no ]; then
|
|
||||||
STEP_MB=$(( ceiling / 64 ))
|
|
||||||
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
|
||||||
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
|
||||||
# depends on what it is protecting. Under a cgroup cap, running out kills
|
|
||||||
# only this container's own processes, so it need cover no more than the
|
|
||||||
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
|
||||||
# halve the answer. On a host there is everything else to protect, and the
|
|
||||||
# OOM killer does not promise to pick the process that caused the problem.
|
|
||||||
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
|
||||||
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
|
||||||
|
|
||||||
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
|
||||||
trap cleanup EXIT
|
|
||||||
# INT kills the child and lets the summary below print anyway, so an
|
|
||||||
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
|
||||||
# still gives the memory back.
|
|
||||||
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
|
||||||
|
|
||||||
echo "push"
|
|
||||||
echo " step ${STEP_MB} MB per allocation, every page touched"
|
|
||||||
echo " ceiling ${ceiling} MB claimed"
|
|
||||||
if [ -n "$TO_MB" ]; then
|
|
||||||
echo " stopping at ${TO_MB} MB (--to)"
|
|
||||||
elif [ "$TO_OOM" = yes ]; then
|
|
||||||
echo " ! stopping only when the kernel stops it (--to-oom)"
|
|
||||||
echo " the allocating child is marked as the preferred OOM victim,"
|
|
||||||
echo " but nothing about an OOM kill is entirely polite. Not on a box"
|
|
||||||
echo " running anything you mind losing."
|
|
||||||
else
|
|
||||||
echo " stopping when headroom drops below ${FLOOR_MB} MB"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
allocator &
|
|
||||||
CHILD=$!
|
|
||||||
wait "$CHILD" || rc=$?
|
|
||||||
CHILD=""
|
|
||||||
trap - INT
|
|
||||||
|
|
||||||
last=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 || true)
|
|
||||||
held=$(echo "$last" | awk '{print $1}')
|
|
||||||
rss=$(echo "$last" | awk '{print $2}')
|
|
||||||
swapat=$(awk '/^swapat/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
|
||||||
stop=$(awk '/^stop/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [ -z "$held" ]; then
|
|
||||||
echo " ! nothing was allocated. Even one ${STEP_MB} MB chunk failed —"
|
|
||||||
echo " try a smaller --step, or check ulimit -v in 'status'."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " reached ${rss:-$held} MB resident"
|
|
||||||
[ -n "$swapat" ] && echo " swapping from ${swapat} MB"
|
|
||||||
|
|
||||||
case "$stop" in
|
|
||||||
reached-the-cap)
|
|
||||||
echo " outcome stopped at the --to cap, not at a limit."
|
|
||||||
echo " The box held ${TO_MB} MB without complaint; there is more." ;;
|
|
||||||
floor)
|
|
||||||
echo " outcome stopped with a cushion intact, by choice."
|
|
||||||
echo " The real ceiling is higher — --to-oom finds it, at the"
|
|
||||||
echo " cost of an actual OOM kill." ;;
|
|
||||||
interrupted)
|
|
||||||
echo " outcome interrupted at ${rss:-$held} MB — where you stopped it,"
|
|
||||||
echo " not where the box did." ;;
|
|
||||||
*)
|
|
||||||
# No stop line means the child did not decide to stop: it was ended.
|
|
||||||
if [ "$rc" -ge 128 ]; then
|
|
||||||
echo " outcome the child was killed (signal $((rc - 128))) at ${rss:-$held} MB."
|
|
||||||
elif [ "$rc" -ne 0 ]; then
|
|
||||||
echo " outcome the allocation failed at ${rss:-$held} MB (exit ${rc})."
|
|
||||||
echo " bash could not get the next chunk — an honest malloc"
|
|
||||||
echo " failure rather than a kill. That is the strict-overcommit"
|
|
||||||
echo " or ulimit path."
|
|
||||||
else
|
|
||||||
echo " outcome ended at ${rss:-$held} MB."
|
|
||||||
fi
|
|
||||||
local ev
|
|
||||||
ev=$(dmesg 2>/dev/null | tail -80 | grep -iE 'oom-kill|killed process' | tail -1 || true)
|
|
||||||
if [ -n "$ev" ]; then
|
|
||||||
echo " kernel ${ev#*] }"
|
|
||||||
else
|
|
||||||
echo " - dmesg is unreadable here (dmesg_restrict, or no privilege),"
|
|
||||||
echo " so the kill cannot be confirmed from this side. The number stands."
|
|
||||||
fi ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# The gap between the claim and the measurement is the finding — but only
|
|
||||||
# when the BOX chose where to stop. An empty $stop means the child was ended
|
|
||||||
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
|
||||||
# asked for, and flagging those as short of the ceiling would put a warning
|
|
||||||
# on every deliberately small run.
|
|
||||||
local got="${rss:-$held}"
|
|
||||||
echo
|
|
||||||
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
|
||||||
echo " ! claimed ${ceiling} MB, gave up ${got} MB — under 70% of it."
|
|
||||||
echo " Something is taking the difference. 'status' names the candidates:"
|
|
||||||
echo " a cgroup cap, ulimit -v, or memory already resident."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── all ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
all() {
|
|
||||||
status
|
|
||||||
echo
|
|
||||||
echo "────────────────────────────────────────────────────────────"
|
|
||||||
echo
|
|
||||||
push
|
|
||||||
|
|
||||||
local got budget_mb ceiling
|
|
||||||
load_config
|
|
||||||
if [ -n "$BUDGET_GB" ]; then
|
|
||||||
budget_mb=$(( BUDGET_GB * 1024 ))
|
|
||||||
else
|
|
||||||
budget_mb=$(( NODES * NODE_MB ))
|
|
||||||
fi
|
|
||||||
ceiling=$(effective_ceiling_mb)
|
|
||||||
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
|
||||||
[ -n "$got" ] || got=0
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "verdict"
|
|
||||||
if [ -n "$BUDGET_GB" ]; then
|
|
||||||
echo " budget ${budget_mb} MB (--budget)"
|
|
||||||
else
|
|
||||||
# rig's own figure for this profile: nodes times what one node costs.
|
|
||||||
# Addons carry no memory figure in rig yet, so this is the cluster alone
|
|
||||||
# and whatever you deploy comes on top. --budget once you know that too.
|
|
||||||
echo " budget ${budget_mb} MB — profile ${PROFILE_NAME}: ${NODES} node(s) x ${NODE_MB} MB,"
|
|
||||||
echo " the cluster alone; your workload comes on top (--budget GB)"
|
|
||||||
fi
|
|
||||||
echo " measured ${got} MB handed over"
|
|
||||||
|
|
||||||
if [ "$got" -ge "$budget_mb" ]; then
|
|
||||||
echo " fits, with $(( got - budget_mb )) MB spare."
|
|
||||||
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
|
||||||
echo " - under 30% spare is thin once a workload runs on top: memory use"
|
|
||||||
echo " is spiky, and the spikes are what get killed."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! short by $(( budget_mb - got )) MB."
|
|
||||||
if [ "$ceiling" -ge "$budget_mb" ]; then
|
|
||||||
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
|
||||||
echo " Free something, or read the caps section again."
|
|
||||||
else
|
|
||||||
echo " The box does not have it to give. A bigger machine, or a profile"
|
|
||||||
echo " with fewer nodes."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
parse_flags() {
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--to) TO_MB=$(( ${2:?--to needs a value in GB} * 1024 )); shift 2 ;;
|
|
||||||
--to-mb) TO_MB="${2:?--to-mb needs a value in MB}"; shift 2 ;;
|
|
||||||
--step) STEP_MB="${2:?--step needs a value in MB}"; STEP_EXPLICIT=yes; shift 2 ;;
|
|
||||||
--to-oom) TO_OOM=yes; shift ;;
|
|
||||||
--budget) BUDGET_GB="${2:?--budget needs a value in GB}"; BUDGET_EXPLICIT=yes; shift 2 ;;
|
|
||||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if [ "$TO_OOM" = yes ] && [ -n "$TO_MB" ]; then
|
|
||||||
echo "--to and --to-oom contradict each other: one stops early, the other" >&2
|
|
||||||
echo "refuses to stop at all. Pick one." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
require_linux
|
|
||||||
find_cgroup
|
|
||||||
|
|
||||||
cmd="${1:-status}"
|
|
||||||
[ $# -gt 0 ] && shift
|
|
||||||
|
|
||||||
case "$cmd" in
|
|
||||||
status) parse_flags "$@"; status ;;
|
|
||||||
push) parse_flags "$@"; push ;;
|
|
||||||
all) parse_flags "$@"; all ;;
|
|
||||||
backup) backup ;;
|
|
||||||
restore) restore ;;
|
|
||||||
*) echo "usage: $0 [status|push|all|backup|restore]" >&2
|
|
||||||
echo " push [--to GB] [--to-mb MB] [--step MB] [--to-oom]" >&2
|
|
||||||
echo " all [--budget GB]" >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -16,7 +16,7 @@ endif
|
|||||||
help: ## list targets
|
help: ## list targets
|
||||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
||||||
|
|
||||||
deps: ## rigdeps.sh [detect|list|verify|fetch|install] (default detect)
|
deps: ## rigdeps.sh [detect|list|verify|fetch|install|manifest|manifests|snapshot] (default detect)
|
||||||
bash $(HERE)rigdeps.sh $(or $(ARGS),detect)
|
bash $(HERE)rigdeps.sh $(or $(ARGS),detect)
|
||||||
|
|
||||||
mem: ## rigmini.sh [status|push|all|backup|restore] (default status)
|
mem: ## rigmini.sh [status|push|all|backup|restore] (default status)
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# GENERATED by make standalone — do not edit
|
# GENERATED by make standalone — do not edit
|
||||||
#
|
#
|
||||||
# rigdeps.sh for profile 'minimal', flattened from:
|
# rigdeps.sh for profile 'default', flattened from:
|
||||||
# ctrl/deps.sh
|
# ctrl/deps.sh
|
||||||
# ctrl/lib/config.sh
|
# ctrl/lib/config.sh
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
# ── from the libraries ──
|
# ── from the libraries ──
|
||||||
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'
|
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 )
|
||||||
|
}
|
||||||
_config_restore ()
|
_config_restore ()
|
||||||
{
|
{
|
||||||
local line;
|
local line;
|
||||||
@@ -19,10 +24,25 @@ _config_restore ()
|
|||||||
done <<< "$1";
|
done <<< "$1";
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
_from_ctrl ()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
/*)
|
||||||
|
echo "$1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "../$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
default_cluster_name ()
|
default_cluster_name ()
|
||||||
{
|
{
|
||||||
local n;
|
local n;
|
||||||
n=$(basename "$(cd .. && pwd)");
|
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" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
echo "${n:-rig}"
|
echo "${n:-rig}"
|
||||||
@@ -35,19 +55,22 @@ derive_port_base ()
|
|||||||
}
|
}
|
||||||
render_kind_config ()
|
render_kind_config ()
|
||||||
{
|
{
|
||||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}" overlay_dir="";
|
||||||
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_PATH"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'minimal' ──
|
# ── configuration, frozen for profile 'default' ──
|
||||||
load_config() {
|
load_config() {
|
||||||
local k saved=""
|
local k saved=""
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
done
|
done
|
||||||
declare -gx ADDONS=""
|
declare -g ADDONS=""
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
declare -gx CERT_MANAGER_MANIFEST_SHA256="5f6a499b8c1857d57f560f536e0dcc830914b45c420899fe7ad0692c8624e408"
|
||||||
declare -g AUDIT="off"
|
declare -gx CERT_MANAGER_MANIFEST_URL="https://github.com/cert-manager/cert-manager/releases/download/v1.21.1/cert-manager.yaml"
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
declare -g CLUSTER="rig"
|
declare -g CLUSTER="rig"
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
@@ -56,17 +79,15 @@ load_config() {
|
|||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
declare -gx DNS_MODE="hosts"
|
declare -g DNS_MODE="hosts"
|
||||||
declare -g HTTPS_PORT="20311"
|
declare -g HTTPS_PORT="20311"
|
||||||
declare -g HTTP_PORT="20310"
|
declare -g HTTP_PORT="20310"
|
||||||
declare -gx INGRESS_MODE="hostport"
|
declare -g INGRESS_MODE="hostport"
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
declare -gx K8S_VERSION="v1_36"
|
declare -g K8S_VERSION="v1_36"
|
||||||
declare -gx KIND_CONFIG="kind-config.yaml.tpl"
|
declare -g KIND_CONFIG="./k8s/kind-config.yaml.tpl"
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
@@ -74,8 +95,13 @@ load_config() {
|
|||||||
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
declare -gx KUBECTL_VERSION="v1.36.3"
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
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 -gx METALLB_VERSION="v0.16.0"
|
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 -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
declare -g NODES="1"
|
declare -g NODES="1"
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
@@ -84,13 +110,12 @@ load_config() {
|
|||||||
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
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 -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
declare -g NODE_MB="800"
|
declare -g NODE_MB="800"
|
||||||
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
declare -g OVERLAY_DIR="examples/starter"
|
||||||
declare -gx PROFILE_NAME="minimal"
|
declare -gx PROFILE="default"
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
declare -g PROFILE_NAME="default"
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
declare -gx REGISTRY_MODE="local"
|
declare -g REGISTRY_MODE="local"
|
||||||
declare -g REGISTRY_PORT="20313"
|
declare -g REGISTRY_PORT="20313"
|
||||||
declare -gx STUB_IMAGE="python:3.12-slim"
|
|
||||||
declare -g TILT_PORT="20312"
|
declare -g TILT_PORT="20312"
|
||||||
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
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"
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
@@ -100,39 +125,20 @@ load_config() {
|
|||||||
# ── end of frozen configuration ──
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
# ── ctrl/deps.sh ──
|
# ── ctrl/deps.sh ──
|
||||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
# Toolchain installer: detect the host, install pinned tools into $OUT_BIN, report
|
||||||
# report what it could not do.
|
# 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]
|
||||||
# It never runs the cluster, never uses sudo or apt, and writes only into
|
# | manifest NAME | manifests [--to DIR] | snapshot [DIR]]
|
||||||
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
# Notes: docs/notes/deps.md
|
||||||
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
|
||||||
# decide on, never performed. That is what makes it safe to run on a machine that
|
|
||||||
# already has a working setup.
|
|
||||||
#
|
|
||||||
# Usage (normally via `make deps`, or directly):
|
|
||||||
# deps.sh detect # report host facts only, change nothing
|
|
||||||
# deps.sh list # the pinned versions
|
|
||||||
# deps.sh verify [core|dev] # run what is installed and see if it works
|
|
||||||
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
|
||||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
|
||||||
#
|
|
||||||
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
|
||||||
# Default is dev.
|
|
||||||
#
|
|
||||||
# Runs both inside the installer container and bare on a host. Inside the
|
|
||||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
|
||||||
# falls back to /.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
# Keep the caller's cwd so a relative --to resolves there, not against ctrl/.
|
||||||
# not against ctrl/ once we've moved.
|
|
||||||
INVOKED_FROM="$PWD"
|
INVOKED_FROM="$PWD"
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
# Pins arrive through load_config like every other setting, not by sourcing
|
# Pins arrive through load_config, not by sourcing versions.env, so `make
|
||||||
# versions.env here. That is what lets `make standalone` freeze them into a
|
# standalone` can freeze them in.
|
||||||
# one-file installer: configuration has exactly one way in.
|
|
||||||
# (sourced library inlined above)
|
# (sourced library inlined above)
|
||||||
load_config
|
load_config
|
||||||
|
|
||||||
@@ -146,6 +152,14 @@ abspath() {
|
|||||||
|
|
||||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||||
HOST_ROOT="${HOST_ROOT:-/}"
|
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_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||||
@@ -153,12 +167,11 @@ BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
|||||||
# Collected by detect(), printed by report_manual() at the very end.
|
# Collected by detect(), printed by report_manual() at the very end.
|
||||||
MANUAL=()
|
MANUAL=()
|
||||||
|
|
||||||
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
# Facts print only with VERBOSE (`detect all`); problems (! and -) always print.
|
||||||
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
fact() { if [ -n "${VERBOSE:-}" ]; then echo "$@"; fi; }
|
||||||
# container's own view is already the host's.
|
|
||||||
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
# Host FILES are read through $HOST_ROOT; kernel facts are shared with the container.
|
||||||
# tight and does-not-fit branches can be exercised against a real machine's
|
# A /proc/meminfo field in MB, 0 if absent. MEMINFO overrides the source for testing.
|
||||||
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
|
||||||
mb_of() {
|
mb_of() {
|
||||||
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
@@ -183,9 +196,7 @@ arch() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# The pins above are amd64. Rather than download something that cannot execute
|
# Pins are amd64 only: refuse elsewhere and print how to get the right checksums.
|
||||||
# and let it fail as "cannot execute binary file: Exec format error", say so
|
|
||||||
# here and hand over the commands that produce the right checksums.
|
|
||||||
require_amd64() {
|
require_amd64() {
|
||||||
local a; a=$(arch)
|
local a; a=$(arch)
|
||||||
[ "$a" = "amd64" ] && return 0
|
[ "$a" = "amd64" ] && return 0
|
||||||
@@ -238,9 +249,7 @@ pick_sha() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── package manager, for the instructions only ─────────────────────────────
|
# ── package manager, for the instructions only ─────────────────────────────
|
||||||
# This never runs a package manager. It names one so the reported action is
|
# Never runs one; names the right one so reported actions are pasteable.
|
||||||
# something you can paste, on the distro you are actually on — an apt line on
|
|
||||||
# Amazon Linux 2 is a wrong answer dressed up as help.
|
|
||||||
|
|
||||||
pkg_install_cmd() {
|
pkg_install_cmd() {
|
||||||
local pkg="$1"
|
local pkg="$1"
|
||||||
@@ -260,11 +269,9 @@ docker_pkg() {
|
|||||||
|
|
||||||
# ── detect ─────────────────────────────────────────────────────────────────
|
# ── detect ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
# Windows outside WSL (Git Bash, MSYS, Cygwin) fails confusingly; name it instead.
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
require_linux() {
|
||||||
case "$(uname -s)" in
|
case "${UNAME_S:-$(uname -s)}" in
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
@@ -283,40 +290,34 @@ EOF
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
is_wsl() { grep -qi microsoft "$(host_file /proc/version)" 2>/dev/null; }
|
||||||
|
|
||||||
detect() {
|
detect() {
|
||||||
echo "host"
|
echo "host"
|
||||||
echo " kernel $(uname -r)"
|
fact " kernel $(uname -r)"
|
||||||
echo " arch $(arch) ($(uname -m))"
|
|
||||||
|
|
||||||
local osr; osr=$(host_file /etc/os-release)
|
local osr distro=""; osr=$(host_file /etc/os-release)
|
||||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
[ -r "$osr" ] && distro=$(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")
|
||||||
|
echo " distro ${distro:-unknown} $(arch), $(if is_wsl; then echo WSL; else echo native linux; fi)"
|
||||||
|
|
||||||
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
# In MB (whole GB rounds away too much). Facts only; check.sh judges sufficiency.
|
||||||
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
|
||||||
# that is enough depends on the profile, which check.sh knows and this does not.
|
|
||||||
local total_mb avail_mb swap_total_mb swap_used_mb om
|
local total_mb avail_mb swap_total_mb swap_used_mb om
|
||||||
total_mb=$(mb_of MemTotal)
|
total_mb=$(mb_of MemTotal)
|
||||||
avail_mb=$(mb_of MemAvailable)
|
avail_mb=$(mb_of MemAvailable)
|
||||||
swap_total_mb=$(mb_of SwapTotal)
|
swap_total_mb=$(mb_of SwapTotal)
|
||||||
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
||||||
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
printf " memory %d MB total, %d MB available%s\n" "$total_mb" "$avail_mb" \
|
||||||
if [ "$swap_total_mb" -gt 0 ]; then
|
"$(if [ "$swap_used_mb" -gt 0 ]; then echo ", $swap_used_mb MB in swap"; fi)"
|
||||||
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
# Overcommit mode: with 1 the OOM killer settles up later, after a clean start.
|
||||||
# always says yes and settles up later with the OOM killer, so a cluster that
|
|
||||||
# starts cleanly can still lose processes afterwards.
|
|
||||||
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||||
case "$om" in
|
case "$om" in
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
0) fact " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
1) fact " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||||
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
2) fact " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo " install to $OUT_BIN"
|
fact " install to $OUT_BIN"
|
||||||
detect_libc
|
detect_libc
|
||||||
detect_prereqs
|
detect_prereqs
|
||||||
detect_wsl
|
detect_wsl
|
||||||
@@ -328,18 +329,13 @@ detect() {
|
|||||||
|
|
||||||
detect_wsl() {
|
detect_wsl() {
|
||||||
if ! is_wsl; then
|
if ! is_wsl; then
|
||||||
echo " platform native linux"
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " platform WSL"
|
# systemd is off by default in WSL; enabling it needs a Windows-side restart.
|
||||||
|
|
||||||
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
|
||||||
# host service need it. Enabling it requires a Windows-side restart, which
|
|
||||||
# cannot be issued from inside the distro.
|
|
||||||
local wc; wc=$(host_file /etc/wsl.conf)
|
local wc; wc=$(host_file /etc/wsl.conf)
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||||
echo " systemd enabled in wsl.conf"
|
fact " systemd enabled in wsl.conf"
|
||||||
else
|
else
|
||||||
echo " ! systemd not enabled in /etc/wsl.conf"
|
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||||
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||||
@@ -351,27 +347,24 @@ detect_wsl() {
|
|||||||
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||||
# local DNS setup.
|
# local DNS setup.
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||||
echo " resolv.conf pinned (generateResolvConf=false)"
|
fact " resolv.conf pinned (generateResolvConf=false)"
|
||||||
else
|
else
|
||||||
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
fact " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local wcfg
|
local wcfg
|
||||||
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||||
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
fact " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||||
else
|
else
|
||||||
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||||
make mem status
|
make check mem
|
||||||
It prints the edit to make and the command to apply it.")
|
It prints the edit to make and the command to apply it.")
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
# Filesystem types that deliver no inotify events (9p, drvfs, network, fuse).
|
||||||
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
# Checks the fs type, not the path.
|
||||||
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
|
||||||
# way. None of them deliver inotify events, so anything watching files goes
|
|
||||||
# quiet without saying why.
|
|
||||||
watch_hostile_fs() {
|
watch_hostile_fs() {
|
||||||
local dir="$1" fstype
|
local dir="$1" fstype
|
||||||
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||||
@@ -392,28 +385,22 @@ detect_filesystem() {
|
|||||||
on a $fstype mount, and everything else is slower:
|
on a $fstype mount, and everything else is slower:
|
||||||
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||||
else
|
else
|
||||||
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
fact " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
# tilt needs glibc >= 2.34 (measured on Amazon Linux 2). Report the version here;
|
||||||
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
# `verify` catches the actual failure after installing.
|
||||||
#
|
|
||||||
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
|
||||||
#
|
|
||||||
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
|
||||||
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
|
||||||
# version now; `verify` catches the actual failure after installing.
|
|
||||||
detect_libc() {
|
detect_libc() {
|
||||||
local v=""
|
local v=""
|
||||||
if command -v ldd >/dev/null 2>&1; then
|
if command -v ldd >/dev/null 2>&1; then
|
||||||
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
||||||
fi
|
fi
|
||||||
if [ -z "$v" ]; then
|
if [ -z "$v" ]; then
|
||||||
echo " libc unknown (no ldd) — 'verify' is the real test"
|
fact " libc unknown (no ldd) — 'verify' is the real test"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
echo " libc glibc $v"
|
fact " libc glibc $v"
|
||||||
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
||||||
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
||||||
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
||||||
@@ -422,25 +409,23 @@ detect_libc() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# What this script needs to do its own job. Reported here so `detect` answers
|
# What this script itself needs, so `detect` answers "will install work?".
|
||||||
# "will install work?" instead of leaving you to find out one download in.
|
|
||||||
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
|
||||||
detect_prereqs() {
|
detect_prereqs() {
|
||||||
local missing=""
|
local missing=""
|
||||||
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
if command -v curl >/dev/null 2>&1; then fact " download curl"
|
||||||
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
elif command -v wget >/dev/null 2>&1; then fact " download wget"
|
||||||
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
||||||
echo " checksums ok"
|
fact " checksums ok"
|
||||||
else
|
else
|
||||||
echo " ! no sha256sum or shasum — downloads could not be verified"
|
echo " ! no sha256sum or shasum — downloads could not be verified"
|
||||||
missing+=" coreutils"
|
missing+=" coreutils"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
||||||
echo " archives tar + gzip"
|
fact " archives tar + gzip"
|
||||||
else
|
else
|
||||||
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
||||||
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
||||||
@@ -455,10 +440,7 @@ detect_prereqs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detect_docker() {
|
detect_docker() {
|
||||||
# Reachability of the daemon is the real question, and the CLI is only how
|
# Daemon reachability is the real question; the CLI is only how we ask.
|
||||||
# we ask it. Note that when this runs inside the installer container, Docker
|
|
||||||
# necessarily exists on the host — otherwise nothing would be executing —
|
|
||||||
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
if [ -S /var/run/docker.sock ]; then
|
if [ -S /var/run/docker.sock ]; then
|
||||||
echo " docker socket present (no cli in this context)"
|
echo " docker socket present (no cli in this context)"
|
||||||
@@ -477,12 +459,9 @@ detect_docker() {
|
|||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||||
local n
|
local n
|
||||||
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||||
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
# Must be an `if`, not `[ ] && echo`: a zero count would return 1 under set -e.
|
||||||
# function the latter returns 1 when the count is zero, and `set -e`
|
|
||||||
# then kills the caller. That is the fresh-machine case — no clusters
|
|
||||||
# yet — so the bug only ever shows up where it does most harm.
|
|
||||||
if [ "$n" -gt 0 ]; then
|
if [ "$n" -gt 0 ]; then
|
||||||
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
echo " kind $n node container(s) running — 'make cluster list'"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
@@ -497,7 +476,7 @@ detect_inotify() {
|
|||||||
local w i
|
local w i
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
echo " inotify watches=$w instances=$i"
|
fact " inotify watches=$w instances=$i"
|
||||||
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||||
@@ -557,18 +536,13 @@ fetch_tgz() {
|
|||||||
echo " fetching $name"
|
echo " fetching $name"
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
verify "$tmp" "$sha" "$name"
|
verify "$tmp" "$sha" "$name"
|
||||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
# --no-same-owner: as root, tar would restore the archive's uid/gid.
|
||||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
|
||||||
# user does not own.
|
|
||||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
chmod +x "$dest/$name"
|
chmod +x "$dest/$name"
|
||||||
}
|
}
|
||||||
|
|
||||||
# The installer runs as root so it can reach the docker socket, which means
|
# The installer runs as root; hand files in a mounted dir back to the mount point's owner.
|
||||||
# everything it writes into a mounted volume lands root-owned and unusable from
|
|
||||||
# the host. Hand it back to whoever owns the mount point (the host user created
|
|
||||||
# that directory before mounting it).
|
|
||||||
fix_ownership() {
|
fix_ownership() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
[ -d "$dir" ] || return 0
|
[ -d "$dir" ] || return 0
|
||||||
@@ -580,38 +554,14 @@ fix_ownership() {
|
|||||||
chown -R "$owner" "$dir" 2>/dev/null || true
|
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Two tiers, because not every machine should get cluster tooling.
|
# core: talk to a cluster someone else runs. dev: core plus tools that build clusters.
|
||||||
#
|
|
||||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
|
||||||
# creates one. Appropriate on a managed or corporate-issued machine
|
|
||||||
# where development tools are not wanted by default.
|
|
||||||
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
|
||||||
#
|
|
||||||
# The split exists because "install the toolchain" is not one decision: on a
|
|
||||||
# managed workspace the right answer is kubectl and nothing else.
|
|
||||||
CORE_TOOLS="kubectl jq"
|
CORE_TOOLS="kubectl jq"
|
||||||
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
# No helm (nothing uses a chart). ctlptl wires in a local registry; compose is often
|
||||||
# has ever invoked it. Add it back the day something actually needs a chart.
|
# missing from distro docker packages.
|
||||||
#
|
|
||||||
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
|
||||||
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
|
||||||
# because it is what wires a cluster to a local registry — without one, an
|
|
||||||
# unqualified image name resolves to docker.io/library/<name> and there is
|
|
||||||
# nothing structural stopping a push there.
|
|
||||||
#
|
|
||||||
# docker-compose is 'dev' for the same reason, and is here because the distro
|
|
||||||
# docker packages ship the daemon and CLI but frequently not the compose
|
|
||||||
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
|
||||||
# working Docker, and nothing about that message names the missing piece.
|
|
||||||
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
||||||
|
|
||||||
# ── what is already on this machine ───────────────────────────────────────
|
# ── what is already on this machine ───────────────────────────────────────
|
||||||
#
|
# A tool already on PATH at its pinned version is left where it is.
|
||||||
# A tool already on PATH at its pinned version is left where it is. Without
|
|
||||||
# this, install downloads a second copy into OUT_BIN and then reports the first
|
|
||||||
# one as shadowed — noise, and wrong, when both are the same version. That is
|
|
||||||
# the normal state of any machine someone set up by hand, whatever directory
|
|
||||||
# they happened to choose.
|
|
||||||
|
|
||||||
pin_of() {
|
pin_of() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -624,9 +574,7 @@ pin_of() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# The version string a binary reports. Each tool spells the question
|
# The version string a binary reports (kubectl needs --client).
|
||||||
# differently, and kubectl has to be told --client or it goes looking for a
|
|
||||||
# server to ask.
|
|
||||||
reported_version() {
|
reported_version() {
|
||||||
local tool="$1" path="$2"
|
local tool="$1" path="$2"
|
||||||
case "$tool" in
|
case "$tool" in
|
||||||
@@ -636,13 +584,8 @@ reported_version() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
# Does the binary at PATH report PIN? Whole-token match, leading v optional.
|
||||||
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
# Bash regex rather than grep, deliberately.
|
||||||
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
|
||||||
#
|
|
||||||
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
|
||||||
# on every machine — some builds reject patterns that others accept — and a
|
|
||||||
# failed grep inside a count reads exactly like a zero.
|
|
||||||
version_matches() {
|
version_matches() {
|
||||||
local tool="$1" path="$2" pin="$3" out v re
|
local tool="$1" path="$2" pin="$3" out v re
|
||||||
out=$(reported_version "$tool" "$path") || return 1
|
out=$(reported_version "$tool" "$path") || return 1
|
||||||
@@ -652,10 +595,8 @@ version_matches() {
|
|||||||
[[ $out =~ $re ]]
|
[[ $out =~ $re ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
# DEPS_ONLY narrows a fetch to the tools it names; unset means the whole tier.
|
||||||
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
# Only install() sets it.
|
||||||
# DIR" must not quietly skip something because this machine happens to have it.
|
|
||||||
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
|
||||||
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
||||||
|
|
||||||
# Every tool in the tier with its state, probed once and reported once. What
|
# Every tool in the tier with its state, probed once and reported once. What
|
||||||
@@ -664,20 +605,18 @@ TOOLCHAIN_NEED=""
|
|||||||
detect_toolchain() {
|
detect_toolchain() {
|
||||||
local tier="${TIER:-dev}" b pin path found
|
local tier="${TIER:-dev}" b pin path found
|
||||||
TOOLCHAIN_NEED=""
|
TOOLCHAIN_NEED=""
|
||||||
|
local n=0
|
||||||
echo
|
echo
|
||||||
echo "toolchain (pinned, tier '$tier')"
|
fact "toolchain (pinned, tier '$tier')"
|
||||||
for b in $(tier_tools "$tier"); do
|
for b in $(tier_tools "$tier"); do
|
||||||
|
n=$((n + 1))
|
||||||
pin=$(pin_of "$b")
|
pin=$(pin_of "$b")
|
||||||
path=$(command -v "$b" 2>/dev/null || true)
|
path=$(command -v "$b" 2>/dev/null || true)
|
||||||
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
# compose is normally a docker CLI plugin, not on PATH: ask docker instead.
|
||||||
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
|
||||||
# has no `docker-compose` to find — and probing only PATH would report it
|
|
||||||
# missing and re-download a copy that is already there. That is the exact
|
|
||||||
# noise the version-aware skip exists to prevent, so ask docker instead.
|
|
||||||
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
||||||
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
||||||
if [ "${found#v}" = "${pin#v}" ]; then
|
if [ "${found#v}" = "${pin#v}" ]; then
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
fact "$(printf " %-8s %-9s %s" "$b" "$pin" "docker cli plugin")"
|
||||||
else
|
else
|
||||||
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
||||||
"$b" "$pin" "$found"
|
"$b" "$pin" "$found"
|
||||||
@@ -690,7 +629,7 @@ detect_toolchain() {
|
|||||||
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
||||||
TOOLCHAIN_NEED+="$b "
|
TOOLCHAIN_NEED+="$b "
|
||||||
elif version_matches "$b" "$path" "$pin"; then
|
elif version_matches "$b" "$path" "$pin"; then
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
fact "$(printf " %-8s %-9s %s" "$b" "$pin" "$path")"
|
||||||
else
|
else
|
||||||
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
||||||
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
||||||
@@ -698,9 +637,10 @@ detect_toolchain() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ -z "$TOOLCHAIN_NEED" ]; then
|
if [ -z "$TOOLCHAIN_NEED" ]; then
|
||||||
echo " every pinned tool is already on PATH — nothing to fetch"
|
if [ -n "${VERBOSE:-}" ]; then echo " all $n on PATH — nothing to fetch"
|
||||||
|
else echo "toolchain all $n pinned tools on PATH (tier $tier)"; fi
|
||||||
else
|
else
|
||||||
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
echo "toolchain 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,8 +681,7 @@ fetch() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
fix_ownership "$dest"
|
fix_ownership "$dest"
|
||||||
# kind writes the kubeconfig as root too; hand that back as well when it's
|
# kind writes the kubeconfig as root too; hand that back as well.
|
||||||
# a mounted host directory rather than container-local state.
|
|
||||||
fix_ownership "${KUBE_DIR:-/out/kube}"
|
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,14 +703,8 @@ report_manual() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Installing into a directory that sits early in PATH silently replaces whatever
|
# A verified download proves the right file, not that this machine can run it
|
||||||
# the machine was already using — which on a shared or client machine can break
|
# (old glibc breaks tilt). Run each one now.
|
||||||
# unrelated work (kubectl more than one minor away from a cluster is the common
|
|
||||||
# one). Say so; never decide it for them.
|
|
||||||
# Downloading a verified binary proves it is the right file, not that this
|
|
||||||
# machine can run it. On an old distro tilt fails here, with a linker error
|
|
||||||
# about a missing symbol, and finding that out now beats finding out during a
|
|
||||||
# first cluster build.
|
|
||||||
verify_tools() {
|
verify_tools() {
|
||||||
local tier="${1:-dev}" b bin out rc broke=0
|
local tier="${1:-dev}" b bin out rc broke=0
|
||||||
echo "checking that each one actually runs"
|
echo "checking that each one actually runs"
|
||||||
@@ -781,11 +714,7 @@ verify_tools() {
|
|||||||
printf ' %-14s not installed\n' "$b"
|
printf ' %-14s not installed\n' "$b"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
# Not piped into `head`: under pipefail, SIGPIPE (141) looked like failure.
|
||||||
# than one line gets SIGPIPE when head closes the pipe, and the
|
|
||||||
# pipeline reports 141 — so a working kubectl was announced as "does
|
|
||||||
# not run here", with its own correct version string as the evidence.
|
|
||||||
# Take the first line afterwards, from the string.
|
|
||||||
rc=0
|
rc=0
|
||||||
case "$b" in
|
case "$b" in
|
||||||
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
||||||
@@ -863,18 +792,12 @@ warn_shadowing() {
|
|||||||
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||||
}
|
}
|
||||||
|
|
||||||
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
# Link the fetched docker-compose into ~/.docker/cli-plugins so `docker compose` works.
|
||||||
# retired v1 spelling; every compose file written in the last few years assumes
|
|
||||||
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
|
||||||
# So the binary is fetched like any other and then linked, in your own home —
|
|
||||||
# no root, and nothing outside it.
|
|
||||||
install_compose_plugin() {
|
install_compose_plugin() {
|
||||||
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
||||||
[ -x "$src" ] || return 0
|
[ -x "$src" ] || return 0
|
||||||
mkdir -p "$dir"
|
mkdir -p "$dir"
|
||||||
# Something else already owns that name — docker-desktop and some distro
|
# A real file there belongs to something else (docker-desktop, distro): don't overwrite.
|
||||||
# packages install a real file there. Overwriting it would take the plugin
|
|
||||||
# away from whatever put it there, so say so and let the user decide.
|
|
||||||
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
||||||
MANUAL+=("Something already installs the compose plugin at
|
MANUAL+=("Something already installs the compose plugin at
|
||||||
$dir/docker-compose
|
$dir/docker-compose
|
||||||
@@ -905,16 +828,12 @@ install() {
|
|||||||
if [ "$tier" = "core" ]; then
|
if [ "$tier" = "core" ]; then
|
||||||
echo " (no kind/tilt — 'make deps dev' adds them)"
|
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||||
fi
|
fi
|
||||||
# Only when compose was one of the things fetched: linking a binary
|
# Only when compose was fetched, never at a copy rig did not install.
|
||||||
# that is already satisfied elsewhere on PATH would point the plugin at
|
|
||||||
# a copy rig did not install.
|
|
||||||
case " $TOOLCHAIN_NEED " in
|
case " $TOOLCHAIN_NEED " in
|
||||||
*" docker-compose "*) install_compose_plugin ;;
|
*" docker-compose "*) install_compose_plugin ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Only worth saying when something actually landed in OUT_BIN. When every
|
# PATH advice only when something actually landed in OUT_BIN.
|
||||||
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
|
||||||
# telling the user to add it would be advice to fix nothing.
|
|
||||||
case ":${PATH}:" in
|
case ":${PATH}:" in
|
||||||
*":$OUT_BIN:"*) ;;
|
*":$OUT_BIN:"*) ;;
|
||||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||||
@@ -930,13 +849,103 @@ install() {
|
|||||||
|
|
||||||
require_linux
|
require_linux
|
||||||
|
|
||||||
# Read the command, THEN shift — and shift only if there is something there.
|
# Shift only if there is an argument: a bare `shift` returns 1 under set -e.
|
||||||
# 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.
|
|
||||||
cmd="${1:-install}"
|
cmd="${1:-install}"
|
||||||
[ $# -gt 0 ] && shift
|
[ $# -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.
|
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||||
need_downloads() {
|
need_downloads() {
|
||||||
require_amd64
|
require_amd64
|
||||||
@@ -945,14 +954,20 @@ need_downloads() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
detect) detect; report_manual ;;
|
detect) if [ "${1:-}" = all ]; then VERBOSE=1; fi; detect; report_manual ;;
|
||||||
list) list ;;
|
list) list ;;
|
||||||
verify) verify_tools "${1:-dev}" ;;
|
verify) verify_tools "${1:-dev}" ;;
|
||||||
fetch) need_downloads; fetch "$@" ;;
|
fetch) need_downloads; fetch "$@" ;;
|
||||||
install) need_downloads; install "${1:-dev}" ;;
|
install) need_downloads; install "${1:-dev}" ;;
|
||||||
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
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 " install [core|dev] (default dev)" >&2
|
echo " install [core|dev] (default dev)" >&2
|
||||||
echo " fetch [core|dev] [--to DIR]" >&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
|
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||||
exit 1 ;;
|
exit 1 ;;
|
||||||
esac
|
esac
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# GENERATED by make standalone — do not edit
|
# GENERATED by make standalone — do not edit
|
||||||
#
|
#
|
||||||
# rigmini.sh for profile 'minimal', flattened from:
|
# rigmini.sh for profile 'default', flattened from:
|
||||||
# ctrl/mem.sh
|
# ctrl/mem.sh
|
||||||
# ctrl/lib/config.sh
|
# ctrl/lib/config.sh
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
# ── from the libraries ──
|
# ── from the libraries ──
|
||||||
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'
|
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 )
|
||||||
|
}
|
||||||
_config_restore ()
|
_config_restore ()
|
||||||
{
|
{
|
||||||
local line;
|
local line;
|
||||||
@@ -19,10 +24,25 @@ _config_restore ()
|
|||||||
done <<< "$1";
|
done <<< "$1";
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
_from_ctrl ()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
/*)
|
||||||
|
echo "$1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "../$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
default_cluster_name ()
|
default_cluster_name ()
|
||||||
{
|
{
|
||||||
local n;
|
local n;
|
||||||
n=$(basename "$(cd .. && pwd)");
|
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" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
echo "${n:-rig}"
|
echo "${n:-rig}"
|
||||||
@@ -35,19 +55,22 @@ derive_port_base ()
|
|||||||
}
|
}
|
||||||
render_kind_config ()
|
render_kind_config ()
|
||||||
{
|
{
|
||||||
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}" overlay_dir="";
|
||||||
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_PATH"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'minimal' ──
|
# ── configuration, frozen for profile 'default' ──
|
||||||
load_config() {
|
load_config() {
|
||||||
local k saved=""
|
local k saved=""
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
done
|
done
|
||||||
declare -gx ADDONS=""
|
declare -g ADDONS=""
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
declare -gx CERT_MANAGER_MANIFEST_SHA256="5f6a499b8c1857d57f560f536e0dcc830914b45c420899fe7ad0692c8624e408"
|
||||||
declare -g AUDIT="off"
|
declare -gx CERT_MANAGER_MANIFEST_URL="https://github.com/cert-manager/cert-manager/releases/download/v1.21.1/cert-manager.yaml"
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
declare -g CLUSTER="rig"
|
declare -g CLUSTER="rig"
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
@@ -56,17 +79,15 @@ load_config() {
|
|||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
declare -gx DNS_MODE="hosts"
|
declare -g DNS_MODE="hosts"
|
||||||
declare -g HTTPS_PORT="20311"
|
declare -g HTTPS_PORT="20311"
|
||||||
declare -g HTTP_PORT="20310"
|
declare -g HTTP_PORT="20310"
|
||||||
declare -gx INGRESS_MODE="hostport"
|
declare -g INGRESS_MODE="hostport"
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
declare -gx K8S_VERSION="v1_36"
|
declare -g K8S_VERSION="v1_36"
|
||||||
declare -gx KIND_CONFIG="kind-config.yaml.tpl"
|
declare -g KIND_CONFIG="./k8s/kind-config.yaml.tpl"
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
@@ -74,8 +95,13 @@ load_config() {
|
|||||||
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
declare -gx KUBECTL_VERSION="v1.36.3"
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
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 -gx METALLB_VERSION="v0.16.0"
|
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 -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
declare -g NODES="1"
|
declare -g NODES="1"
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
@@ -84,13 +110,12 @@ load_config() {
|
|||||||
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
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 -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
declare -g NODE_MB="800"
|
declare -g NODE_MB="800"
|
||||||
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
declare -g OVERLAY_DIR="examples/starter"
|
||||||
declare -gx PROFILE_NAME="minimal"
|
declare -gx PROFILE="default"
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
declare -g PROFILE_NAME="default"
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
declare -gx REGISTRY_MODE="local"
|
declare -g REGISTRY_MODE="local"
|
||||||
declare -g REGISTRY_PORT="20313"
|
declare -g REGISTRY_PORT="20313"
|
||||||
declare -gx STUB_IMAGE="python:3.12-slim"
|
|
||||||
declare -g TILT_PORT="20312"
|
declare -g TILT_PORT="20312"
|
||||||
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
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"
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
@@ -100,33 +125,9 @@ load_config() {
|
|||||||
# ── end of frozen configuration ──
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
# ── ctrl/mem.sh ──
|
# ── ctrl/mem.sh ──
|
||||||
# How much memory this machine will actually give you before something dies —
|
# rig's memory tool (also generated as rigmini.sh): what the machine advertises vs. what it survives.
|
||||||
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
# Usage: mem.sh status | push [--to GB] [--to-oom] | all [--budget GB] | backup | restore (WSL)
|
||||||
#
|
# Notes: docs/notes/mem.md
|
||||||
# There are two numbers and they are rarely the same. `status` reports what the
|
|
||||||
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
|
||||||
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
|
||||||
# against what this profile's cluster needs.
|
|
||||||
#
|
|
||||||
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
|
||||||
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
|
||||||
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
|
||||||
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
|
||||||
# on a box that OOMs at 2.
|
|
||||||
#
|
|
||||||
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
|
||||||
# allocation that can be raised, and the commonest failure is raising it without
|
|
||||||
# restarting — so status compares what .wslconfig says with what actually booted.
|
|
||||||
#
|
|
||||||
# Reports and instructs. It never raises a limit, frees anything or installs a
|
|
||||||
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
|
||||||
# itself, so that `restore` has something to put back after a hand edit.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# mem.sh status what it has, what caps it
|
|
||||||
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
|
||||||
# mem.sh all [--budget GB] both, then the verdict
|
|
||||||
# mem.sh backup | restore .wslconfig, WSL only
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
# (sourced library inlined above)
|
# (sourced library inlined above)
|
||||||
@@ -142,11 +143,9 @@ BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess belo
|
|||||||
|
|
||||||
# ── platform ───────────────────────────────────────────────────────────────
|
# ── platform ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
# Refuse Git Bash / MSYS / Cygwin and kernels without /proc, with a clear message.
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
require_linux() {
|
||||||
case "$(uname -s)" in
|
case "${UNAME_S:-$(uname -s)}" in
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
@@ -199,9 +198,7 @@ avail_meminfo_mb() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
# This cgroup's limit/usage files, set once by find_cgroup (cheap for the poll loop).
|
||||||
# find_cgroup, because every later reading needs both and hunting for the files
|
|
||||||
# on each call would be the slow part of the poll loop.
|
|
||||||
CG_MAX_FILE=""
|
CG_MAX_FILE=""
|
||||||
CG_CUR_FILE=""
|
CG_CUR_FILE=""
|
||||||
CG_VERSION=""
|
CG_VERSION=""
|
||||||
@@ -209,10 +206,8 @@ CG_VERSION=""
|
|||||||
find_cgroup() {
|
find_cgroup() {
|
||||||
local rel
|
local rel
|
||||||
|
|
||||||
# Inside a container the cgroup namespace makes the top of the tree BE the
|
# Top of tree first (right inside a container), then this shell's own slice
|
||||||
# container's own cgroup, so the unqualified path is already the right one.
|
# from /proc/self/cgroup (right on a host).
|
||||||
# On a host it is the root cgroup, which is never limited — hence the second
|
|
||||||
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
|
||||||
if [ -r /sys/fs/cgroup/memory.max ]; then
|
if [ -r /sys/fs/cgroup/memory.max ]; then
|
||||||
CG_VERSION=v2
|
CG_VERSION=v2
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
||||||
@@ -241,10 +236,7 @@ find_cgroup() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
# The cap in MB, or "" when unlimited ("max", or any value >= MemTotal).
|
||||||
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
|
||||||
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
|
||||||
# own memory is not a limit, however it is written.
|
|
||||||
cgroup_cap_mb() {
|
cgroup_cap_mb() {
|
||||||
local raw cap
|
local raw cap
|
||||||
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
||||||
@@ -285,10 +277,7 @@ effective_ceiling_mb() {
|
|||||||
echo "$c"
|
echo "$c"
|
||||||
}
|
}
|
||||||
|
|
||||||
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
# Room left right now: cgroup cap minus usage when capped, else MemAvailable.
|
||||||
# In a capped container /proc/meminfo describes the host and is worse than
|
|
||||||
# useless for this — it would report tens of gigabytes free on a box that is one
|
|
||||||
# allocation from being killed.
|
|
||||||
headroom_mb() {
|
headroom_mb() {
|
||||||
local cap used
|
local cap used
|
||||||
cap=$(cgroup_cap_mb)
|
cap=$(cgroup_cap_mb)
|
||||||
@@ -302,9 +291,7 @@ headroom_mb() {
|
|||||||
|
|
||||||
# ── status ─────────────────────────────────────────────────────────────────
|
# ── status ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
# Ask Windows for %USERPROFILE%; fall back to whichever profile owns a .wslconfig.
|
||||||
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
|
||||||
# Windows, then fall back to whichever profile actually owns a config.
|
|
||||||
wslconfig_path() {
|
wslconfig_path() {
|
||||||
local profile winpath found
|
local profile winpath found
|
||||||
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
||||||
@@ -360,9 +347,7 @@ status() {
|
|||||||
echo " ulimit -v unlimited"
|
echo " ulimit -v unlimited"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# overcommit_memory=0 is the default heuristic: a large allocation is
|
# Overcommit mode decides whether limits show as failed mallocs or OOM kills.
|
||||||
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
|
||||||
# than as a failed malloc. It is why `push` touches every page it asks for.
|
|
||||||
local om or_
|
local om or_
|
||||||
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
||||||
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
||||||
@@ -435,9 +420,7 @@ status() {
|
|||||||
echo " ! docker cli present but the daemon is unreachable"
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
# WSL: report the .wslconfig cap and whether it was applied (needs wsl --shutdown).
|
||||||
# not usefully apply — the change costs a full VM restart. Report it, and
|
|
||||||
# report the commonest mistake, which is editing it and not restarting.
|
|
||||||
if is_wsl; then
|
if is_wsl; then
|
||||||
local cfg conf conf_mb n
|
local cfg conf conf_mb n
|
||||||
cfg=$(wslconfig_path)
|
cfg=$(wslconfig_path)
|
||||||
@@ -497,7 +480,7 @@ require_wsl() {
|
|||||||
|
|
||||||
# backup and restore act on the file, so unlike status they must not guess.
|
# backup and restore act on the file, so unlike status they must not guess.
|
||||||
wslconfig_required() {
|
wslconfig_required() {
|
||||||
local cfg; cfg=$(wslconfig_required)
|
local cfg; cfg=$(wslconfig_path)
|
||||||
if [ -z "$cfg" ]; then
|
if [ -z "$cfg" ]; then
|
||||||
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
||||||
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
||||||
@@ -529,8 +512,7 @@ backup() {
|
|||||||
local cfg dest
|
local cfg dest
|
||||||
cfg=$(wslconfig_required)
|
cfg=$(wslconfig_required)
|
||||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
# Timestamped, never overwritten.
|
||||||
# second run is not a backup.
|
|
||||||
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
||||||
cp "$cfg" "$dest"
|
cp "$cfg" "$dest"
|
||||||
echo "backed up $dest"
|
echo "backed up $dest"
|
||||||
@@ -549,9 +531,7 @@ restore() {
|
|||||||
echo " -> $cfg"
|
echo " -> $cfg"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Newest is the right default — undo the last edit — but if you backed up
|
# Restores the newest; list the others in case an older one is wanted.
|
||||||
# *after* editing, the state you want is older. Show the rest so a no-op
|
|
||||||
# restore is obviously a no-op rather than a mystery.
|
|
||||||
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
if [ "$count" -gt 1 ]; then
|
if [ "$count" -gt 1 ]; then
|
||||||
echo "$count backups exist, newest first:"
|
echo "$count backups exist, newest first:"
|
||||||
@@ -595,14 +575,9 @@ cleanup() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# The child allocates and stops itself; the parent only watches. That split is
|
# Runs as a child that may be OOM-killed; the parent survives to report.
|
||||||
# the point: under --to-oom the allocating process is expected to be killed, and
|
|
||||||
# something has to survive to say how far it got.
|
|
||||||
allocator() {
|
allocator() {
|
||||||
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
# Make this process the preferred OOM victim (raising needs no privilege).
|
||||||
# first. Raising needs no privilege (only lowering does). Without it, the
|
|
||||||
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
|
||||||
# box you are still using, that is not an acceptable coin toss.
|
|
||||||
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
||||||
|
|
||||||
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
||||||
@@ -611,16 +586,7 @@ allocator() {
|
|||||||
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
||||||
|
|
||||||
while :; do
|
while :; do
|
||||||
# Written STRAIGHT INTO the array element. The obvious spelling —
|
# Write straight into the element (one copy, not three) and touch every page.
|
||||||
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
|
||||||
# not one: the template stays resident, expanding "$chunk" makes a
|
|
||||||
# temporary word, and the append makes the element. A 128 MB step then
|
|
||||||
# needs 384 MB transiently, and on a small box it is killed on the
|
|
||||||
# first append while reporting a third of the true ceiling.
|
|
||||||
#
|
|
||||||
# printf -v into a subscript also means every page is written, so it is
|
|
||||||
# resident rather than merely promised — the only kind of allocation
|
|
||||||
# that measures anything under heuristic overcommit.
|
|
||||||
printf -v "arr[$i]" '%*s' "$bytes" ''
|
printf -v "arr[$i]" '%*s' "$bytes" ''
|
||||||
i=$((i + 1)); held=$((held + STEP_MB))
|
i=$((i + 1)); held=$((held + STEP_MB))
|
||||||
|
|
||||||
@@ -633,9 +599,7 @@ allocator() {
|
|||||||
"$held" "$rss" "$avail" "$swapped"
|
"$held" "$rss" "$avail" "$swapped"
|
||||||
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
||||||
|
|
||||||
# Worth calling out separately from the ceiling: this is where the box
|
# First swap is reported separately: slow comes before killed.
|
||||||
# stops being fast and starts being unusable, which for a scheduler is
|
|
||||||
# a different and earlier problem than being killed.
|
|
||||||
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
||||||
first_swap=$held
|
first_swap=$held
|
||||||
echo " - first swap page at ${held} MB — past here it works but crawls"
|
echo " - first swap page at ${held} MB — past here it works but crawls"
|
||||||
@@ -656,30 +620,20 @@ push() {
|
|||||||
total=$(mb MemTotal)
|
total=$(mb MemTotal)
|
||||||
ceiling=$(effective_ceiling_mb)
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
|
||||||
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
# Default step: ceiling/64, clamped to 4..256 MB.
|
||||||
# find the edge, few enough lines to read, and small enough that the
|
|
||||||
# transient cost of one allocation never dominates a small box. A fixed
|
|
||||||
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
|
||||||
if [ "$STEP_EXPLICIT" = no ]; then
|
if [ "$STEP_EXPLICIT" = no ]; then
|
||||||
STEP_MB=$(( ceiling / 64 ))
|
STEP_MB=$(( ceiling / 64 ))
|
||||||
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
||||||
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
# Stop with a cushion: 64 MB under a cgroup cap, 512 MB on a host, or 5% of ceiling if larger.
|
||||||
# depends on what it is protecting. Under a cgroup cap, running out kills
|
|
||||||
# only this container's own processes, so it need cover no more than the
|
|
||||||
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
|
||||||
# halve the answer. On a host there is everything else to protect, and the
|
|
||||||
# OOM killer does not promise to pick the process that caused the problem.
|
|
||||||
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
||||||
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
||||||
|
|
||||||
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
# INT kills the child and lets the summary below print anyway, so an
|
# Ctrl-C kills the child, frees the memory, and still prints the summary.
|
||||||
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
|
||||||
# still gives the memory back.
|
|
||||||
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
||||||
|
|
||||||
echo "push"
|
echo "push"
|
||||||
@@ -752,11 +706,7 @@ push() {
|
|||||||
fi ;;
|
fi ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# The gap between the claim and the measurement is the finding — but only
|
# Warn about claimed-vs-measured gap only when the box, not us, chose the stop.
|
||||||
# when the BOX chose where to stop. An empty $stop means the child was ended
|
|
||||||
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
|
||||||
# asked for, and flagging those as short of the ceiling would put a warning
|
|
||||||
# on every deliberately small run.
|
|
||||||
local got="${rss:-$held}"
|
local got="${rss:-$held}"
|
||||||
echo
|
echo
|
||||||
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# Shorthand for the scripts beside it; they run without it. Every target
|
|
||||||
# calls a verb its script accepts — read from that script's own dispatch.
|
|
||||||
|
|
||||||
HERE := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|
||||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
|
||||||
ifneq ($(ARGS),)
|
|
||||||
$(eval $(ARGS):;@:)
|
|
||||||
.PHONY: $(ARGS)
|
|
||||||
endif
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
.PHONY: help deps mem
|
|
||||||
|
|
||||||
help: ## list targets
|
|
||||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
|
||||||
|
|
||||||
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)
|
|
||||||
bash $(HERE)rigmini.sh $(or $(ARGS),status)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# Shorthand for the scripts beside it; they run without it. Every target
|
|
||||||
# calls a verb its script accepts — read from that script's own dispatch.
|
|
||||||
|
|
||||||
HERE := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|
||||||
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
|
||||||
ifneq ($(ARGS),)
|
|
||||||
$(eval $(ARGS):;@:)
|
|
||||||
.PHONY: $(ARGS)
|
|
||||||
endif
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
.PHONY: help deps mem
|
|
||||||
|
|
||||||
help: ## list targets
|
|
||||||
@grep -hE '^[a-z][a-z-]*:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
|
|
||||||
|
|
||||||
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)
|
|
||||||
bash $(HERE)rigmini.sh $(or $(ARGS),status)
|
|
||||||
@@ -1,958 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# rigdeps.sh for profile 'offline', flattened from:
|
|
||||||
# ctrl/deps.sh
|
|
||||||
# ctrl/lib/config.sh
|
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
|
||||||
|
|
||||||
# ── from the libraries ──
|
|
||||||
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;
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [ -n "$line" ]; then
|
|
||||||
eval "export $line";
|
|
||||||
fi;
|
|
||||||
done <<< "$1";
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
default_cluster_name ()
|
|
||||||
{
|
|
||||||
local n;
|
|
||||||
n=$(basename "$(cd .. && pwd)");
|
|
||||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
|
||||||
echo "${n:-rig}"
|
|
||||||
}
|
|
||||||
derive_port_base ()
|
|
||||||
{
|
|
||||||
local h;
|
|
||||||
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
|
||||||
echo $((20000 + (h % 200) * 10))
|
|
||||||
}
|
|
||||||
render_kind_config ()
|
|
||||||
{
|
|
||||||
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_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'offline' ──
|
|
||||||
load_config() {
|
|
||||||
local k saved=""
|
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
|
||||||
done
|
|
||||||
declare -gx ADDONS="metallb"
|
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
|
||||||
declare -g AUDIT="on"
|
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
|
||||||
declare -g CLUSTER="rig"
|
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
|
||||||
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
|
||||||
declare -gx COMPOSE_VERSION="5.5.1"
|
|
||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
|
||||||
declare -gx DNS_MODE="hosts"
|
|
||||||
declare -g HTTPS_PORT="20311"
|
|
||||||
declare -g HTTP_PORT="20310"
|
|
||||||
declare -gx INGRESS_MODE="hostport"
|
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
|
||||||
declare -gx K8S_VERSION="v1_36"
|
|
||||||
declare -gx KIND_CONFIG="kind-config.audit.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.audit.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.audit.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
|
||||||
declare -g KUBECONTEXT="kind-rig"
|
|
||||||
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="ctrl/k8s/overlays/dev"
|
|
||||||
declare -gx METALLB_VERSION="v0.16.0"
|
|
||||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
|
||||||
declare -g NODES="1"
|
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
|
||||||
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
|
||||||
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
|
||||||
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 -gx POSTGRES_IMAGE="postgres:16-alpine"
|
|
||||||
declare -gx PROFILE_NAME="offline"
|
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
|
||||||
declare -gx 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"
|
|
||||||
declare -gx TILT_VERSION="0.37.6"
|
|
||||||
_config_restore "$saved"
|
|
||||||
}
|
|
||||||
# ── end of frozen configuration ──
|
|
||||||
|
|
||||||
# ── ctrl/deps.sh ──
|
|
||||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
|
||||||
# report what it could not do.
|
|
||||||
#
|
|
||||||
# It never runs the cluster, never uses sudo or apt, and writes only into
|
|
||||||
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
|
||||||
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
|
||||||
# decide on, never performed. That is what makes it safe to run on a machine that
|
|
||||||
# already has a working setup.
|
|
||||||
#
|
|
||||||
# Usage (normally via `make deps`, or directly):
|
|
||||||
# deps.sh detect # report host facts only, change nothing
|
|
||||||
# deps.sh list # the pinned versions
|
|
||||||
# deps.sh verify [core|dev] # run what is installed and see if it works
|
|
||||||
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
|
||||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
|
||||||
#
|
|
||||||
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
|
||||||
# Default is dev.
|
|
||||||
#
|
|
||||||
# Runs both inside the installer container and bare on a host. Inside the
|
|
||||||
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
|
||||||
# falls back to /.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
|
||||||
# not against ctrl/ once we've moved.
|
|
||||||
INVOKED_FROM="$PWD"
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# Pins arrive through load_config like every other setting, not by sourcing
|
|
||||||
# versions.env here. That is what lets `make standalone` freeze them into a
|
|
||||||
# one-file installer: configuration has exactly one way in.
|
|
||||||
# (sourced library inlined above)
|
|
||||||
load_config
|
|
||||||
|
|
||||||
# Resolve a possibly-relative path against the caller's original directory.
|
|
||||||
abspath() {
|
|
||||||
case "$1" in
|
|
||||||
/*) echo "$1" ;;
|
|
||||||
*) echo "$INVOKED_FROM/$1" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
|
||||||
HOST_ROOT="${HOST_ROOT:-/}"
|
|
||||||
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
|
||||||
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
|
||||||
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
|
||||||
|
|
||||||
# Collected by detect(), printed by report_manual() at the very end.
|
|
||||||
MANUAL=()
|
|
||||||
|
|
||||||
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
|
||||||
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
|
||||||
# container's own view is already the host's.
|
|
||||||
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
|
||||||
# tight and does-not-fit branches can be exercised against a real machine's
|
|
||||||
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
|
||||||
mb_of() {
|
|
||||||
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
|
||||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
|
||||||
}
|
|
||||||
|
|
||||||
host_file() {
|
|
||||||
local p="${1#/}"
|
|
||||||
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
|
||||||
echo "$HOST_ROOT/$p"
|
|
||||||
else
|
|
||||||
echo "/$p"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── the tools this script itself needs ─────────────────────────────────────
|
|
||||||
|
|
||||||
arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) echo amd64 ;;
|
|
||||||
aarch64|arm64) echo arm64 ;;
|
|
||||||
*) uname -m ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The pins above are amd64. Rather than download something that cannot execute
|
|
||||||
# and let it fail as "cannot execute binary file: Exec format error", say so
|
|
||||||
# here and hand over the commands that produce the right checksums.
|
|
||||||
require_amd64() {
|
|
||||||
local a; a=$(arch)
|
|
||||||
[ "$a" = "amd64" ] && return 0
|
|
||||||
cat >&2 <<EOF
|
|
||||||
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
|
||||||
|
|
||||||
Nothing here would run, so it does not download. To make an ${a} version, the
|
|
||||||
URLs need the ${a} artifact and the checksums need to come from each project's
|
|
||||||
own published list — not from these values, and not from a download you did:
|
|
||||||
|
|
||||||
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
|
||||||
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
|
||||||
|
|
||||||
Edit the pinned block at the top of this file with what those print.
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DL=""
|
|
||||||
pick_downloader() {
|
|
||||||
if command -v curl >/dev/null 2>&1; then DL=curl
|
|
||||||
elif command -v wget >/dev/null 2>&1; then DL=wget
|
|
||||||
else
|
|
||||||
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
|
||||||
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download() {
|
|
||||||
local url="$1" out="$2"
|
|
||||||
case "$DL" in
|
|
||||||
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
|
||||||
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
SHA=""
|
|
||||||
pick_sha() {
|
|
||||||
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
|
||||||
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
|
||||||
else
|
|
||||||
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
|
||||||
echo "Refusing to install unverified binaries." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── package manager, for the instructions only ─────────────────────────────
|
|
||||||
# This never runs a package manager. It names one so the reported action is
|
|
||||||
# something you can paste, on the distro you are actually on — an apt line on
|
|
||||||
# Amazon Linux 2 is a wrong answer dressed up as help.
|
|
||||||
|
|
||||||
pkg_install_cmd() {
|
|
||||||
local pkg="$1"
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
|
||||||
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
|
||||||
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
|
||||||
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
|
||||||
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
|
||||||
else echo "install '$pkg' with this system's package manager"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
docker_pkg() {
|
|
||||||
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── detect ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
|
||||||
case "$(uname -s)" in
|
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
|
||||||
|
|
||||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
|
||||||
|
|
||||||
wsl --install
|
|
||||||
|
|
||||||
That enables Windows features and needs a reboot, so it is not something this
|
|
||||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
|
||||||
this from there.
|
|
||||||
|
|
||||||
See "Starting from plain Windows" in README.md.
|
|
||||||
EOF
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|
||||||
|
|
||||||
detect() {
|
|
||||||
echo "host"
|
|
||||||
echo " kernel $(uname -r)"
|
|
||||||
echo " arch $(arch) ($(uname -m))"
|
|
||||||
|
|
||||||
local osr; osr=$(host_file /etc/os-release)
|
|
||||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
|
||||||
|
|
||||||
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
|
||||||
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
|
||||||
# that is enough depends on the profile, which check.sh knows and this does not.
|
|
||||||
local total_mb avail_mb swap_total_mb swap_used_mb om
|
|
||||||
total_mb=$(mb_of MemTotal)
|
|
||||||
avail_mb=$(mb_of MemAvailable)
|
|
||||||
swap_total_mb=$(mb_of SwapTotal)
|
|
||||||
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
|
||||||
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
|
||||||
if [ "$swap_total_mb" -gt 0 ]; then
|
|
||||||
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
|
||||||
# always says yes and settles up later with the OOM killer, so a cluster that
|
|
||||||
# starts cleanly can still lose processes afterwards.
|
|
||||||
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
|
||||||
case "$om" in
|
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
|
||||||
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo " install to $OUT_BIN"
|
|
||||||
detect_libc
|
|
||||||
detect_prereqs
|
|
||||||
detect_wsl
|
|
||||||
detect_filesystem
|
|
||||||
detect_docker
|
|
||||||
detect_inotify
|
|
||||||
detect_toolchain
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_wsl() {
|
|
||||||
if ! is_wsl; then
|
|
||||||
echo " platform native linux"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " platform WSL"
|
|
||||||
|
|
||||||
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
|
||||||
# host service need it. Enabling it requires a Windows-side restart, which
|
|
||||||
# cannot be issued from inside the distro.
|
|
||||||
local wc; wc=$(host_file /etc/wsl.conf)
|
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
|
||||||
echo " systemd enabled in wsl.conf"
|
|
||||||
else
|
|
||||||
echo " ! systemd not enabled in /etc/wsl.conf"
|
|
||||||
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
|
||||||
[boot]
|
|
||||||
systemd=true
|
|
||||||
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
|
||||||
# local DNS setup.
|
|
||||||
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
|
||||||
echo " resolv.conf pinned (generateResolvConf=false)"
|
|
||||||
else
|
|
||||||
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local wcfg
|
|
||||||
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
|
||||||
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
|
||||||
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
|
||||||
else
|
|
||||||
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
|
||||||
make mem status
|
|
||||||
It prints the edit to make and the command to apply it.")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
|
||||||
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
|
||||||
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
|
||||||
# way. None of them deliver inotify events, so anything watching files goes
|
|
||||||
# quiet without saying why.
|
|
||||||
watch_hostile_fs() {
|
|
||||||
local dir="$1" fstype
|
|
||||||
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
|
||||||
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
|
||||||
case "$fstype" in
|
|
||||||
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
|
||||||
*) echo "" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_filesystem() {
|
|
||||||
local root fstype
|
|
||||||
root=$(cd .. && pwd -P)
|
|
||||||
fstype=$(watch_hostile_fs "$root")
|
|
||||||
if [ -n "$fstype" ]; then
|
|
||||||
echo " ! this directory is on $fstype — file watching will not work"
|
|
||||||
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
|
||||||
on a $fstype mount, and everything else is slower:
|
|
||||||
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
|
||||||
else
|
|
||||||
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
|
||||||
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
|
||||||
#
|
|
||||||
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
|
||||||
#
|
|
||||||
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
|
||||||
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
|
||||||
# version now; `verify` catches the actual failure after installing.
|
|
||||||
detect_libc() {
|
|
||||||
local v=""
|
|
||||||
if command -v ldd >/dev/null 2>&1; then
|
|
||||||
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
|
||||||
fi
|
|
||||||
if [ -z "$v" ]; then
|
|
||||||
echo " libc unknown (no ldd) — 'verify' is the real test"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo " libc glibc $v"
|
|
||||||
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
|
||||||
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
|
||||||
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
|
||||||
echo " Install the core tier, or run tilt from a container."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# What this script needs to do its own job. Reported here so `detect` answers
|
|
||||||
# "will install work?" instead of leaving you to find out one download in.
|
|
||||||
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
|
||||||
detect_prereqs() {
|
|
||||||
local missing=""
|
|
||||||
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
|
||||||
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
|
||||||
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
|
||||||
echo " checksums ok"
|
|
||||||
else
|
|
||||||
echo " ! no sha256sum or shasum — downloads could not be verified"
|
|
||||||
missing+=" coreutils"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
|
||||||
echo " archives tar + gzip"
|
|
||||||
else
|
|
||||||
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
|
||||||
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
|
||||||
missing+=" tar gzip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$missing" ]; then
|
|
||||||
MANUAL+=("Install what this script needs to run at all:
|
|
||||||
$(pkg_install_cmd "${missing# }")")
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_docker() {
|
|
||||||
# Reachability of the daemon is the real question, and the CLI is only how
|
|
||||||
# we ask it. Note that when this runs inside the installer container, Docker
|
|
||||||
# necessarily exists on the host — otherwise nothing would be executing —
|
|
||||||
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
echo " docker socket present (no cli in this context)"
|
|
||||||
else
|
|
||||||
echo " ! docker not found and no socket at /var/run/docker.sock"
|
|
||||||
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
|
||||||
that needs root:
|
|
||||||
$(pkg_install_cmd "$(docker_pkg)")
|
|
||||||
sudo systemctl enable --now docker
|
|
||||||
sudo usermod -aG docker \"\$USER\"
|
|
||||||
then log out and back in, so the new group applies to your shell.")
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if docker info >/dev/null 2>&1; then
|
|
||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
|
||||||
local n
|
|
||||||
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
|
||||||
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
|
||||||
# function the latter returns 1 when the count is zero, and `set -e`
|
|
||||||
# then kills the caller. That is the fresh-machine case — no clusters
|
|
||||||
# yet — so the bug only ever shows up where it does most harm.
|
|
||||||
if [ "$n" -gt 0 ]; then
|
|
||||||
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
|
||||||
MANUAL+=("Start Docker, or add yourself to the docker group:
|
|
||||||
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
|
||||||
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
|
||||||
detect_inotify() {
|
|
||||||
local w i
|
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
|
||||||
echo " inotify watches=$w instances=$i"
|
|
||||||
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
|
||||||
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
|
||||||
MANUAL+=("Raise inotify limits (needs root on the host):
|
|
||||||
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
|
||||||
| sudo tee /etc/sysctl.d/99-rig.conf
|
|
||||||
sudo sysctl --system")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── fetch ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
|
||||||
resolve_url() {
|
|
||||||
local upstream="$1"
|
|
||||||
case "$DEPS_SOURCE" in
|
|
||||||
upstream) echo "$upstream" ;;
|
|
||||||
artifactory)
|
|
||||||
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
|
||||||
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
|
||||||
;;
|
|
||||||
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
verify() {
|
|
||||||
local file="$1" want="$2" name="$3" got
|
|
||||||
got=$($SHA "$file" | awk '{print $1}')
|
|
||||||
if [ "$got" != "$want" ]; then
|
|
||||||
echo "checksum mismatch for $name" >&2
|
|
||||||
echo " expected $want" >&2
|
|
||||||
echo " got $got" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
|
||||||
fetch_bin() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4"
|
|
||||||
local tmp="$dest/.$name.tmp"
|
|
||||||
echo " fetching $name"
|
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
|
||||||
verify "$tmp" "$sha" "$name"
|
|
||||||
mv "$tmp" "$dest/$name"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
|
||||||
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
|
||||||
# others nest it a directory down — so the caller says which.
|
|
||||||
fetch_tgz() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
|
||||||
local tmp="$dest/.$name.tgz"
|
|
||||||
echo " fetching $name"
|
|
||||||
download "$(resolve_url "$url")" "$tmp"
|
|
||||||
verify "$tmp" "$sha" "$name"
|
|
||||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
|
||||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
|
||||||
# user does not own.
|
|
||||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
|
||||||
rm -f "$tmp"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# The installer runs as root so it can reach the docker socket, which means
|
|
||||||
# everything it writes into a mounted volume lands root-owned and unusable from
|
|
||||||
# the host. Hand it back to whoever owns the mount point (the host user created
|
|
||||||
# that directory before mounting it).
|
|
||||||
fix_ownership() {
|
|
||||||
local dir="$1"
|
|
||||||
[ -d "$dir" ] || return 0
|
|
||||||
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
|
||||||
if [ "$owner" = ":" ]; then
|
|
||||||
owner=$(stat -c '%u:%g' "$dir")
|
|
||||||
fi
|
|
||||||
[ "$owner" = "0:0" ] && return 0
|
|
||||||
chown -R "$owner" "$dir" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# Two tiers, because not every machine should get cluster tooling.
|
|
||||||
#
|
|
||||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
|
||||||
# creates one. Appropriate on a managed or corporate-issued machine
|
|
||||||
# where development tools are not wanted by default.
|
|
||||||
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
|
||||||
#
|
|
||||||
# The split exists because "install the toolchain" is not one decision: on a
|
|
||||||
# managed workspace the right answer is kubectl and nothing else.
|
|
||||||
CORE_TOOLS="kubectl jq"
|
|
||||||
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
|
||||||
# has ever invoked it. Add it back the day something actually needs a chart.
|
|
||||||
#
|
|
||||||
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
|
||||||
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
|
||||||
# because it is what wires a cluster to a local registry — without one, an
|
|
||||||
# unqualified image name resolves to docker.io/library/<name> and there is
|
|
||||||
# nothing structural stopping a push there.
|
|
||||||
#
|
|
||||||
# docker-compose is 'dev' for the same reason, and is here because the distro
|
|
||||||
# docker packages ship the daemon and CLI but frequently not the compose
|
|
||||||
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
|
||||||
# working Docker, and nothing about that message names the missing piece.
|
|
||||||
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
|
||||||
|
|
||||||
# ── what is already on this machine ───────────────────────────────────────
|
|
||||||
#
|
|
||||||
# A tool already on PATH at its pinned version is left where it is. Without
|
|
||||||
# this, install downloads a second copy into OUT_BIN and then reports the first
|
|
||||||
# one as shadowed — noise, and wrong, when both are the same version. That is
|
|
||||||
# the normal state of any machine someone set up by hand, whatever directory
|
|
||||||
# they happened to choose.
|
|
||||||
|
|
||||||
pin_of() {
|
|
||||||
case "$1" in
|
|
||||||
kubectl) echo "$KUBECTL_VERSION" ;;
|
|
||||||
jq) echo "$JQ_VERSION" ;;
|
|
||||||
kind) echo "$KIND_VERSION" ;;
|
|
||||||
tilt) echo "$TILT_VERSION" ;;
|
|
||||||
ctlptl) echo "$CTLPTL_VERSION" ;;
|
|
||||||
docker-compose) echo "$COMPOSE_VERSION" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The version string a binary reports. Each tool spells the question
|
|
||||||
# differently, and kubectl has to be told --client or it goes looking for a
|
|
||||||
# server to ask.
|
|
||||||
reported_version() {
|
|
||||||
local tool="$1" path="$2"
|
|
||||||
case "$tool" in
|
|
||||||
kubectl) "$path" version --client 2>/dev/null ;;
|
|
||||||
jq) "$path" --version 2>/dev/null ;;
|
|
||||||
*) "$path" version 2>/dev/null ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
|
||||||
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
|
||||||
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
|
||||||
#
|
|
||||||
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
|
||||||
# on every machine — some builds reject patterns that others accept — and a
|
|
||||||
# failed grep inside a count reads exactly like a zero.
|
|
||||||
version_matches() {
|
|
||||||
local tool="$1" path="$2" pin="$3" out v re
|
|
||||||
out=$(reported_version "$tool" "$path") || return 1
|
|
||||||
v="${pin#v}"
|
|
||||||
v="${v//./\\.}"
|
|
||||||
re="(^|[^0-9.])v?${v}([^0-9.]|\$)"
|
|
||||||
[[ $out =~ $re ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
|
||||||
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
|
||||||
# DIR" must not quietly skip something because this machine happens to have it.
|
|
||||||
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
|
||||||
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
|
||||||
|
|
||||||
# Every tool in the tier with its state, probed once and reported once. What
|
|
||||||
# still needs fetching is left in TOOLCHAIN_NEED for install() to act on.
|
|
||||||
TOOLCHAIN_NEED=""
|
|
||||||
detect_toolchain() {
|
|
||||||
local tier="${TIER:-dev}" b pin path found
|
|
||||||
TOOLCHAIN_NEED=""
|
|
||||||
echo
|
|
||||||
echo "toolchain (pinned, tier '$tier')"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
pin=$(pin_of "$b")
|
|
||||||
path=$(command -v "$b" 2>/dev/null || true)
|
|
||||||
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
|
||||||
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
|
||||||
# has no `docker-compose` to find — and probing only PATH would report it
|
|
||||||
# missing and re-download a copy that is already there. That is the exact
|
|
||||||
# noise the version-aware skip exists to prevent, so ask docker instead.
|
|
||||||
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
|
||||||
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
|
||||||
if [ "${found#v}" = "${pin#v}" ]; then
|
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
|
||||||
else
|
|
||||||
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
|
||||||
"$b" "$pin" "$found"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
fi
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ -z "$path" ]; then
|
|
||||||
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
elif version_matches "$b" "$path" "$pin"; then
|
|
||||||
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
|
||||||
else
|
|
||||||
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
|
||||||
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
|
||||||
TOOLCHAIN_NEED+="$b "
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -z "$TOOLCHAIN_NEED" ]; then
|
|
||||||
echo " every pinned tool is already on PATH — nothing to fetch"
|
|
||||||
else
|
|
||||||
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch() {
|
|
||||||
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--to) dest="$2"; shift 2 ;;
|
|
||||||
core|dev) tier="$1"; shift ;;
|
|
||||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
dest="$(abspath "$dest")"
|
|
||||||
mkdir -p "$dest"
|
|
||||||
TIER="$tier"
|
|
||||||
|
|
||||||
if [ "$DEPS_SOURCE" = "baked" ]; then
|
|
||||||
echo "installing baked binaries from $BAKED_BIN"
|
|
||||||
cp -a "$BAKED_BIN"/. "$dest"/
|
|
||||||
fix_ownership "$dest"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${DEPS_ONLY:-}" ]; then
|
|
||||||
echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)"
|
|
||||||
else
|
|
||||||
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
|
||||||
fi
|
|
||||||
if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi
|
|
||||||
if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi
|
|
||||||
if [ "$tier" = "dev" ]; then
|
|
||||||
if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi
|
|
||||||
if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi
|
|
||||||
if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi
|
|
||||||
if want docker-compose; then
|
|
||||||
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
fix_ownership "$dest"
|
|
||||||
# kind writes the kubeconfig as root too; hand that back as well when it's
|
|
||||||
# a mounted host directory rather than container-local state.
|
|
||||||
fix_ownership "${KUBE_DIR:-/out/kube}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── install ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
report_manual() {
|
|
||||||
echo
|
|
||||||
if [ ${#MANUAL[@]} -eq 0 ]; then
|
|
||||||
echo "nothing left to do by hand."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
|
||||||
echo
|
|
||||||
local n=1
|
|
||||||
for m in "${MANUAL[@]}"; do
|
|
||||||
echo " $n. $m"
|
|
||||||
echo
|
|
||||||
n=$((n + 1))
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Installing into a directory that sits early in PATH silently replaces whatever
|
|
||||||
# the machine was already using — which on a shared or client machine can break
|
|
||||||
# unrelated work (kubectl more than one minor away from a cluster is the common
|
|
||||||
# one). Say so; never decide it for them.
|
|
||||||
# Downloading a verified binary proves it is the right file, not that this
|
|
||||||
# machine can run it. On an old distro tilt fails here, with a linker error
|
|
||||||
# about a missing symbol, and finding that out now beats finding out during a
|
|
||||||
# first cluster build.
|
|
||||||
verify_tools() {
|
|
||||||
local tier="${1:-dev}" b bin out rc broke=0
|
|
||||||
echo "checking that each one actually runs"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
bin="$OUT_BIN/$b"
|
|
||||||
if [ ! -x "$bin" ]; then
|
|
||||||
printf ' %-14s not installed\n' "$b"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
|
||||||
# than one line gets SIGPIPE when head closes the pipe, and the
|
|
||||||
# pipeline reports 141 — so a working kubectl was announced as "does
|
|
||||||
# not run here", with its own correct version string as the evidence.
|
|
||||||
# Take the first line afterwards, from the string.
|
|
||||||
rc=0
|
|
||||||
case "$b" in
|
|
||||||
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
|
||||||
jq) out=$("$bin" --version 2>&1) || rc=$? ;;
|
|
||||||
*) out=$("$bin" version 2>&1) || rc=$? ;;
|
|
||||||
esac
|
|
||||||
out=${out%%$'\n'*}
|
|
||||||
if [ "$rc" -eq 0 ]; then
|
|
||||||
printf ' %-14s %s\n' "$b" "$out"
|
|
||||||
else
|
|
||||||
printf ' ! %-12s does not run here: %s\n' "$b" "$out"
|
|
||||||
broke=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ "$broke" -eq 1 ]; then
|
|
||||||
echo
|
|
||||||
echo " A binary that downloads and verifies but will not start is almost"
|
|
||||||
echo " always this distro's libc being older than the release needs."
|
|
||||||
echo " 'detect' prints the glibc version. The core tier (kubectl + jq)"
|
|
||||||
echo " has no such dependency and will work regardless."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
list() {
|
|
||||||
echo "pinned, linux/amd64 only:"
|
|
||||||
printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION"
|
|
||||||
printf ' %-14s %s\n' jq "$JQ_VERSION"
|
|
||||||
printf ' %-14s %s\n' kind "$KIND_VERSION"
|
|
||||||
printf ' %-14s %s\n' tilt "$TILT_VERSION"
|
|
||||||
printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION"
|
|
||||||
printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION"
|
|
||||||
echo
|
|
||||||
echo " core = $CORE_TOOLS"
|
|
||||||
echo " dev = $CORE_TOOLS $DEV_TOOLS"
|
|
||||||
echo
|
|
||||||
echo "Checksums are pinned in the block at the top of this file. To bump one,"
|
|
||||||
echo "take the new checksum from the publisher's own release list — the header"
|
|
||||||
echo "comment has the exact commands."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
|
||||||
|
|
||||||
warn_shadowing() {
|
|
||||||
local b existing shadowed="" tier="${1:-dev}"
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
[ -x "$OUT_BIN/$b" ] || continue
|
|
||||||
# Where would this resolve if OUT_BIN weren't in the way?
|
|
||||||
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
|
||||||
command -v "$b" 2>/dev/null || true)
|
|
||||||
[ -n "$existing" ] || continue
|
|
||||||
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
|
||||||
# The same version in both places is not a conflict: nothing changes for
|
|
||||||
# any other project whichever copy PATH happens to find first.
|
|
||||||
if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi
|
|
||||||
shadowed+=" $b $existing"$'\n'
|
|
||||||
done
|
|
||||||
|
|
||||||
[ -n "$shadowed" ] || return 0
|
|
||||||
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
|
||||||
printf '%s' "$shadowed"
|
|
||||||
echo " Other projects on this machine will pick up the new versions."
|
|
||||||
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
|
||||||
was just installed:
|
|
||||||
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
|
||||||
Or install somewhere private instead:
|
|
||||||
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
|
||||||
}
|
|
||||||
|
|
||||||
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
|
||||||
# retired v1 spelling; every compose file written in the last few years assumes
|
|
||||||
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
|
||||||
# So the binary is fetched like any other and then linked, in your own home —
|
|
||||||
# no root, and nothing outside it.
|
|
||||||
install_compose_plugin() {
|
|
||||||
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
|
||||||
[ -x "$src" ] || return 0
|
|
||||||
mkdir -p "$dir"
|
|
||||||
# Something else already owns that name — docker-desktop and some distro
|
|
||||||
# packages install a real file there. Overwriting it would take the plugin
|
|
||||||
# away from whatever put it there, so say so and let the user decide.
|
|
||||||
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
|
||||||
MANUAL+=("Something already installs the compose plugin at
|
|
||||||
$dir/docker-compose
|
|
||||||
To use rig's pinned build instead:
|
|
||||||
ln -sf $src $dir/docker-compose")
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
ln -sfn "$src" "$dir/docker-compose"
|
|
||||||
echo " compose plugin -> $dir/docker-compose"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
install() {
|
|
||||||
local tier="${1:-dev}" b
|
|
||||||
TIER="$tier"
|
|
||||||
detect
|
|
||||||
|
|
||||||
# detect_toolchain has already probed PATH. Fetch only what it found missing
|
|
||||||
# or at the wrong version; a tool already present at its pin stays where it is.
|
|
||||||
if [ -n "$TOOLCHAIN_NEED" ]; then
|
|
||||||
echo
|
|
||||||
DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier"
|
|
||||||
echo
|
|
||||||
echo "installed to $OUT_BIN ($tier):"
|
|
||||||
for b in $TOOLCHAIN_NEED; do
|
|
||||||
if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi
|
|
||||||
done
|
|
||||||
if [ "$tier" = "core" ]; then
|
|
||||||
echo " (no kind/tilt — 'make deps dev' adds them)"
|
|
||||||
fi
|
|
||||||
# Only when compose was one of the things fetched: linking a binary
|
|
||||||
# that is already satisfied elsewhere on PATH would point the plugin at
|
|
||||||
# a copy rig did not install.
|
|
||||||
case " $TOOLCHAIN_NEED " in
|
|
||||||
*" docker-compose "*) install_compose_plugin ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Only worth saying when something actually landed in OUT_BIN. When every
|
|
||||||
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
|
||||||
# telling the user to add it would be advice to fix nothing.
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
|
||||||
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
warn_shadowing "$tier"
|
|
||||||
|
|
||||||
report_manual
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
require_linux
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
cmd="${1:-install}"
|
|
||||||
[ $# -gt 0 ] && shift
|
|
||||||
|
|
||||||
# Baked mode copies binaries already in the image, so it needs no downloader.
|
|
||||||
need_downloads() {
|
|
||||||
require_amd64
|
|
||||||
if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi
|
|
||||||
pick_sha
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$cmd" in
|
|
||||||
detect) detect; report_manual ;;
|
|
||||||
list) list ;;
|
|
||||||
verify) verify_tools "${1:-dev}" ;;
|
|
||||||
fetch) need_downloads; fetch "$@" ;;
|
|
||||||
install) need_downloads; install "${1:-dev}" ;;
|
|
||||||
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
|
||||||
echo " install [core|dev] (default dev)" >&2
|
|
||||||
echo " fetch [core|dev] [--to DIR]" >&2
|
|
||||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,859 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# GENERATED by make standalone — do not edit
|
|
||||||
#
|
|
||||||
# rigmini.sh for profile 'offline', flattened from:
|
|
||||||
# ctrl/mem.sh
|
|
||||||
# ctrl/lib/config.sh
|
|
||||||
# Edit those and run `make standalone`. Changes made here are lost, and
|
|
||||||
# `make selftest` fails while this file differs from what rig generates.
|
|
||||||
|
|
||||||
# ── from the libraries ──
|
|
||||||
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;
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [ -n "$line" ]; then
|
|
||||||
eval "export $line";
|
|
||||||
fi;
|
|
||||||
done <<< "$1";
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
default_cluster_name ()
|
|
||||||
{
|
|
||||||
local n;
|
|
||||||
n=$(basename "$(cd .. && pwd)");
|
|
||||||
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
|
||||||
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
|
||||||
echo "${n:-rig}"
|
|
||||||
}
|
|
||||||
derive_port_base ()
|
|
||||||
{
|
|
||||||
local h;
|
|
||||||
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
|
||||||
echo $((20000 + (h % 200) * 10))
|
|
||||||
}
|
|
||||||
render_kind_config ()
|
|
||||||
{
|
|
||||||
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_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── configuration, frozen for profile 'offline' ──
|
|
||||||
load_config() {
|
|
||||||
local k saved=""
|
|
||||||
for k in $CONFIG_OVERRIDABLE; do
|
|
||||||
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
|
||||||
done
|
|
||||||
declare -gx ADDONS="metallb"
|
|
||||||
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
|
||||||
declare -g AUDIT="on"
|
|
||||||
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
|
||||||
declare -g CLUSTER="rig"
|
|
||||||
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
|
||||||
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
|
||||||
declare -gx COMPOSE_VERSION="5.5.1"
|
|
||||||
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
|
||||||
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
|
||||||
declare -gx CTLPTL_VERSION="0.9.4"
|
|
||||||
declare -gx DNS_MODE="hosts"
|
|
||||||
declare -g HTTPS_PORT="20311"
|
|
||||||
declare -g HTTP_PORT="20310"
|
|
||||||
declare -gx INGRESS_MODE="hostport"
|
|
||||||
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
|
||||||
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
|
||||||
declare -gx JQ_VERSION="1.8.2"
|
|
||||||
declare -gx K8S_VERSION="v1_36"
|
|
||||||
declare -gx KIND_CONFIG="kind-config.audit.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_PATH="./k8s/kind-config.audit.yaml.tpl"
|
|
||||||
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.audit.yaml.tpl"
|
|
||||||
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
|
||||||
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
|
||||||
declare -gx KIND_VERSION="v0.32.0"
|
|
||||||
declare -g KUBECONTEXT="kind-rig"
|
|
||||||
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="ctrl/k8s/overlays/dev"
|
|
||||||
declare -gx METALLB_VERSION="v0.16.0"
|
|
||||||
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
|
||||||
declare -g NODES="1"
|
|
||||||
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
|
||||||
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
|
||||||
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
|
||||||
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 -gx POSTGRES_IMAGE="postgres:16-alpine"
|
|
||||||
declare -gx PROFILE_NAME="offline"
|
|
||||||
declare -gx REDIS_IMAGE="redis:7-alpine"
|
|
||||||
declare -gx REGISTRY_IMAGE="registry:2"
|
|
||||||
declare -gx 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"
|
|
||||||
declare -gx TILT_VERSION="0.37.6"
|
|
||||||
_config_restore "$saved"
|
|
||||||
}
|
|
||||||
# ── end of frozen configuration ──
|
|
||||||
|
|
||||||
# ── ctrl/mem.sh ──
|
|
||||||
# How much memory this machine will actually give you before something dies —
|
|
||||||
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
|
||||||
#
|
|
||||||
# There are two numbers and they are rarely the same. `status` reports what the
|
|
||||||
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
|
||||||
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
|
||||||
# against what this profile's cluster needs.
|
|
||||||
#
|
|
||||||
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
|
||||||
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
|
||||||
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
|
||||||
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
|
||||||
# on a box that OOMs at 2.
|
|
||||||
#
|
|
||||||
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
|
||||||
# allocation that can be raised, and the commonest failure is raising it without
|
|
||||||
# restarting — so status compares what .wslconfig says with what actually booted.
|
|
||||||
#
|
|
||||||
# Reports and instructs. It never raises a limit, frees anything or installs a
|
|
||||||
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
|
||||||
# itself, so that `restore` has something to put back after a hand edit.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# mem.sh status what it has, what caps it
|
|
||||||
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
|
||||||
# mem.sh all [--budget GB] both, then the verdict
|
|
||||||
# mem.sh backup | restore .wslconfig, WSL only
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
# (sourced library inlined above)
|
|
||||||
|
|
||||||
# ── defaults ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
STEP_MB=0 # per allocation; 0 means scale it to the ceiling. See push().
|
|
||||||
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
|
||||||
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
|
||||||
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
|
||||||
BUDGET_GB="" # --budget; empty means what this profile's cluster needs, from rig.
|
|
||||||
BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess below.
|
|
||||||
|
|
||||||
# ── platform ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
|
||||||
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
|
||||||
# the tooling. Detectable, so name it instead.
|
|
||||||
require_linux() {
|
|
||||||
case "$(uname -s)" in
|
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
|
||||||
|
|
||||||
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
|
||||||
|
|
||||||
wsl --install
|
|
||||||
|
|
||||||
That enables Windows features and needs a reboot, so it is not something this
|
|
||||||
script will do for you. Afterwards, open the Linux shell it installs and run
|
|
||||||
this from there.
|
|
||||||
EOF
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Everything below reads /proc. Without it there is nothing to measure, and
|
|
||||||
# failing here beats printing a page of empty fields.
|
|
||||||
if [ ! -r /proc/meminfo ]; then
|
|
||||||
echo "no readable /proc/meminfo — this needs a Linux kernel." >&2
|
|
||||||
echo "On macOS or a BSD none of the numbers below exist." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|
||||||
|
|
||||||
is_container() {
|
|
||||||
[ -f /.dockerenv ] && return 0
|
|
||||||
grep -qE '(docker|containerd|kubepods|lxc|podman)' /proc/1/cgroup 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
platform() {
|
|
||||||
if is_wsl; then echo WSL
|
|
||||||
elif is_container; then echo container
|
|
||||||
else echo "native linux"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── reading memory ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
|
||||||
|
|
||||||
# MemAvailable arrived in kernel 3.14. Older kernels — and they turn up on
|
|
||||||
# corporate images — need the estimate it replaced, which is worse but not wrong.
|
|
||||||
avail_meminfo_mb() {
|
|
||||||
if grep -q '^MemAvailable:' /proc/meminfo; then
|
|
||||||
mb MemAvailable
|
|
||||||
else
|
|
||||||
awk '/^(MemFree|Buffers|Cached):/{t+=$2} END{print int(t/1024)}' /proc/meminfo
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
|
||||||
# find_cgroup, because every later reading needs both and hunting for the files
|
|
||||||
# on each call would be the slow part of the poll loop.
|
|
||||||
CG_MAX_FILE=""
|
|
||||||
CG_CUR_FILE=""
|
|
||||||
CG_VERSION=""
|
|
||||||
|
|
||||||
find_cgroup() {
|
|
||||||
local rel
|
|
||||||
|
|
||||||
# Inside a container the cgroup namespace makes the top of the tree BE the
|
|
||||||
# container's own cgroup, so the unqualified path is already the right one.
|
|
||||||
# On a host it is the root cgroup, which is never limited — hence the second
|
|
||||||
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
|
||||||
if [ -r /sys/fs/cgroup/memory.max ]; then
|
|
||||||
CG_VERSION=v2
|
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
|
||||||
CG_CUR_FILE=/sys/fs/cgroup/memory.current
|
|
||||||
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
|
|
||||||
CG_VERSION=v1
|
|
||||||
CG_MAX_FILE=/sys/fs/cgroup/memory/memory.limit_in_bytes
|
|
||||||
CG_CUR_FILE=/sys/fs/cgroup/memory/memory.usage_in_bytes
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel=$(awk -F: '$1=="0"{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
|
||||||
if [ -n "$rel" ] && [ "$rel" != "/" ] && [ -r "/sys/fs/cgroup${rel}/memory.max" ]; then
|
|
||||||
CG_VERSION=v2
|
|
||||||
CG_MAX_FILE="/sys/fs/cgroup${rel}/memory.max"
|
|
||||||
CG_CUR_FILE="/sys/fs/cgroup${rel}/memory.current"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel=$(awk -F: '$2 ~ /(^|,)memory(,|$)/{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
|
||||||
if [ -n "$rel" ] && [ "$rel" != "/" ] \
|
|
||||||
&& [ -r "/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes" ]; then
|
|
||||||
CG_VERSION=v1
|
|
||||||
CG_MAX_FILE="/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes"
|
|
||||||
CG_CUR_FILE="/sys/fs/cgroup/memory${rel}/memory.usage_in_bytes"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
|
||||||
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
|
||||||
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
|
||||||
# own memory is not a limit, however it is written.
|
|
||||||
cgroup_cap_mb() {
|
|
||||||
local raw cap
|
|
||||||
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
|
||||||
raw=$(cat "$CG_MAX_FILE" 2>/dev/null || echo max)
|
|
||||||
[ "$raw" = "max" ] && { echo ""; return 0; }
|
|
||||||
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
cap=$((raw / 1024 / 1024))
|
|
||||||
[ "$cap" -ge "$(mb MemTotal)" ] && { echo ""; return 0; }
|
|
||||||
echo "$cap"
|
|
||||||
}
|
|
||||||
|
|
||||||
cgroup_used_mb() {
|
|
||||||
local raw
|
|
||||||
[ -n "$CG_CUR_FILE" ] && [ -r "$CG_CUR_FILE" ] || { echo ""; return 0; }
|
|
||||||
raw=$(cat "$CG_CUR_FILE" 2>/dev/null || echo "")
|
|
||||||
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
echo $((raw / 1024 / 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
# ulimit -v is a per-process address-space cap. It stops YOU long before the box
|
|
||||||
# does, and because it is inherited from a login shell it is easy to hit without
|
|
||||||
# knowing it is set.
|
|
||||||
ulimit_v_mb() {
|
|
||||||
local v; v=$(ulimit -v 2>/dev/null || echo unlimited)
|
|
||||||
[ "$v" = "unlimited" ] && { echo ""; return 0; }
|
|
||||||
case "$v" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
|
||||||
echo $((v / 1024))
|
|
||||||
}
|
|
||||||
|
|
||||||
# The number everything else is about: the lowest of the things that can stop
|
|
||||||
# you. Printed at the end of `status` and used as the sanity bound in `push`.
|
|
||||||
effective_ceiling_mb() {
|
|
||||||
local c; c=$(mb MemTotal)
|
|
||||||
local cap; cap=$(cgroup_cap_mb)
|
|
||||||
local ul; ul=$(ulimit_v_mb)
|
|
||||||
[ -n "$cap" ] && [ "$cap" -lt "$c" ] && c="$cap"
|
|
||||||
[ -n "$ul" ] && [ "$ul" -lt "$c" ] && c="$ul"
|
|
||||||
echo "$c"
|
|
||||||
}
|
|
||||||
|
|
||||||
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
|
||||||
# In a capped container /proc/meminfo describes the host and is worse than
|
|
||||||
# useless for this — it would report tens of gigabytes free on a box that is one
|
|
||||||
# allocation from being killed.
|
|
||||||
headroom_mb() {
|
|
||||||
local cap used
|
|
||||||
cap=$(cgroup_cap_mb)
|
|
||||||
used=$(cgroup_used_mb)
|
|
||||||
if [ -n "$cap" ] && [ -n "$used" ]; then
|
|
||||||
echo $(( cap - used ))
|
|
||||||
else
|
|
||||||
avail_meminfo_mb
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── status ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
|
||||||
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
|
||||||
# Windows, then fall back to whichever profile actually owns a config.
|
|
||||||
wslconfig_path() {
|
|
||||||
local profile winpath found
|
|
||||||
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
|
||||||
case "$profile" in
|
|
||||||
""|*%*) ;;
|
|
||||||
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
|
||||||
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
|
||||||
echo "$winpath/.wslconfig"; return 0
|
|
||||||
fi ;;
|
|
||||||
esac
|
|
||||||
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
|
||||||
[ -n "$found" ] && echo "$found"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
hogs() {
|
|
||||||
echo " holding the most:"
|
|
||||||
ps -eo rss,comm --sort=-rss 2>/dev/null \
|
|
||||||
| awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}'
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
status() {
|
|
||||||
local total avail swap_total swap_free cap ul cur
|
|
||||||
|
|
||||||
echo "host"
|
|
||||||
echo " platform $(platform)"
|
|
||||||
echo " kernel $(uname -r)"
|
|
||||||
[ -r /etc/os-release ] && \
|
|
||||||
echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' /etc/os-release)"
|
|
||||||
echo " cpu $(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '?') online, load $(cut -d' ' -f1-3 /proc/loadavg)"
|
|
||||||
|
|
||||||
# ── the caps first, because they decide what the totals below are worth ──
|
|
||||||
echo
|
|
||||||
echo "caps"
|
|
||||||
cap=$(cgroup_cap_mb)
|
|
||||||
if [ -n "$cap" ]; then
|
|
||||||
cur=$(cgroup_used_mb)
|
|
||||||
echo " cgroup ${cap} MB (${CG_VERSION}, ${CG_CUR_FILE##*/} says ${cur:-?} MB used)"
|
|
||||||
echo " ! /proc/meminfo below describes the HOST, not this cgroup."
|
|
||||||
echo " $(mb MemTotal) MB total is not yours; ${cap} MB is."
|
|
||||||
elif [ -n "$CG_VERSION" ]; then
|
|
||||||
echo " cgroup none (${CG_VERSION} present, no memory limit set)"
|
|
||||||
else
|
|
||||||
echo " cgroup no memory controller found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ul=$(ulimit_v_mb)
|
|
||||||
if [ -n "$ul" ]; then
|
|
||||||
echo " ! ulimit -v ${ul} MB — a per-process cap, inherited from your shell"
|
|
||||||
echo " it stops this process long before the machine runs out"
|
|
||||||
else
|
|
||||||
echo " ulimit -v unlimited"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# overcommit_memory=0 is the default heuristic: a large allocation is
|
|
||||||
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
|
||||||
# than as a failed malloc. It is why `push` touches every page it asks for.
|
|
||||||
local om or_
|
|
||||||
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
|
||||||
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
|
||||||
case "$om" in
|
|
||||||
0) echo " overcommit 0 heuristic — allocations are granted on a guess," ;;
|
|
||||||
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit," ;;
|
|
||||||
2) echo " overcommit 2 strict (ratio ${or_}%) — allocation fails honestly instead of killing later," ;;
|
|
||||||
*) echo " overcommit ${om}" ;;
|
|
||||||
esac
|
|
||||||
[ "$om" != "?" ] && echo " so RSS is the number to trust, not what a process asked for"
|
|
||||||
|
|
||||||
# ── what it says it has ──
|
|
||||||
total=$(mb MemTotal); avail=$(avail_meminfo_mb)
|
|
||||||
swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree)
|
|
||||||
echo
|
|
||||||
echo "memory"
|
|
||||||
echo " total ${total} MB"
|
|
||||||
echo " available ${avail} MB"
|
|
||||||
echo " swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
|
||||||
if [ "$swap_total" -eq 0 ]; then
|
|
||||||
echo " - no swap: this box has no cushion. It goes from fine to OOM-killed"
|
|
||||||
echo " with nothing in between, which is the abrupt failure you get in a VM."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# postgres puts its shared buffers in /dev/shm. Docker's default is 64 MB,
|
|
||||||
# and the resulting failure names neither shm nor the size.
|
|
||||||
if [ -d /dev/shm ]; then
|
|
||||||
local shm; shm=$(df -Pm /dev/shm 2>/dev/null | awk 'NR==2{print $2}')
|
|
||||||
if [ -n "$shm" ]; then
|
|
||||||
if [ "$shm" -le 64 ]; then
|
|
||||||
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
|
||||||
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
|
||||||
else
|
|
||||||
echo " /dev/shm ${shm} MB"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "disk"
|
|
||||||
local d
|
|
||||||
for d in / /tmp /var/lib/docker; do
|
|
||||||
[ -d "$d" ] || continue
|
|
||||||
df -Pm "$d" 2>/dev/null | awk -v p="$d" 'NR==2{printf " %-12s %s MB free of %s MB\n", p, $4, $2}'
|
|
||||||
done
|
|
||||||
|
|
||||||
# kind and Tilt both watch large trees, and the failure mode is silent:
|
|
||||||
# they simply stop noticing file changes. Cheap to report while we are here.
|
|
||||||
local w i
|
|
||||||
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
|
||||||
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
|
||||||
echo
|
|
||||||
echo "tooling"
|
|
||||||
echo " inotify watches=$w instances=$i"
|
|
||||||
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
|
||||||
echo " ! low — anything watching files will silently stop seeing changes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
echo " docker socket present, no cli"
|
|
||||||
else
|
|
||||||
echo " docker not installed"
|
|
||||||
fi
|
|
||||||
elif docker info >/dev/null 2>&1; then
|
|
||||||
local n
|
|
||||||
n=$(docker ps -q 2>/dev/null | wc -l)
|
|
||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null), ${n} container(s) running"
|
|
||||||
else
|
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
|
||||||
# not usefully apply — the change costs a full VM restart. Report it, and
|
|
||||||
# report the commonest mistake, which is editing it and not restarting.
|
|
||||||
if is_wsl; then
|
|
||||||
local cfg conf conf_mb n
|
|
||||||
cfg=$(wslconfig_path)
|
|
||||||
echo
|
|
||||||
echo "wsl"
|
|
||||||
if [ -z "$cfg" ]; then
|
|
||||||
echo " ! cannot tell which Windows profile owns .wslconfig"
|
|
||||||
else
|
|
||||||
echo " config $cfg"
|
|
||||||
conf=$(configured_memory "$cfg")
|
|
||||||
if [ -n "$conf" ]; then
|
|
||||||
conf_mb=$(to_mb "$conf")
|
|
||||||
echo " configured $conf (${conf_mb} MB), booted ${total} MB"
|
|
||||||
# The VM reports a little less than allocated; 15% covers the
|
|
||||||
# kernel without calling every healthy machine a mismatch.
|
|
||||||
if [ -n "$conf_mb" ] && [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
|
||||||
echo " ! configured ${conf_mb} MB but booted ${total} MB — not applied yet."
|
|
||||||
echo " From a WINDOWS terminal: wsl --shutdown then start the distro again."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " configured no memory= set (WSL defaults to 50% of host RAM, or 8 GB,"
|
|
||||||
echo " whichever is less). To raise it, add on the Windows side:"
|
|
||||||
echo " [wsl2]"
|
|
||||||
echo " memory=8GB"
|
|
||||||
echo " then from a WINDOWS terminal: wsl --shutdown"
|
|
||||||
fi
|
|
||||||
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
|
||||||
if [ "$n" -gt 0 ]; then
|
|
||||||
echo " backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo " - native linux: no VM allocation to raise. If memory is tight the levers"
|
|
||||||
echo " are freeing something or adding swap."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "effective ceiling $(effective_ceiling_mb) MB"
|
|
||||||
echo " the lowest of MemTotal, the cgroup cap and ulimit -v. What the box"
|
|
||||||
echo " claims. 'push' measures what it will actually hand over."
|
|
||||||
|
|
||||||
[ "$avail" -lt $(( total / 5 )) ] && { echo; hogs; }
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── .wslconfig ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
require_wsl() {
|
|
||||||
if ! is_wsl; then
|
|
||||||
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
|
||||||
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
|
||||||
echo "Use 'status' to see what the machine actually has." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# backup and restore act on the file, so unlike status they must not guess.
|
|
||||||
wslconfig_required() {
|
|
||||||
local cfg; cfg=$(wslconfig_required)
|
|
||||||
if [ -z "$cfg" ]; then
|
|
||||||
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
|
||||||
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
|
||||||
| grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "$cfg"
|
|
||||||
}
|
|
||||||
|
|
||||||
configured_memory() {
|
|
||||||
[ -r "$1" ] || { echo ""; return; }
|
|
||||||
sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
# "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo.
|
|
||||||
to_mb() {
|
|
||||||
local v="${1^^}" n
|
|
||||||
n=$(echo "$v" | tr -dc '0-9')
|
|
||||||
[ -n "$n" ] || { echo ""; return; }
|
|
||||||
case "$v" in
|
|
||||||
*GB|*G) echo $(( n * 1024 )) ;;
|
|
||||||
*MB|*M) echo "$n" ;;
|
|
||||||
*) echo $(( n / 1024 / 1024 )) ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
backup() {
|
|
||||||
require_wsl backup
|
|
||||||
local cfg dest
|
|
||||||
cfg=$(wslconfig_required)
|
|
||||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
|
||||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
|
||||||
# second run is not a backup.
|
|
||||||
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
|
||||||
cp "$cfg" "$dest"
|
|
||||||
echo "backed up $dest"
|
|
||||||
echo
|
|
||||||
echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
require_wsl restore
|
|
||||||
local cfg newest count
|
|
||||||
cfg=$(wslconfig_required)
|
|
||||||
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
|
||||||
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
|
||||||
|
|
||||||
echo "restoring $newest"
|
|
||||||
echo " -> $cfg"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Newest is the right default — undo the last edit — but if you backed up
|
|
||||||
# *after* editing, the state you want is older. Show the rest so a no-op
|
|
||||||
# restore is obviously a no-op rather than a mystery.
|
|
||||||
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
|
||||||
if [ "$count" -gt 1 ]; then
|
|
||||||
echo "$count backups exist, newest first:"
|
|
||||||
ls -t "$cfg".*.bak | sed 's/^/ /'
|
|
||||||
echo " (restoring the newest; copy another by hand to pick an older one)"
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -r "$cfg" ]; then
|
|
||||||
echo "what changes:"
|
|
||||||
if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then
|
|
||||||
echo " nothing — that backup is identical to the current config"
|
|
||||||
else
|
|
||||||
sed 's/^/ /' /tmp/mem.diff
|
|
||||||
fi
|
|
||||||
rm -f /tmp/mem.diff
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "proceed? [y/N] "
|
|
||||||
read -r reply
|
|
||||||
case "$reply" in
|
|
||||||
y|Y|yes|Yes) ;;
|
|
||||||
*) echo "left alone"; return 0 ;;
|
|
||||||
esac
|
|
||||||
cp "$newest" "$cfg"
|
|
||||||
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── push ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
STATE=""
|
|
||||||
CHILD=""
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [ -n "$CHILD" ] && kill -0 "$CHILD" 2>/dev/null; then
|
|
||||||
kill -KILL "$CHILD" 2>/dev/null || true
|
|
||||||
wait "$CHILD" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
[ -n "$STATE" ] && rm -f "$STATE"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# The child allocates and stops itself; the parent only watches. That split is
|
|
||||||
# the point: under --to-oom the allocating process is expected to be killed, and
|
|
||||||
# something has to survive to say how far it got.
|
|
||||||
allocator() {
|
|
||||||
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
|
||||||
# first. Raising needs no privilege (only lowering does). Without it, the
|
|
||||||
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
|
||||||
# box you are still using, that is not an acceptable coin toss.
|
|
||||||
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
|
||||||
|
|
||||||
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
|
||||||
local bytes=$((STEP_MB * 1024 * 1024))
|
|
||||||
local swap_used_start
|
|
||||||
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
|
||||||
|
|
||||||
while :; do
|
|
||||||
# Written STRAIGHT INTO the array element. The obvious spelling —
|
|
||||||
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
|
||||||
# not one: the template stays resident, expanding "$chunk" makes a
|
|
||||||
# temporary word, and the append makes the element. A 128 MB step then
|
|
||||||
# needs 384 MB transiently, and on a small box it is killed on the
|
|
||||||
# first append while reporting a third of the true ceiling.
|
|
||||||
#
|
|
||||||
# printf -v into a subscript also means every page is written, so it is
|
|
||||||
# resident rather than merely promised — the only kind of allocation
|
|
||||||
# that measures anything under heuristic overcommit.
|
|
||||||
printf -v "arr[$i]" '%*s' "$bytes" ''
|
|
||||||
i=$((i + 1)); held=$((held + STEP_MB))
|
|
||||||
|
|
||||||
rss=$(awk '/^VmRSS:/{print int($2/1024)}' "/proc/$BASHPID/status" 2>/dev/null || echo 0)
|
|
||||||
avail=$(headroom_mb)
|
|
||||||
swapped=$(( $(mb SwapTotal) - $(mb SwapFree) - swap_used_start ))
|
|
||||||
[ "$swapped" -lt 0 ] && swapped=0
|
|
||||||
|
|
||||||
printf '%8s MB held rss %7s MB headroom %7s MB swap +%s MB\n' \
|
|
||||||
"$held" "$rss" "$avail" "$swapped"
|
|
||||||
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
|
||||||
|
|
||||||
# Worth calling out separately from the ceiling: this is where the box
|
|
||||||
# stops being fast and starts being unusable, which for a scheduler is
|
|
||||||
# a different and earlier problem than being killed.
|
|
||||||
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
|
||||||
first_swap=$held
|
|
||||||
echo " - first swap page at ${held} MB — past here it works but crawls"
|
|
||||||
echo "swapat $held" >> "$STATE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$TO_MB" ] && [ "$held" -ge "$TO_MB" ]; then
|
|
||||||
echo "stop reached-the-cap" >> "$STATE"; return 0
|
|
||||||
fi
|
|
||||||
if [ "$TO_OOM" = no ] && [ "$avail" -lt "$FLOOR_MB" ]; then
|
|
||||||
echo "stop floor" >> "$STATE"; return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
push() {
|
|
||||||
local total ceiling rc=0 last held rss swapat stop
|
|
||||||
total=$(mb MemTotal)
|
|
||||||
ceiling=$(effective_ceiling_mb)
|
|
||||||
|
|
||||||
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
|
||||||
# find the edge, few enough lines to read, and small enough that the
|
|
||||||
# transient cost of one allocation never dominates a small box. A fixed
|
|
||||||
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
|
||||||
if [ "$STEP_EXPLICIT" = no ]; then
|
|
||||||
STEP_MB=$(( ceiling / 64 ))
|
|
||||||
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
|
||||||
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
|
||||||
# depends on what it is protecting. Under a cgroup cap, running out kills
|
|
||||||
# only this container's own processes, so it need cover no more than the
|
|
||||||
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
|
||||||
# halve the answer. On a host there is everything else to protect, and the
|
|
||||||
# OOM killer does not promise to pick the process that caused the problem.
|
|
||||||
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
|
||||||
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
|
||||||
|
|
||||||
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
|
||||||
trap cleanup EXIT
|
|
||||||
# INT kills the child and lets the summary below print anyway, so an
|
|
||||||
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
|
||||||
# still gives the memory back.
|
|
||||||
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
|
||||||
|
|
||||||
echo "push"
|
|
||||||
echo " step ${STEP_MB} MB per allocation, every page touched"
|
|
||||||
echo " ceiling ${ceiling} MB claimed"
|
|
||||||
if [ -n "$TO_MB" ]; then
|
|
||||||
echo " stopping at ${TO_MB} MB (--to)"
|
|
||||||
elif [ "$TO_OOM" = yes ]; then
|
|
||||||
echo " ! stopping only when the kernel stops it (--to-oom)"
|
|
||||||
echo " the allocating child is marked as the preferred OOM victim,"
|
|
||||||
echo " but nothing about an OOM kill is entirely polite. Not on a box"
|
|
||||||
echo " running anything you mind losing."
|
|
||||||
else
|
|
||||||
echo " stopping when headroom drops below ${FLOOR_MB} MB"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
allocator &
|
|
||||||
CHILD=$!
|
|
||||||
wait "$CHILD" || rc=$?
|
|
||||||
CHILD=""
|
|
||||||
trap - INT
|
|
||||||
|
|
||||||
last=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 || true)
|
|
||||||
held=$(echo "$last" | awk '{print $1}')
|
|
||||||
rss=$(echo "$last" | awk '{print $2}')
|
|
||||||
swapat=$(awk '/^swapat/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
|
||||||
stop=$(awk '/^stop/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [ -z "$held" ]; then
|
|
||||||
echo " ! nothing was allocated. Even one ${STEP_MB} MB chunk failed —"
|
|
||||||
echo " try a smaller --step, or check ulimit -v in 'status'."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " reached ${rss:-$held} MB resident"
|
|
||||||
[ -n "$swapat" ] && echo " swapping from ${swapat} MB"
|
|
||||||
|
|
||||||
case "$stop" in
|
|
||||||
reached-the-cap)
|
|
||||||
echo " outcome stopped at the --to cap, not at a limit."
|
|
||||||
echo " The box held ${TO_MB} MB without complaint; there is more." ;;
|
|
||||||
floor)
|
|
||||||
echo " outcome stopped with a cushion intact, by choice."
|
|
||||||
echo " The real ceiling is higher — --to-oom finds it, at the"
|
|
||||||
echo " cost of an actual OOM kill." ;;
|
|
||||||
interrupted)
|
|
||||||
echo " outcome interrupted at ${rss:-$held} MB — where you stopped it,"
|
|
||||||
echo " not where the box did." ;;
|
|
||||||
*)
|
|
||||||
# No stop line means the child did not decide to stop: it was ended.
|
|
||||||
if [ "$rc" -ge 128 ]; then
|
|
||||||
echo " outcome the child was killed (signal $((rc - 128))) at ${rss:-$held} MB."
|
|
||||||
elif [ "$rc" -ne 0 ]; then
|
|
||||||
echo " outcome the allocation failed at ${rss:-$held} MB (exit ${rc})."
|
|
||||||
echo " bash could not get the next chunk — an honest malloc"
|
|
||||||
echo " failure rather than a kill. That is the strict-overcommit"
|
|
||||||
echo " or ulimit path."
|
|
||||||
else
|
|
||||||
echo " outcome ended at ${rss:-$held} MB."
|
|
||||||
fi
|
|
||||||
local ev
|
|
||||||
ev=$(dmesg 2>/dev/null | tail -80 | grep -iE 'oom-kill|killed process' | tail -1 || true)
|
|
||||||
if [ -n "$ev" ]; then
|
|
||||||
echo " kernel ${ev#*] }"
|
|
||||||
else
|
|
||||||
echo " - dmesg is unreadable here (dmesg_restrict, or no privilege),"
|
|
||||||
echo " so the kill cannot be confirmed from this side. The number stands."
|
|
||||||
fi ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# The gap between the claim and the measurement is the finding — but only
|
|
||||||
# when the BOX chose where to stop. An empty $stop means the child was ended
|
|
||||||
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
|
||||||
# asked for, and flagging those as short of the ceiling would put a warning
|
|
||||||
# on every deliberately small run.
|
|
||||||
local got="${rss:-$held}"
|
|
||||||
echo
|
|
||||||
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
|
||||||
echo " ! claimed ${ceiling} MB, gave up ${got} MB — under 70% of it."
|
|
||||||
echo " Something is taking the difference. 'status' names the candidates:"
|
|
||||||
echo " a cgroup cap, ulimit -v, or memory already resident."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── all ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
all() {
|
|
||||||
status
|
|
||||||
echo
|
|
||||||
echo "────────────────────────────────────────────────────────────"
|
|
||||||
echo
|
|
||||||
push
|
|
||||||
|
|
||||||
local got budget_mb ceiling
|
|
||||||
load_config
|
|
||||||
if [ -n "$BUDGET_GB" ]; then
|
|
||||||
budget_mb=$(( BUDGET_GB * 1024 ))
|
|
||||||
else
|
|
||||||
budget_mb=$(( NODES * NODE_MB ))
|
|
||||||
fi
|
|
||||||
ceiling=$(effective_ceiling_mb)
|
|
||||||
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
|
||||||
[ -n "$got" ] || got=0
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "verdict"
|
|
||||||
if [ -n "$BUDGET_GB" ]; then
|
|
||||||
echo " budget ${budget_mb} MB (--budget)"
|
|
||||||
else
|
|
||||||
# rig's own figure for this profile: nodes times what one node costs.
|
|
||||||
# Addons carry no memory figure in rig yet, so this is the cluster alone
|
|
||||||
# and whatever you deploy comes on top. --budget once you know that too.
|
|
||||||
echo " budget ${budget_mb} MB — profile ${PROFILE_NAME}: ${NODES} node(s) x ${NODE_MB} MB,"
|
|
||||||
echo " the cluster alone; your workload comes on top (--budget GB)"
|
|
||||||
fi
|
|
||||||
echo " measured ${got} MB handed over"
|
|
||||||
|
|
||||||
if [ "$got" -ge "$budget_mb" ]; then
|
|
||||||
echo " fits, with $(( got - budget_mb )) MB spare."
|
|
||||||
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
|
||||||
echo " - under 30% spare is thin once a workload runs on top: memory use"
|
|
||||||
echo " is spiky, and the spikes are what get killed."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! short by $(( budget_mb - got )) MB."
|
|
||||||
if [ "$ceiling" -ge "$budget_mb" ]; then
|
|
||||||
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
|
||||||
echo " Free something, or read the caps section again."
|
|
||||||
else
|
|
||||||
echo " The box does not have it to give. A bigger machine, or a profile"
|
|
||||||
echo " with fewer nodes."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── main ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
parse_flags() {
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--to) TO_MB=$(( ${2:?--to needs a value in GB} * 1024 )); shift 2 ;;
|
|
||||||
--to-mb) TO_MB="${2:?--to-mb needs a value in MB}"; shift 2 ;;
|
|
||||||
--step) STEP_MB="${2:?--step needs a value in MB}"; STEP_EXPLICIT=yes; shift 2 ;;
|
|
||||||
--to-oom) TO_OOM=yes; shift ;;
|
|
||||||
--budget) BUDGET_GB="${2:?--budget needs a value in GB}"; BUDGET_EXPLICIT=yes; shift 2 ;;
|
|
||||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if [ "$TO_OOM" = yes ] && [ -n "$TO_MB" ]; then
|
|
||||||
echo "--to and --to-oom contradict each other: one stops early, the other" >&2
|
|
||||||
echo "refuses to stop at all. Pick one." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
require_linux
|
|
||||||
find_cgroup
|
|
||||||
|
|
||||||
cmd="${1:-status}"
|
|
||||||
[ $# -gt 0 ] && shift
|
|
||||||
|
|
||||||
case "$cmd" in
|
|
||||||
status) parse_flags "$@"; status ;;
|
|
||||||
push) parse_flags "$@"; push ;;
|
|
||||||
all) parse_flags "$@"; all ;;
|
|
||||||
backup) backup ;;
|
|
||||||
restore) restore ;;
|
|
||||||
*) echo "usage: $0 [status|push|all|backup|restore]" >&2
|
|
||||||
echo " push [--to GB] [--to-mb MB] [--step MB] [--to-oom]" >&2
|
|
||||||
echo " all [--budget GB]" >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
7
rig/tests/hosts/debian-trixie/expect.txt
Normal file
7
rig/tests/hosts/debian-trixie/expect.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 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
|
||||||
4
rig/tests/hosts/debian-trixie/root/etc/os-release
Normal file
4
rig/tests/hosts/debian-trixie/root/etc/os-release
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
|
||||||
|
NAME="Debian GNU/Linux"
|
||||||
|
VERSION_ID="13"
|
||||||
|
ID=debian
|
||||||
4
rig/tests/hosts/debian-trixie/root/proc/meminfo
Normal file
4
rig/tests/hosts/debian-trixie/root/proc/meminfo
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
MemTotal: 16241664 kB
|
||||||
|
MemAvailable: 11534336 kB
|
||||||
|
SwapTotal: 0 kB
|
||||||
|
SwapFree: 0 kB
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
rig/tests/hosts/debian-trixie/root/proc/version
Normal file
1
rig/tests/hosts/debian-trixie/root/proc/version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Linux version 6.12.48+deb13-amd64
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user