From 730ebaff2f5e5f21879d96bd19d7911cf82b3603 Mon Sep 17 00:00:00 2001 From: buenosairesam Date: Thu, 17 Sep 2026 00:14:27 -0300 Subject: [PATCH] remove profile dependency --- rig/Makefile | 6 +- rig/README.md | 25 +- rig/ctrl/.env.example | 7 +- rig/ctrl/Dockerfile.deps | 2 +- rig/ctrl/addons/postgres.sh | 2 +- .../env.d/{client.env => client.env.example} | 5 + rig/ctrl/env.d/{data.env => data.env.example} | 14 +- rig/ctrl/env.d/minimal.env | 21 - .../{offline.env => offline.env.example} | 5 + rig/ctrl/k8s/README.md | 10 +- rig/ctrl/lib/config.sh | 5 +- rig/docs/index.html | 12 +- rig/standalone/README.md | 61 -- rig/standalone/client/rigdeps.sh | 959 ----------------- rig/standalone/client/rigmini.sh | 860 ---------------- rig/standalone/data/Makefile | 23 - rig/standalone/data/rigdeps.sh | 964 ------------------ rig/standalone/data/rigmini.sh | 865 ---------------- rig/standalone/{client => default}/Makefile | 0 .../{minimal => default}/rigdeps.sh | 20 +- .../{minimal => default}/rigmini.sh | 20 +- rig/standalone/minimal/Makefile | 23 - rig/standalone/offline/Makefile | 23 - rig/standalone/offline/rigdeps.sh | 959 ----------------- rig/standalone/offline/rigmini.sh | 860 ---------------- 25 files changed, 75 insertions(+), 5676 deletions(-) rename rig/ctrl/env.d/{client.env => client.env.example} (79%) rename rig/ctrl/env.d/{data.env => data.env.example} (69%) delete mode 100644 rig/ctrl/env.d/minimal.env rename rig/ctrl/env.d/{offline.env => offline.env.example} (65%) delete mode 100644 rig/standalone/README.md delete mode 100755 rig/standalone/client/rigdeps.sh delete mode 100755 rig/standalone/client/rigmini.sh delete mode 100644 rig/standalone/data/Makefile delete mode 100755 rig/standalone/data/rigdeps.sh delete mode 100755 rig/standalone/data/rigmini.sh rename rig/standalone/{client => default}/Makefile (100%) rename rig/standalone/{minimal => default}/rigdeps.sh (98%) rename rig/standalone/{minimal => default}/rigmini.sh (98%) delete mode 100644 rig/standalone/minimal/Makefile delete mode 100644 rig/standalone/offline/Makefile delete mode 100755 rig/standalone/offline/rigdeps.sh delete mode 100755 rig/standalone/offline/rigmini.sh 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