diff --git a/rig/Makefile b/rig/Makefile
index 8b11855..5eed100 100644
--- a/rig/Makefile
+++ b/rig/Makefile
@@ -6,9 +6,9 @@
#
# make cluster up -> ctrl/cluster.sh up
#
-# Config layers, weakest first: ctrl/versions.env (pinned toolchain) <
-# ctrl/env.d/.env (cluster shape) < ctrl/.env (local, gitignored) <
-# the environment. So `make cluster up PROFILE=client` beats everything.
+# Config layers, weakest first: built-in defaults < ctrl/versions.env (pinned
+# toolchain) < ctrl/env.d/.env (optional shape) < ctrl/.env (local,
+# gitignored) < the environment. So `make cluster up PROFILE=` beats them all.
#
# Start with: make setup (then: make cluster up && make docs)
diff --git a/rig/README.md b/rig/README.md
index 3da367e..b47cf82 100644
--- a/rig/README.md
+++ b/rig/README.md
@@ -135,17 +135,22 @@ directory beside it.
## Profiles
-A profile is the shape of the cluster: how many nodes, which addons, whether the
-apiserver audits. They live in `ctrl/env.d/`, and the active one is `PROFILE`.
+**rig needs no profile.** With none named it runs on built-in defaults: one node,
+no addons, a local registry, the newest Kubernetes version it pins. A profile is
+an optional overlay — a file in `ctrl/env.d/`, named by `PROFILE` — for when you
+want a different shape.
-| Profile | For |
+rig ships **examples**, not active profiles, because each one is a use case rather
+than something every rig needs. Copy one to use it:
+
+| example | shape |
| --- | --- |
-| `minimal` | the default. One node, no addons, boots fast. |
-| `client` | the regulated-estate shape — multi-node, audit on, registry mirror. |
-| `offline` | air-gapped: everything from a preloaded local registry. |
-| `data` | the cabinets an environment asks for. |
+| `client.env.example` | multi-node, audit on, images through a mirror of a corporate registry |
+| `offline.env.example` | air-gapped: everything from a preloaded local registry |
+| `data.env.example` | databases and a scheduler: postgres, redis, airflow |
```bash
+cp ctrl/env.d/data.env.example ctrl/env.d/data.env
PROFILE=data make cluster up
PROFILE=data make addons install
make addons # what the active profile wants, and what exists
@@ -156,9 +161,9 @@ restating node count and audit as variables:
| 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` |
+| `kind-config.yaml.tpl` | 1 | off | the default; the `data` example |
+| `kind-config.audit.yaml.tpl` | 1 | on | the `offline` example |
+| `kind-config.client.yaml.tpl` | 3 | on | the `client` example |
Both numbers are read back out of the chosen file, so the YAML is the only place
that decides and there is nothing to drift. The layout under `ctrl/k8s/` is the
diff --git a/rig/ctrl/.env.example b/rig/ctrl/.env.example
index 8f482d8..4e9bc30 100644
--- a/rig/ctrl/.env.example
+++ b/rig/ctrl/.env.example
@@ -1,9 +1,10 @@
# Machine-local config. Copy to ctrl/.env (gitignored) and edit.
-# Cluster SHAPE lives in ctrl/env.d/.env — not here.
+# Cluster SHAPE: an optional profile in ctrl/env.d/ — see the *.env.example there.
# The architecture MODEL lives in arch/.json — not here either.
-# Which profile in ctrl/env.d/ to build. minimal | client | offline
-PROFILE=minimal
+# A profile in ctrl/env.d/ to build. Empty means rig's built-in defaults, which
+# need no profile at all. Copy an env.d/*.env.example to .env to add one.
+PROFILE=
# Cluster name; the kubectl context becomes kind-.
# LEAVE THIS UNSET unless you need a name that differs from the directory —
diff --git a/rig/ctrl/Dockerfile.deps b/rig/ctrl/Dockerfile.deps
index 8493814..4b5b917 100644
--- a/rig/ctrl/Dockerfile.deps
+++ b/rig/ctrl/Dockerfile.deps
@@ -28,7 +28,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# 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.
-ARG PROFILE=minimal
+ARG PROFILE=default
WORKDIR /work
COPY standalone/${PROFILE}/rigdeps.sh /work/rigdeps.sh
RUN chmod +x /work/rigdeps.sh
diff --git a/rig/ctrl/addons/postgres.sh b/rig/ctrl/addons/postgres.sh
index b694f30..b6813bc 100755
--- a/rig/ctrl/addons/postgres.sh
+++ b/rig/ctrl/addons/postgres.sh
@@ -8,7 +8,7 @@
# 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
+# repo is a network dependency, and the offline example profile exists precisely so
# there is a path with none. The image is pinned in ctrl/versions.env and can be
# preloaded into a local registry like every other image here.
#
diff --git a/rig/ctrl/env.d/client.env b/rig/ctrl/env.d/client.env.example
similarity index 79%
rename from rig/ctrl/env.d/client.env
rename to rig/ctrl/env.d/client.env.example
index 2c2fff1..67f950c 100644
--- a/rig/ctrl/env.d/client.env
+++ b/rig/ctrl/env.d/client.env.example
@@ -1,3 +1,8 @@
+# EXAMPLE PROFILE. rig needs none of these: with no profile it runs on its
+# built-in defaults (lib/config.sh). To use this one, copy it to client.env in this
+# directory and name it — PROFILE=client in ctrl/.env, or on the command line. It
+# then overlays the defaults; anything it does not set, they still supply.
+#
# 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.
diff --git a/rig/ctrl/env.d/data.env b/rig/ctrl/env.d/data.env.example
similarity index 69%
rename from rig/ctrl/env.d/data.env
rename to rig/ctrl/env.d/data.env.example
index 4b98143..5547d49 100644
--- a/rig/ctrl/env.d/data.env
+++ b/rig/ctrl/env.d/data.env.example
@@ -1,10 +1,10 @@
-# data — a cluster with the cabinets an environment asks for.
+# EXAMPLE PROFILE. rig needs none of these: with no profile it runs on its
+# built-in defaults (lib/config.sh). To use this one, copy it to data.env in this
+# directory and name it — PROFILE=data in ctrl/.env, or on the command line. It
+# then overlays the defaults; anything it does not set, they still supply.
#
-# 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/.sh.
+# data — databases and a scheduler for an environment that needs them: postgres,
+# redis and airflow, each an upstream image run unmodified.
#
# Everything lands in the `data` namespace (DATA_NAMESPACE to move it), so
# `make cluster reset` on the app namespace leaves the databases alone.
@@ -18,7 +18,7 @@ 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.
+# local, not none — see the defaults in lib/config.sh: `none` has no outward-push guard.
REGISTRY_MODE=local
INGRESS_MODE=hostport
DNS_MODE=hosts
diff --git a/rig/ctrl/env.d/minimal.env b/rig/ctrl/env.d/minimal.env
deleted file mode 100644
index 1ee8073..0000000
--- a/rig/ctrl/env.d/minimal.env
+++ /dev/null
@@ -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/. 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.
diff --git a/rig/ctrl/env.d/offline.env b/rig/ctrl/env.d/offline.env.example
similarity index 65%
rename from rig/ctrl/env.d/offline.env
rename to rig/ctrl/env.d/offline.env.example
index ae59379..8f19bcc 100644
--- a/rig/ctrl/env.d/offline.env
+++ b/rig/ctrl/env.d/offline.env.example
@@ -1,3 +1,8 @@
+# EXAMPLE PROFILE. rig needs none of these: with no profile it runs on its
+# built-in defaults (lib/config.sh). To use this one, copy it to offline.env in this
+# directory and name it — PROFILE=offline in ctrl/.env, or on the command line. It
+# then overlays the defaults; anything it does not set, they still supply.
+#
# 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.
diff --git a/rig/ctrl/k8s/README.md b/rig/ctrl/k8s/README.md
index 8d906e8..0ab16a3 100644
--- a/rig/ctrl/k8s/README.md
+++ b/rig/ctrl/k8s/README.md
@@ -28,12 +28,12 @@ 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` |
+| `kind-config.yaml.tpl` | 1 | off | the default; the `data` example |
+| `kind-config.audit.yaml.tpl` | 1 | on | the `offline` example |
+| `kind-config.client.yaml.tpl` | 3 | on | the `client` example |
-A profile picks one with `KIND_CONFIG` in `ctrl/env.d/.env`. Adding a
-shape is adding a file — there is no dispatcher to edit.
+With no profile the default shape is used; a profile picks another with
+`KIND_CONFIG`. 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.
diff --git a/rig/ctrl/lib/config.sh b/rig/ctrl/lib/config.sh
index 5606ea9..cb977c6 100644
--- a/rig/ctrl/lib/config.sh
+++ b/rig/ctrl/lib/config.sh
@@ -5,10 +5,11 @@
# definition of how the config layers compose, which every script has to agree
# on exactly. Precedence, weakest first:
#
+# built-in defaults below; fill only what nothing else set
# ctrl/versions.env pinned toolchain + image digests (committed)
-# ctrl/env.d/ cluster shape (committed)
+# ctrl/env.d/ cluster shape — OPTIONAL, examples ship as *.env.example
# ctrl/.env machine-local values and secrets (gitignored)
-# the caller's env `make cluster up PROFILE=client` (always wins)
+# the caller's env `make cluster up PROFILE=` (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
diff --git a/rig/docs/index.html b/rig/docs/index.html
index dde3a27..75a7c23 100644
--- a/rig/docs/index.html
+++ b/rig/docs/index.html
@@ -308,8 +308,8 @@ make cluster up # build the cluster for the active profile
-make cluster up # default profile
-make cluster up PROFILE=client # three nodes, audit on, cached registry
+make cluster up # built-in defaults — no profile needed
+make cluster up PROFILE=client # after copying env.d/client.env.example: three nodes, audit, cached registry
make cluster reset # destroy and rebuild — the only way to change CNI or audit
@@ -382,7 +382,7 @@ make setup core # same distinction, via setup
make deps-image full # bakes every binary into the image
docker save …-deps:full | gzip > rig.tgz
# carry that one file in, then:
-docker load < rig.tgz && make cluster up PROFILE=offline
+docker load < rig.tgz && make cluster up PROFILE=offline # from env.d/offline.env.example
@@ -408,9 +408,9 @@ docker load < rig.tgz && make cluster up PROFILE=offline
ctrl/.env if you want it fixed rather than derived.
Configuration layers
- Weakest first, later wins: pinned versions → the profile →
- ctrl/.env → the environment. So
- make cluster up PROFILE=client always beats every file.
+ Weakest first, later wins: built-in defaults → pinned versions → a
+ profile, if you name one → ctrl/.env → the environment. So
+ make cluster up PROFILE=<name> always beats every file.
diff --git a/rig/standalone/README.md b/rig/standalone/README.md
deleted file mode 100644
index 4fd4a78..0000000
--- a/rig/standalone/README.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# standalone — rig as single files, one folder per profile
-
-**Everything in the `/` 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//rigdeps.sh rig's toolchain installer (ctrl/deps.sh)
-standalone//rigmini.sh rig's memory tool (ctrl/mem.sh)
-standalone//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.
-
-## Your own setup, somewhere else — `export`
-
-The folders here are rig's **profiles**, identical on any machine. To take the
-setup **this machine runs** instead — its profile plus the choices in its local
-`ctrl/.env` (ports, manifest directory, mirror mode) — export it:
-
-```bash
-make standalone export ~/rig-kit # one kit, outside the repository
-```
-
-An export carries your **choices, never your credentials**. Anything in `ctrl/.env`
-that is not a setting a caller may override — registry logins, a mirror URL — is
-left out, and the export lists those names so you know what to set on the target.
-It refuses to write inside the repository: it reflects one machine, and the
-repository holds what is true for every machine. `make selftest` proves both with
-sentinel values.
-
-## 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`.
diff --git a/rig/standalone/client/rigdeps.sh b/rig/standalone/client/rigdeps.sh
deleted file mode 100755
index 391fe30..0000000
--- a/rig/standalone/client/rigdeps.sh
+++ /dev/null
@@ -1,959 +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="client"
- 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 </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 — 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
-# 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 `, 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/ 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= overrides the install directory" >&2
- exit 1 ;;
-esac
diff --git a/rig/standalone/client/rigmini.sh b/rig/standalone/client/rigmini.sh
deleted file mode 100755
index f02221e..0000000
--- a/rig/standalone/client/rigmini.sh
+++ /dev/null
@@ -1,860 +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="client"
- 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
diff --git a/rig/standalone/data/Makefile b/rig/standalone/data/Makefile
deleted file mode 100644
index 56ac921..0000000
--- a/rig/standalone/data/Makefile
+++ /dev/null
@@ -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)
diff --git a/rig/standalone/data/rigdeps.sh b/rig/standalone/data/rigdeps.sh
deleted file mode 100755
index be3a305..0000000
--- a/rig/standalone/data/rigdeps.sh
+++ /dev/null
@@ -1,964 +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="data"
- 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 </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 — 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
-# 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 `, 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/ 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= overrides the install directory" >&2
- exit 1 ;;
-esac
diff --git a/rig/standalone/data/rigmini.sh b/rig/standalone/data/rigmini.sh
deleted file mode 100755
index f7e9510..0000000
--- a/rig/standalone/data/rigmini.sh
+++ /dev/null
@@ -1,865 +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="data"
- 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
diff --git a/rig/standalone/client/Makefile b/rig/standalone/default/Makefile
similarity index 100%
rename from rig/standalone/client/Makefile
rename to rig/standalone/default/Makefile
diff --git a/rig/standalone/minimal/rigdeps.sh b/rig/standalone/default/rigdeps.sh
similarity index 98%
rename from rig/standalone/minimal/rigdeps.sh
rename to rig/standalone/default/rigdeps.sh
index dde2372..067564d 100755
--- a/rig/standalone/minimal/rigdeps.sh
+++ b/rig/standalone/default/rigdeps.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# 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/lib/config.sh
# Edit those and run `make standalone`. Changes made here are lost, and
@@ -39,13 +39,13 @@ render_kind_config ()
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 'minimal' ──
+# ── configuration, frozen for profile 'default' ──
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=""
+ declare -g ADDONS=""
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
declare -g AUDIT="off"
declare -gx CERT_MANAGER_VERSION="v1.21.1"
@@ -56,15 +56,15 @@ load_config() {
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 DNS_MODE="hosts"
declare -g HTTPS_PORT="20311"
declare -g HTTP_PORT="20310"
- declare -gx INGRESS_MODE="hostport"
+ declare -g 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 K8S_VERSION="v1_36"
+ declare -g 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"
@@ -85,11 +85,11 @@ load_config() {
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="minimal"
- declare -gx PROFILE_NAME="minimal"
+ declare -gx PROFILE="default"
+ declare -g PROFILE_NAME="default"
declare -gx REDIS_IMAGE="redis:7-alpine"
declare -gx REGISTRY_IMAGE="registry:2"
- declare -gx REGISTRY_MODE="local"
+ declare -g REGISTRY_MODE="local"
declare -g REGISTRY_PORT="20313"
declare -gx STUB_IMAGE="python:3.12-slim"
declare -g TILT_PORT="20312"
diff --git a/rig/standalone/minimal/rigmini.sh b/rig/standalone/default/rigmini.sh
similarity index 98%
rename from rig/standalone/minimal/rigmini.sh
rename to rig/standalone/default/rigmini.sh
index 761e072..2913ce6 100755
--- a/rig/standalone/minimal/rigmini.sh
+++ b/rig/standalone/default/rigmini.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# 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/lib/config.sh
# Edit those and run `make standalone`. Changes made here are lost, and
@@ -39,13 +39,13 @@ render_kind_config ()
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 'minimal' ──
+# ── configuration, frozen for profile 'default' ──
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=""
+ declare -g ADDONS=""
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
declare -g AUDIT="off"
declare -gx CERT_MANAGER_VERSION="v1.21.1"
@@ -56,15 +56,15 @@ load_config() {
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 DNS_MODE="hosts"
declare -g HTTPS_PORT="20311"
declare -g HTTP_PORT="20310"
- declare -gx INGRESS_MODE="hostport"
+ declare -g 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 K8S_VERSION="v1_36"
+ declare -g 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"
@@ -85,11 +85,11 @@ load_config() {
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="minimal"
- declare -gx PROFILE_NAME="minimal"
+ declare -gx PROFILE="default"
+ declare -g PROFILE_NAME="default"
declare -gx REDIS_IMAGE="redis:7-alpine"
declare -gx REGISTRY_IMAGE="registry:2"
- declare -gx REGISTRY_MODE="local"
+ declare -g REGISTRY_MODE="local"
declare -g REGISTRY_PORT="20313"
declare -gx STUB_IMAGE="python:3.12-slim"
declare -g TILT_PORT="20312"
diff --git a/rig/standalone/minimal/Makefile b/rig/standalone/minimal/Makefile
deleted file mode 100644
index 56ac921..0000000
--- a/rig/standalone/minimal/Makefile
+++ /dev/null
@@ -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)
diff --git a/rig/standalone/offline/Makefile b/rig/standalone/offline/Makefile
deleted file mode 100644
index 56ac921..0000000
--- a/rig/standalone/offline/Makefile
+++ /dev/null
@@ -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)
diff --git a/rig/standalone/offline/rigdeps.sh b/rig/standalone/offline/rigdeps.sh
deleted file mode 100755
index 207168d..0000000
--- a/rig/standalone/offline/rigdeps.sh
+++ /dev/null
@@ -1,959 +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="offline"
- 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 </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 — 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
-# 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 `, 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/ 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= overrides the install directory" >&2
- exit 1 ;;
-esac
diff --git a/rig/standalone/offline/rigmini.sh b/rig/standalone/offline/rigmini.sh
deleted file mode 100755
index ed6474c..0000000
--- a/rig/standalone/offline/rigmini.sh
+++ /dev/null
@@ -1,860 +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="offline"
- 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