Merge branch 'rig-work'
This commit is contained in:
13
rig/Makefile
13
rig/Makefile
@@ -50,7 +50,7 @@ $(eval $(ARGS):;@:)
|
|||||||
.PHONY: $(ARGS)
|
.PHONY: $(ARGS)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: help setup check selftest mem deps deps-image pins cluster registry addons ports \
|
.PHONY: help setup check selftest mem deps deps-image standalone cluster registry addons ports \
|
||||||
docs tilt \
|
docs tilt \
|
||||||
kind-up kind-down kind-reset tilt-up tilt-down
|
kind-up kind-down kind-reset tilt-up tilt-down
|
||||||
|
|
||||||
@@ -66,19 +66,22 @@ check: ## is this machine ready? reports, never fixes
|
|||||||
bash ctrl/check.sh
|
bash ctrl/check.sh
|
||||||
|
|
||||||
# The counterpart to check: that one asks about the MACHINE and never fails,
|
# The counterpart to check: that one asks about the MACHINE and never fails,
|
||||||
# this one asks about RIG and exits 1, the way pins does. The checks are written
|
# this one asks about RIG and exits 1, as `make standalone check` does. Checks are written
|
||||||
# as the decisions they defend, so a failure names what is being undone.
|
# as the decisions they defend, so a failure names what is being undone.
|
||||||
selftest: ## does rig still do what it says? exits 1 if not
|
selftest: ## does rig still do what it says? exits 1 if not
|
||||||
bash ctrl/selftest.sh
|
bash ctrl/selftest.sh
|
||||||
|
|
||||||
mem: ## memory, and any cap holding it [status|backup|restore]
|
mem: ## memory, its caps, what it survives [status|push|all|backup|restore]
|
||||||
bash ctrl/mem.sh $(or $(ARGS),status)
|
bash ctrl/mem.sh $(or $(ARGS),status)
|
||||||
|
|
||||||
deps: ## install the toolchain [core|dev] (default dev)
|
deps: ## install the toolchain [core|dev] (default dev)
|
||||||
bash ctrl/deps.sh install $(or $(ARGS),dev)
|
bash ctrl/deps.sh install $(or $(ARGS),dev)
|
||||||
|
|
||||||
pins: ## standalone/rigdeps.sh still installs what rig pins?
|
# The one-file versions of rig's tools, one folder per profile, for machines the
|
||||||
bash ctrl/pins.sh
|
# full rig is not going to. Generated from rig as it is, never edited by hand;
|
||||||
|
# `check` is what selftest runs to catch a kit left behind by a change to rig.
|
||||||
|
standalone: ## generate standalone/<profile>/ kits [write|check] (default write)
|
||||||
|
bash ctrl/standalone.sh $(or $(ARGS),write)
|
||||||
|
|
||||||
deps-image: ## build the installer image [full]
|
deps-image: ## build the installer image [full]
|
||||||
docker build -f ctrl/Dockerfile.deps \
|
docker build -f ctrl/Dockerfile.deps \
|
||||||
|
|||||||
@@ -24,23 +24,27 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
ca-certificates curl jq graphviz python3 docker-cli \
|
ca-certificates curl jq graphviz python3 docker-cli \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# The installer is the generated standalone kit, not deps.sh plus the files it
|
||||||
|
# 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
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY ctrl/versions.env /work/ctrl/versions.env
|
COPY standalone/${PROFILE}/rigdeps.sh /work/rigdeps.sh
|
||||||
COPY ctrl/deps.sh /work/ctrl/deps.sh
|
RUN chmod +x /work/rigdeps.sh
|
||||||
RUN chmod +x /work/ctrl/deps.sh
|
|
||||||
|
|
||||||
# Defaults; every one is overridable with -e at run time.
|
# Defaults; every one is overridable with -e at run time.
|
||||||
ENV DEPS_SOURCE=upstream \
|
ENV DEPS_SOURCE=upstream \
|
||||||
OUT_BIN=/out/bin \
|
OUT_BIN=/out/bin \
|
||||||
HOST_ROOT=/host
|
HOST_ROOT=/host
|
||||||
|
|
||||||
ENTRYPOINT ["/work/ctrl/deps.sh"]
|
ENTRYPOINT ["/work/rigdeps.sh"]
|
||||||
CMD ["install"]
|
CMD ["install"]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# deps-full — same image, binaries baked in, works with no network at all.
|
# deps-full — same image, binaries baked in, works with no network at all.
|
||||||
FROM deps AS deps-full
|
FROM deps AS deps-full
|
||||||
RUN /work/ctrl/deps.sh fetch --to /opt/rig/bin
|
RUN /work/rigdeps.sh fetch --to /opt/rig/bin
|
||||||
ENV DEPS_SOURCE=baked \
|
ENV DEPS_SOURCE=baked \
|
||||||
BAKED_BIN=/opt/rig/bin
|
BAKED_BIN=/opt/rig/bin
|
||||||
|
|||||||
@@ -46,13 +46,10 @@ mb_of() {
|
|||||||
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# What one node costs, measured rather than guessed. On 2026-09-11 a minimal
|
# NODE_MB — what one node costs — comes from load_config (lib/config.sh), where
|
||||||
# control-plane node ran at 620 MiB idle and ~728 MiB with a small mock, plus
|
# its measurement is recorded. It lives there, not here, because the memory tool
|
||||||
# 16 MiB for the local registry — ~745 MiB of working set. 800 rounds that up,
|
# and every standalone kit need the same number: a copy of it is how rigmini.sh
|
||||||
# and agrees with the 800 MB observed independently on a larger rig. Worker
|
# came to say 2 GB per node long after rig had measured 800 MB.
|
||||||
# nodes carry no etcd or apiserver and are lighter, so for a multi-node shape
|
|
||||||
# this errs high. It is the cluster alone: whatever you deploy comes on top.
|
|
||||||
NODE_MB=800
|
|
||||||
|
|
||||||
# Every running container's working set in MB, tagged with the kind cluster it
|
# Every running container's working set in MB, tagged with the kind cluster it
|
||||||
# belongs to ('-' when it is not kind). docker stats reports usage minus page
|
# belongs to ('-' when it is not kind). docker stats reports usage minus page
|
||||||
|
|||||||
259
rig/ctrl/deps.sh
259
rig/ctrl/deps.sh
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# rig:standalone rigdeps detect
|
||||||
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
||||||
# report what it could not do.
|
# report what it could not do.
|
||||||
#
|
#
|
||||||
@@ -10,6 +11,8 @@
|
|||||||
#
|
#
|
||||||
# Usage (normally via `make deps`, or directly):
|
# Usage (normally via `make deps`, or directly):
|
||||||
# deps.sh detect # report host facts only, change nothing
|
# 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 fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||||
# deps.sh install [core|dev] # detect, fetch, install, report
|
# deps.sh install [core|dev] # detect, fetch, install, report
|
||||||
#
|
#
|
||||||
@@ -27,7 +30,11 @@ set -euo pipefail
|
|||||||
INVOKED_FROM="$PWD"
|
INVOKED_FROM="$PWD"
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
source ./versions.env
|
# 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.
|
||||||
|
source ./lib/config.sh
|
||||||
|
load_config
|
||||||
|
|
||||||
# Resolve a possibly-relative path against the caller's original directory.
|
# Resolve a possibly-relative path against the caller's original directory.
|
||||||
abspath() {
|
abspath() {
|
||||||
@@ -66,6 +73,91 @@ host_file() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── the tools this script itself needs ─────────────────────────────────────
|
||||||
|
|
||||||
|
arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) echo amd64 ;;
|
||||||
|
aarch64|arm64) echo arm64 ;;
|
||||||
|
*) uname -m ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The pins above are amd64. Rather than download something that cannot execute
|
||||||
|
# and let it fail as "cannot execute binary file: Exec format error", say so
|
||||||
|
# here and hand over the commands that produce the right checksums.
|
||||||
|
require_amd64() {
|
||||||
|
local a; a=$(arch)
|
||||||
|
[ "$a" = "amd64" ] && return 0
|
||||||
|
cat >&2 <<EOF
|
||||||
|
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
||||||
|
|
||||||
|
Nothing here would run, so it does not download. To make an ${a} version, the
|
||||||
|
URLs need the ${a} artifact and the checksums need to come from each project's
|
||||||
|
own published list — not from these values, and not from a download you did:
|
||||||
|
|
||||||
|
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
||||||
|
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
||||||
|
|
||||||
|
Edit the pinned block at the top of this file with what those print.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DL=""
|
||||||
|
pick_downloader() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then DL=curl
|
||||||
|
elif command -v wget >/dev/null 2>&1; then DL=wget
|
||||||
|
else
|
||||||
|
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
||||||
|
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
local url="$1" out="$2"
|
||||||
|
case "$DL" in
|
||||||
|
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
||||||
|
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
SHA=""
|
||||||
|
pick_sha() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
||||||
|
else
|
||||||
|
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
||||||
|
echo "Refusing to install unverified binaries." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── package manager, for the instructions only ─────────────────────────────
|
||||||
|
# This never runs a package manager. It names one so the reported action is
|
||||||
|
# something you can paste, on the distro you are actually on — an apt line on
|
||||||
|
# Amazon Linux 2 is a wrong answer dressed up as help.
|
||||||
|
|
||||||
|
pkg_install_cmd() {
|
||||||
|
local pkg="$1"
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
||||||
|
else echo "install '$pkg' with this system's package manager"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_pkg() {
|
||||||
|
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── detect ─────────────────────────────────────────────────────────────────
|
# ── detect ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
@@ -96,6 +188,7 @@ is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|||||||
detect() {
|
detect() {
|
||||||
echo "host"
|
echo "host"
|
||||||
echo " kernel $(uname -r)"
|
echo " kernel $(uname -r)"
|
||||||
|
echo " arch $(arch) ($(uname -m))"
|
||||||
|
|
||||||
local osr; osr=$(host_file /etc/os-release)
|
local osr; osr=$(host_file /etc/os-release)
|
||||||
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
||||||
@@ -123,6 +216,9 @@ detect() {
|
|||||||
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
echo " install to $OUT_BIN"
|
||||||
|
detect_libc
|
||||||
|
detect_prereqs
|
||||||
detect_wsl
|
detect_wsl
|
||||||
detect_filesystem
|
detect_filesystem
|
||||||
detect_docker
|
detect_docker
|
||||||
@@ -200,6 +296,64 @@ detect_filesystem() {
|
|||||||
fi
|
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() {
|
detect_docker() {
|
||||||
# Reachability of the daemon is the real question, and the CLI is only how
|
# 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
|
# we ask it. Note that when this runs inside the installer container, Docker
|
||||||
@@ -210,9 +364,12 @@ detect_docker() {
|
|||||||
echo " docker socket present (no cli in this context)"
|
echo " docker socket present (no cli in this context)"
|
||||||
else
|
else
|
||||||
echo " ! docker not found and no socket at /var/run/docker.sock"
|
echo " ! docker not found and no socket at /var/run/docker.sock"
|
||||||
MANUAL+=("Install Docker — the one true prerequisite:
|
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
||||||
sudo apt-get install -y docker.io && sudo usermod -aG docker \"\$USER\"
|
that needs root:
|
||||||
then log out and back in.")
|
$(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
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -271,7 +428,7 @@ resolve_url() {
|
|||||||
|
|
||||||
verify() {
|
verify() {
|
||||||
local file="$1" want="$2" name="$3" got
|
local file="$1" want="$2" name="$3" got
|
||||||
got=$(sha256sum "$file" | awk '{print $1}')
|
got=$($SHA "$file" | awk '{print $1}')
|
||||||
if [ "$got" != "$want" ]; then
|
if [ "$got" != "$want" ]; then
|
||||||
echo "checksum mismatch for $name" >&2
|
echo "checksum mismatch for $name" >&2
|
||||||
echo " expected $want" >&2
|
echo " expected $want" >&2
|
||||||
@@ -285,7 +442,7 @@ fetch_bin() {
|
|||||||
local name="$1" url="$2" sha="$3" dest="$4"
|
local name="$1" url="$2" sha="$3" dest="$4"
|
||||||
local tmp="$dest/.$name.tmp"
|
local tmp="$dest/.$name.tmp"
|
||||||
echo " fetching $name"
|
echo " fetching $name"
|
||||||
curl -fsSL --retry 3 -o "$tmp" "$(resolve_url "$url")"
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
verify "$tmp" "$sha" "$name"
|
verify "$tmp" "$sha" "$name"
|
||||||
mv "$tmp" "$dest/$name"
|
mv "$tmp" "$dest/$name"
|
||||||
chmod +x "$dest/$name"
|
chmod +x "$dest/$name"
|
||||||
@@ -298,7 +455,7 @@ fetch_tgz() {
|
|||||||
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
||||||
local tmp="$dest/.$name.tgz"
|
local tmp="$dest/.$name.tgz"
|
||||||
echo " fetching $name"
|
echo " fetching $name"
|
||||||
curl -fsSL --retry 3 -o "$tmp" "$(resolve_url "$url")"
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
verify "$tmp" "$sha" "$name"
|
verify "$tmp" "$sha" "$name"
|
||||||
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
||||||
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
||||||
@@ -511,6 +668,66 @@ report_manual() {
|
|||||||
# the machine was already using — which on a shared or client machine can break
|
# 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
|
# unrelated work (kubectl more than one minor away from a cluster is the common
|
||||||
# one). Say so; never decide it for them.
|
# 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"; }
|
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
||||||
|
|
||||||
warn_shadowing() {
|
warn_shadowing() {
|
||||||
@@ -613,9 +830,29 @@ install() {
|
|||||||
|
|
||||||
require_linux
|
require_linux
|
||||||
|
|
||||||
case "${1:-install}" in
|
# 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 ;;
|
detect) detect; report_manual ;;
|
||||||
fetch) shift; fetch "$@" ;;
|
list) list ;;
|
||||||
install) shift; install "${1:-dev}" ;;
|
verify) verify_tools "${1:-dev}" ;;
|
||||||
*) echo "usage: $0 [detect|fetch|install]" >&2; exit 1 ;;
|
fetch) need_downloads; fetch "$@" ;;
|
||||||
|
install) need_downloads; install "${1:-dev}" ;;
|
||||||
|
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
||||||
|
echo " install [core|dev] (default dev)" >&2
|
||||||
|
echo " fetch [core|dev] [--to DIR]" >&2
|
||||||
|
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||||
|
exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ load_config() {
|
|||||||
|
|
||||||
set -a
|
set -a
|
||||||
source ./versions.env
|
source ./versions.env
|
||||||
[ -f ./.env ] && source ./.env
|
# RIG_PORTABLE skips the machine-local layer. config_snapshot sets it, so a
|
||||||
|
# generated standalone kit never carries this machine's .env — which holds
|
||||||
|
# local values and, by its own description, secrets.
|
||||||
|
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
# Re-apply overrides now so PROFILE is the caller's before we pick the file.
|
# Re-apply overrides now so PROFILE is the caller's before we pick the file.
|
||||||
@@ -76,7 +79,7 @@ load_config() {
|
|||||||
|
|
||||||
set -a
|
set -a
|
||||||
source "./env.d/${profile}.env"
|
source "./env.d/${profile}.env"
|
||||||
[ -f ./.env ] && source ./.env
|
if [ -z "${RIG_PORTABLE:-}" ] && [ -f ./.env ]; then source ./.env; fi
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
_config_restore "$saved"
|
_config_restore "$saved"
|
||||||
@@ -138,6 +141,17 @@ load_config() {
|
|||||||
# changed afterwards — both would mislead if the numbers drifted.
|
# changed afterwards — both would mislead if the numbers drifted.
|
||||||
NODES=$(grep -c '^ - role:' "$KIND_CONFIG_PATH")
|
NODES=$(grep -c '^ - role:' "$KIND_CONFIG_PATH")
|
||||||
if grep -q 'audit-policy-file' "$KIND_CONFIG_PATH"; then AUDIT=on; else AUDIT=off; fi
|
if grep -q 'audit-policy-file' "$KIND_CONFIG_PATH"; then AUDIT=on; else AUDIT=off; fi
|
||||||
|
|
||||||
|
# What one node costs, measured rather than guessed. On 2026-09-11 a minimal
|
||||||
|
# control-plane node ran at 620 MiB idle and ~728 MiB with a small mock, plus
|
||||||
|
# 16 MiB for the local registry — ~745 MiB of working set. 800 rounds that up,
|
||||||
|
# and agrees with the 800 MB observed independently on a larger rig. Worker
|
||||||
|
# nodes carry no etcd or apiserver and are lighter, so for a multi-node shape
|
||||||
|
# this errs high. It is the cluster alone: whatever you deploy comes on top.
|
||||||
|
#
|
||||||
|
# Here rather than in check.sh because the memory tool and every standalone
|
||||||
|
# kit need the same figure.
|
||||||
|
NODE_MB=800
|
||||||
}
|
}
|
||||||
|
|
||||||
# Render a cluster shape to stdout. sed rather than envsubst: envsubst is
|
# Render a cluster shape to stdout. sed rather than envsubst: envsubst is
|
||||||
@@ -167,3 +181,78 @@ _config_restore() {
|
|||||||
# line would otherwise make this return 1 and trip `set -e` in the caller.
|
# line would otherwise make this return 1 and trip `set -e` in the caller.
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── what a standalone kit needs to know ────────────────────────────────────
|
||||||
|
# Two questions the kit generator (ctrl/standalone.sh) asks, so that it never has
|
||||||
|
# to know how configuration is stored. Where profiles live, which files are
|
||||||
|
# layered and what is derived are this file's business and can change freely;
|
||||||
|
# the generator only calls these.
|
||||||
|
|
||||||
|
# Every profile rig can be run as, one per line.
|
||||||
|
config_profiles() {
|
||||||
|
local f
|
||||||
|
for f in ./env.d/*.env; do
|
||||||
|
[ -e "$f" ] || continue
|
||||||
|
f=${f##*/}; echo "${f%.env}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# The resolved configuration for one profile, as `declare -p` lines — exactly
|
||||||
|
# what load_config leaves behind, minus the machine-local layer. A kit freezes
|
||||||
|
# this in place of load_config, so it carries rig's decisions for that profile
|
||||||
|
# and nothing about the machine it was generated on.
|
||||||
|
#
|
||||||
|
# Found by difference, not by a list: whatever load_config sets today, it sets.
|
||||||
|
# A list here would be one more place to forget a variable.
|
||||||
|
config_snapshot() {
|
||||||
|
local _rig_snap_profile="$1"
|
||||||
|
(
|
||||||
|
# Nothing from the caller's shell may leak into a kit.
|
||||||
|
for _rig_snap_n in $CONFIG_OVERRIDABLE; do unset "$_rig_snap_n"; done
|
||||||
|
declare -A _rig_snap_was=()
|
||||||
|
for _rig_snap_n in $(compgen -v); do
|
||||||
|
_rig_snap_was[$_rig_snap_n]="${!_rig_snap_n-}"
|
||||||
|
done
|
||||||
|
PROFILE="$_rig_snap_profile" RIG_PORTABLE=1 load_config >/dev/null
|
||||||
|
for _rig_snap_n in $(compgen -v); do
|
||||||
|
case "$_rig_snap_n" in
|
||||||
|
_rig_snap_*|RIG_PORTABLE|BASH*|FUNCNAME|PIPESTATUS|LINENO|RANDOM|SRANDOM|\
|
||||||
|
SECONDS|EPOCH*|HISTCMD|COLUMNS|LINES|PWD|OLDPWD|_|SHLVL|OPTIND|OPTERR) continue ;;
|
||||||
|
esac
|
||||||
|
if [ -z "${_rig_snap_was[$_rig_snap_n]+x}" ] \
|
||||||
|
|| [ "${_rig_snap_was[$_rig_snap_n]}" != "${!_rig_snap_n-}" ]; then
|
||||||
|
declare -p "$_rig_snap_n"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# A replacement for load_config with one profile's resolution frozen in, printed
|
||||||
|
# as a function definition for a standalone kit to carry. The generator embeds
|
||||||
|
# whatever this prints and interprets none of it, so what "frozen" means stays
|
||||||
|
# rig's decision.
|
||||||
|
#
|
||||||
|
# It keeps load_config's one stated rule: the caller's env wins for anything in
|
||||||
|
# CONFIG_OVERRIDABLE. A kit therefore behaves like rig — `OUT_BIN=... rigdeps.sh`
|
||||||
|
# still works — rather than like a copy with everything pinned.
|
||||||
|
#
|
||||||
|
# What freezing does give up, knowingly: values DERIVED from an overridable one
|
||||||
|
# are fixed at generation. Override CLUSTER and the ports stay the ones derived
|
||||||
|
# for the original name. Re-deriving would mean carrying the layering itself,
|
||||||
|
# which is exactly what a kit exists not to need.
|
||||||
|
config_freeze() {
|
||||||
|
local snap
|
||||||
|
snap=$(config_snapshot "$1") || return 1
|
||||||
|
cat <<'EOF'
|
||||||
|
load_config() {
|
||||||
|
local k saved=""
|
||||||
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
|
done
|
||||||
|
EOF
|
||||||
|
printf '%s\n' "$snap" | sed -E 's/^declare --* / declare -g /; s/^declare -([a-zA-Z]+) / declare -g\1 /'
|
||||||
|
cat <<'EOF'
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|||||||
736
rig/ctrl/mem.sh
736
rig/ctrl/mem.sh
@@ -1,24 +1,46 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# What memory this machine has, what is left, and — where there is one — what
|
# rig:standalone rigmini status
|
||||||
# cap is holding it there.
|
# 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.
|
||||||
#
|
#
|
||||||
# Runs on native Linux and under WSL, because rig is developed on one and used
|
# There are two numbers and they are rarely the same. `status` reports what the
|
||||||
# on the other. The difference is not cosmetic: on WSL the memory you see is a
|
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
||||||
# VM allocation that can be raised, and the commonest failure is raising it
|
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
||||||
# without restarting, so the number on disk and the number in /proc disagree.
|
# against what this profile's cluster needs.
|
||||||
# On native Linux there is no such cap and pretending otherwise sends you to a
|
|
||||||
# file that does not exist.
|
|
||||||
#
|
#
|
||||||
# This reports and instructs. It never writes a .wslconfig — applying one costs
|
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
||||||
# a full VM restart that takes every shell, mount and container with it, and
|
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
||||||
# choosing that moment is yours.
|
# 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.
|
||||||
#
|
#
|
||||||
# `backup` exists so `restore` has something to read: back up, hand-edit
|
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
||||||
# following the printed instruction, restore if it goes wrong. Both are
|
# allocation that can be raised, and the commonest failure is raising it without
|
||||||
# WSL-only, because .wslconfig is the only thing here worth backing up.
|
# restarting — so status compares what .wslconfig says with what actually booted.
|
||||||
#
|
#
|
||||||
# Usage: mem.sh status | backup | restore
|
# Reports and instructs. It never raises a limit, frees anything or installs a
|
||||||
|
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
||||||
|
# itself, so that `restore` has something to put back after a hand edit.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# mem.sh status what it has, what caps it
|
||||||
|
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
||||||
|
# mem.sh all [--budget GB] both, then the verdict
|
||||||
|
# mem.sh backup | restore .wslconfig, WSL only
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
source ./lib/config.sh
|
||||||
|
|
||||||
|
# ── 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
|
# 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
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
@@ -36,26 +58,150 @@ If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
|||||||
That enables Windows features and needs a reboot, so it is not something this
|
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
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
this from there.
|
this from there.
|
||||||
|
|
||||||
See "Starting from plain Windows" in README.md.
|
|
||||||
EOF
|
EOF
|
||||||
exit 1 ;;
|
exit 1 ;;
|
||||||
esac
|
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_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
require_wsl() {
|
is_container() {
|
||||||
if ! is_wsl; then
|
[ -f /.dockerenv ] && return 0
|
||||||
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
grep -qE '(docker|containerd|kubepods|lxc|podman)' /proc/1/cgroup 2>/dev/null
|
||||||
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
}
|
||||||
echo "Use 'mem.sh status' to see what the machine actually has." >&2
|
|
||||||
exit 1
|
platform() {
|
||||||
|
if is_wsl; then echo WSL
|
||||||
|
elif is_container; then echo container
|
||||||
|
else echo "native linux"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── reading memory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
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
|
# /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
|
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
||||||
# Windows, then fall back to whichever profile actually owns a config.
|
# Windows, then fall back to whichever profile actually owns a config.
|
||||||
@@ -66,110 +212,222 @@ wslconfig_path() {
|
|||||||
""|*%*) ;;
|
""|*%*) ;;
|
||||||
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
||||||
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
||||||
echo "$winpath/.wslconfig"; return
|
echo "$winpath/.wslconfig"; return 0
|
||||||
fi ;;
|
fi ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
if [ -n "$found" ]; then echo "$found"; return; fi
|
[ -n "$found" ] && echo "$found"
|
||||||
|
return 0
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hogs() {
|
hogs() {
|
||||||
echo "holding the most:"
|
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}'
|
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() {
|
status() {
|
||||||
local total avail swap_total swap_free
|
local total avail swap_total swap_free cap ul cur
|
||||||
total=$(mb MemTotal); avail=$(mb MemAvailable)
|
|
||||||
|
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)
|
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
|
||||||
|
|
||||||
if is_wsl; then
|
# postgres puts its shared buffers in /dev/shm. Docker's default is 64 MB,
|
||||||
local cfg conf conf_mb
|
# and the resulting failure names neither shm nor the size.
|
||||||
cfg=$(wslconfig_path)
|
if [ -d /dev/shm ]; then
|
||||||
conf=$(configured_memory "$cfg")
|
local shm; shm=$(df -Pm /dev/shm 2>/dev/null | awk 'NR==2{print $2}')
|
||||||
echo "platform WSL"
|
if [ -n "$shm" ]; then
|
||||||
echo "config $cfg"
|
if [ "$shm" -le 64 ]; then
|
||||||
if [ -n "$conf" ]; then
|
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
||||||
conf_mb=$(to_mb "$conf")
|
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
||||||
echo "configured $conf (${conf_mb} MB)"
|
else
|
||||||
else
|
echo " /dev/shm ${shm} MB"
|
||||||
conf_mb=""
|
|
||||||
echo "configured (no memory= set — WSL defaults to 50% of host RAM, or 8GB, whichever is less)"
|
|
||||||
fi
|
|
||||||
echo "booted ${total} MB"
|
|
||||||
echo "available ${avail} MB"
|
|
||||||
echo "swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
|
||||||
|
|
||||||
if [ -n "$conf_mb" ]; then
|
|
||||||
# The VM reports a little less than allocated; 15% covers the kernel
|
|
||||||
# without calling every healthy machine a mismatch.
|
|
||||||
if [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
|
||||||
echo
|
|
||||||
echo "! configured ${conf_mb} MB but booted ${total} MB."
|
|
||||||
echo " The change has not been applied. From a WINDOWS terminal:"
|
|
||||||
echo
|
|
||||||
echo " wsl --shutdown"
|
|
||||||
echo
|
|
||||||
echo " then start the distro again."
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo "To raise it, add to $cfg on the Windows side:"
|
|
||||||
echo
|
|
||||||
echo " [wsl2]"
|
|
||||||
echo " memory=8GB"
|
|
||||||
echo
|
|
||||||
echo "then, from a WINDOWS terminal: wsl --shutdown"
|
|
||||||
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
|
local n
|
||||||
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
n=$(docker ps -q 2>/dev/null | wc -l)
|
||||||
[ "$n" -gt 0 ] && echo "backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null), ${n} container(s) running"
|
||||||
else
|
else
|
||||||
echo "platform native linux"
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
echo "total ${total} MB"
|
|
||||||
echo "available ${avail} MB"
|
|
||||||
echo "swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
|
||||||
echo
|
|
||||||
echo "No VM allocation to raise here — this is the machine's own memory."
|
|
||||||
echo "If it is tight the levers are freeing something or adding swap."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Under a fifth left is worth naming wherever you are running.
|
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
||||||
if [ "$avail" -lt $(( total / 5 )) ]; then
|
# 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
|
||||||
hogs
|
echo " - native linux: no VM allocation to raise. If memory is tight the levers"
|
||||||
|
echo " are freeing something or adding swap."
|
||||||
fi
|
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
|
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() {
|
backup() {
|
||||||
require_wsl backup
|
require_wsl backup
|
||||||
local cfg dest
|
local cfg dest
|
||||||
cfg=$(wslconfig_path)
|
cfg=$(wslconfig_required)
|
||||||
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||||
# Timestamped and never overwritten: a backup that can destroy itself on a
|
# Timestamped and never overwritten: a backup that can destroy itself on a
|
||||||
# second run is not a backup.
|
# second run is not a backup.
|
||||||
@@ -183,7 +441,7 @@ backup() {
|
|||||||
restore() {
|
restore() {
|
||||||
require_wsl restore
|
require_wsl restore
|
||||||
local cfg newest count
|
local cfg newest count
|
||||||
cfg=$(wslconfig_path)
|
cfg=$(wslconfig_required)
|
||||||
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
||||||
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
||||||
|
|
||||||
@@ -223,11 +481,279 @@ restore() {
|
|||||||
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
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
|
require_linux
|
||||||
|
find_cgroup
|
||||||
|
|
||||||
|
cmd="${1:-status}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
case "${1:-status}" in
|
case "$cmd" in
|
||||||
status) status ;;
|
status) parse_flags "$@"; status ;;
|
||||||
|
push) parse_flags "$@"; push ;;
|
||||||
|
all) parse_flags "$@"; all ;;
|
||||||
backup) backup ;;
|
backup) backup ;;
|
||||||
restore) restore ;;
|
restore) restore ;;
|
||||||
*) echo "usage: $0 [status|backup|restore]" >&2; exit 1 ;;
|
*) 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
|
esac
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Do the standalone scripts still install what rig pins?
|
|
||||||
#
|
|
||||||
# standalone/rigdeps.sh carries its toolchain pins inline, because it exists for
|
|
||||||
# a machine that will never have ctrl/versions.env. That makes two copies of the
|
|
||||||
# same versions and checksums, and two copies drift the day one is edited and
|
|
||||||
# the other forgotten. This is the check that notices.
|
|
||||||
#
|
|
||||||
# ctrl/versions.env is the source of truth. Only the keys rigdeps.sh itself
|
|
||||||
# defines are compared: versions.env also pins addon images (cert-manager,
|
|
||||||
# metallb, metrics-server) that rigdeps.sh never installs, and demanding those
|
|
||||||
# would make this fail forever for no reason.
|
|
||||||
#
|
|
||||||
# Exits non-zero on any mismatch — unlike the host checks, this one is a test.
|
|
||||||
#
|
|
||||||
# Usage: pins.sh
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
SOURCE=./versions.env
|
|
||||||
COPY=../standalone/rigdeps.sh
|
|
||||||
|
|
||||||
[ -r "$COPY" ] || { echo "no $COPY to compare" >&2; exit 1; }
|
|
||||||
|
|
||||||
# KEY=value for the pin keys a file defines, quotes stripped. awk rather than a
|
|
||||||
# grep regex, which is not the same program everywhere.
|
|
||||||
pins() {
|
|
||||||
awk -F= '/^[A-Z_]+_(VERSION|SHA256)=/ {
|
|
||||||
v = substr($0, index($0, "=") + 1); gsub(/^["\x27]|["\x27]$/, "", v)
|
|
||||||
print $1 "=" v }' "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "pins: standalone/rigdeps.sh against ctrl/versions.env"
|
|
||||||
bad=0
|
|
||||||
while IFS='=' read -r key copy_val; do
|
|
||||||
[ -n "$key" ] || continue
|
|
||||||
src_val=$(pins "$SOURCE" | sed -n "s/^${key}=//p" | head -1)
|
|
||||||
if [ -z "$src_val" ]; then
|
|
||||||
printf " ! %-16s in rigdeps.sh but not in versions.env\n" "$key"
|
|
||||||
bad=1
|
|
||||||
elif [ "$src_val" = "$copy_val" ]; then
|
|
||||||
printf " %-16s %s\n" "$key" "$( [ ${#src_val} -gt 20 ] && echo "${src_val:0:12}…" || echo "$src_val" )"
|
|
||||||
else
|
|
||||||
printf " ! %-16s versions.env %s\n" "$key" "$src_val"
|
|
||||||
printf " %-16s rigdeps.sh %s\n" "" "$copy_val"
|
|
||||||
bad=1
|
|
||||||
fi
|
|
||||||
done < <(pins "$COPY")
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [ "$bad" -eq 0 ]; then
|
|
||||||
echo "in step — rigdeps.sh installs exactly what rig pins."
|
|
||||||
else
|
|
||||||
echo "DRIFT. versions.env is the source of truth: copy the differing lines from it"
|
|
||||||
echo "into standalone/rigdeps.sh, taking checksums from the publisher's release list."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
# Scope, on purpose:
|
# Scope, on purpose:
|
||||||
# - no cluster, no docker, no network. It must be cheap enough to actually run.
|
# - no cluster, no docker, no network. It must be cheap enough to actually run.
|
||||||
# - it asserts about RIG. `make check` asserts about the MACHINE and never
|
# - it asserts about RIG. `make check` asserts about the MACHINE and never
|
||||||
# fails; this exits 1, the way `make pins` does.
|
# fails; this exits 1, the way `make standalone check` does.
|
||||||
# - what actually deploys is not testable here. `tilt ci` stays a manual step.
|
# - what actually deploys is not testable here. `tilt ci` stays a manual step.
|
||||||
#
|
#
|
||||||
# Usage: make selftest (or: bash ctrl/selftest.sh)
|
# Usage: make selftest (or: bash ctrl/selftest.sh)
|
||||||
@@ -168,24 +168,37 @@ check "guards on the variable" "1" "$(grep -c 'allow_k8s_contexts(CTX)' Tiltfil
|
|||||||
check "asks ports.sh for facts" "1" "$(grep -c "local('bash ports.sh active'" Tiltfile)"
|
check "asks ports.sh for facts" "1" "$(grep -c "local('bash ports.sh active'" Tiltfile)"
|
||||||
|
|
||||||
|
|
||||||
note "the standalone Makefile only calls real verbs"
|
note "standalone kits are generated, current, and call only real verbs"
|
||||||
# That file exists so nothing wrapping these scripts has to GUESS how to call
|
# The kits under standalone/<profile>/ are rig flattened into single files, one
|
||||||
# them. A generated Makefile once did guess: `rigmini.sh on`, which is not a
|
# per profile. A kit left behind by a change to rig is exactly the drift they
|
||||||
# verb, and a bare `rigdeps.sh` for "check and report", which installs. So each
|
# replaced — rigmini.sh once said 2 GB per node long after rig measured 800 MB —
|
||||||
# target's default verb must be one the script's own dispatch accepts — read
|
# so a stale kit fails here rather than waiting to be noticed on another machine.
|
||||||
# from the script's `case`, not from a list here that could drift from it.
|
check "every kit matches what rig generates now" "yes" \
|
||||||
|
"$(bash standalone.sh check >/dev/null 2>&1 && echo yes || echo "no — run make standalone")"
|
||||||
|
|
||||||
|
# Each kit's Makefile exists so nothing wrapping these scripts has to GUESS how to
|
||||||
|
# call them. A generated Makefile once did guess: `rigmini.sh on`, not a verb,
|
||||||
|
# and a bare `rigdeps.sh` for "check and report", which installs. So every
|
||||||
|
# target's default verb must be one its script's own dispatch accepts — read
|
||||||
|
# from that dispatch, not from a list here that could drift from it.
|
||||||
verbs_of() {
|
verbs_of() {
|
||||||
sed -n '/^case "\$cmd" in/,/^esac/p' "$1" | grep -oE '^ [a-z]+\)' | tr -d ' )'
|
sed -n '/^case "\$cmd" in/,/^esac/p' "$1" | grep -oE '^ [a-z]+\)' | tr -d ' )'
|
||||||
}
|
}
|
||||||
for pair in deps:rigdeps.sh mem:rigmini.sh; do
|
kits=0
|
||||||
target=${pair%%:*}; script=${pair##*:}
|
for mk in ../standalone/*/Makefile; do
|
||||||
verb="$(make --no-print-directory -s -n -f ../standalone/Makefile "$target" 2>/dev/null \
|
[ -f "$mk" ] || continue
|
||||||
| awk -v s="$script" 'index($0, s) { print $NF; exit }')"
|
kit=$(dirname "$mk"); kits=$((kits + 1))
|
||||||
check "make $target -> $script ${verb:-?}, a verb it accepts" "yes" \
|
for target in $(grep -oE '^[a-z][a-z-]*:' "$mk" | tr -d ':' | grep -vx help); do
|
||||||
"$(verbs_of "../standalone/$script" | grep -qx "$verb" && echo yes || echo "no: '$verb'")"
|
line="$(make --no-print-directory -s -n -f "$mk" "$target" 2>/dev/null | head -1)"
|
||||||
|
script=$(basename "$(printf '%s' "$line" | awk '{print $2}')")
|
||||||
|
verb=$(printf '%s' "$line" | awk '{print $NF}')
|
||||||
|
check "$(basename "$kit"): make $target -> $script $verb, a verb it accepts" "yes" \
|
||||||
|
"$(verbs_of "$kit/$script" | grep -qx "$verb" && echo yes || echo "no: '$verb'")"
|
||||||
|
done
|
||||||
|
check "$(basename "$kit"): no \`mini\` target, which already means minimal footprint" "0" \
|
||||||
|
"$(grep -cE '^mini:' "$mk")"
|
||||||
done
|
done
|
||||||
check "no \`mini\` target, which already means minimal footprint" "0" \
|
check "there is a kit for every profile" "$(config_profiles | wc -l)" "$kits"
|
||||||
"$(grep -cE '^mini:' ../standalone/Makefile)"
|
|
||||||
|
|
||||||
|
|
||||||
note "optional — needs tilt and this rig's cluster"
|
note "optional — needs tilt and this rig's cluster"
|
||||||
|
|||||||
341
rig/ctrl/standalone.sh
Normal file
341
rig/ctrl/standalone.sh
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate the standalone kits: single-file versions of rig's own tools, one
|
||||||
|
# folder per profile, for machines the full rig is not going to.
|
||||||
|
#
|
||||||
|
# A kit is a pure function of rig as it is right now. It gains nothing rig lacks
|
||||||
|
# and loses nothing rig has — improve rig, regenerate, and every kit follows.
|
||||||
|
# Nothing in standalone/<profile>/ is ever edited by hand.
|
||||||
|
#
|
||||||
|
# What this file does NOT know, on purpose: which tools rig has, what they are
|
||||||
|
# called, how its libraries are split, where configuration lives or what it
|
||||||
|
# contains. Rig will change shape — scripts get split, renamed and grow new
|
||||||
|
# libraries — and a generator that encoded today's layout would quietly produce
|
||||||
|
# a wrong kit the first time it did. So this works from a contract a script opts
|
||||||
|
# into, and from nothing else:
|
||||||
|
#
|
||||||
|
# 1. A marker comment, alone on a line near the top, declares an entry point:
|
||||||
|
# (hash) rig:standalone <kit-name> <default-verb>
|
||||||
|
# The default verb must only REPORT: it is run as a smoke test.
|
||||||
|
# 2. Every `source` an entry point makes names a .sh file by a path that
|
||||||
|
# resolves relative to the entry point. Libraries may source further
|
||||||
|
# libraries however they like — bash follows those itself.
|
||||||
|
# 3. Configuration enters through `load_config`, and the libraries provide
|
||||||
|
# `config_profiles` and `config_freeze <profile>` — the latter prints a
|
||||||
|
# replacement load_config with that profile resolved. How config is layered,
|
||||||
|
# stored, derived or frozen is rig's business; this only asks, and embeds
|
||||||
|
# the answer without interpreting it.
|
||||||
|
#
|
||||||
|
# Bash does the resolving, not a parser here. Libraries are sourced in a clean
|
||||||
|
# shell and read back with `declare -f` and `declare -p`, so any structure bash
|
||||||
|
# can load, this can flatten.
|
||||||
|
#
|
||||||
|
# And every kit is PROVEN to stand alone before it is written: no `source` left,
|
||||||
|
# no path into rig's tree in its code, `bash -n` clean, and its default verb run
|
||||||
|
# in an empty directory with nothing from rig present. A shape this has never
|
||||||
|
# seen either passes that, or generation stops and names the kit, the file, the
|
||||||
|
# line and what is wrong. It never writes a kit that only looks finished.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# standalone.sh write generate every kit into standalone/<profile>/
|
||||||
|
# standalone.sh check generate into a scratch dir and fail if any kit differs
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
CTRL="$PWD"
|
||||||
|
ROOT="$(cd .. && pwd)"
|
||||||
|
OUT="$ROOT/standalone"
|
||||||
|
SELF_REL="ctrl/${0##*/}"
|
||||||
|
|
||||||
|
GENERATED_TAG="GENERATED by make standalone — do not edit"
|
||||||
|
FROZEN_OPEN="# ── configuration, frozen"
|
||||||
|
FROZEN_CLOSE="# ── end of frozen configuration"
|
||||||
|
|
||||||
|
refuse() { echo >&2; echo "standalone: refusing — $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# A clean bash with nothing from the caller's shell in it. What the kit carries
|
||||||
|
# must not depend on who ran the generator or what they had exported.
|
||||||
|
clean_bash() { env -i PATH="$PATH" HOME="$HOME" bash --noprofile --norc "$@"; }
|
||||||
|
|
||||||
|
# ── 1. entry points ────────────────────────────────────────────────────────
|
||||||
|
entries() {
|
||||||
|
grep -rlE --include='*.sh' '^# rig:standalone [a-z0-9-]+ [a-z0-9-]+' . 2>/dev/null \
|
||||||
|
| sed 's|^\./||' | LC_ALL=C sort
|
||||||
|
}
|
||||||
|
marker_of() { # entry -> "kit verb"
|
||||||
|
sed -nE 's/^# rig:standalone ([a-z0-9-]+) ([a-z0-9-]+).*/\1 \2/p' "$1" | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. the libraries an entry point sources ────────────────────────────────
|
||||||
|
# Only the entry point's own `source` lines are read as text. Everything those
|
||||||
|
# libraries pull in is resolved by bash when they are sourced in step 3.
|
||||||
|
libs_of() { # entry -> one resolved lib path per line, relative to ctrl/
|
||||||
|
local entry="$1" dir line n path
|
||||||
|
dir=$(dirname "$entry")
|
||||||
|
while IFS=: read -r n line; do
|
||||||
|
path=$(printf '%s' "$line" | sed -E 's/^[[:space:]]*(source|\.)[[:space:]]+//; s/[[:space:]]+(#.*)?$//')
|
||||||
|
path=${path#\"}; path=${path%\"}; path=${path#\'}; path=${path%\'}
|
||||||
|
case "$path" in
|
||||||
|
*'$'*) refuse "$entry:$n sources '$path' — a path with a variable in it cannot be resolved; name the file" ;;
|
||||||
|
esac
|
||||||
|
case "$path" in
|
||||||
|
*.sh) ;;
|
||||||
|
*) refuse "$entry:$n sources '$path' directly — only libraries (.sh) may be sourced; configuration has to enter through load_config" ;;
|
||||||
|
esac
|
||||||
|
path="$dir/${path#./}"; path=${path#./}
|
||||||
|
[ -f "$path" ] || refuse "$entry:$n sources '$path', which does not exist"
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
done < <(grep -nE '^[[:space:]]*(source|\.)[[:space:]]+[^=]' "$entry" || true)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Into the global array `libs`. Not `mapfile < <(libs_of ...)`: a refusal inside
|
||||||
|
# a process substitution only ends that subshell, so generation would carry on
|
||||||
|
# past it and fail later with a message about something else entirely.
|
||||||
|
libs_into() {
|
||||||
|
local out
|
||||||
|
out=$(libs_of "$1") || exit 1
|
||||||
|
libs=()
|
||||||
|
[ -n "$out" ] && mapfile -t libs <<< "$out"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3. what the libraries define, read back from bash itself ───────────────
|
||||||
|
# The frozen config replaces load_config, and the generator's own two questions
|
||||||
|
# are useless inside a kit, so none of the three is carried.
|
||||||
|
lib_defs() { # entry lib... -> declare -p globals, then declare -f functions
|
||||||
|
local entry="$1"; shift
|
||||||
|
( cd "$(dirname "$entry")" && clean_bash -c '
|
||||||
|
skip_var() { case "$1" in BASH*|FUNCNAME|PIPESTATUS|LINENO|RANDOM|SRANDOM|SECONDS|EPOCH*|HISTCMD|COLUMNS|LINES|PWD|OLDPWD|_|SHLVL|OPTIND|OPTERR|IFS|PS4|PATH|HOME|v|f|l|before_v|before_f) return 0 ;; esac; return 1; }
|
||||||
|
before_v=" $(compgen -v | tr "\n" " ") "
|
||||||
|
before_f=" $(compgen -A function | tr "\n" " ") "
|
||||||
|
for l in "$@"; do source "$l" || { echo "__FAIL__ sourcing $l" ; exit 1; }; done
|
||||||
|
for v in $(compgen -v); do
|
||||||
|
skip_var "$v" && continue
|
||||||
|
case "$before_v" in *" $v "*) continue ;; esac
|
||||||
|
declare -p "$v"
|
||||||
|
done
|
||||||
|
for f in $(compgen -A function); do
|
||||||
|
case "$before_f" in *" $f "*) continue ;; esac
|
||||||
|
case "$f" in skip_var|load_config|config_profiles|config_snapshot|config_freeze) continue ;; esac
|
||||||
|
declare -f "$f"
|
||||||
|
done
|
||||||
|
' _ "$@" ) || refuse "$entry: its libraries could not be sourced cleanly"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 4. ask rig for profiles and resolved config ────────────────────────────
|
||||||
|
ask() { # entry lib... -- function args... -> that function's stdout
|
||||||
|
local entry="$1"; shift
|
||||||
|
local libs=() a
|
||||||
|
while [ $# -gt 0 ] && [ "$1" != -- ]; do libs+=("$1"); shift; done
|
||||||
|
shift
|
||||||
|
( cd "$(dirname "$entry")" && clean_bash -c '
|
||||||
|
n=0; for a in "$@"; do n=$((n+1)); [ "$a" = -- ] && break; done
|
||||||
|
for l in "${@:1:$((n-1))}"; do source "$l"; done
|
||||||
|
shift "$n"
|
||||||
|
declare -F "$1" >/dev/null || exit 3
|
||||||
|
"$@"
|
||||||
|
' _ "${libs[@]}" -- "$@" )
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 5. assemble one kit file ───────────────────────────────────────────────
|
||||||
|
assemble() { # entry profile out-file lib...
|
||||||
|
local entry="$1" profile="$2" dest="$3"; shift 3
|
||||||
|
local libs=("$@") calls_config=no
|
||||||
|
grep -qE '(^|[^A-Za-z0-9_])load_config([^A-Za-z0-9_]|$)' "$entry" && calls_config=yes
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '#!/usr/bin/env bash'
|
||||||
|
echo "# $GENERATED_TAG"
|
||||||
|
echo "#"
|
||||||
|
echo "# $(basename "$dest") for profile '$profile', flattened from:"
|
||||||
|
echo "# ctrl/$entry"
|
||||||
|
local l; for l in ${libs[@]+"${libs[@]}"}; do echo "# ctrl/$l"; done
|
||||||
|
echo "# Edit those and run \`make standalone\`. Changes made here are lost, and"
|
||||||
|
echo "# \`make selftest\` fails while this file differs from what rig generates."
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ ${#libs[@]} -gt 0 ]; then
|
||||||
|
echo "# ── from the libraries ──"
|
||||||
|
lib_defs "$entry" "${libs[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$calls_config" = yes ]; then
|
||||||
|
local frozen
|
||||||
|
frozen=$(ask "$entry" ${libs[@]+"${libs[@]}"} -- config_freeze "$profile") \
|
||||||
|
|| refuse "ctrl/$entry calls load_config, but its libraries do not answer config_freeze for '$profile'"
|
||||||
|
echo "$FROZEN_OPEN for profile '$profile' ──"
|
||||||
|
printf '%s\n' "$frozen"
|
||||||
|
echo "$FROZEN_CLOSE ──"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "# ── ctrl/$entry ──"
|
||||||
|
# The entry point itself, minus its shebang and marker, with each source
|
||||||
|
# line it made replaced by a note — what it sourced is already above.
|
||||||
|
awk '
|
||||||
|
NR == 1 && /^#!/ { next }
|
||||||
|
/^# rig:standalone / { next }
|
||||||
|
/^[[:space:]]*(source|\.)[[:space:]]+[^=]/ { print "# (sourced library inlined above)"; next }
|
||||||
|
{ print }
|
||||||
|
' "$entry"
|
||||||
|
} > "$dest"
|
||||||
|
chmod +x "$dest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 6. the kit's Makefile, from the markers ────────────────────────────────
|
||||||
|
verbs_of() { # entry -> its top-level dispatch arms
|
||||||
|
awk '/^case / { inb=1; next } /^esac/ { inb=0 } inb && match($0, /^ [a-z][a-z-]*\)/) { v=substr($0, 5, RLENGTH-5); printf "%s%s", (n++ ? "|" : ""), v }' "$1"
|
||||||
|
}
|
||||||
|
makefile() { # out-dir entry...
|
||||||
|
local dir="$1"; shift
|
||||||
|
local e kit verb target verbs targets=""
|
||||||
|
for e in "$@"; do targets+=" $(basename "$e" .sh)"; done
|
||||||
|
{
|
||||||
|
echo "# $GENERATED_TAG"
|
||||||
|
echo "#"
|
||||||
|
echo "# Shorthand for the scripts beside it; they run without it. Every target"
|
||||||
|
echo "# calls a verb its script accepts — read from that script's own dispatch."
|
||||||
|
echo
|
||||||
|
echo 'HERE := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))'
|
||||||
|
echo 'ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))'
|
||||||
|
echo 'ifneq ($(ARGS),)'
|
||||||
|
echo '$(eval $(ARGS):;@:)'
|
||||||
|
echo '.PHONY: $(ARGS)'
|
||||||
|
echo 'endif'
|
||||||
|
echo
|
||||||
|
echo '.DEFAULT_GOAL := help'
|
||||||
|
echo ".PHONY: help$targets"
|
||||||
|
echo
|
||||||
|
echo 'help: ## list targets'
|
||||||
|
printf '\t%s\n' "@grep -hE '^[a-z][a-z-]*:.*?##' \$(MAKEFILE_LIST) | sed 's/:.*##/\\t/' | expand -t16"
|
||||||
|
for e in "$@"; do
|
||||||
|
read -r kit verb <<< "$(marker_of "$e")"
|
||||||
|
target=$(basename "$e" .sh)
|
||||||
|
verbs=$(verbs_of "$e")
|
||||||
|
echo
|
||||||
|
printf '%-30s ## %s.sh [%s] (default %s)\n' "$target:" "$kit" "${verbs:-?}" "$verb"
|
||||||
|
printf '\tbash $(HERE)%s.sh $(or $(ARGS),%s)\n' "$kit" "$verb"
|
||||||
|
done
|
||||||
|
} > "$dir/Makefile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 7. prove a kit stands alone ────────────────────────────────────────────
|
||||||
|
verify_kit() { # dir profile entry...
|
||||||
|
local dir="$1" profile="$2"; shift 2
|
||||||
|
local e kit verb f bad smoke rc
|
||||||
|
for e in "$@"; do
|
||||||
|
read -r kit verb <<< "$(marker_of "$e")"
|
||||||
|
f="$dir/$kit.sh"
|
||||||
|
|
||||||
|
bash -n "$f" 2>/dev/null || refuse "$profile/$kit.sh does not parse: $(bash -n "$f" 2>&1 | head -1)"
|
||||||
|
|
||||||
|
# Code only: comments are free to mention anything, and the frozen block
|
||||||
|
# is data — a value that happens to hold a path is harmless unless code
|
||||||
|
# opens it, and opening it is what the smoke run below would catch.
|
||||||
|
bad=$(awk -v fz_open="$FROZEN_OPEN" -v fz_close="$FROZEN_CLOSE" '
|
||||||
|
index($0, fz_open) == 1 { fz=1; next }
|
||||||
|
index($0, fz_close) == 1 { fz=0; next }
|
||||||
|
fz || /^[[:space:]]*#/ { next }
|
||||||
|
/^[[:space:]]*(source|\.)[[:space:]]+[^=]/ { printf "%d: still sources: %s\n", NR, $0; next }
|
||||||
|
# Rig-relative only. The preceding character may not be "/", so an
|
||||||
|
# absolute system path such as /var/lib/docker is not mistaken for
|
||||||
|
# rig lib/; an explicit ./ or ../ prefix is matched on its own.
|
||||||
|
/(^|[^A-Za-z0-9_.\/])(ctrl\/|lib\/|env\.d\/)|\.\.?\/(ctrl\/|lib\/|env\.d\/)|versions\.env|(^|[^A-Za-z0-9_])\.env([^A-Za-z0-9_]|$)/ {
|
||||||
|
printf "%d: refers into rig'"'"'s tree: %s\n", NR, $0
|
||||||
|
}' "$f" | head -3)
|
||||||
|
[ -z "$bad" ] || refuse "$profile/$kit.sh does not stand alone —"$'\n'"$(printf '%s\n' "$bad" | sed 's/^/ line /')"
|
||||||
|
done
|
||||||
|
|
||||||
|
# The real test: a folder holding only this kit, and nothing else from rig.
|
||||||
|
smoke=$(mktemp -d)
|
||||||
|
cp "$dir"/* "$smoke"/
|
||||||
|
for e in "$@"; do
|
||||||
|
read -r kit verb <<< "$(marker_of "$e")"
|
||||||
|
rc=0
|
||||||
|
out=$( (cd "$smoke" && timeout 120 bash "./$kit.sh" "$verb") 2>&1 ) || rc=$?
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
rm -rf "$smoke"
|
||||||
|
refuse "$profile/$kit.sh $verb exits $rc in an empty directory:"$'\n'"$(printf '%s\n' "$out" | tail -5 | sed 's/^/ /')"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
( cd "$smoke" && make -s help >/dev/null ) || { rm -rf "$smoke"; refuse "$profile/Makefile: make help fails"; }
|
||||||
|
rm -rf "$smoke"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── generate ───────────────────────────────────────────────────────────────
|
||||||
|
generate() { # into-dir
|
||||||
|
local into="$1" e profiles="" p kit verb libs
|
||||||
|
local -a all_entries=()
|
||||||
|
while IFS= read -r e; do all_entries+=("$e"); done < <(entries)
|
||||||
|
[ ${#all_entries[@]} -gt 0 ] || refuse "no script under ctrl/ carries a '# rig:standalone <kit> <verb>' marker"
|
||||||
|
|
||||||
|
# Profiles come from whichever entry point's libraries can answer for them.
|
||||||
|
for e in "${all_entries[@]}"; do
|
||||||
|
libs_into "$e"
|
||||||
|
profiles=$(ask "$e" ${libs[@]+"${libs[@]}"} -- config_profiles 2>/dev/null) && [ -n "$profiles" ] && break
|
||||||
|
profiles=""
|
||||||
|
done
|
||||||
|
[ -n "$profiles" ] || refuse "no entry point's libraries answer config_profiles, so there is nothing to generate a kit per"
|
||||||
|
|
||||||
|
for p in $profiles; do
|
||||||
|
mkdir -p "$into/$p"
|
||||||
|
for e in "${all_entries[@]}"; do
|
||||||
|
read -r kit verb <<< "$(marker_of "$e")"
|
||||||
|
libs_into "$e"
|
||||||
|
assemble "$e" "$p" "$into/$p/$kit.sh" ${libs[@]+"${libs[@]}"}
|
||||||
|
done
|
||||||
|
makefile "$into/$p" "${all_entries[@]}"
|
||||||
|
verify_kit "$into/$p" "$p" "${all_entries[@]}"
|
||||||
|
echo " $p: $(cd "$into/$p" && ls | tr '\n' ' ')"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# A kit folder is ours if its Makefile says so. Anything else under standalone/
|
||||||
|
# is left alone, so a hand-written file there is never swept away.
|
||||||
|
is_generated_dir() { grep -qF "$GENERATED_TAG" "$1/Makefile" 2>/dev/null; }
|
||||||
|
|
||||||
|
cmd="${1:-write}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
write)
|
||||||
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||||
|
echo "generating kits from rig's current tree"
|
||||||
|
generate "$tmp"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
for d in "$OUT"/*/; do
|
||||||
|
d=${d%/}; [ -d "$d" ] || continue
|
||||||
|
if is_generated_dir "$d" && [ ! -d "$tmp/${d##*/}" ]; then
|
||||||
|
echo " removed $(basename "$d") — no such profile any more"
|
||||||
|
rm -rf "$d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for d in "$tmp"/*/; do
|
||||||
|
d=${d%/}
|
||||||
|
rm -rf "$OUT/${d##*/}"
|
||||||
|
cp -r "$d" "$OUT/${d##*/}"
|
||||||
|
done
|
||||||
|
echo "wrote standalone/<profile>/ — every kit verified to stand alone"
|
||||||
|
;;
|
||||||
|
check)
|
||||||
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||||
|
generate "$tmp" >/dev/null
|
||||||
|
stale=0
|
||||||
|
for d in "$tmp"/*/; do
|
||||||
|
d=${d%/}; p=${d##*/}
|
||||||
|
if ! diff -rq "$d" "$OUT/$p" >/dev/null 2>&1; then
|
||||||
|
echo "stale: standalone/$p — $(diff -rq "$d" "$OUT/$p" 2>&1 | head -1)"
|
||||||
|
stale=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for d in "$OUT"/*/; do
|
||||||
|
d=${d%/}; [ -d "$d" ] || continue
|
||||||
|
if is_generated_dir "$d" && [ ! -d "$tmp/${d##*/}" ]; then
|
||||||
|
echo "stale: standalone/${d##*/} — no such profile any more"; stale=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$stale" -eq 0 ] || { echo "run: make standalone"; exit 1; }
|
||||||
|
echo "every kit is current"
|
||||||
|
;;
|
||||||
|
*) echo "usage: $SELF_REL [write|check]" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Optional shorthand for the two standalone scripts. They run without it:
|
|
||||||
# `bash rigdeps.sh detect` and `bash rigmini.sh status` are the whole interface,
|
|
||||||
# and this file only spells those out so nobody has to guess them.
|
|
||||||
#
|
|
||||||
# Guessing is what went wrong before. A Makefile generated around these scripts
|
|
||||||
# invented `make mini` -> `rigmini.sh on`, a verb that does not exist, and a bare
|
|
||||||
# `rigdeps.sh` for "check and report", which actually installs. So every target
|
|
||||||
# here calls only a verb the script accepts, and ctrl/selftest.sh checks that.
|
|
||||||
#
|
|
||||||
# Works wherever the three files sit together — the scripts are found beside
|
|
||||||
# this Makefile, not in the current directory, so `make -f path/Makefile` works.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# make deps report the host and toolchain; changes nothing
|
|
||||||
# make deps install [dev] download, verify and install into ~/.local/bin
|
|
||||||
# make mem advertised memory and what caps it; safe
|
|
||||||
# make mem push allocates until it stops — not on a box you need
|
|
||||||
|
|
||||||
HERE := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|
||||||
|
|
||||||
# Words after the target become the script's verb and arguments.
|
|
||||||
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
|
|
||||||
|
|
||||||
# Defaults to `detect`, not `install`: on a machine you are still evaluating,
|
|
||||||
# the bare command should report. Installing is a word you type.
|
|
||||||
deps: ## toolchain [detect|list|verify|install|fetch] (default detect)
|
|
||||||
bash $(HERE)rigdeps.sh $(or $(ARGS),detect)
|
|
||||||
|
|
||||||
# `mem`, matching the full rig's `make mem`, and deliberately NOT `mini`: that
|
|
||||||
# word already means "minimal footprint" in the projects that use this, and one
|
|
||||||
# name pointing at two jobs is how the invented target happened.
|
|
||||||
mem: ## memory and its caps [status|push|all] (default status)
|
|
||||||
bash $(HERE)rigmini.sh $(or $(ARGS),status)
|
|
||||||
@@ -1,69 +1,44 @@
|
|||||||
# standalone — single files for a machine the full rig is not going to
|
# standalone — rig as single files, one folder per profile
|
||||||
|
|
||||||
Each script here does one of rig's jobs without the rest of the tree. Copy one
|
**Everything in the `<profile>/` folders here is generated. Do not edit it.**
|
||||||
file onto a machine, run it, read the output. Nothing to clone, nothing to
|
It is rig's own tools flattened into single self-contained files, with one
|
||||||
install first.
|
profile's configuration resolved in, for a machine the full rig is not going to.
|
||||||
|
|
||||||
| file | does | full-rig equivalent |
|
```
|
||||||
| --- | --- | --- |
|
standalone/<profile>/rigdeps.sh rig's toolchain installer (ctrl/deps.sh)
|
||||||
| `rigdeps.sh` | installs kind, kubectl, tilt, ctlptl, jq and docker compose at rig's pins, checksum-verified, no sudo | `make deps` (`ctrl/deps.sh`) |
|
standalone/<profile>/rigmini.sh rig's memory tool (ctrl/mem.sh)
|
||||||
| `rigmini.sh` | reports how much memory the machine *advertises* and what caps it; `push` measures what it will actually *survive* | `make mem`, and the memory section of `make check` |
|
standalone/<profile>/Makefile shorthand for calling them
|
||||||
|
|
||||||
**These are transitional.** Where the full rig is installed, use its own
|
|
||||||
targets instead; they read `ctrl/versions.env` and the profile, which these
|
|
||||||
cannot.
|
|
||||||
|
|
||||||
## Why single files
|
|
||||||
|
|
||||||
`rigdeps.sh` carries its pins inline, because `ctrl/versions.env` is not on the
|
|
||||||
machine it is for. That makes two copies of the same versions and checksums.
|
|
||||||
`make pins` compares them and fails on any difference — `ctrl/versions.env` is
|
|
||||||
the source of truth.
|
|
||||||
|
|
||||||
`rigmini.sh` exists because on a container or managed workspace `/proc/meminfo`
|
|
||||||
reports the *host's* memory while a cgroup cap kills processes at a fraction of
|
|
||||||
it. `status` reads the caps; `push` allocates until something stops it.
|
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
**These two files are the whole setup.** No folder to create, no PATH to edit by
|
|
||||||
hand, nothing else to download first. The toolchain goes into `~/.local/bin`,
|
|
||||||
which Ubuntu already puts on PATH at login once the directory exists — so a new
|
|
||||||
shell after `install` is all it takes. If it is not on PATH, `install` says so
|
|
||||||
and prints the one line to add.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash rigdeps.sh detect # report, change nothing
|
|
||||||
bash rigdeps.sh install dev # install into ~/.local/bin
|
|
||||||
bash rigmini.sh status # advertised memory and caps; safe
|
|
||||||
bash rigmini.sh push # allocates until it stops — not on a machine you need
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `Makefile` beside them is **optional shorthand** for exactly those calls —
|
One folder per file in `ctrl/env.d/`. Pick the profile you mean to run and copy
|
||||||
copy it along or don't; the scripts do not need it:
|
that folder; nothing else from rig is needed. The scripts run without the
|
||||||
|
Makefile.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make deps # = rigdeps.sh detect (reports; installing is `make deps install`)
|
bash rigdeps.sh detect # report the host and toolchain; changes nothing
|
||||||
make mem # = rigmini.sh status
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
If you wrap these scripts in a Makefile of your own, copy the calls from that
|
`rigmini.sh push` and `all` deliberately consume memory. Run `status` first, and
|
||||||
file rather than guessing them. `rigmini.sh` measures memory; it has no `on` or
|
only run them somewhere other processes may be squeezed.
|
||||||
`off`, and toggling a heavy service off for a smaller footprint is a job for the
|
|
||||||
project's own manifests, not for this script. `make selftest` in the full rig
|
## Why generated
|
||||||
fails if this Makefile ever calls a verb its script does not accept.
|
|
||||||
|
|
||||||
If an earlier setup already put these tools in some other directory on PATH,
|
These used to be hand-kept copies, and they drifted: the standalone memory tool
|
||||||
remove that directory and the line that added it — do not rely on `install` to
|
said 2 GB per node long after rig had measured 800 MB. Now a kit is a pure
|
||||||
notice. It only reports shadowing once `~/.local/bin` is itself on PATH, which on
|
function of rig. It gains nothing rig lacks; improve rig and every kit follows.
|
||||||
a fresh machine it is not until the next login. After a new shell, check which
|
|
||||||
copy wins:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command -v kind kubectl tilt # each should be ~/.local/bin/...
|
make standalone # regenerate every kit
|
||||||
|
make standalone check # fail if any kit differs from what rig generates now
|
||||||
```
|
```
|
||||||
|
|
||||||
One toolchain, in the one place everyone else will also look.
|
`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.
|
||||||
|
|
||||||
`rigmini.sh push` deliberately consumes memory. Run `status` first, and only run
|
How the generator stays correct as rig changes shape — it knows no file, function
|
||||||
`push` somewhere it is acceptable for other processes to be squeezed.
|
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`.
|
||||||
|
|||||||
23
rig/standalone/client/Makefile
Normal file
23
rig/standalone/client/Makefile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 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)
|
||||||
958
rig/standalone/client/rigdeps.sh
Executable file
958
rig/standalone/client/rigdeps.sh
Executable file
@@ -0,0 +1,958 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GENERATED by make standalone — do not edit
|
||||||
|
#
|
||||||
|
# rigdeps.sh for profile 'client', flattened from:
|
||||||
|
# ctrl/deps.sh
|
||||||
|
# ctrl/lib/config.sh
|
||||||
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
|
# ── from the libraries ──
|
||||||
|
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||||
|
_config_restore ()
|
||||||
|
{
|
||||||
|
local line;
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
eval "export $line";
|
||||||
|
fi;
|
||||||
|
done <<< "$1";
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
default_cluster_name ()
|
||||||
|
{
|
||||||
|
local n;
|
||||||
|
n=$(basename "$(cd .. && pwd)");
|
||||||
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
|
echo "${n:-rig}"
|
||||||
|
}
|
||||||
|
derive_port_base ()
|
||||||
|
{
|
||||||
|
local h;
|
||||||
|
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
||||||
|
echo $((20000 + (h % 200) * 10))
|
||||||
|
}
|
||||||
|
render_kind_config ()
|
||||||
|
{
|
||||||
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||||
|
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── configuration, frozen for profile 'client' ──
|
||||||
|
load_config() {
|
||||||
|
local k saved=""
|
||||||
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
|
done
|
||||||
|
declare -gx ADDONS="metallb cert-manager metrics-server"
|
||||||
|
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||||
|
declare -g AUDIT="on"
|
||||||
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
|
declare -g CLUSTER="rig"
|
||||||
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
|
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
||||||
|
declare -gx COMPOSE_VERSION="5.5.1"
|
||||||
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
|
declare -gx DNS_MODE="hosts"
|
||||||
|
declare -g HTTPS_PORT="20311"
|
||||||
|
declare -g HTTP_PORT="20310"
|
||||||
|
declare -gx INGRESS_MODE="hostport"
|
||||||
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
|
declare -gx K8S_VERSION="v1_36"
|
||||||
|
declare -gx KIND_CONFIG="kind-config.client.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_PATH="./k8s/kind-config.client.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.client.yaml.tpl"
|
||||||
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
|
declare -g KUBECONTEXT="kind-rig"
|
||||||
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
|
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||||
|
declare -gx METALLB_VERSION="v0.16.0"
|
||||||
|
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
|
declare -g NODES="3"
|
||||||
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
||||||
|
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
||||||
|
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||||
|
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -g NODE_MB="800"
|
||||||
|
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx PROFILE_NAME="client"
|
||||||
|
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||||
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
|
declare -gx REGISTRY_MODE="mirror"
|
||||||
|
declare -g REGISTRY_PORT="20313"
|
||||||
|
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||||
|
declare -g TILT_PORT="20312"
|
||||||
|
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||||
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
|
declare -gx TILT_VERSION="0.37.6"
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
|
# ── ctrl/deps.sh ──
|
||||||
|
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
||||||
|
# report what it could not do.
|
||||||
|
#
|
||||||
|
# It never runs the cluster, never uses sudo or apt, and writes only into
|
||||||
|
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
||||||
|
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
||||||
|
# decide on, never performed. That is what makes it safe to run on a machine that
|
||||||
|
# already has a working setup.
|
||||||
|
#
|
||||||
|
# Usage (normally via `make deps`, or directly):
|
||||||
|
# deps.sh detect # report host facts only, change nothing
|
||||||
|
# deps.sh list # the pinned versions
|
||||||
|
# deps.sh verify [core|dev] # run what is installed and see if it works
|
||||||
|
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||||
|
# deps.sh install [core|dev] # detect, fetch, install, report
|
||||||
|
#
|
||||||
|
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
||||||
|
# Default is dev.
|
||||||
|
#
|
||||||
|
# Runs both inside the installer container and bare on a host. Inside the
|
||||||
|
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
||||||
|
# falls back to /.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
||||||
|
# not against ctrl/ once we've moved.
|
||||||
|
INVOKED_FROM="$PWD"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Pins arrive through load_config like every other setting, not by sourcing
|
||||||
|
# versions.env here. That is what lets `make standalone` freeze them into a
|
||||||
|
# one-file installer: configuration has exactly one way in.
|
||||||
|
# (sourced library inlined above)
|
||||||
|
load_config
|
||||||
|
|
||||||
|
# Resolve a possibly-relative path against the caller's original directory.
|
||||||
|
abspath() {
|
||||||
|
case "$1" in
|
||||||
|
/*) echo "$1" ;;
|
||||||
|
*) echo "$INVOKED_FROM/$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||||
|
HOST_ROOT="${HOST_ROOT:-/}"
|
||||||
|
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||||
|
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||||
|
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||||
|
|
||||||
|
# Collected by detect(), printed by report_manual() at the very end.
|
||||||
|
MANUAL=()
|
||||||
|
|
||||||
|
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
||||||
|
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
||||||
|
# container's own view is already the host's.
|
||||||
|
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
||||||
|
# tight and does-not-fit branches can be exercised against a real machine's
|
||||||
|
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
||||||
|
mb_of() {
|
||||||
|
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||||
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
|
}
|
||||||
|
|
||||||
|
host_file() {
|
||||||
|
local p="${1#/}"
|
||||||
|
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
||||||
|
echo "$HOST_ROOT/$p"
|
||||||
|
else
|
||||||
|
echo "/$p"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── the tools this script itself needs ─────────────────────────────────────
|
||||||
|
|
||||||
|
arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) echo amd64 ;;
|
||||||
|
aarch64|arm64) echo arm64 ;;
|
||||||
|
*) uname -m ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The pins above are amd64. Rather than download something that cannot execute
|
||||||
|
# and let it fail as "cannot execute binary file: Exec format error", say so
|
||||||
|
# here and hand over the commands that produce the right checksums.
|
||||||
|
require_amd64() {
|
||||||
|
local a; a=$(arch)
|
||||||
|
[ "$a" = "amd64" ] && return 0
|
||||||
|
cat >&2 <<EOF
|
||||||
|
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
||||||
|
|
||||||
|
Nothing here would run, so it does not download. To make an ${a} version, the
|
||||||
|
URLs need the ${a} artifact and the checksums need to come from each project's
|
||||||
|
own published list — not from these values, and not from a download you did:
|
||||||
|
|
||||||
|
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
||||||
|
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
||||||
|
|
||||||
|
Edit the pinned block at the top of this file with what those print.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DL=""
|
||||||
|
pick_downloader() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then DL=curl
|
||||||
|
elif command -v wget >/dev/null 2>&1; then DL=wget
|
||||||
|
else
|
||||||
|
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
||||||
|
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
local url="$1" out="$2"
|
||||||
|
case "$DL" in
|
||||||
|
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
||||||
|
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
SHA=""
|
||||||
|
pick_sha() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
||||||
|
else
|
||||||
|
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
||||||
|
echo "Refusing to install unverified binaries." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── package manager, for the instructions only ─────────────────────────────
|
||||||
|
# This never runs a package manager. It names one so the reported action is
|
||||||
|
# something you can paste, on the distro you are actually on — an apt line on
|
||||||
|
# Amazon Linux 2 is a wrong answer dressed up as help.
|
||||||
|
|
||||||
|
pkg_install_cmd() {
|
||||||
|
local pkg="$1"
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
||||||
|
else echo "install '$pkg' with this system's package manager"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_pkg() {
|
||||||
|
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── detect ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
|
# the tooling. Detectable, so name it instead.
|
||||||
|
require_linux() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
|
|
||||||
|
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
wsl --install
|
||||||
|
|
||||||
|
That enables Windows features and needs a reboot, so it is not something this
|
||||||
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
|
this from there.
|
||||||
|
|
||||||
|
See "Starting from plain Windows" in README.md.
|
||||||
|
EOF
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
|
detect() {
|
||||||
|
echo "host"
|
||||||
|
echo " kernel $(uname -r)"
|
||||||
|
echo " arch $(arch) ($(uname -m))"
|
||||||
|
|
||||||
|
local osr; osr=$(host_file /etc/os-release)
|
||||||
|
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
||||||
|
|
||||||
|
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
||||||
|
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
||||||
|
# that is enough depends on the profile, which check.sh knows and this does not.
|
||||||
|
local total_mb avail_mb swap_total_mb swap_used_mb om
|
||||||
|
total_mb=$(mb_of MemTotal)
|
||||||
|
avail_mb=$(mb_of MemAvailable)
|
||||||
|
swap_total_mb=$(mb_of SwapTotal)
|
||||||
|
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
||||||
|
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
||||||
|
if [ "$swap_total_mb" -gt 0 ]; then
|
||||||
|
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
||||||
|
# always says yes and settles up later with the OOM killer, so a cluster that
|
||||||
|
# starts cleanly can still lose processes afterwards.
|
||||||
|
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||||
|
case "$om" in
|
||||||
|
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||||
|
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||||
|
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo " install to $OUT_BIN"
|
||||||
|
detect_libc
|
||||||
|
detect_prereqs
|
||||||
|
detect_wsl
|
||||||
|
detect_filesystem
|
||||||
|
detect_docker
|
||||||
|
detect_inotify
|
||||||
|
detect_toolchain
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_wsl() {
|
||||||
|
if ! is_wsl; then
|
||||||
|
echo " platform native linux"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " platform WSL"
|
||||||
|
|
||||||
|
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
||||||
|
# host service need it. Enabling it requires a Windows-side restart, which
|
||||||
|
# cannot be issued from inside the distro.
|
||||||
|
local wc; wc=$(host_file /etc/wsl.conf)
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||||
|
echo " systemd enabled in wsl.conf"
|
||||||
|
else
|
||||||
|
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||||
|
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||||
|
[boot]
|
||||||
|
systemd=true
|
||||||
|
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||||
|
# local DNS setup.
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||||
|
echo " resolv.conf pinned (generateResolvConf=false)"
|
||||||
|
else
|
||||||
|
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local wcfg
|
||||||
|
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
|
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||||
|
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||||
|
else
|
||||||
|
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||||
|
make mem status
|
||||||
|
It prints the edit to make and the command to apply it.")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
||||||
|
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
||||||
|
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
||||||
|
# way. None of them deliver inotify events, so anything watching files goes
|
||||||
|
# quiet without saying why.
|
||||||
|
watch_hostile_fs() {
|
||||||
|
local dir="$1" fstype
|
||||||
|
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||||
|
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
||||||
|
case "$fstype" in
|
||||||
|
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_filesystem() {
|
||||||
|
local root fstype
|
||||||
|
root=$(cd .. && pwd -P)
|
||||||
|
fstype=$(watch_hostile_fs "$root")
|
||||||
|
if [ -n "$fstype" ]; then
|
||||||
|
echo " ! this directory is on $fstype — file watching will not work"
|
||||||
|
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
||||||
|
on a $fstype mount, and everything else is slower:
|
||||||
|
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||||
|
else
|
||||||
|
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
||||||
|
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
||||||
|
#
|
||||||
|
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
||||||
|
#
|
||||||
|
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
||||||
|
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
||||||
|
# version now; `verify` catches the actual failure after installing.
|
||||||
|
detect_libc() {
|
||||||
|
local v=""
|
||||||
|
if command -v ldd >/dev/null 2>&1; then
|
||||||
|
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$v" ]; then
|
||||||
|
echo " libc unknown (no ldd) — 'verify' is the real test"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo " libc glibc $v"
|
||||||
|
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
||||||
|
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
||||||
|
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
||||||
|
echo " Install the core tier, or run tilt from a container."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# What this script needs to do its own job. Reported here so `detect` answers
|
||||||
|
# "will install work?" instead of leaving you to find out one download in.
|
||||||
|
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
||||||
|
detect_prereqs() {
|
||||||
|
local missing=""
|
||||||
|
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
||||||
|
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
||||||
|
echo " checksums ok"
|
||||||
|
else
|
||||||
|
echo " ! no sha256sum or shasum — downloads could not be verified"
|
||||||
|
missing+=" coreutils"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
||||||
|
echo " archives tar + gzip"
|
||||||
|
else
|
||||||
|
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
||||||
|
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
||||||
|
missing+=" tar gzip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
MANUAL+=("Install what this script needs to run at all:
|
||||||
|
$(pkg_install_cmd "${missing# }")")
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_docker() {
|
||||||
|
# Reachability of the daemon is the real question, and the CLI is only how
|
||||||
|
# we ask it. Note that when this runs inside the installer container, Docker
|
||||||
|
# necessarily exists on the host — otherwise nothing would be executing —
|
||||||
|
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
echo " docker socket present (no cli in this context)"
|
||||||
|
else
|
||||||
|
echo " ! docker not found and no socket at /var/run/docker.sock"
|
||||||
|
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
||||||
|
that needs root:
|
||||||
|
$(pkg_install_cmd "$(docker_pkg)")
|
||||||
|
sudo systemctl enable --now docker
|
||||||
|
sudo usermod -aG docker \"\$USER\"
|
||||||
|
then log out and back in, so the new group applies to your shell.")
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||||
|
local n
|
||||||
|
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||||
|
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
||||||
|
# function the latter returns 1 when the count is zero, and `set -e`
|
||||||
|
# then kills the caller. That is the fresh-machine case — no clusters
|
||||||
|
# yet — so the bug only ever shows up where it does most harm.
|
||||||
|
if [ "$n" -gt 0 ]; then
|
||||||
|
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
|
MANUAL+=("Start Docker, or add yourself to the docker group:
|
||||||
|
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
||||||
|
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
||||||
|
detect_inotify() {
|
||||||
|
local w i
|
||||||
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
|
echo " inotify watches=$w instances=$i"
|
||||||
|
|
||||||
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
|
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||||
|
MANUAL+=("Raise inotify limits (needs root on the host):
|
||||||
|
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
||||||
|
| sudo tee /etc/sysctl.d/99-rig.conf
|
||||||
|
sudo sysctl --system")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── fetch ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
||||||
|
resolve_url() {
|
||||||
|
local upstream="$1"
|
||||||
|
case "$DEPS_SOURCE" in
|
||||||
|
upstream) echo "$upstream" ;;
|
||||||
|
artifactory)
|
||||||
|
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
||||||
|
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
||||||
|
;;
|
||||||
|
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
local file="$1" want="$2" name="$3" got
|
||||||
|
got=$($SHA "$file" | awk '{print $1}')
|
||||||
|
if [ "$got" != "$want" ]; then
|
||||||
|
echo "checksum mismatch for $name" >&2
|
||||||
|
echo " expected $want" >&2
|
||||||
|
echo " got $got" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
||||||
|
fetch_bin() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4"
|
||||||
|
local tmp="$dest/.$name.tmp"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
mv "$tmp" "$dest/$name"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
||||||
|
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
||||||
|
# others nest it a directory down — so the caller says which.
|
||||||
|
fetch_tgz() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
||||||
|
local tmp="$dest/.$name.tgz"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
||||||
|
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
||||||
|
# user does not own.
|
||||||
|
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||||
|
rm -f "$tmp"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The installer runs as root so it can reach the docker socket, which means
|
||||||
|
# everything it writes into a mounted volume lands root-owned and unusable from
|
||||||
|
# the host. Hand it back to whoever owns the mount point (the host user created
|
||||||
|
# that directory before mounting it).
|
||||||
|
fix_ownership() {
|
||||||
|
local dir="$1"
|
||||||
|
[ -d "$dir" ] || return 0
|
||||||
|
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
||||||
|
if [ "$owner" = ":" ]; then
|
||||||
|
owner=$(stat -c '%u:%g' "$dir")
|
||||||
|
fi
|
||||||
|
[ "$owner" = "0:0" ] && return 0
|
||||||
|
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Two tiers, because not every machine should get cluster tooling.
|
||||||
|
#
|
||||||
|
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
||||||
|
# creates one. Appropriate on a managed or corporate-issued machine
|
||||||
|
# where development tools are not wanted by default.
|
||||||
|
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
||||||
|
#
|
||||||
|
# The split exists because "install the toolchain" is not one decision: on a
|
||||||
|
# managed workspace the right answer is kubectl and nothing else.
|
||||||
|
CORE_TOOLS="kubectl jq"
|
||||||
|
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
||||||
|
# has ever invoked it. Add it back the day something actually needs a chart.
|
||||||
|
#
|
||||||
|
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
||||||
|
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
||||||
|
# because it is what wires a cluster to a local registry — without one, an
|
||||||
|
# unqualified image name resolves to docker.io/library/<name> and there is
|
||||||
|
# nothing structural stopping a push there.
|
||||||
|
#
|
||||||
|
# docker-compose is 'dev' for the same reason, and is here because the distro
|
||||||
|
# docker packages ship the daemon and CLI but frequently not the compose
|
||||||
|
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
||||||
|
# working Docker, and nothing about that message names the missing piece.
|
||||||
|
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
||||||
|
|
||||||
|
# ── what is already on this machine ───────────────────────────────────────
|
||||||
|
#
|
||||||
|
# A tool already on PATH at its pinned version is left where it is. Without
|
||||||
|
# this, install downloads a second copy into OUT_BIN and then reports the first
|
||||||
|
# one as shadowed — noise, and wrong, when both are the same version. That is
|
||||||
|
# the normal state of any machine someone set up by hand, whatever directory
|
||||||
|
# they happened to choose.
|
||||||
|
|
||||||
|
pin_of() {
|
||||||
|
case "$1" in
|
||||||
|
kubectl) echo "$KUBECTL_VERSION" ;;
|
||||||
|
jq) echo "$JQ_VERSION" ;;
|
||||||
|
kind) echo "$KIND_VERSION" ;;
|
||||||
|
tilt) echo "$TILT_VERSION" ;;
|
||||||
|
ctlptl) echo "$CTLPTL_VERSION" ;;
|
||||||
|
docker-compose) echo "$COMPOSE_VERSION" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The version string a binary reports. Each tool spells the question
|
||||||
|
# differently, and kubectl has to be told --client or it goes looking for a
|
||||||
|
# server to ask.
|
||||||
|
reported_version() {
|
||||||
|
local tool="$1" path="$2"
|
||||||
|
case "$tool" in
|
||||||
|
kubectl) "$path" version --client 2>/dev/null ;;
|
||||||
|
jq) "$path" --version 2>/dev/null ;;
|
||||||
|
*) "$path" version 2>/dev/null ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
||||||
|
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
||||||
|
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
||||||
|
#
|
||||||
|
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
||||||
|
# on every machine — some builds reject patterns that others accept — and a
|
||||||
|
# failed grep inside a count reads exactly like a zero.
|
||||||
|
version_matches() {
|
||||||
|
local tool="$1" path="$2" pin="$3" out v re
|
||||||
|
out=$(reported_version "$tool" "$path") || return 1
|
||||||
|
v="${pin#v}"
|
||||||
|
v="${v//./\\.}"
|
||||||
|
re="(^|[^0-9.])v?${v}([^0-9.]|\$)"
|
||||||
|
[[ $out =~ $re ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
||||||
|
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
||||||
|
# DIR" must not quietly skip something because this machine happens to have it.
|
||||||
|
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
||||||
|
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
||||||
|
|
||||||
|
# Every tool in the tier with its state, probed once and reported once. What
|
||||||
|
# still needs fetching is left in TOOLCHAIN_NEED for install() to act on.
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
detect_toolchain() {
|
||||||
|
local tier="${TIER:-dev}" b pin path found
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
echo
|
||||||
|
echo "toolchain (pinned, tier '$tier')"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
pin=$(pin_of "$b")
|
||||||
|
path=$(command -v "$b" 2>/dev/null || true)
|
||||||
|
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
||||||
|
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
||||||
|
# has no `docker-compose` to find — and probing only PATH would report it
|
||||||
|
# missing and re-download a copy that is already there. That is the exact
|
||||||
|
# noise the version-aware skip exists to prevent, so ask docker instead.
|
||||||
|
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
||||||
|
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
||||||
|
if [ "${found#v}" = "${pin#v}" ]; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
||||||
|
else
|
||||||
|
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
||||||
|
"$b" "$pin" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
elif version_matches "$b" "$path" "$pin"; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
||||||
|
else
|
||||||
|
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
||||||
|
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo " every pinned tool is already on PATH — nothing to fetch"
|
||||||
|
else
|
||||||
|
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) dest="$2"; shift 2 ;;
|
||||||
|
core|dev) tier="$1"; shift ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
dest="$(abspath "$dest")"
|
||||||
|
mkdir -p "$dest"
|
||||||
|
TIER="$tier"
|
||||||
|
|
||||||
|
if [ "$DEPS_SOURCE" = "baked" ]; then
|
||||||
|
echo "installing baked binaries from $BAKED_BIN"
|
||||||
|
cp -a "$BAKED_BIN"/. "$dest"/
|
||||||
|
fix_ownership "$dest"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${DEPS_ONLY:-}" ]; then
|
||||||
|
echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)"
|
||||||
|
else
|
||||||
|
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
||||||
|
fi
|
||||||
|
if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi
|
||||||
|
if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi
|
||||||
|
if [ "$tier" = "dev" ]; then
|
||||||
|
if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi
|
||||||
|
if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi
|
||||||
|
if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi
|
||||||
|
if want docker-compose; then
|
||||||
|
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
fix_ownership "$dest"
|
||||||
|
# kind writes the kubeconfig as root too; hand that back as well when it's
|
||||||
|
# a mounted host directory rather than container-local state.
|
||||||
|
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── install ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
report_manual() {
|
||||||
|
echo
|
||||||
|
if [ ${#MANUAL[@]} -eq 0 ]; then
|
||||||
|
echo "nothing left to do by hand."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
||||||
|
echo
|
||||||
|
local n=1
|
||||||
|
for m in "${MANUAL[@]}"; do
|
||||||
|
echo " $n. $m"
|
||||||
|
echo
|
||||||
|
n=$((n + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installing into a directory that sits early in PATH silently replaces whatever
|
||||||
|
# the machine was already using — which on a shared or client machine can break
|
||||||
|
# unrelated work (kubectl more than one minor away from a cluster is the common
|
||||||
|
# one). Say so; never decide it for them.
|
||||||
|
# Downloading a verified binary proves it is the right file, not that this
|
||||||
|
# machine can run it. On an old distro tilt fails here, with a linker error
|
||||||
|
# about a missing symbol, and finding that out now beats finding out during a
|
||||||
|
# first cluster build.
|
||||||
|
verify_tools() {
|
||||||
|
local tier="${1:-dev}" b bin out rc broke=0
|
||||||
|
echo "checking that each one actually runs"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
bin="$OUT_BIN/$b"
|
||||||
|
if [ ! -x "$bin" ]; then
|
||||||
|
printf ' %-14s not installed\n' "$b"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
||||||
|
# than one line gets SIGPIPE when head closes the pipe, and the
|
||||||
|
# pipeline reports 141 — so a working kubectl was announced as "does
|
||||||
|
# not run here", with its own correct version string as the evidence.
|
||||||
|
# Take the first line afterwards, from the string.
|
||||||
|
rc=0
|
||||||
|
case "$b" in
|
||||||
|
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
||||||
|
jq) out=$("$bin" --version 2>&1) || rc=$? ;;
|
||||||
|
*) out=$("$bin" version 2>&1) || rc=$? ;;
|
||||||
|
esac
|
||||||
|
out=${out%%$'\n'*}
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
printf ' %-14s %s\n' "$b" "$out"
|
||||||
|
else
|
||||||
|
printf ' ! %-12s does not run here: %s\n' "$b" "$out"
|
||||||
|
broke=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$broke" -eq 1 ]; then
|
||||||
|
echo
|
||||||
|
echo " A binary that downloads and verifies but will not start is almost"
|
||||||
|
echo " always this distro's libc being older than the release needs."
|
||||||
|
echo " 'detect' prints the glibc version. The core tier (kubectl + jq)"
|
||||||
|
echo " has no such dependency and will work regardless."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
echo "pinned, linux/amd64 only:"
|
||||||
|
printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' jq "$JQ_VERSION"
|
||||||
|
printf ' %-14s %s\n' kind "$KIND_VERSION"
|
||||||
|
printf ' %-14s %s\n' tilt "$TILT_VERSION"
|
||||||
|
printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION"
|
||||||
|
echo
|
||||||
|
echo " core = $CORE_TOOLS"
|
||||||
|
echo " dev = $CORE_TOOLS $DEV_TOOLS"
|
||||||
|
echo
|
||||||
|
echo "Checksums are pinned in the block at the top of this file. To bump one,"
|
||||||
|
echo "take the new checksum from the publisher's own release list — the header"
|
||||||
|
echo "comment has the exact commands."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
||||||
|
|
||||||
|
warn_shadowing() {
|
||||||
|
local b existing shadowed="" tier="${1:-dev}"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
[ -x "$OUT_BIN/$b" ] || continue
|
||||||
|
# Where would this resolve if OUT_BIN weren't in the way?
|
||||||
|
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
||||||
|
command -v "$b" 2>/dev/null || true)
|
||||||
|
[ -n "$existing" ] || continue
|
||||||
|
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
||||||
|
# The same version in both places is not a conflict: nothing changes for
|
||||||
|
# any other project whichever copy PATH happens to find first.
|
||||||
|
if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi
|
||||||
|
shadowed+=" $b $existing"$'\n'
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$shadowed" ] || return 0
|
||||||
|
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
||||||
|
printf '%s' "$shadowed"
|
||||||
|
echo " Other projects on this machine will pick up the new versions."
|
||||||
|
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
||||||
|
was just installed:
|
||||||
|
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
||||||
|
Or install somewhere private instead:
|
||||||
|
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
||||||
|
# retired v1 spelling; every compose file written in the last few years assumes
|
||||||
|
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
||||||
|
# So the binary is fetched like any other and then linked, in your own home —
|
||||||
|
# no root, and nothing outside it.
|
||||||
|
install_compose_plugin() {
|
||||||
|
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
||||||
|
[ -x "$src" ] || return 0
|
||||||
|
mkdir -p "$dir"
|
||||||
|
# Something else already owns that name — docker-desktop and some distro
|
||||||
|
# packages install a real file there. Overwriting it would take the plugin
|
||||||
|
# away from whatever put it there, so say so and let the user decide.
|
||||||
|
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
||||||
|
MANUAL+=("Something already installs the compose plugin at
|
||||||
|
$dir/docker-compose
|
||||||
|
To use rig's pinned build instead:
|
||||||
|
ln -sf $src $dir/docker-compose")
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ln -sfn "$src" "$dir/docker-compose"
|
||||||
|
echo " compose plugin -> $dir/docker-compose"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
local tier="${1:-dev}" b
|
||||||
|
TIER="$tier"
|
||||||
|
detect
|
||||||
|
|
||||||
|
# detect_toolchain has already probed PATH. Fetch only what it found missing
|
||||||
|
# or at the wrong version; a tool already present at its pin stays where it is.
|
||||||
|
if [ -n "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo
|
||||||
|
DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier"
|
||||||
|
echo
|
||||||
|
echo "installed to $OUT_BIN ($tier):"
|
||||||
|
for b in $TOOLCHAIN_NEED; do
|
||||||
|
if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi
|
||||||
|
done
|
||||||
|
if [ "$tier" = "core" ]; then
|
||||||
|
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||||
|
fi
|
||||||
|
# Only when compose was one of the things fetched: linking a binary
|
||||||
|
# that is already satisfied elsewhere on PATH would point the plugin at
|
||||||
|
# a copy rig did not install.
|
||||||
|
case " $TOOLCHAIN_NEED " in
|
||||||
|
*" docker-compose "*) install_compose_plugin ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Only worth saying when something actually landed in OUT_BIN. When every
|
||||||
|
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
||||||
|
# telling the user to add it would be advice to fix nothing.
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||||
|
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
warn_shadowing "$tier"
|
||||||
|
|
||||||
|
report_manual
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_linux
|
||||||
|
|
||||||
|
# Read the command, THEN shift — and shift only if there is something there.
|
||||||
|
# A bare `shift` with no positional parameters returns 1, and under `set -e`
|
||||||
|
# that ended the script before a single line was printed: running this with no
|
||||||
|
# arguments at all, the documented default, did nothing and said nothing.
|
||||||
|
cmd="${1:-install}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||||
|
need_downloads() {
|
||||||
|
require_amd64
|
||||||
|
if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi
|
||||||
|
pick_sha
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
detect) detect; report_manual ;;
|
||||||
|
list) list ;;
|
||||||
|
verify) verify_tools "${1:-dev}" ;;
|
||||||
|
fetch) need_downloads; fetch "$@" ;;
|
||||||
|
install) need_downloads; install "${1:-dev}" ;;
|
||||||
|
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
||||||
|
echo " install [core|dev] (default dev)" >&2
|
||||||
|
echo " fetch [core|dev] [--to DIR]" >&2
|
||||||
|
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
859
rig/standalone/client/rigmini.sh
Executable file
859
rig/standalone/client/rigmini.sh
Executable file
@@ -0,0 +1,859 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GENERATED by make standalone — do not edit
|
||||||
|
#
|
||||||
|
# rigmini.sh for profile 'client', flattened from:
|
||||||
|
# ctrl/mem.sh
|
||||||
|
# ctrl/lib/config.sh
|
||||||
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
|
# ── from the libraries ──
|
||||||
|
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||||
|
_config_restore ()
|
||||||
|
{
|
||||||
|
local line;
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
eval "export $line";
|
||||||
|
fi;
|
||||||
|
done <<< "$1";
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
default_cluster_name ()
|
||||||
|
{
|
||||||
|
local n;
|
||||||
|
n=$(basename "$(cd .. && pwd)");
|
||||||
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
|
echo "${n:-rig}"
|
||||||
|
}
|
||||||
|
derive_port_base ()
|
||||||
|
{
|
||||||
|
local h;
|
||||||
|
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
||||||
|
echo $((20000 + (h % 200) * 10))
|
||||||
|
}
|
||||||
|
render_kind_config ()
|
||||||
|
{
|
||||||
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||||
|
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── configuration, frozen for profile 'client' ──
|
||||||
|
load_config() {
|
||||||
|
local k saved=""
|
||||||
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
|
done
|
||||||
|
declare -gx ADDONS="metallb cert-manager metrics-server"
|
||||||
|
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||||
|
declare -g AUDIT="on"
|
||||||
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
|
declare -g CLUSTER="rig"
|
||||||
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
|
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
||||||
|
declare -gx COMPOSE_VERSION="5.5.1"
|
||||||
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
|
declare -gx DNS_MODE="hosts"
|
||||||
|
declare -g HTTPS_PORT="20311"
|
||||||
|
declare -g HTTP_PORT="20310"
|
||||||
|
declare -gx INGRESS_MODE="hostport"
|
||||||
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
|
declare -gx K8S_VERSION="v1_36"
|
||||||
|
declare -gx KIND_CONFIG="kind-config.client.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_PATH="./k8s/kind-config.client.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.client.yaml.tpl"
|
||||||
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
|
declare -g KUBECONTEXT="kind-rig"
|
||||||
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
|
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||||
|
declare -gx METALLB_VERSION="v0.16.0"
|
||||||
|
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
|
declare -g NODES="3"
|
||||||
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
||||||
|
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
||||||
|
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||||
|
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -g NODE_MB="800"
|
||||||
|
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx PROFILE_NAME="client"
|
||||||
|
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||||
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
|
declare -gx REGISTRY_MODE="mirror"
|
||||||
|
declare -g REGISTRY_PORT="20313"
|
||||||
|
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||||
|
declare -g TILT_PORT="20312"
|
||||||
|
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||||
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
|
declare -gx TILT_VERSION="0.37.6"
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
|
# ── ctrl/mem.sh ──
|
||||||
|
# How much memory this machine will actually give you before something dies —
|
||||||
|
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
||||||
|
#
|
||||||
|
# There are two numbers and they are rarely the same. `status` reports what the
|
||||||
|
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
||||||
|
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
||||||
|
# against what this profile's cluster needs.
|
||||||
|
#
|
||||||
|
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
||||||
|
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
||||||
|
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
||||||
|
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
||||||
|
# on a box that OOMs at 2.
|
||||||
|
#
|
||||||
|
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
||||||
|
# allocation that can be raised, and the commonest failure is raising it without
|
||||||
|
# restarting — so status compares what .wslconfig says with what actually booted.
|
||||||
|
#
|
||||||
|
# Reports and instructs. It never raises a limit, frees anything or installs a
|
||||||
|
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
||||||
|
# itself, so that `restore` has something to put back after a hand edit.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# mem.sh status what it has, what caps it
|
||||||
|
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
||||||
|
# mem.sh all [--budget GB] both, then the verdict
|
||||||
|
# mem.sh backup | restore .wslconfig, WSL only
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
# (sourced library inlined above)
|
||||||
|
|
||||||
|
# ── defaults ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
STEP_MB=0 # per allocation; 0 means scale it to the ceiling. See push().
|
||||||
|
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
||||||
|
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
||||||
|
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
||||||
|
BUDGET_GB="" # --budget; empty means what this profile's cluster needs, from rig.
|
||||||
|
BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess below.
|
||||||
|
|
||||||
|
# ── platform ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
|
# the tooling. Detectable, so name it instead.
|
||||||
|
require_linux() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
|
|
||||||
|
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
wsl --install
|
||||||
|
|
||||||
|
That enables Windows features and needs a reboot, so it is not something this
|
||||||
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
|
this from there.
|
||||||
|
EOF
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Everything below reads /proc. Without it there is nothing to measure, and
|
||||||
|
# failing here beats printing a page of empty fields.
|
||||||
|
if [ ! -r /proc/meminfo ]; then
|
||||||
|
echo "no readable /proc/meminfo — this needs a Linux kernel." >&2
|
||||||
|
echo "On macOS or a BSD none of the numbers below exist." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
|
is_container() {
|
||||||
|
[ -f /.dockerenv ] && return 0
|
||||||
|
grep -qE '(docker|containerd|kubepods|lxc|podman)' /proc/1/cgroup 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
platform() {
|
||||||
|
if is_wsl; then echo WSL
|
||||||
|
elif is_container; then echo container
|
||||||
|
else echo "native linux"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── reading memory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
||||||
|
|
||||||
|
# MemAvailable arrived in kernel 3.14. Older kernels — and they turn up on
|
||||||
|
# corporate images — need the estimate it replaced, which is worse but not wrong.
|
||||||
|
avail_meminfo_mb() {
|
||||||
|
if grep -q '^MemAvailable:' /proc/meminfo; then
|
||||||
|
mb MemAvailable
|
||||||
|
else
|
||||||
|
awk '/^(MemFree|Buffers|Cached):/{t+=$2} END{print int(t/1024)}' /proc/meminfo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
||||||
|
# find_cgroup, because every later reading needs both and hunting for the files
|
||||||
|
# on each call would be the slow part of the poll loop.
|
||||||
|
CG_MAX_FILE=""
|
||||||
|
CG_CUR_FILE=""
|
||||||
|
CG_VERSION=""
|
||||||
|
|
||||||
|
find_cgroup() {
|
||||||
|
local rel
|
||||||
|
|
||||||
|
# Inside a container the cgroup namespace makes the top of the tree BE the
|
||||||
|
# container's own cgroup, so the unqualified path is already the right one.
|
||||||
|
# On a host it is the root cgroup, which is never limited — hence the second
|
||||||
|
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
||||||
|
if [ -r /sys/fs/cgroup/memory.max ]; then
|
||||||
|
CG_VERSION=v2
|
||||||
|
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
||||||
|
CG_CUR_FILE=/sys/fs/cgroup/memory.current
|
||||||
|
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
|
||||||
|
CG_VERSION=v1
|
||||||
|
CG_MAX_FILE=/sys/fs/cgroup/memory/memory.limit_in_bytes
|
||||||
|
CG_CUR_FILE=/sys/fs/cgroup/memory/memory.usage_in_bytes
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel=$(awk -F: '$1=="0"{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
||||||
|
if [ -n "$rel" ] && [ "$rel" != "/" ] && [ -r "/sys/fs/cgroup${rel}/memory.max" ]; then
|
||||||
|
CG_VERSION=v2
|
||||||
|
CG_MAX_FILE="/sys/fs/cgroup${rel}/memory.max"
|
||||||
|
CG_CUR_FILE="/sys/fs/cgroup${rel}/memory.current"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel=$(awk -F: '$2 ~ /(^|,)memory(,|$)/{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
||||||
|
if [ -n "$rel" ] && [ "$rel" != "/" ] \
|
||||||
|
&& [ -r "/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes" ]; then
|
||||||
|
CG_VERSION=v1
|
||||||
|
CG_MAX_FILE="/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes"
|
||||||
|
CG_CUR_FILE="/sys/fs/cgroup/memory${rel}/memory.usage_in_bytes"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
||||||
|
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
||||||
|
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
||||||
|
# own memory is not a limit, however it is written.
|
||||||
|
cgroup_cap_mb() {
|
||||||
|
local raw cap
|
||||||
|
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
||||||
|
raw=$(cat "$CG_MAX_FILE" 2>/dev/null || echo max)
|
||||||
|
[ "$raw" = "max" ] && { echo ""; return 0; }
|
||||||
|
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
cap=$((raw / 1024 / 1024))
|
||||||
|
[ "$cap" -ge "$(mb MemTotal)" ] && { echo ""; return 0; }
|
||||||
|
echo "$cap"
|
||||||
|
}
|
||||||
|
|
||||||
|
cgroup_used_mb() {
|
||||||
|
local raw
|
||||||
|
[ -n "$CG_CUR_FILE" ] && [ -r "$CG_CUR_FILE" ] || { echo ""; return 0; }
|
||||||
|
raw=$(cat "$CG_CUR_FILE" 2>/dev/null || echo "")
|
||||||
|
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
echo $((raw / 1024 / 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
# ulimit -v is a per-process address-space cap. It stops YOU long before the box
|
||||||
|
# does, and because it is inherited from a login shell it is easy to hit without
|
||||||
|
# knowing it is set.
|
||||||
|
ulimit_v_mb() {
|
||||||
|
local v; v=$(ulimit -v 2>/dev/null || echo unlimited)
|
||||||
|
[ "$v" = "unlimited" ] && { echo ""; return 0; }
|
||||||
|
case "$v" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
echo $((v / 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
# The number everything else is about: the lowest of the things that can stop
|
||||||
|
# you. Printed at the end of `status` and used as the sanity bound in `push`.
|
||||||
|
effective_ceiling_mb() {
|
||||||
|
local c; c=$(mb MemTotal)
|
||||||
|
local cap; cap=$(cgroup_cap_mb)
|
||||||
|
local ul; ul=$(ulimit_v_mb)
|
||||||
|
[ -n "$cap" ] && [ "$cap" -lt "$c" ] && c="$cap"
|
||||||
|
[ -n "$ul" ] && [ "$ul" -lt "$c" ] && c="$ul"
|
||||||
|
echo "$c"
|
||||||
|
}
|
||||||
|
|
||||||
|
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
||||||
|
# In a capped container /proc/meminfo describes the host and is worse than
|
||||||
|
# useless for this — it would report tens of gigabytes free on a box that is one
|
||||||
|
# allocation from being killed.
|
||||||
|
headroom_mb() {
|
||||||
|
local cap used
|
||||||
|
cap=$(cgroup_cap_mb)
|
||||||
|
used=$(cgroup_used_mb)
|
||||||
|
if [ -n "$cap" ] && [ -n "$used" ]; then
|
||||||
|
echo $(( cap - used ))
|
||||||
|
else
|
||||||
|
avail_meminfo_mb
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── status ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
||||||
|
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
||||||
|
# Windows, then fall back to whichever profile actually owns a config.
|
||||||
|
wslconfig_path() {
|
||||||
|
local profile winpath found
|
||||||
|
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
||||||
|
case "$profile" in
|
||||||
|
""|*%*) ;;
|
||||||
|
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
||||||
|
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
||||||
|
echo "$winpath/.wslconfig"; return 0
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$found" ] && echo "$found"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
hogs() {
|
||||||
|
echo " holding the most:"
|
||||||
|
ps -eo rss,comm --sort=-rss 2>/dev/null \
|
||||||
|
| awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
local total avail swap_total swap_free cap ul cur
|
||||||
|
|
||||||
|
echo "host"
|
||||||
|
echo " platform $(platform)"
|
||||||
|
echo " kernel $(uname -r)"
|
||||||
|
[ -r /etc/os-release ] && \
|
||||||
|
echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' /etc/os-release)"
|
||||||
|
echo " cpu $(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '?') online, load $(cut -d' ' -f1-3 /proc/loadavg)"
|
||||||
|
|
||||||
|
# ── the caps first, because they decide what the totals below are worth ──
|
||||||
|
echo
|
||||||
|
echo "caps"
|
||||||
|
cap=$(cgroup_cap_mb)
|
||||||
|
if [ -n "$cap" ]; then
|
||||||
|
cur=$(cgroup_used_mb)
|
||||||
|
echo " cgroup ${cap} MB (${CG_VERSION}, ${CG_CUR_FILE##*/} says ${cur:-?} MB used)"
|
||||||
|
echo " ! /proc/meminfo below describes the HOST, not this cgroup."
|
||||||
|
echo " $(mb MemTotal) MB total is not yours; ${cap} MB is."
|
||||||
|
elif [ -n "$CG_VERSION" ]; then
|
||||||
|
echo " cgroup none (${CG_VERSION} present, no memory limit set)"
|
||||||
|
else
|
||||||
|
echo " cgroup no memory controller found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ul=$(ulimit_v_mb)
|
||||||
|
if [ -n "$ul" ]; then
|
||||||
|
echo " ! ulimit -v ${ul} MB — a per-process cap, inherited from your shell"
|
||||||
|
echo " it stops this process long before the machine runs out"
|
||||||
|
else
|
||||||
|
echo " ulimit -v unlimited"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# overcommit_memory=0 is the default heuristic: a large allocation is
|
||||||
|
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
||||||
|
# than as a failed malloc. It is why `push` touches every page it asks for.
|
||||||
|
local om or_
|
||||||
|
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
||||||
|
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
||||||
|
case "$om" in
|
||||||
|
0) echo " overcommit 0 heuristic — allocations are granted on a guess," ;;
|
||||||
|
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit," ;;
|
||||||
|
2) echo " overcommit 2 strict (ratio ${or_}%) — allocation fails honestly instead of killing later," ;;
|
||||||
|
*) echo " overcommit ${om}" ;;
|
||||||
|
esac
|
||||||
|
[ "$om" != "?" ] && echo " so RSS is the number to trust, not what a process asked for"
|
||||||
|
|
||||||
|
# ── what it says it has ──
|
||||||
|
total=$(mb MemTotal); avail=$(avail_meminfo_mb)
|
||||||
|
swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree)
|
||||||
|
echo
|
||||||
|
echo "memory"
|
||||||
|
echo " total ${total} MB"
|
||||||
|
echo " available ${avail} MB"
|
||||||
|
echo " swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
||||||
|
if [ "$swap_total" -eq 0 ]; then
|
||||||
|
echo " - no swap: this box has no cushion. It goes from fine to OOM-killed"
|
||||||
|
echo " with nothing in between, which is the abrupt failure you get in a VM."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# postgres puts its shared buffers in /dev/shm. Docker's default is 64 MB,
|
||||||
|
# and the resulting failure names neither shm nor the size.
|
||||||
|
if [ -d /dev/shm ]; then
|
||||||
|
local shm; shm=$(df -Pm /dev/shm 2>/dev/null | awk 'NR==2{print $2}')
|
||||||
|
if [ -n "$shm" ]; then
|
||||||
|
if [ "$shm" -le 64 ]; then
|
||||||
|
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
||||||
|
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
||||||
|
else
|
||||||
|
echo " /dev/shm ${shm} MB"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "disk"
|
||||||
|
local d
|
||||||
|
for d in / /tmp /var/lib/docker; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
df -Pm "$d" 2>/dev/null | awk -v p="$d" 'NR==2{printf " %-12s %s MB free of %s MB\n", p, $4, $2}'
|
||||||
|
done
|
||||||
|
|
||||||
|
# kind and Tilt both watch large trees, and the failure mode is silent:
|
||||||
|
# they simply stop noticing file changes. Cheap to report while we are here.
|
||||||
|
local w i
|
||||||
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
|
echo
|
||||||
|
echo "tooling"
|
||||||
|
echo " inotify watches=$w instances=$i"
|
||||||
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
|
echo " ! low — anything watching files will silently stop seeing changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
echo " docker socket present, no cli"
|
||||||
|
else
|
||||||
|
echo " docker not installed"
|
||||||
|
fi
|
||||||
|
elif docker info >/dev/null 2>&1; then
|
||||||
|
local n
|
||||||
|
n=$(docker ps -q 2>/dev/null | wc -l)
|
||||||
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null), ${n} container(s) running"
|
||||||
|
else
|
||||||
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
||||||
|
# not usefully apply — the change costs a full VM restart. Report it, and
|
||||||
|
# report the commonest mistake, which is editing it and not restarting.
|
||||||
|
if is_wsl; then
|
||||||
|
local cfg conf conf_mb n
|
||||||
|
cfg=$(wslconfig_path)
|
||||||
|
echo
|
||||||
|
echo "wsl"
|
||||||
|
if [ -z "$cfg" ]; then
|
||||||
|
echo " ! cannot tell which Windows profile owns .wslconfig"
|
||||||
|
else
|
||||||
|
echo " config $cfg"
|
||||||
|
conf=$(configured_memory "$cfg")
|
||||||
|
if [ -n "$conf" ]; then
|
||||||
|
conf_mb=$(to_mb "$conf")
|
||||||
|
echo " configured $conf (${conf_mb} MB), booted ${total} MB"
|
||||||
|
# The VM reports a little less than allocated; 15% covers the
|
||||||
|
# kernel without calling every healthy machine a mismatch.
|
||||||
|
if [ -n "$conf_mb" ] && [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
||||||
|
echo " ! configured ${conf_mb} MB but booted ${total} MB — not applied yet."
|
||||||
|
echo " From a WINDOWS terminal: wsl --shutdown then start the distro again."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " configured no memory= set (WSL defaults to 50% of host RAM, or 8 GB,"
|
||||||
|
echo " whichever is less). To raise it, add on the Windows side:"
|
||||||
|
echo " [wsl2]"
|
||||||
|
echo " memory=8GB"
|
||||||
|
echo " then from a WINDOWS terminal: wsl --shutdown"
|
||||||
|
fi
|
||||||
|
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
|
if [ "$n" -gt 0 ]; then
|
||||||
|
echo " backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo " - native linux: no VM allocation to raise. If memory is tight the levers"
|
||||||
|
echo " are freeing something or adding swap."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "effective ceiling $(effective_ceiling_mb) MB"
|
||||||
|
echo " the lowest of MemTotal, the cgroup cap and ulimit -v. What the box"
|
||||||
|
echo " claims. 'push' measures what it will actually hand over."
|
||||||
|
|
||||||
|
[ "$avail" -lt $(( total / 5 )) ] && { echo; hogs; }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── .wslconfig ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_wsl() {
|
||||||
|
if ! is_wsl; then
|
||||||
|
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
||||||
|
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
||||||
|
echo "Use 'status' to see what the machine actually has." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# backup and restore act on the file, so unlike status they must not guess.
|
||||||
|
wslconfig_required() {
|
||||||
|
local cfg; cfg=$(wslconfig_required)
|
||||||
|
if [ -z "$cfg" ]; then
|
||||||
|
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
||||||
|
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
||||||
|
| grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$cfg"
|
||||||
|
}
|
||||||
|
|
||||||
|
configured_memory() {
|
||||||
|
[ -r "$1" ] || { echo ""; return; }
|
||||||
|
sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo.
|
||||||
|
to_mb() {
|
||||||
|
local v="${1^^}" n
|
||||||
|
n=$(echo "$v" | tr -dc '0-9')
|
||||||
|
[ -n "$n" ] || { echo ""; return; }
|
||||||
|
case "$v" in
|
||||||
|
*GB|*G) echo $(( n * 1024 )) ;;
|
||||||
|
*MB|*M) echo "$n" ;;
|
||||||
|
*) echo $(( n / 1024 / 1024 )) ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
backup() {
|
||||||
|
require_wsl backup
|
||||||
|
local cfg dest
|
||||||
|
cfg=$(wslconfig_required)
|
||||||
|
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||||
|
# Timestamped and never overwritten: a backup that can destroy itself on a
|
||||||
|
# second run is not a backup.
|
||||||
|
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
||||||
|
cp "$cfg" "$dest"
|
||||||
|
echo "backed up $dest"
|
||||||
|
echo
|
||||||
|
echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
require_wsl restore
|
||||||
|
local cfg newest count
|
||||||
|
cfg=$(wslconfig_required)
|
||||||
|
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "restoring $newest"
|
||||||
|
echo " -> $cfg"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Newest is the right default — undo the last edit — but if you backed up
|
||||||
|
# *after* editing, the state you want is older. Show the rest so a no-op
|
||||||
|
# restore is obviously a no-op rather than a mystery.
|
||||||
|
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
|
if [ "$count" -gt 1 ]; then
|
||||||
|
echo "$count backups exist, newest first:"
|
||||||
|
ls -t "$cfg".*.bak | sed 's/^/ /'
|
||||||
|
echo " (restoring the newest; copy another by hand to pick an older one)"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -r "$cfg" ]; then
|
||||||
|
echo "what changes:"
|
||||||
|
if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then
|
||||||
|
echo " nothing — that backup is identical to the current config"
|
||||||
|
else
|
||||||
|
sed 's/^/ /' /tmp/mem.diff
|
||||||
|
fi
|
||||||
|
rm -f /tmp/mem.diff
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "proceed? [y/N] "
|
||||||
|
read -r reply
|
||||||
|
case "$reply" in
|
||||||
|
y|Y|yes|Yes) ;;
|
||||||
|
*) echo "left alone"; return 0 ;;
|
||||||
|
esac
|
||||||
|
cp "$newest" "$cfg"
|
||||||
|
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── push ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
STATE=""
|
||||||
|
CHILD=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$CHILD" ] && kill -0 "$CHILD" 2>/dev/null; then
|
||||||
|
kill -KILL "$CHILD" 2>/dev/null || true
|
||||||
|
wait "$CHILD" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
[ -n "$STATE" ] && rm -f "$STATE"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The child allocates and stops itself; the parent only watches. That split is
|
||||||
|
# the point: under --to-oom the allocating process is expected to be killed, and
|
||||||
|
# something has to survive to say how far it got.
|
||||||
|
allocator() {
|
||||||
|
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
||||||
|
# first. Raising needs no privilege (only lowering does). Without it, the
|
||||||
|
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
||||||
|
# box you are still using, that is not an acceptable coin toss.
|
||||||
|
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
||||||
|
|
||||||
|
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
||||||
|
local bytes=$((STEP_MB * 1024 * 1024))
|
||||||
|
local swap_used_start
|
||||||
|
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
# Written STRAIGHT INTO the array element. The obvious spelling —
|
||||||
|
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
||||||
|
# not one: the template stays resident, expanding "$chunk" makes a
|
||||||
|
# temporary word, and the append makes the element. A 128 MB step then
|
||||||
|
# needs 384 MB transiently, and on a small box it is killed on the
|
||||||
|
# first append while reporting a third of the true ceiling.
|
||||||
|
#
|
||||||
|
# printf -v into a subscript also means every page is written, so it is
|
||||||
|
# resident rather than merely promised — the only kind of allocation
|
||||||
|
# that measures anything under heuristic overcommit.
|
||||||
|
printf -v "arr[$i]" '%*s' "$bytes" ''
|
||||||
|
i=$((i + 1)); held=$((held + STEP_MB))
|
||||||
|
|
||||||
|
rss=$(awk '/^VmRSS:/{print int($2/1024)}' "/proc/$BASHPID/status" 2>/dev/null || echo 0)
|
||||||
|
avail=$(headroom_mb)
|
||||||
|
swapped=$(( $(mb SwapTotal) - $(mb SwapFree) - swap_used_start ))
|
||||||
|
[ "$swapped" -lt 0 ] && swapped=0
|
||||||
|
|
||||||
|
printf '%8s MB held rss %7s MB headroom %7s MB swap +%s MB\n' \
|
||||||
|
"$held" "$rss" "$avail" "$swapped"
|
||||||
|
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
||||||
|
|
||||||
|
# Worth calling out separately from the ceiling: this is where the box
|
||||||
|
# stops being fast and starts being unusable, which for a scheduler is
|
||||||
|
# a different and earlier problem than being killed.
|
||||||
|
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
||||||
|
first_swap=$held
|
||||||
|
echo " - first swap page at ${held} MB — past here it works but crawls"
|
||||||
|
echo "swapat $held" >> "$STATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$TO_MB" ] && [ "$held" -ge "$TO_MB" ]; then
|
||||||
|
echo "stop reached-the-cap" >> "$STATE"; return 0
|
||||||
|
fi
|
||||||
|
if [ "$TO_OOM" = no ] && [ "$avail" -lt "$FLOOR_MB" ]; then
|
||||||
|
echo "stop floor" >> "$STATE"; return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
push() {
|
||||||
|
local total ceiling rc=0 last held rss swapat stop
|
||||||
|
total=$(mb MemTotal)
|
||||||
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
|
||||||
|
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
||||||
|
# find the edge, few enough lines to read, and small enough that the
|
||||||
|
# transient cost of one allocation never dominates a small box. A fixed
|
||||||
|
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
||||||
|
if [ "$STEP_EXPLICIT" = no ]; then
|
||||||
|
STEP_MB=$(( ceiling / 64 ))
|
||||||
|
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
||||||
|
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
||||||
|
# depends on what it is protecting. Under a cgroup cap, running out kills
|
||||||
|
# only this container's own processes, so it need cover no more than the
|
||||||
|
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
||||||
|
# halve the answer. On a host there is everything else to protect, and the
|
||||||
|
# OOM killer does not promise to pick the process that caused the problem.
|
||||||
|
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
||||||
|
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
||||||
|
|
||||||
|
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
||||||
|
trap cleanup EXIT
|
||||||
|
# INT kills the child and lets the summary below print anyway, so an
|
||||||
|
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
||||||
|
# still gives the memory back.
|
||||||
|
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
||||||
|
|
||||||
|
echo "push"
|
||||||
|
echo " step ${STEP_MB} MB per allocation, every page touched"
|
||||||
|
echo " ceiling ${ceiling} MB claimed"
|
||||||
|
if [ -n "$TO_MB" ]; then
|
||||||
|
echo " stopping at ${TO_MB} MB (--to)"
|
||||||
|
elif [ "$TO_OOM" = yes ]; then
|
||||||
|
echo " ! stopping only when the kernel stops it (--to-oom)"
|
||||||
|
echo " the allocating child is marked as the preferred OOM victim,"
|
||||||
|
echo " but nothing about an OOM kill is entirely polite. Not on a box"
|
||||||
|
echo " running anything you mind losing."
|
||||||
|
else
|
||||||
|
echo " stopping when headroom drops below ${FLOOR_MB} MB"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
allocator &
|
||||||
|
CHILD=$!
|
||||||
|
wait "$CHILD" || rc=$?
|
||||||
|
CHILD=""
|
||||||
|
trap - INT
|
||||||
|
|
||||||
|
last=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 || true)
|
||||||
|
held=$(echo "$last" | awk '{print $1}')
|
||||||
|
rss=$(echo "$last" | awk '{print $2}')
|
||||||
|
swapat=$(awk '/^swapat/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
||||||
|
stop=$(awk '/^stop/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ -z "$held" ]; then
|
||||||
|
echo " ! nothing was allocated. Even one ${STEP_MB} MB chunk failed —"
|
||||||
|
echo " try a smaller --step, or check ulimit -v in 'status'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " reached ${rss:-$held} MB resident"
|
||||||
|
[ -n "$swapat" ] && echo " swapping from ${swapat} MB"
|
||||||
|
|
||||||
|
case "$stop" in
|
||||||
|
reached-the-cap)
|
||||||
|
echo " outcome stopped at the --to cap, not at a limit."
|
||||||
|
echo " The box held ${TO_MB} MB without complaint; there is more." ;;
|
||||||
|
floor)
|
||||||
|
echo " outcome stopped with a cushion intact, by choice."
|
||||||
|
echo " The real ceiling is higher — --to-oom finds it, at the"
|
||||||
|
echo " cost of an actual OOM kill." ;;
|
||||||
|
interrupted)
|
||||||
|
echo " outcome interrupted at ${rss:-$held} MB — where you stopped it,"
|
||||||
|
echo " not where the box did." ;;
|
||||||
|
*)
|
||||||
|
# No stop line means the child did not decide to stop: it was ended.
|
||||||
|
if [ "$rc" -ge 128 ]; then
|
||||||
|
echo " outcome the child was killed (signal $((rc - 128))) at ${rss:-$held} MB."
|
||||||
|
elif [ "$rc" -ne 0 ]; then
|
||||||
|
echo " outcome the allocation failed at ${rss:-$held} MB (exit ${rc})."
|
||||||
|
echo " bash could not get the next chunk — an honest malloc"
|
||||||
|
echo " failure rather than a kill. That is the strict-overcommit"
|
||||||
|
echo " or ulimit path."
|
||||||
|
else
|
||||||
|
echo " outcome ended at ${rss:-$held} MB."
|
||||||
|
fi
|
||||||
|
local ev
|
||||||
|
ev=$(dmesg 2>/dev/null | tail -80 | grep -iE 'oom-kill|killed process' | tail -1 || true)
|
||||||
|
if [ -n "$ev" ]; then
|
||||||
|
echo " kernel ${ev#*] }"
|
||||||
|
else
|
||||||
|
echo " - dmesg is unreadable here (dmesg_restrict, or no privilege),"
|
||||||
|
echo " so the kill cannot be confirmed from this side. The number stands."
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The gap between the claim and the measurement is the finding — but only
|
||||||
|
# when the BOX chose where to stop. An empty $stop means the child was ended
|
||||||
|
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
||||||
|
# asked for, and flagging those as short of the ceiling would put a warning
|
||||||
|
# on every deliberately small run.
|
||||||
|
local got="${rss:-$held}"
|
||||||
|
echo
|
||||||
|
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
||||||
|
echo " ! claimed ${ceiling} MB, gave up ${got} MB — under 70% of it."
|
||||||
|
echo " Something is taking the difference. 'status' names the candidates:"
|
||||||
|
echo " a cgroup cap, ulimit -v, or memory already resident."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── all ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
all() {
|
||||||
|
status
|
||||||
|
echo
|
||||||
|
echo "────────────────────────────────────────────────────────────"
|
||||||
|
echo
|
||||||
|
push
|
||||||
|
|
||||||
|
local got budget_mb ceiling
|
||||||
|
load_config
|
||||||
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
|
budget_mb=$(( BUDGET_GB * 1024 ))
|
||||||
|
else
|
||||||
|
budget_mb=$(( NODES * NODE_MB ))
|
||||||
|
fi
|
||||||
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
||||||
|
[ -n "$got" ] || got=0
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "verdict"
|
||||||
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
|
echo " budget ${budget_mb} MB (--budget)"
|
||||||
|
else
|
||||||
|
# rig's own figure for this profile: nodes times what one node costs.
|
||||||
|
# Addons carry no memory figure in rig yet, so this is the cluster alone
|
||||||
|
# and whatever you deploy comes on top. --budget once you know that too.
|
||||||
|
echo " budget ${budget_mb} MB — profile ${PROFILE_NAME}: ${NODES} node(s) x ${NODE_MB} MB,"
|
||||||
|
echo " the cluster alone; your workload comes on top (--budget GB)"
|
||||||
|
fi
|
||||||
|
echo " measured ${got} MB handed over"
|
||||||
|
|
||||||
|
if [ "$got" -ge "$budget_mb" ]; then
|
||||||
|
echo " fits, with $(( got - budget_mb )) MB spare."
|
||||||
|
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
||||||
|
echo " - under 30% spare is thin once a workload runs on top: memory use"
|
||||||
|
echo " is spiky, and the spikes are what get killed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ! short by $(( budget_mb - got )) MB."
|
||||||
|
if [ "$ceiling" -ge "$budget_mb" ]; then
|
||||||
|
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
||||||
|
echo " Free something, or read the caps section again."
|
||||||
|
else
|
||||||
|
echo " The box does not have it to give. A bigger machine, or a profile"
|
||||||
|
echo " with fewer nodes."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
parse_flags() {
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) TO_MB=$(( ${2:?--to needs a value in GB} * 1024 )); shift 2 ;;
|
||||||
|
--to-mb) TO_MB="${2:?--to-mb needs a value in MB}"; shift 2 ;;
|
||||||
|
--step) STEP_MB="${2:?--step needs a value in MB}"; STEP_EXPLICIT=yes; shift 2 ;;
|
||||||
|
--to-oom) TO_OOM=yes; shift ;;
|
||||||
|
--budget) BUDGET_GB="${2:?--budget needs a value in GB}"; BUDGET_EXPLICIT=yes; shift 2 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ "$TO_OOM" = yes ] && [ -n "$TO_MB" ]; then
|
||||||
|
echo "--to and --to-oom contradict each other: one stops early, the other" >&2
|
||||||
|
echo "refuses to stop at all. Pick one." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
require_linux
|
||||||
|
find_cgroup
|
||||||
|
|
||||||
|
cmd="${1:-status}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
status) parse_flags "$@"; status ;;
|
||||||
|
push) parse_flags "$@"; push ;;
|
||||||
|
all) parse_flags "$@"; all ;;
|
||||||
|
backup) backup ;;
|
||||||
|
restore) restore ;;
|
||||||
|
*) echo "usage: $0 [status|push|all|backup|restore]" >&2
|
||||||
|
echo " push [--to GB] [--to-mb MB] [--step MB] [--to-oom]" >&2
|
||||||
|
echo " all [--budget GB]" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
23
rig/standalone/data/Makefile
Normal file
23
rig/standalone/data/Makefile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 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)
|
||||||
963
rig/standalone/data/rigdeps.sh
Executable file
963
rig/standalone/data/rigdeps.sh
Executable file
@@ -0,0 +1,963 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GENERATED by make standalone — do not edit
|
||||||
|
#
|
||||||
|
# rigdeps.sh for profile 'data', flattened from:
|
||||||
|
# ctrl/deps.sh
|
||||||
|
# ctrl/lib/config.sh
|
||||||
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
|
# ── from the libraries ──
|
||||||
|
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||||
|
_config_restore ()
|
||||||
|
{
|
||||||
|
local line;
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
eval "export $line";
|
||||||
|
fi;
|
||||||
|
done <<< "$1";
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
default_cluster_name ()
|
||||||
|
{
|
||||||
|
local n;
|
||||||
|
n=$(basename "$(cd .. && pwd)");
|
||||||
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
|
echo "${n:-rig}"
|
||||||
|
}
|
||||||
|
derive_port_base ()
|
||||||
|
{
|
||||||
|
local h;
|
||||||
|
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
||||||
|
echo $((20000 + (h % 200) * 10))
|
||||||
|
}
|
||||||
|
render_kind_config ()
|
||||||
|
{
|
||||||
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||||
|
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── configuration, frozen for profile 'data' ──
|
||||||
|
load_config() {
|
||||||
|
local k saved=""
|
||||||
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
|
done
|
||||||
|
declare -gx ADDONS="metallb postgres redis airflow"
|
||||||
|
declare -gx AIRFLOW_ADMIN_USER="admin"
|
||||||
|
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||||
|
declare -g AUDIT="off"
|
||||||
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
|
declare -g CLUSTER="rig"
|
||||||
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
|
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
||||||
|
declare -gx COMPOSE_VERSION="5.5.1"
|
||||||
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
|
declare -gx DATA_NAMESPACE="data"
|
||||||
|
declare -gx DNS_MODE="hosts"
|
||||||
|
declare -g HTTPS_PORT="20311"
|
||||||
|
declare -g HTTP_PORT="20310"
|
||||||
|
declare -gx INGRESS_MODE="hostport"
|
||||||
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
|
declare -gx K8S_VERSION="v1_36"
|
||||||
|
declare -gx KIND_CONFIG="kind-config.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_PATH="./k8s/kind-config.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.yaml.tpl"
|
||||||
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
|
declare -g KUBECONTEXT="kind-rig"
|
||||||
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
|
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||||
|
declare -gx METALLB_VERSION="v0.16.0"
|
||||||
|
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
|
declare -g NODES="1"
|
||||||
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
||||||
|
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
||||||
|
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||||
|
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -g NODE_MB="800"
|
||||||
|
declare -gx POSTGRES_DB="app"
|
||||||
|
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx POSTGRES_STORAGE="2Gi"
|
||||||
|
declare -gx POSTGRES_USER="app"
|
||||||
|
declare -gx PROFILE_NAME="data"
|
||||||
|
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||||
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
|
declare -gx REGISTRY_MODE="local"
|
||||||
|
declare -g REGISTRY_PORT="20313"
|
||||||
|
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||||
|
declare -g TILT_PORT="20312"
|
||||||
|
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||||
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
|
declare -gx TILT_VERSION="0.37.6"
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
|
# ── ctrl/deps.sh ──
|
||||||
|
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
||||||
|
# report what it could not do.
|
||||||
|
#
|
||||||
|
# It never runs the cluster, never uses sudo or apt, and writes only into
|
||||||
|
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
||||||
|
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
||||||
|
# decide on, never performed. That is what makes it safe to run on a machine that
|
||||||
|
# already has a working setup.
|
||||||
|
#
|
||||||
|
# Usage (normally via `make deps`, or directly):
|
||||||
|
# deps.sh detect # report host facts only, change nothing
|
||||||
|
# deps.sh list # the pinned versions
|
||||||
|
# deps.sh verify [core|dev] # run what is installed and see if it works
|
||||||
|
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||||
|
# deps.sh install [core|dev] # detect, fetch, install, report
|
||||||
|
#
|
||||||
|
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
||||||
|
# Default is dev.
|
||||||
|
#
|
||||||
|
# Runs both inside the installer container and bare on a host. Inside the
|
||||||
|
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
||||||
|
# falls back to /.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
||||||
|
# not against ctrl/ once we've moved.
|
||||||
|
INVOKED_FROM="$PWD"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Pins arrive through load_config like every other setting, not by sourcing
|
||||||
|
# versions.env here. That is what lets `make standalone` freeze them into a
|
||||||
|
# one-file installer: configuration has exactly one way in.
|
||||||
|
# (sourced library inlined above)
|
||||||
|
load_config
|
||||||
|
|
||||||
|
# Resolve a possibly-relative path against the caller's original directory.
|
||||||
|
abspath() {
|
||||||
|
case "$1" in
|
||||||
|
/*) echo "$1" ;;
|
||||||
|
*) echo "$INVOKED_FROM/$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||||
|
HOST_ROOT="${HOST_ROOT:-/}"
|
||||||
|
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||||
|
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||||
|
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||||
|
|
||||||
|
# Collected by detect(), printed by report_manual() at the very end.
|
||||||
|
MANUAL=()
|
||||||
|
|
||||||
|
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
||||||
|
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
||||||
|
# container's own view is already the host's.
|
||||||
|
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
||||||
|
# tight and does-not-fit branches can be exercised against a real machine's
|
||||||
|
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
||||||
|
mb_of() {
|
||||||
|
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||||
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
|
}
|
||||||
|
|
||||||
|
host_file() {
|
||||||
|
local p="${1#/}"
|
||||||
|
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
||||||
|
echo "$HOST_ROOT/$p"
|
||||||
|
else
|
||||||
|
echo "/$p"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── the tools this script itself needs ─────────────────────────────────────
|
||||||
|
|
||||||
|
arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) echo amd64 ;;
|
||||||
|
aarch64|arm64) echo arm64 ;;
|
||||||
|
*) uname -m ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The pins above are amd64. Rather than download something that cannot execute
|
||||||
|
# and let it fail as "cannot execute binary file: Exec format error", say so
|
||||||
|
# here and hand over the commands that produce the right checksums.
|
||||||
|
require_amd64() {
|
||||||
|
local a; a=$(arch)
|
||||||
|
[ "$a" = "amd64" ] && return 0
|
||||||
|
cat >&2 <<EOF
|
||||||
|
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
||||||
|
|
||||||
|
Nothing here would run, so it does not download. To make an ${a} version, the
|
||||||
|
URLs need the ${a} artifact and the checksums need to come from each project's
|
||||||
|
own published list — not from these values, and not from a download you did:
|
||||||
|
|
||||||
|
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
||||||
|
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
||||||
|
|
||||||
|
Edit the pinned block at the top of this file with what those print.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DL=""
|
||||||
|
pick_downloader() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then DL=curl
|
||||||
|
elif command -v wget >/dev/null 2>&1; then DL=wget
|
||||||
|
else
|
||||||
|
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
||||||
|
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
local url="$1" out="$2"
|
||||||
|
case "$DL" in
|
||||||
|
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
||||||
|
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
SHA=""
|
||||||
|
pick_sha() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
||||||
|
else
|
||||||
|
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
||||||
|
echo "Refusing to install unverified binaries." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── package manager, for the instructions only ─────────────────────────────
|
||||||
|
# This never runs a package manager. It names one so the reported action is
|
||||||
|
# something you can paste, on the distro you are actually on — an apt line on
|
||||||
|
# Amazon Linux 2 is a wrong answer dressed up as help.
|
||||||
|
|
||||||
|
pkg_install_cmd() {
|
||||||
|
local pkg="$1"
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
||||||
|
else echo "install '$pkg' with this system's package manager"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_pkg() {
|
||||||
|
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── detect ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
|
# the tooling. Detectable, so name it instead.
|
||||||
|
require_linux() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
|
|
||||||
|
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
wsl --install
|
||||||
|
|
||||||
|
That enables Windows features and needs a reboot, so it is not something this
|
||||||
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
|
this from there.
|
||||||
|
|
||||||
|
See "Starting from plain Windows" in README.md.
|
||||||
|
EOF
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
|
detect() {
|
||||||
|
echo "host"
|
||||||
|
echo " kernel $(uname -r)"
|
||||||
|
echo " arch $(arch) ($(uname -m))"
|
||||||
|
|
||||||
|
local osr; osr=$(host_file /etc/os-release)
|
||||||
|
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
||||||
|
|
||||||
|
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
||||||
|
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
||||||
|
# that is enough depends on the profile, which check.sh knows and this does not.
|
||||||
|
local total_mb avail_mb swap_total_mb swap_used_mb om
|
||||||
|
total_mb=$(mb_of MemTotal)
|
||||||
|
avail_mb=$(mb_of MemAvailable)
|
||||||
|
swap_total_mb=$(mb_of SwapTotal)
|
||||||
|
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
||||||
|
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
||||||
|
if [ "$swap_total_mb" -gt 0 ]; then
|
||||||
|
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
||||||
|
# always says yes and settles up later with the OOM killer, so a cluster that
|
||||||
|
# starts cleanly can still lose processes afterwards.
|
||||||
|
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||||
|
case "$om" in
|
||||||
|
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||||
|
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||||
|
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo " install to $OUT_BIN"
|
||||||
|
detect_libc
|
||||||
|
detect_prereqs
|
||||||
|
detect_wsl
|
||||||
|
detect_filesystem
|
||||||
|
detect_docker
|
||||||
|
detect_inotify
|
||||||
|
detect_toolchain
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_wsl() {
|
||||||
|
if ! is_wsl; then
|
||||||
|
echo " platform native linux"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " platform WSL"
|
||||||
|
|
||||||
|
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
||||||
|
# host service need it. Enabling it requires a Windows-side restart, which
|
||||||
|
# cannot be issued from inside the distro.
|
||||||
|
local wc; wc=$(host_file /etc/wsl.conf)
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||||
|
echo " systemd enabled in wsl.conf"
|
||||||
|
else
|
||||||
|
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||||
|
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||||
|
[boot]
|
||||||
|
systemd=true
|
||||||
|
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||||
|
# local DNS setup.
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||||
|
echo " resolv.conf pinned (generateResolvConf=false)"
|
||||||
|
else
|
||||||
|
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local wcfg
|
||||||
|
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
|
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||||
|
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||||
|
else
|
||||||
|
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||||
|
make mem status
|
||||||
|
It prints the edit to make and the command to apply it.")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
||||||
|
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
||||||
|
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
||||||
|
# way. None of them deliver inotify events, so anything watching files goes
|
||||||
|
# quiet without saying why.
|
||||||
|
watch_hostile_fs() {
|
||||||
|
local dir="$1" fstype
|
||||||
|
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||||
|
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
||||||
|
case "$fstype" in
|
||||||
|
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_filesystem() {
|
||||||
|
local root fstype
|
||||||
|
root=$(cd .. && pwd -P)
|
||||||
|
fstype=$(watch_hostile_fs "$root")
|
||||||
|
if [ -n "$fstype" ]; then
|
||||||
|
echo " ! this directory is on $fstype — file watching will not work"
|
||||||
|
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
||||||
|
on a $fstype mount, and everything else is slower:
|
||||||
|
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||||
|
else
|
||||||
|
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
||||||
|
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
||||||
|
#
|
||||||
|
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
||||||
|
#
|
||||||
|
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
||||||
|
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
||||||
|
# version now; `verify` catches the actual failure after installing.
|
||||||
|
detect_libc() {
|
||||||
|
local v=""
|
||||||
|
if command -v ldd >/dev/null 2>&1; then
|
||||||
|
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$v" ]; then
|
||||||
|
echo " libc unknown (no ldd) — 'verify' is the real test"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo " libc glibc $v"
|
||||||
|
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
||||||
|
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
||||||
|
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
||||||
|
echo " Install the core tier, or run tilt from a container."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# What this script needs to do its own job. Reported here so `detect` answers
|
||||||
|
# "will install work?" instead of leaving you to find out one download in.
|
||||||
|
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
||||||
|
detect_prereqs() {
|
||||||
|
local missing=""
|
||||||
|
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
||||||
|
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
||||||
|
echo " checksums ok"
|
||||||
|
else
|
||||||
|
echo " ! no sha256sum or shasum — downloads could not be verified"
|
||||||
|
missing+=" coreutils"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
||||||
|
echo " archives tar + gzip"
|
||||||
|
else
|
||||||
|
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
||||||
|
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
||||||
|
missing+=" tar gzip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
MANUAL+=("Install what this script needs to run at all:
|
||||||
|
$(pkg_install_cmd "${missing# }")")
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_docker() {
|
||||||
|
# Reachability of the daemon is the real question, and the CLI is only how
|
||||||
|
# we ask it. Note that when this runs inside the installer container, Docker
|
||||||
|
# necessarily exists on the host — otherwise nothing would be executing —
|
||||||
|
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
echo " docker socket present (no cli in this context)"
|
||||||
|
else
|
||||||
|
echo " ! docker not found and no socket at /var/run/docker.sock"
|
||||||
|
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
||||||
|
that needs root:
|
||||||
|
$(pkg_install_cmd "$(docker_pkg)")
|
||||||
|
sudo systemctl enable --now docker
|
||||||
|
sudo usermod -aG docker \"\$USER\"
|
||||||
|
then log out and back in, so the new group applies to your shell.")
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||||
|
local n
|
||||||
|
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||||
|
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
||||||
|
# function the latter returns 1 when the count is zero, and `set -e`
|
||||||
|
# then kills the caller. That is the fresh-machine case — no clusters
|
||||||
|
# yet — so the bug only ever shows up where it does most harm.
|
||||||
|
if [ "$n" -gt 0 ]; then
|
||||||
|
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
|
MANUAL+=("Start Docker, or add yourself to the docker group:
|
||||||
|
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
||||||
|
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
||||||
|
detect_inotify() {
|
||||||
|
local w i
|
||||||
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
|
echo " inotify watches=$w instances=$i"
|
||||||
|
|
||||||
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
|
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||||
|
MANUAL+=("Raise inotify limits (needs root on the host):
|
||||||
|
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
||||||
|
| sudo tee /etc/sysctl.d/99-rig.conf
|
||||||
|
sudo sysctl --system")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── fetch ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
||||||
|
resolve_url() {
|
||||||
|
local upstream="$1"
|
||||||
|
case "$DEPS_SOURCE" in
|
||||||
|
upstream) echo "$upstream" ;;
|
||||||
|
artifactory)
|
||||||
|
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
||||||
|
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
||||||
|
;;
|
||||||
|
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
local file="$1" want="$2" name="$3" got
|
||||||
|
got=$($SHA "$file" | awk '{print $1}')
|
||||||
|
if [ "$got" != "$want" ]; then
|
||||||
|
echo "checksum mismatch for $name" >&2
|
||||||
|
echo " expected $want" >&2
|
||||||
|
echo " got $got" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
||||||
|
fetch_bin() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4"
|
||||||
|
local tmp="$dest/.$name.tmp"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
mv "$tmp" "$dest/$name"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
||||||
|
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
||||||
|
# others nest it a directory down — so the caller says which.
|
||||||
|
fetch_tgz() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
||||||
|
local tmp="$dest/.$name.tgz"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
||||||
|
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
||||||
|
# user does not own.
|
||||||
|
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||||
|
rm -f "$tmp"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The installer runs as root so it can reach the docker socket, which means
|
||||||
|
# everything it writes into a mounted volume lands root-owned and unusable from
|
||||||
|
# the host. Hand it back to whoever owns the mount point (the host user created
|
||||||
|
# that directory before mounting it).
|
||||||
|
fix_ownership() {
|
||||||
|
local dir="$1"
|
||||||
|
[ -d "$dir" ] || return 0
|
||||||
|
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
||||||
|
if [ "$owner" = ":" ]; then
|
||||||
|
owner=$(stat -c '%u:%g' "$dir")
|
||||||
|
fi
|
||||||
|
[ "$owner" = "0:0" ] && return 0
|
||||||
|
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Two tiers, because not every machine should get cluster tooling.
|
||||||
|
#
|
||||||
|
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
||||||
|
# creates one. Appropriate on a managed or corporate-issued machine
|
||||||
|
# where development tools are not wanted by default.
|
||||||
|
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
||||||
|
#
|
||||||
|
# The split exists because "install the toolchain" is not one decision: on a
|
||||||
|
# managed workspace the right answer is kubectl and nothing else.
|
||||||
|
CORE_TOOLS="kubectl jq"
|
||||||
|
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
||||||
|
# has ever invoked it. Add it back the day something actually needs a chart.
|
||||||
|
#
|
||||||
|
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
||||||
|
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
||||||
|
# because it is what wires a cluster to a local registry — without one, an
|
||||||
|
# unqualified image name resolves to docker.io/library/<name> and there is
|
||||||
|
# nothing structural stopping a push there.
|
||||||
|
#
|
||||||
|
# docker-compose is 'dev' for the same reason, and is here because the distro
|
||||||
|
# docker packages ship the daemon and CLI but frequently not the compose
|
||||||
|
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
||||||
|
# working Docker, and nothing about that message names the missing piece.
|
||||||
|
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
||||||
|
|
||||||
|
# ── what is already on this machine ───────────────────────────────────────
|
||||||
|
#
|
||||||
|
# A tool already on PATH at its pinned version is left where it is. Without
|
||||||
|
# this, install downloads a second copy into OUT_BIN and then reports the first
|
||||||
|
# one as shadowed — noise, and wrong, when both are the same version. That is
|
||||||
|
# the normal state of any machine someone set up by hand, whatever directory
|
||||||
|
# they happened to choose.
|
||||||
|
|
||||||
|
pin_of() {
|
||||||
|
case "$1" in
|
||||||
|
kubectl) echo "$KUBECTL_VERSION" ;;
|
||||||
|
jq) echo "$JQ_VERSION" ;;
|
||||||
|
kind) echo "$KIND_VERSION" ;;
|
||||||
|
tilt) echo "$TILT_VERSION" ;;
|
||||||
|
ctlptl) echo "$CTLPTL_VERSION" ;;
|
||||||
|
docker-compose) echo "$COMPOSE_VERSION" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The version string a binary reports. Each tool spells the question
|
||||||
|
# differently, and kubectl has to be told --client or it goes looking for a
|
||||||
|
# server to ask.
|
||||||
|
reported_version() {
|
||||||
|
local tool="$1" path="$2"
|
||||||
|
case "$tool" in
|
||||||
|
kubectl) "$path" version --client 2>/dev/null ;;
|
||||||
|
jq) "$path" --version 2>/dev/null ;;
|
||||||
|
*) "$path" version 2>/dev/null ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
||||||
|
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
||||||
|
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
||||||
|
#
|
||||||
|
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
||||||
|
# on every machine — some builds reject patterns that others accept — and a
|
||||||
|
# failed grep inside a count reads exactly like a zero.
|
||||||
|
version_matches() {
|
||||||
|
local tool="$1" path="$2" pin="$3" out v re
|
||||||
|
out=$(reported_version "$tool" "$path") || return 1
|
||||||
|
v="${pin#v}"
|
||||||
|
v="${v//./\\.}"
|
||||||
|
re="(^|[^0-9.])v?${v}([^0-9.]|\$)"
|
||||||
|
[[ $out =~ $re ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
||||||
|
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
||||||
|
# DIR" must not quietly skip something because this machine happens to have it.
|
||||||
|
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
||||||
|
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
||||||
|
|
||||||
|
# Every tool in the tier with its state, probed once and reported once. What
|
||||||
|
# still needs fetching is left in TOOLCHAIN_NEED for install() to act on.
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
detect_toolchain() {
|
||||||
|
local tier="${TIER:-dev}" b pin path found
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
echo
|
||||||
|
echo "toolchain (pinned, tier '$tier')"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
pin=$(pin_of "$b")
|
||||||
|
path=$(command -v "$b" 2>/dev/null || true)
|
||||||
|
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
||||||
|
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
||||||
|
# has no `docker-compose` to find — and probing only PATH would report it
|
||||||
|
# missing and re-download a copy that is already there. That is the exact
|
||||||
|
# noise the version-aware skip exists to prevent, so ask docker instead.
|
||||||
|
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
||||||
|
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
||||||
|
if [ "${found#v}" = "${pin#v}" ]; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
||||||
|
else
|
||||||
|
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
||||||
|
"$b" "$pin" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
elif version_matches "$b" "$path" "$pin"; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
||||||
|
else
|
||||||
|
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
||||||
|
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo " every pinned tool is already on PATH — nothing to fetch"
|
||||||
|
else
|
||||||
|
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) dest="$2"; shift 2 ;;
|
||||||
|
core|dev) tier="$1"; shift ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
dest="$(abspath "$dest")"
|
||||||
|
mkdir -p "$dest"
|
||||||
|
TIER="$tier"
|
||||||
|
|
||||||
|
if [ "$DEPS_SOURCE" = "baked" ]; then
|
||||||
|
echo "installing baked binaries from $BAKED_BIN"
|
||||||
|
cp -a "$BAKED_BIN"/. "$dest"/
|
||||||
|
fix_ownership "$dest"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${DEPS_ONLY:-}" ]; then
|
||||||
|
echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)"
|
||||||
|
else
|
||||||
|
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
||||||
|
fi
|
||||||
|
if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi
|
||||||
|
if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi
|
||||||
|
if [ "$tier" = "dev" ]; then
|
||||||
|
if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi
|
||||||
|
if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi
|
||||||
|
if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi
|
||||||
|
if want docker-compose; then
|
||||||
|
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
fix_ownership "$dest"
|
||||||
|
# kind writes the kubeconfig as root too; hand that back as well when it's
|
||||||
|
# a mounted host directory rather than container-local state.
|
||||||
|
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── install ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
report_manual() {
|
||||||
|
echo
|
||||||
|
if [ ${#MANUAL[@]} -eq 0 ]; then
|
||||||
|
echo "nothing left to do by hand."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
||||||
|
echo
|
||||||
|
local n=1
|
||||||
|
for m in "${MANUAL[@]}"; do
|
||||||
|
echo " $n. $m"
|
||||||
|
echo
|
||||||
|
n=$((n + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installing into a directory that sits early in PATH silently replaces whatever
|
||||||
|
# the machine was already using — which on a shared or client machine can break
|
||||||
|
# unrelated work (kubectl more than one minor away from a cluster is the common
|
||||||
|
# one). Say so; never decide it for them.
|
||||||
|
# Downloading a verified binary proves it is the right file, not that this
|
||||||
|
# machine can run it. On an old distro tilt fails here, with a linker error
|
||||||
|
# about a missing symbol, and finding that out now beats finding out during a
|
||||||
|
# first cluster build.
|
||||||
|
verify_tools() {
|
||||||
|
local tier="${1:-dev}" b bin out rc broke=0
|
||||||
|
echo "checking that each one actually runs"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
bin="$OUT_BIN/$b"
|
||||||
|
if [ ! -x "$bin" ]; then
|
||||||
|
printf ' %-14s not installed\n' "$b"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
||||||
|
# than one line gets SIGPIPE when head closes the pipe, and the
|
||||||
|
# pipeline reports 141 — so a working kubectl was announced as "does
|
||||||
|
# not run here", with its own correct version string as the evidence.
|
||||||
|
# Take the first line afterwards, from the string.
|
||||||
|
rc=0
|
||||||
|
case "$b" in
|
||||||
|
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
||||||
|
jq) out=$("$bin" --version 2>&1) || rc=$? ;;
|
||||||
|
*) out=$("$bin" version 2>&1) || rc=$? ;;
|
||||||
|
esac
|
||||||
|
out=${out%%$'\n'*}
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
printf ' %-14s %s\n' "$b" "$out"
|
||||||
|
else
|
||||||
|
printf ' ! %-12s does not run here: %s\n' "$b" "$out"
|
||||||
|
broke=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$broke" -eq 1 ]; then
|
||||||
|
echo
|
||||||
|
echo " A binary that downloads and verifies but will not start is almost"
|
||||||
|
echo " always this distro's libc being older than the release needs."
|
||||||
|
echo " 'detect' prints the glibc version. The core tier (kubectl + jq)"
|
||||||
|
echo " has no such dependency and will work regardless."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
echo "pinned, linux/amd64 only:"
|
||||||
|
printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' jq "$JQ_VERSION"
|
||||||
|
printf ' %-14s %s\n' kind "$KIND_VERSION"
|
||||||
|
printf ' %-14s %s\n' tilt "$TILT_VERSION"
|
||||||
|
printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION"
|
||||||
|
echo
|
||||||
|
echo " core = $CORE_TOOLS"
|
||||||
|
echo " dev = $CORE_TOOLS $DEV_TOOLS"
|
||||||
|
echo
|
||||||
|
echo "Checksums are pinned in the block at the top of this file. To bump one,"
|
||||||
|
echo "take the new checksum from the publisher's own release list — the header"
|
||||||
|
echo "comment has the exact commands."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
||||||
|
|
||||||
|
warn_shadowing() {
|
||||||
|
local b existing shadowed="" tier="${1:-dev}"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
[ -x "$OUT_BIN/$b" ] || continue
|
||||||
|
# Where would this resolve if OUT_BIN weren't in the way?
|
||||||
|
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
||||||
|
command -v "$b" 2>/dev/null || true)
|
||||||
|
[ -n "$existing" ] || continue
|
||||||
|
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
||||||
|
# The same version in both places is not a conflict: nothing changes for
|
||||||
|
# any other project whichever copy PATH happens to find first.
|
||||||
|
if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi
|
||||||
|
shadowed+=" $b $existing"$'\n'
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$shadowed" ] || return 0
|
||||||
|
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
||||||
|
printf '%s' "$shadowed"
|
||||||
|
echo " Other projects on this machine will pick up the new versions."
|
||||||
|
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
||||||
|
was just installed:
|
||||||
|
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
||||||
|
Or install somewhere private instead:
|
||||||
|
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
||||||
|
# retired v1 spelling; every compose file written in the last few years assumes
|
||||||
|
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
||||||
|
# So the binary is fetched like any other and then linked, in your own home —
|
||||||
|
# no root, and nothing outside it.
|
||||||
|
install_compose_plugin() {
|
||||||
|
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
||||||
|
[ -x "$src" ] || return 0
|
||||||
|
mkdir -p "$dir"
|
||||||
|
# Something else already owns that name — docker-desktop and some distro
|
||||||
|
# packages install a real file there. Overwriting it would take the plugin
|
||||||
|
# away from whatever put it there, so say so and let the user decide.
|
||||||
|
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
||||||
|
MANUAL+=("Something already installs the compose plugin at
|
||||||
|
$dir/docker-compose
|
||||||
|
To use rig's pinned build instead:
|
||||||
|
ln -sf $src $dir/docker-compose")
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ln -sfn "$src" "$dir/docker-compose"
|
||||||
|
echo " compose plugin -> $dir/docker-compose"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
local tier="${1:-dev}" b
|
||||||
|
TIER="$tier"
|
||||||
|
detect
|
||||||
|
|
||||||
|
# detect_toolchain has already probed PATH. Fetch only what it found missing
|
||||||
|
# or at the wrong version; a tool already present at its pin stays where it is.
|
||||||
|
if [ -n "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo
|
||||||
|
DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier"
|
||||||
|
echo
|
||||||
|
echo "installed to $OUT_BIN ($tier):"
|
||||||
|
for b in $TOOLCHAIN_NEED; do
|
||||||
|
if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi
|
||||||
|
done
|
||||||
|
if [ "$tier" = "core" ]; then
|
||||||
|
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||||
|
fi
|
||||||
|
# Only when compose was one of the things fetched: linking a binary
|
||||||
|
# that is already satisfied elsewhere on PATH would point the plugin at
|
||||||
|
# a copy rig did not install.
|
||||||
|
case " $TOOLCHAIN_NEED " in
|
||||||
|
*" docker-compose "*) install_compose_plugin ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Only worth saying when something actually landed in OUT_BIN. When every
|
||||||
|
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
||||||
|
# telling the user to add it would be advice to fix nothing.
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||||
|
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
warn_shadowing "$tier"
|
||||||
|
|
||||||
|
report_manual
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_linux
|
||||||
|
|
||||||
|
# Read the command, THEN shift — and shift only if there is something there.
|
||||||
|
# A bare `shift` with no positional parameters returns 1, and under `set -e`
|
||||||
|
# that ended the script before a single line was printed: running this with no
|
||||||
|
# arguments at all, the documented default, did nothing and said nothing.
|
||||||
|
cmd="${1:-install}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||||
|
need_downloads() {
|
||||||
|
require_amd64
|
||||||
|
if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi
|
||||||
|
pick_sha
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
detect) detect; report_manual ;;
|
||||||
|
list) list ;;
|
||||||
|
verify) verify_tools "${1:-dev}" ;;
|
||||||
|
fetch) need_downloads; fetch "$@" ;;
|
||||||
|
install) need_downloads; install "${1:-dev}" ;;
|
||||||
|
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
||||||
|
echo " install [core|dev] (default dev)" >&2
|
||||||
|
echo " fetch [core|dev] [--to DIR]" >&2
|
||||||
|
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
864
rig/standalone/data/rigmini.sh
Executable file
864
rig/standalone/data/rigmini.sh
Executable file
@@ -0,0 +1,864 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GENERATED by make standalone — do not edit
|
||||||
|
#
|
||||||
|
# rigmini.sh for profile 'data', flattened from:
|
||||||
|
# ctrl/mem.sh
|
||||||
|
# ctrl/lib/config.sh
|
||||||
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
|
# ── from the libraries ──
|
||||||
|
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||||
|
_config_restore ()
|
||||||
|
{
|
||||||
|
local line;
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
eval "export $line";
|
||||||
|
fi;
|
||||||
|
done <<< "$1";
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
default_cluster_name ()
|
||||||
|
{
|
||||||
|
local n;
|
||||||
|
n=$(basename "$(cd .. && pwd)");
|
||||||
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
|
echo "${n:-rig}"
|
||||||
|
}
|
||||||
|
derive_port_base ()
|
||||||
|
{
|
||||||
|
local h;
|
||||||
|
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
||||||
|
echo $((20000 + (h % 200) * 10))
|
||||||
|
}
|
||||||
|
render_kind_config ()
|
||||||
|
{
|
||||||
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||||
|
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── configuration, frozen for profile 'data' ──
|
||||||
|
load_config() {
|
||||||
|
local k saved=""
|
||||||
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
|
done
|
||||||
|
declare -gx ADDONS="metallb postgres redis airflow"
|
||||||
|
declare -gx AIRFLOW_ADMIN_USER="admin"
|
||||||
|
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||||
|
declare -g AUDIT="off"
|
||||||
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
|
declare -g CLUSTER="rig"
|
||||||
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
|
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
||||||
|
declare -gx COMPOSE_VERSION="5.5.1"
|
||||||
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
|
declare -gx DATA_NAMESPACE="data"
|
||||||
|
declare -gx DNS_MODE="hosts"
|
||||||
|
declare -g HTTPS_PORT="20311"
|
||||||
|
declare -g HTTP_PORT="20310"
|
||||||
|
declare -gx INGRESS_MODE="hostport"
|
||||||
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
|
declare -gx K8S_VERSION="v1_36"
|
||||||
|
declare -gx KIND_CONFIG="kind-config.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_PATH="./k8s/kind-config.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.yaml.tpl"
|
||||||
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
|
declare -g KUBECONTEXT="kind-rig"
|
||||||
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
|
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||||
|
declare -gx METALLB_VERSION="v0.16.0"
|
||||||
|
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
|
declare -g NODES="1"
|
||||||
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
||||||
|
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
||||||
|
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||||
|
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -g NODE_MB="800"
|
||||||
|
declare -gx POSTGRES_DB="app"
|
||||||
|
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx POSTGRES_STORAGE="2Gi"
|
||||||
|
declare -gx POSTGRES_USER="app"
|
||||||
|
declare -gx PROFILE_NAME="data"
|
||||||
|
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||||
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
|
declare -gx REGISTRY_MODE="local"
|
||||||
|
declare -g REGISTRY_PORT="20313"
|
||||||
|
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||||
|
declare -g TILT_PORT="20312"
|
||||||
|
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||||
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
|
declare -gx TILT_VERSION="0.37.6"
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
|
# ── ctrl/mem.sh ──
|
||||||
|
# How much memory this machine will actually give you before something dies —
|
||||||
|
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
||||||
|
#
|
||||||
|
# There are two numbers and they are rarely the same. `status` reports what the
|
||||||
|
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
||||||
|
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
||||||
|
# against what this profile's cluster needs.
|
||||||
|
#
|
||||||
|
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
||||||
|
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
||||||
|
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
||||||
|
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
||||||
|
# on a box that OOMs at 2.
|
||||||
|
#
|
||||||
|
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
||||||
|
# allocation that can be raised, and the commonest failure is raising it without
|
||||||
|
# restarting — so status compares what .wslconfig says with what actually booted.
|
||||||
|
#
|
||||||
|
# Reports and instructs. It never raises a limit, frees anything or installs a
|
||||||
|
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
||||||
|
# itself, so that `restore` has something to put back after a hand edit.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# mem.sh status what it has, what caps it
|
||||||
|
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
||||||
|
# mem.sh all [--budget GB] both, then the verdict
|
||||||
|
# mem.sh backup | restore .wslconfig, WSL only
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
# (sourced library inlined above)
|
||||||
|
|
||||||
|
# ── defaults ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
STEP_MB=0 # per allocation; 0 means scale it to the ceiling. See push().
|
||||||
|
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
||||||
|
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
||||||
|
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
||||||
|
BUDGET_GB="" # --budget; empty means what this profile's cluster needs, from rig.
|
||||||
|
BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess below.
|
||||||
|
|
||||||
|
# ── platform ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
|
# the tooling. Detectable, so name it instead.
|
||||||
|
require_linux() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
|
|
||||||
|
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
wsl --install
|
||||||
|
|
||||||
|
That enables Windows features and needs a reboot, so it is not something this
|
||||||
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
|
this from there.
|
||||||
|
EOF
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Everything below reads /proc. Without it there is nothing to measure, and
|
||||||
|
# failing here beats printing a page of empty fields.
|
||||||
|
if [ ! -r /proc/meminfo ]; then
|
||||||
|
echo "no readable /proc/meminfo — this needs a Linux kernel." >&2
|
||||||
|
echo "On macOS or a BSD none of the numbers below exist." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
|
is_container() {
|
||||||
|
[ -f /.dockerenv ] && return 0
|
||||||
|
grep -qE '(docker|containerd|kubepods|lxc|podman)' /proc/1/cgroup 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
platform() {
|
||||||
|
if is_wsl; then echo WSL
|
||||||
|
elif is_container; then echo container
|
||||||
|
else echo "native linux"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── reading memory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
||||||
|
|
||||||
|
# MemAvailable arrived in kernel 3.14. Older kernels — and they turn up on
|
||||||
|
# corporate images — need the estimate it replaced, which is worse but not wrong.
|
||||||
|
avail_meminfo_mb() {
|
||||||
|
if grep -q '^MemAvailable:' /proc/meminfo; then
|
||||||
|
mb MemAvailable
|
||||||
|
else
|
||||||
|
awk '/^(MemFree|Buffers|Cached):/{t+=$2} END{print int(t/1024)}' /proc/meminfo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
||||||
|
# find_cgroup, because every later reading needs both and hunting for the files
|
||||||
|
# on each call would be the slow part of the poll loop.
|
||||||
|
CG_MAX_FILE=""
|
||||||
|
CG_CUR_FILE=""
|
||||||
|
CG_VERSION=""
|
||||||
|
|
||||||
|
find_cgroup() {
|
||||||
|
local rel
|
||||||
|
|
||||||
|
# Inside a container the cgroup namespace makes the top of the tree BE the
|
||||||
|
# container's own cgroup, so the unqualified path is already the right one.
|
||||||
|
# On a host it is the root cgroup, which is never limited — hence the second
|
||||||
|
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
||||||
|
if [ -r /sys/fs/cgroup/memory.max ]; then
|
||||||
|
CG_VERSION=v2
|
||||||
|
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
||||||
|
CG_CUR_FILE=/sys/fs/cgroup/memory.current
|
||||||
|
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
|
||||||
|
CG_VERSION=v1
|
||||||
|
CG_MAX_FILE=/sys/fs/cgroup/memory/memory.limit_in_bytes
|
||||||
|
CG_CUR_FILE=/sys/fs/cgroup/memory/memory.usage_in_bytes
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel=$(awk -F: '$1=="0"{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
||||||
|
if [ -n "$rel" ] && [ "$rel" != "/" ] && [ -r "/sys/fs/cgroup${rel}/memory.max" ]; then
|
||||||
|
CG_VERSION=v2
|
||||||
|
CG_MAX_FILE="/sys/fs/cgroup${rel}/memory.max"
|
||||||
|
CG_CUR_FILE="/sys/fs/cgroup${rel}/memory.current"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel=$(awk -F: '$2 ~ /(^|,)memory(,|$)/{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
||||||
|
if [ -n "$rel" ] && [ "$rel" != "/" ] \
|
||||||
|
&& [ -r "/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes" ]; then
|
||||||
|
CG_VERSION=v1
|
||||||
|
CG_MAX_FILE="/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes"
|
||||||
|
CG_CUR_FILE="/sys/fs/cgroup/memory${rel}/memory.usage_in_bytes"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
||||||
|
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
||||||
|
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
||||||
|
# own memory is not a limit, however it is written.
|
||||||
|
cgroup_cap_mb() {
|
||||||
|
local raw cap
|
||||||
|
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
||||||
|
raw=$(cat "$CG_MAX_FILE" 2>/dev/null || echo max)
|
||||||
|
[ "$raw" = "max" ] && { echo ""; return 0; }
|
||||||
|
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
cap=$((raw / 1024 / 1024))
|
||||||
|
[ "$cap" -ge "$(mb MemTotal)" ] && { echo ""; return 0; }
|
||||||
|
echo "$cap"
|
||||||
|
}
|
||||||
|
|
||||||
|
cgroup_used_mb() {
|
||||||
|
local raw
|
||||||
|
[ -n "$CG_CUR_FILE" ] && [ -r "$CG_CUR_FILE" ] || { echo ""; return 0; }
|
||||||
|
raw=$(cat "$CG_CUR_FILE" 2>/dev/null || echo "")
|
||||||
|
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
echo $((raw / 1024 / 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
# ulimit -v is a per-process address-space cap. It stops YOU long before the box
|
||||||
|
# does, and because it is inherited from a login shell it is easy to hit without
|
||||||
|
# knowing it is set.
|
||||||
|
ulimit_v_mb() {
|
||||||
|
local v; v=$(ulimit -v 2>/dev/null || echo unlimited)
|
||||||
|
[ "$v" = "unlimited" ] && { echo ""; return 0; }
|
||||||
|
case "$v" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
echo $((v / 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
# The number everything else is about: the lowest of the things that can stop
|
||||||
|
# you. Printed at the end of `status` and used as the sanity bound in `push`.
|
||||||
|
effective_ceiling_mb() {
|
||||||
|
local c; c=$(mb MemTotal)
|
||||||
|
local cap; cap=$(cgroup_cap_mb)
|
||||||
|
local ul; ul=$(ulimit_v_mb)
|
||||||
|
[ -n "$cap" ] && [ "$cap" -lt "$c" ] && c="$cap"
|
||||||
|
[ -n "$ul" ] && [ "$ul" -lt "$c" ] && c="$ul"
|
||||||
|
echo "$c"
|
||||||
|
}
|
||||||
|
|
||||||
|
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
||||||
|
# In a capped container /proc/meminfo describes the host and is worse than
|
||||||
|
# useless for this — it would report tens of gigabytes free on a box that is one
|
||||||
|
# allocation from being killed.
|
||||||
|
headroom_mb() {
|
||||||
|
local cap used
|
||||||
|
cap=$(cgroup_cap_mb)
|
||||||
|
used=$(cgroup_used_mb)
|
||||||
|
if [ -n "$cap" ] && [ -n "$used" ]; then
|
||||||
|
echo $(( cap - used ))
|
||||||
|
else
|
||||||
|
avail_meminfo_mb
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── status ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
||||||
|
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
||||||
|
# Windows, then fall back to whichever profile actually owns a config.
|
||||||
|
wslconfig_path() {
|
||||||
|
local profile winpath found
|
||||||
|
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
||||||
|
case "$profile" in
|
||||||
|
""|*%*) ;;
|
||||||
|
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
||||||
|
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
||||||
|
echo "$winpath/.wslconfig"; return 0
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$found" ] && echo "$found"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
hogs() {
|
||||||
|
echo " holding the most:"
|
||||||
|
ps -eo rss,comm --sort=-rss 2>/dev/null \
|
||||||
|
| awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
local total avail swap_total swap_free cap ul cur
|
||||||
|
|
||||||
|
echo "host"
|
||||||
|
echo " platform $(platform)"
|
||||||
|
echo " kernel $(uname -r)"
|
||||||
|
[ -r /etc/os-release ] && \
|
||||||
|
echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' /etc/os-release)"
|
||||||
|
echo " cpu $(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '?') online, load $(cut -d' ' -f1-3 /proc/loadavg)"
|
||||||
|
|
||||||
|
# ── the caps first, because they decide what the totals below are worth ──
|
||||||
|
echo
|
||||||
|
echo "caps"
|
||||||
|
cap=$(cgroup_cap_mb)
|
||||||
|
if [ -n "$cap" ]; then
|
||||||
|
cur=$(cgroup_used_mb)
|
||||||
|
echo " cgroup ${cap} MB (${CG_VERSION}, ${CG_CUR_FILE##*/} says ${cur:-?} MB used)"
|
||||||
|
echo " ! /proc/meminfo below describes the HOST, not this cgroup."
|
||||||
|
echo " $(mb MemTotal) MB total is not yours; ${cap} MB is."
|
||||||
|
elif [ -n "$CG_VERSION" ]; then
|
||||||
|
echo " cgroup none (${CG_VERSION} present, no memory limit set)"
|
||||||
|
else
|
||||||
|
echo " cgroup no memory controller found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ul=$(ulimit_v_mb)
|
||||||
|
if [ -n "$ul" ]; then
|
||||||
|
echo " ! ulimit -v ${ul} MB — a per-process cap, inherited from your shell"
|
||||||
|
echo " it stops this process long before the machine runs out"
|
||||||
|
else
|
||||||
|
echo " ulimit -v unlimited"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# overcommit_memory=0 is the default heuristic: a large allocation is
|
||||||
|
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
||||||
|
# than as a failed malloc. It is why `push` touches every page it asks for.
|
||||||
|
local om or_
|
||||||
|
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
||||||
|
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
||||||
|
case "$om" in
|
||||||
|
0) echo " overcommit 0 heuristic — allocations are granted on a guess," ;;
|
||||||
|
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit," ;;
|
||||||
|
2) echo " overcommit 2 strict (ratio ${or_}%) — allocation fails honestly instead of killing later," ;;
|
||||||
|
*) echo " overcommit ${om}" ;;
|
||||||
|
esac
|
||||||
|
[ "$om" != "?" ] && echo " so RSS is the number to trust, not what a process asked for"
|
||||||
|
|
||||||
|
# ── what it says it has ──
|
||||||
|
total=$(mb MemTotal); avail=$(avail_meminfo_mb)
|
||||||
|
swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree)
|
||||||
|
echo
|
||||||
|
echo "memory"
|
||||||
|
echo " total ${total} MB"
|
||||||
|
echo " available ${avail} MB"
|
||||||
|
echo " swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
||||||
|
if [ "$swap_total" -eq 0 ]; then
|
||||||
|
echo " - no swap: this box has no cushion. It goes from fine to OOM-killed"
|
||||||
|
echo " with nothing in between, which is the abrupt failure you get in a VM."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# postgres puts its shared buffers in /dev/shm. Docker's default is 64 MB,
|
||||||
|
# and the resulting failure names neither shm nor the size.
|
||||||
|
if [ -d /dev/shm ]; then
|
||||||
|
local shm; shm=$(df -Pm /dev/shm 2>/dev/null | awk 'NR==2{print $2}')
|
||||||
|
if [ -n "$shm" ]; then
|
||||||
|
if [ "$shm" -le 64 ]; then
|
||||||
|
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
||||||
|
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
||||||
|
else
|
||||||
|
echo " /dev/shm ${shm} MB"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "disk"
|
||||||
|
local d
|
||||||
|
for d in / /tmp /var/lib/docker; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
df -Pm "$d" 2>/dev/null | awk -v p="$d" 'NR==2{printf " %-12s %s MB free of %s MB\n", p, $4, $2}'
|
||||||
|
done
|
||||||
|
|
||||||
|
# kind and Tilt both watch large trees, and the failure mode is silent:
|
||||||
|
# they simply stop noticing file changes. Cheap to report while we are here.
|
||||||
|
local w i
|
||||||
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
|
echo
|
||||||
|
echo "tooling"
|
||||||
|
echo " inotify watches=$w instances=$i"
|
||||||
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
|
echo " ! low — anything watching files will silently stop seeing changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
echo " docker socket present, no cli"
|
||||||
|
else
|
||||||
|
echo " docker not installed"
|
||||||
|
fi
|
||||||
|
elif docker info >/dev/null 2>&1; then
|
||||||
|
local n
|
||||||
|
n=$(docker ps -q 2>/dev/null | wc -l)
|
||||||
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null), ${n} container(s) running"
|
||||||
|
else
|
||||||
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
||||||
|
# not usefully apply — the change costs a full VM restart. Report it, and
|
||||||
|
# report the commonest mistake, which is editing it and not restarting.
|
||||||
|
if is_wsl; then
|
||||||
|
local cfg conf conf_mb n
|
||||||
|
cfg=$(wslconfig_path)
|
||||||
|
echo
|
||||||
|
echo "wsl"
|
||||||
|
if [ -z "$cfg" ]; then
|
||||||
|
echo " ! cannot tell which Windows profile owns .wslconfig"
|
||||||
|
else
|
||||||
|
echo " config $cfg"
|
||||||
|
conf=$(configured_memory "$cfg")
|
||||||
|
if [ -n "$conf" ]; then
|
||||||
|
conf_mb=$(to_mb "$conf")
|
||||||
|
echo " configured $conf (${conf_mb} MB), booted ${total} MB"
|
||||||
|
# The VM reports a little less than allocated; 15% covers the
|
||||||
|
# kernel without calling every healthy machine a mismatch.
|
||||||
|
if [ -n "$conf_mb" ] && [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
||||||
|
echo " ! configured ${conf_mb} MB but booted ${total} MB — not applied yet."
|
||||||
|
echo " From a WINDOWS terminal: wsl --shutdown then start the distro again."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " configured no memory= set (WSL defaults to 50% of host RAM, or 8 GB,"
|
||||||
|
echo " whichever is less). To raise it, add on the Windows side:"
|
||||||
|
echo " [wsl2]"
|
||||||
|
echo " memory=8GB"
|
||||||
|
echo " then from a WINDOWS terminal: wsl --shutdown"
|
||||||
|
fi
|
||||||
|
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
|
if [ "$n" -gt 0 ]; then
|
||||||
|
echo " backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo " - native linux: no VM allocation to raise. If memory is tight the levers"
|
||||||
|
echo " are freeing something or adding swap."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "effective ceiling $(effective_ceiling_mb) MB"
|
||||||
|
echo " the lowest of MemTotal, the cgroup cap and ulimit -v. What the box"
|
||||||
|
echo " claims. 'push' measures what it will actually hand over."
|
||||||
|
|
||||||
|
[ "$avail" -lt $(( total / 5 )) ] && { echo; hogs; }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── .wslconfig ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_wsl() {
|
||||||
|
if ! is_wsl; then
|
||||||
|
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
||||||
|
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
||||||
|
echo "Use 'status' to see what the machine actually has." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# backup and restore act on the file, so unlike status they must not guess.
|
||||||
|
wslconfig_required() {
|
||||||
|
local cfg; cfg=$(wslconfig_required)
|
||||||
|
if [ -z "$cfg" ]; then
|
||||||
|
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
||||||
|
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
||||||
|
| grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$cfg"
|
||||||
|
}
|
||||||
|
|
||||||
|
configured_memory() {
|
||||||
|
[ -r "$1" ] || { echo ""; return; }
|
||||||
|
sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo.
|
||||||
|
to_mb() {
|
||||||
|
local v="${1^^}" n
|
||||||
|
n=$(echo "$v" | tr -dc '0-9')
|
||||||
|
[ -n "$n" ] || { echo ""; return; }
|
||||||
|
case "$v" in
|
||||||
|
*GB|*G) echo $(( n * 1024 )) ;;
|
||||||
|
*MB|*M) echo "$n" ;;
|
||||||
|
*) echo $(( n / 1024 / 1024 )) ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
backup() {
|
||||||
|
require_wsl backup
|
||||||
|
local cfg dest
|
||||||
|
cfg=$(wslconfig_required)
|
||||||
|
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||||
|
# Timestamped and never overwritten: a backup that can destroy itself on a
|
||||||
|
# second run is not a backup.
|
||||||
|
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
||||||
|
cp "$cfg" "$dest"
|
||||||
|
echo "backed up $dest"
|
||||||
|
echo
|
||||||
|
echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
require_wsl restore
|
||||||
|
local cfg newest count
|
||||||
|
cfg=$(wslconfig_required)
|
||||||
|
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "restoring $newest"
|
||||||
|
echo " -> $cfg"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Newest is the right default — undo the last edit — but if you backed up
|
||||||
|
# *after* editing, the state you want is older. Show the rest so a no-op
|
||||||
|
# restore is obviously a no-op rather than a mystery.
|
||||||
|
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
|
if [ "$count" -gt 1 ]; then
|
||||||
|
echo "$count backups exist, newest first:"
|
||||||
|
ls -t "$cfg".*.bak | sed 's/^/ /'
|
||||||
|
echo " (restoring the newest; copy another by hand to pick an older one)"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -r "$cfg" ]; then
|
||||||
|
echo "what changes:"
|
||||||
|
if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then
|
||||||
|
echo " nothing — that backup is identical to the current config"
|
||||||
|
else
|
||||||
|
sed 's/^/ /' /tmp/mem.diff
|
||||||
|
fi
|
||||||
|
rm -f /tmp/mem.diff
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "proceed? [y/N] "
|
||||||
|
read -r reply
|
||||||
|
case "$reply" in
|
||||||
|
y|Y|yes|Yes) ;;
|
||||||
|
*) echo "left alone"; return 0 ;;
|
||||||
|
esac
|
||||||
|
cp "$newest" "$cfg"
|
||||||
|
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── push ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
STATE=""
|
||||||
|
CHILD=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$CHILD" ] && kill -0 "$CHILD" 2>/dev/null; then
|
||||||
|
kill -KILL "$CHILD" 2>/dev/null || true
|
||||||
|
wait "$CHILD" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
[ -n "$STATE" ] && rm -f "$STATE"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The child allocates and stops itself; the parent only watches. That split is
|
||||||
|
# the point: under --to-oom the allocating process is expected to be killed, and
|
||||||
|
# something has to survive to say how far it got.
|
||||||
|
allocator() {
|
||||||
|
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
||||||
|
# first. Raising needs no privilege (only lowering does). Without it, the
|
||||||
|
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
||||||
|
# box you are still using, that is not an acceptable coin toss.
|
||||||
|
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
||||||
|
|
||||||
|
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
||||||
|
local bytes=$((STEP_MB * 1024 * 1024))
|
||||||
|
local swap_used_start
|
||||||
|
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
# Written STRAIGHT INTO the array element. The obvious spelling —
|
||||||
|
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
||||||
|
# not one: the template stays resident, expanding "$chunk" makes a
|
||||||
|
# temporary word, and the append makes the element. A 128 MB step then
|
||||||
|
# needs 384 MB transiently, and on a small box it is killed on the
|
||||||
|
# first append while reporting a third of the true ceiling.
|
||||||
|
#
|
||||||
|
# printf -v into a subscript also means every page is written, so it is
|
||||||
|
# resident rather than merely promised — the only kind of allocation
|
||||||
|
# that measures anything under heuristic overcommit.
|
||||||
|
printf -v "arr[$i]" '%*s' "$bytes" ''
|
||||||
|
i=$((i + 1)); held=$((held + STEP_MB))
|
||||||
|
|
||||||
|
rss=$(awk '/^VmRSS:/{print int($2/1024)}' "/proc/$BASHPID/status" 2>/dev/null || echo 0)
|
||||||
|
avail=$(headroom_mb)
|
||||||
|
swapped=$(( $(mb SwapTotal) - $(mb SwapFree) - swap_used_start ))
|
||||||
|
[ "$swapped" -lt 0 ] && swapped=0
|
||||||
|
|
||||||
|
printf '%8s MB held rss %7s MB headroom %7s MB swap +%s MB\n' \
|
||||||
|
"$held" "$rss" "$avail" "$swapped"
|
||||||
|
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
||||||
|
|
||||||
|
# Worth calling out separately from the ceiling: this is where the box
|
||||||
|
# stops being fast and starts being unusable, which for a scheduler is
|
||||||
|
# a different and earlier problem than being killed.
|
||||||
|
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
||||||
|
first_swap=$held
|
||||||
|
echo " - first swap page at ${held} MB — past here it works but crawls"
|
||||||
|
echo "swapat $held" >> "$STATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$TO_MB" ] && [ "$held" -ge "$TO_MB" ]; then
|
||||||
|
echo "stop reached-the-cap" >> "$STATE"; return 0
|
||||||
|
fi
|
||||||
|
if [ "$TO_OOM" = no ] && [ "$avail" -lt "$FLOOR_MB" ]; then
|
||||||
|
echo "stop floor" >> "$STATE"; return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
push() {
|
||||||
|
local total ceiling rc=0 last held rss swapat stop
|
||||||
|
total=$(mb MemTotal)
|
||||||
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
|
||||||
|
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
||||||
|
# find the edge, few enough lines to read, and small enough that the
|
||||||
|
# transient cost of one allocation never dominates a small box. A fixed
|
||||||
|
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
||||||
|
if [ "$STEP_EXPLICIT" = no ]; then
|
||||||
|
STEP_MB=$(( ceiling / 64 ))
|
||||||
|
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
||||||
|
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
||||||
|
# depends on what it is protecting. Under a cgroup cap, running out kills
|
||||||
|
# only this container's own processes, so it need cover no more than the
|
||||||
|
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
||||||
|
# halve the answer. On a host there is everything else to protect, and the
|
||||||
|
# OOM killer does not promise to pick the process that caused the problem.
|
||||||
|
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
||||||
|
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
||||||
|
|
||||||
|
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
||||||
|
trap cleanup EXIT
|
||||||
|
# INT kills the child and lets the summary below print anyway, so an
|
||||||
|
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
||||||
|
# still gives the memory back.
|
||||||
|
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
||||||
|
|
||||||
|
echo "push"
|
||||||
|
echo " step ${STEP_MB} MB per allocation, every page touched"
|
||||||
|
echo " ceiling ${ceiling} MB claimed"
|
||||||
|
if [ -n "$TO_MB" ]; then
|
||||||
|
echo " stopping at ${TO_MB} MB (--to)"
|
||||||
|
elif [ "$TO_OOM" = yes ]; then
|
||||||
|
echo " ! stopping only when the kernel stops it (--to-oom)"
|
||||||
|
echo " the allocating child is marked as the preferred OOM victim,"
|
||||||
|
echo " but nothing about an OOM kill is entirely polite. Not on a box"
|
||||||
|
echo " running anything you mind losing."
|
||||||
|
else
|
||||||
|
echo " stopping when headroom drops below ${FLOOR_MB} MB"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
allocator &
|
||||||
|
CHILD=$!
|
||||||
|
wait "$CHILD" || rc=$?
|
||||||
|
CHILD=""
|
||||||
|
trap - INT
|
||||||
|
|
||||||
|
last=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 || true)
|
||||||
|
held=$(echo "$last" | awk '{print $1}')
|
||||||
|
rss=$(echo "$last" | awk '{print $2}')
|
||||||
|
swapat=$(awk '/^swapat/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
||||||
|
stop=$(awk '/^stop/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ -z "$held" ]; then
|
||||||
|
echo " ! nothing was allocated. Even one ${STEP_MB} MB chunk failed —"
|
||||||
|
echo " try a smaller --step, or check ulimit -v in 'status'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " reached ${rss:-$held} MB resident"
|
||||||
|
[ -n "$swapat" ] && echo " swapping from ${swapat} MB"
|
||||||
|
|
||||||
|
case "$stop" in
|
||||||
|
reached-the-cap)
|
||||||
|
echo " outcome stopped at the --to cap, not at a limit."
|
||||||
|
echo " The box held ${TO_MB} MB without complaint; there is more." ;;
|
||||||
|
floor)
|
||||||
|
echo " outcome stopped with a cushion intact, by choice."
|
||||||
|
echo " The real ceiling is higher — --to-oom finds it, at the"
|
||||||
|
echo " cost of an actual OOM kill." ;;
|
||||||
|
interrupted)
|
||||||
|
echo " outcome interrupted at ${rss:-$held} MB — where you stopped it,"
|
||||||
|
echo " not where the box did." ;;
|
||||||
|
*)
|
||||||
|
# No stop line means the child did not decide to stop: it was ended.
|
||||||
|
if [ "$rc" -ge 128 ]; then
|
||||||
|
echo " outcome the child was killed (signal $((rc - 128))) at ${rss:-$held} MB."
|
||||||
|
elif [ "$rc" -ne 0 ]; then
|
||||||
|
echo " outcome the allocation failed at ${rss:-$held} MB (exit ${rc})."
|
||||||
|
echo " bash could not get the next chunk — an honest malloc"
|
||||||
|
echo " failure rather than a kill. That is the strict-overcommit"
|
||||||
|
echo " or ulimit path."
|
||||||
|
else
|
||||||
|
echo " outcome ended at ${rss:-$held} MB."
|
||||||
|
fi
|
||||||
|
local ev
|
||||||
|
ev=$(dmesg 2>/dev/null | tail -80 | grep -iE 'oom-kill|killed process' | tail -1 || true)
|
||||||
|
if [ -n "$ev" ]; then
|
||||||
|
echo " kernel ${ev#*] }"
|
||||||
|
else
|
||||||
|
echo " - dmesg is unreadable here (dmesg_restrict, or no privilege),"
|
||||||
|
echo " so the kill cannot be confirmed from this side. The number stands."
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The gap between the claim and the measurement is the finding — but only
|
||||||
|
# when the BOX chose where to stop. An empty $stop means the child was ended
|
||||||
|
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
||||||
|
# asked for, and flagging those as short of the ceiling would put a warning
|
||||||
|
# on every deliberately small run.
|
||||||
|
local got="${rss:-$held}"
|
||||||
|
echo
|
||||||
|
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
||||||
|
echo " ! claimed ${ceiling} MB, gave up ${got} MB — under 70% of it."
|
||||||
|
echo " Something is taking the difference. 'status' names the candidates:"
|
||||||
|
echo " a cgroup cap, ulimit -v, or memory already resident."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── all ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
all() {
|
||||||
|
status
|
||||||
|
echo
|
||||||
|
echo "────────────────────────────────────────────────────────────"
|
||||||
|
echo
|
||||||
|
push
|
||||||
|
|
||||||
|
local got budget_mb ceiling
|
||||||
|
load_config
|
||||||
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
|
budget_mb=$(( BUDGET_GB * 1024 ))
|
||||||
|
else
|
||||||
|
budget_mb=$(( NODES * NODE_MB ))
|
||||||
|
fi
|
||||||
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
||||||
|
[ -n "$got" ] || got=0
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "verdict"
|
||||||
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
|
echo " budget ${budget_mb} MB (--budget)"
|
||||||
|
else
|
||||||
|
# rig's own figure for this profile: nodes times what one node costs.
|
||||||
|
# Addons carry no memory figure in rig yet, so this is the cluster alone
|
||||||
|
# and whatever you deploy comes on top. --budget once you know that too.
|
||||||
|
echo " budget ${budget_mb} MB — profile ${PROFILE_NAME}: ${NODES} node(s) x ${NODE_MB} MB,"
|
||||||
|
echo " the cluster alone; your workload comes on top (--budget GB)"
|
||||||
|
fi
|
||||||
|
echo " measured ${got} MB handed over"
|
||||||
|
|
||||||
|
if [ "$got" -ge "$budget_mb" ]; then
|
||||||
|
echo " fits, with $(( got - budget_mb )) MB spare."
|
||||||
|
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
||||||
|
echo " - under 30% spare is thin once a workload runs on top: memory use"
|
||||||
|
echo " is spiky, and the spikes are what get killed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ! short by $(( budget_mb - got )) MB."
|
||||||
|
if [ "$ceiling" -ge "$budget_mb" ]; then
|
||||||
|
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
||||||
|
echo " Free something, or read the caps section again."
|
||||||
|
else
|
||||||
|
echo " The box does not have it to give. A bigger machine, or a profile"
|
||||||
|
echo " with fewer nodes."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
parse_flags() {
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) TO_MB=$(( ${2:?--to needs a value in GB} * 1024 )); shift 2 ;;
|
||||||
|
--to-mb) TO_MB="${2:?--to-mb needs a value in MB}"; shift 2 ;;
|
||||||
|
--step) STEP_MB="${2:?--step needs a value in MB}"; STEP_EXPLICIT=yes; shift 2 ;;
|
||||||
|
--to-oom) TO_OOM=yes; shift ;;
|
||||||
|
--budget) BUDGET_GB="${2:?--budget needs a value in GB}"; BUDGET_EXPLICIT=yes; shift 2 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ "$TO_OOM" = yes ] && [ -n "$TO_MB" ]; then
|
||||||
|
echo "--to and --to-oom contradict each other: one stops early, the other" >&2
|
||||||
|
echo "refuses to stop at all. Pick one." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
require_linux
|
||||||
|
find_cgroup
|
||||||
|
|
||||||
|
cmd="${1:-status}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
status) parse_flags "$@"; status ;;
|
||||||
|
push) parse_flags "$@"; push ;;
|
||||||
|
all) parse_flags "$@"; all ;;
|
||||||
|
backup) backup ;;
|
||||||
|
restore) restore ;;
|
||||||
|
*) echo "usage: $0 [status|push|all|backup|restore]" >&2
|
||||||
|
echo " push [--to GB] [--to-mb MB] [--step MB] [--to-oom]" >&2
|
||||||
|
echo " all [--budget GB]" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
23
rig/standalone/minimal/Makefile
Normal file
23
rig/standalone/minimal/Makefile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 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)
|
||||||
958
rig/standalone/minimal/rigdeps.sh
Executable file
958
rig/standalone/minimal/rigdeps.sh
Executable file
@@ -0,0 +1,958 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GENERATED by make standalone — do not edit
|
||||||
|
#
|
||||||
|
# rigdeps.sh for profile 'minimal', 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 'minimal' ──
|
||||||
|
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 -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 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_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx PROFILE_NAME="minimal"
|
||||||
|
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||||
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
|
declare -gx REGISTRY_MODE="local"
|
||||||
|
declare -g REGISTRY_PORT="20313"
|
||||||
|
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||||
|
declare -g TILT_PORT="20312"
|
||||||
|
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||||
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
|
declare -gx TILT_VERSION="0.37.6"
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
|
# ── ctrl/deps.sh ──
|
||||||
|
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
||||||
|
# report what it could not do.
|
||||||
|
#
|
||||||
|
# It never runs the cluster, never uses sudo or apt, and writes only into
|
||||||
|
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
||||||
|
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
||||||
|
# decide on, never performed. That is what makes it safe to run on a machine that
|
||||||
|
# already has a working setup.
|
||||||
|
#
|
||||||
|
# Usage (normally via `make deps`, or directly):
|
||||||
|
# deps.sh detect # report host facts only, change nothing
|
||||||
|
# deps.sh list # the pinned versions
|
||||||
|
# deps.sh verify [core|dev] # run what is installed and see if it works
|
||||||
|
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||||
|
# deps.sh install [core|dev] # detect, fetch, install, report
|
||||||
|
#
|
||||||
|
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
||||||
|
# Default is dev.
|
||||||
|
#
|
||||||
|
# Runs both inside the installer container and bare on a host. Inside the
|
||||||
|
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
||||||
|
# falls back to /.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
||||||
|
# not against ctrl/ once we've moved.
|
||||||
|
INVOKED_FROM="$PWD"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Pins arrive through load_config like every other setting, not by sourcing
|
||||||
|
# versions.env here. That is what lets `make standalone` freeze them into a
|
||||||
|
# one-file installer: configuration has exactly one way in.
|
||||||
|
# (sourced library inlined above)
|
||||||
|
load_config
|
||||||
|
|
||||||
|
# Resolve a possibly-relative path against the caller's original directory.
|
||||||
|
abspath() {
|
||||||
|
case "$1" in
|
||||||
|
/*) echo "$1" ;;
|
||||||
|
*) echo "$INVOKED_FROM/$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||||
|
HOST_ROOT="${HOST_ROOT:-/}"
|
||||||
|
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||||
|
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||||
|
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||||
|
|
||||||
|
# Collected by detect(), printed by report_manual() at the very end.
|
||||||
|
MANUAL=()
|
||||||
|
|
||||||
|
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
||||||
|
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
||||||
|
# container's own view is already the host's.
|
||||||
|
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
||||||
|
# tight and does-not-fit branches can be exercised against a real machine's
|
||||||
|
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
||||||
|
mb_of() {
|
||||||
|
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||||
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
|
}
|
||||||
|
|
||||||
|
host_file() {
|
||||||
|
local p="${1#/}"
|
||||||
|
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
||||||
|
echo "$HOST_ROOT/$p"
|
||||||
|
else
|
||||||
|
echo "/$p"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── the tools this script itself needs ─────────────────────────────────────
|
||||||
|
|
||||||
|
arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) echo amd64 ;;
|
||||||
|
aarch64|arm64) echo arm64 ;;
|
||||||
|
*) uname -m ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The pins above are amd64. Rather than download something that cannot execute
|
||||||
|
# and let it fail as "cannot execute binary file: Exec format error", say so
|
||||||
|
# here and hand over the commands that produce the right checksums.
|
||||||
|
require_amd64() {
|
||||||
|
local a; a=$(arch)
|
||||||
|
[ "$a" = "amd64" ] && return 0
|
||||||
|
cat >&2 <<EOF
|
||||||
|
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
||||||
|
|
||||||
|
Nothing here would run, so it does not download. To make an ${a} version, the
|
||||||
|
URLs need the ${a} artifact and the checksums need to come from each project's
|
||||||
|
own published list — not from these values, and not from a download you did:
|
||||||
|
|
||||||
|
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
||||||
|
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
||||||
|
|
||||||
|
Edit the pinned block at the top of this file with what those print.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DL=""
|
||||||
|
pick_downloader() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then DL=curl
|
||||||
|
elif command -v wget >/dev/null 2>&1; then DL=wget
|
||||||
|
else
|
||||||
|
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
||||||
|
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
local url="$1" out="$2"
|
||||||
|
case "$DL" in
|
||||||
|
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
||||||
|
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
SHA=""
|
||||||
|
pick_sha() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
||||||
|
else
|
||||||
|
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
||||||
|
echo "Refusing to install unverified binaries." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── package manager, for the instructions only ─────────────────────────────
|
||||||
|
# This never runs a package manager. It names one so the reported action is
|
||||||
|
# something you can paste, on the distro you are actually on — an apt line on
|
||||||
|
# Amazon Linux 2 is a wrong answer dressed up as help.
|
||||||
|
|
||||||
|
pkg_install_cmd() {
|
||||||
|
local pkg="$1"
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
||||||
|
else echo "install '$pkg' with this system's package manager"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_pkg() {
|
||||||
|
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── detect ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
|
# the tooling. Detectable, so name it instead.
|
||||||
|
require_linux() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
|
|
||||||
|
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
wsl --install
|
||||||
|
|
||||||
|
That enables Windows features and needs a reboot, so it is not something this
|
||||||
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
|
this from there.
|
||||||
|
|
||||||
|
See "Starting from plain Windows" in README.md.
|
||||||
|
EOF
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
|
detect() {
|
||||||
|
echo "host"
|
||||||
|
echo " kernel $(uname -r)"
|
||||||
|
echo " arch $(arch) ($(uname -m))"
|
||||||
|
|
||||||
|
local osr; osr=$(host_file /etc/os-release)
|
||||||
|
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
||||||
|
|
||||||
|
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
||||||
|
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
||||||
|
# that is enough depends on the profile, which check.sh knows and this does not.
|
||||||
|
local total_mb avail_mb swap_total_mb swap_used_mb om
|
||||||
|
total_mb=$(mb_of MemTotal)
|
||||||
|
avail_mb=$(mb_of MemAvailable)
|
||||||
|
swap_total_mb=$(mb_of SwapTotal)
|
||||||
|
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
||||||
|
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
||||||
|
if [ "$swap_total_mb" -gt 0 ]; then
|
||||||
|
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
||||||
|
# always says yes and settles up later with the OOM killer, so a cluster that
|
||||||
|
# starts cleanly can still lose processes afterwards.
|
||||||
|
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||||
|
case "$om" in
|
||||||
|
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||||
|
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||||
|
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo " install to $OUT_BIN"
|
||||||
|
detect_libc
|
||||||
|
detect_prereqs
|
||||||
|
detect_wsl
|
||||||
|
detect_filesystem
|
||||||
|
detect_docker
|
||||||
|
detect_inotify
|
||||||
|
detect_toolchain
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_wsl() {
|
||||||
|
if ! is_wsl; then
|
||||||
|
echo " platform native linux"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " platform WSL"
|
||||||
|
|
||||||
|
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
||||||
|
# host service need it. Enabling it requires a Windows-side restart, which
|
||||||
|
# cannot be issued from inside the distro.
|
||||||
|
local wc; wc=$(host_file /etc/wsl.conf)
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||||
|
echo " systemd enabled in wsl.conf"
|
||||||
|
else
|
||||||
|
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||||
|
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||||
|
[boot]
|
||||||
|
systemd=true
|
||||||
|
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||||
|
# local DNS setup.
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||||
|
echo " resolv.conf pinned (generateResolvConf=false)"
|
||||||
|
else
|
||||||
|
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local wcfg
|
||||||
|
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
|
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||||
|
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||||
|
else
|
||||||
|
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||||
|
make mem status
|
||||||
|
It prints the edit to make and the command to apply it.")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
||||||
|
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
||||||
|
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
||||||
|
# way. None of them deliver inotify events, so anything watching files goes
|
||||||
|
# quiet without saying why.
|
||||||
|
watch_hostile_fs() {
|
||||||
|
local dir="$1" fstype
|
||||||
|
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||||
|
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
||||||
|
case "$fstype" in
|
||||||
|
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_filesystem() {
|
||||||
|
local root fstype
|
||||||
|
root=$(cd .. && pwd -P)
|
||||||
|
fstype=$(watch_hostile_fs "$root")
|
||||||
|
if [ -n "$fstype" ]; then
|
||||||
|
echo " ! this directory is on $fstype — file watching will not work"
|
||||||
|
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
||||||
|
on a $fstype mount, and everything else is slower:
|
||||||
|
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||||
|
else
|
||||||
|
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
||||||
|
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
||||||
|
#
|
||||||
|
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
||||||
|
#
|
||||||
|
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
||||||
|
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
||||||
|
# version now; `verify` catches the actual failure after installing.
|
||||||
|
detect_libc() {
|
||||||
|
local v=""
|
||||||
|
if command -v ldd >/dev/null 2>&1; then
|
||||||
|
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$v" ]; then
|
||||||
|
echo " libc unknown (no ldd) — 'verify' is the real test"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo " libc glibc $v"
|
||||||
|
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
||||||
|
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
||||||
|
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
||||||
|
echo " Install the core tier, or run tilt from a container."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# What this script needs to do its own job. Reported here so `detect` answers
|
||||||
|
# "will install work?" instead of leaving you to find out one download in.
|
||||||
|
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
||||||
|
detect_prereqs() {
|
||||||
|
local missing=""
|
||||||
|
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
||||||
|
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
||||||
|
echo " checksums ok"
|
||||||
|
else
|
||||||
|
echo " ! no sha256sum or shasum — downloads could not be verified"
|
||||||
|
missing+=" coreutils"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
||||||
|
echo " archives tar + gzip"
|
||||||
|
else
|
||||||
|
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
||||||
|
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
||||||
|
missing+=" tar gzip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
MANUAL+=("Install what this script needs to run at all:
|
||||||
|
$(pkg_install_cmd "${missing# }")")
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_docker() {
|
||||||
|
# Reachability of the daemon is the real question, and the CLI is only how
|
||||||
|
# we ask it. Note that when this runs inside the installer container, Docker
|
||||||
|
# necessarily exists on the host — otherwise nothing would be executing —
|
||||||
|
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
echo " docker socket present (no cli in this context)"
|
||||||
|
else
|
||||||
|
echo " ! docker not found and no socket at /var/run/docker.sock"
|
||||||
|
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
||||||
|
that needs root:
|
||||||
|
$(pkg_install_cmd "$(docker_pkg)")
|
||||||
|
sudo systemctl enable --now docker
|
||||||
|
sudo usermod -aG docker \"\$USER\"
|
||||||
|
then log out and back in, so the new group applies to your shell.")
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||||
|
local n
|
||||||
|
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||||
|
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
||||||
|
# function the latter returns 1 when the count is zero, and `set -e`
|
||||||
|
# then kills the caller. That is the fresh-machine case — no clusters
|
||||||
|
# yet — so the bug only ever shows up where it does most harm.
|
||||||
|
if [ "$n" -gt 0 ]; then
|
||||||
|
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
|
MANUAL+=("Start Docker, or add yourself to the docker group:
|
||||||
|
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
||||||
|
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
||||||
|
detect_inotify() {
|
||||||
|
local w i
|
||||||
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
|
echo " inotify watches=$w instances=$i"
|
||||||
|
|
||||||
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
|
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||||
|
MANUAL+=("Raise inotify limits (needs root on the host):
|
||||||
|
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
||||||
|
| sudo tee /etc/sysctl.d/99-rig.conf
|
||||||
|
sudo sysctl --system")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── fetch ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
||||||
|
resolve_url() {
|
||||||
|
local upstream="$1"
|
||||||
|
case "$DEPS_SOURCE" in
|
||||||
|
upstream) echo "$upstream" ;;
|
||||||
|
artifactory)
|
||||||
|
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
||||||
|
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
||||||
|
;;
|
||||||
|
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
local file="$1" want="$2" name="$3" got
|
||||||
|
got=$($SHA "$file" | awk '{print $1}')
|
||||||
|
if [ "$got" != "$want" ]; then
|
||||||
|
echo "checksum mismatch for $name" >&2
|
||||||
|
echo " expected $want" >&2
|
||||||
|
echo " got $got" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
||||||
|
fetch_bin() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4"
|
||||||
|
local tmp="$dest/.$name.tmp"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
mv "$tmp" "$dest/$name"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
||||||
|
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
||||||
|
# others nest it a directory down — so the caller says which.
|
||||||
|
fetch_tgz() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
||||||
|
local tmp="$dest/.$name.tgz"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
||||||
|
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
||||||
|
# user does not own.
|
||||||
|
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||||
|
rm -f "$tmp"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The installer runs as root so it can reach the docker socket, which means
|
||||||
|
# everything it writes into a mounted volume lands root-owned and unusable from
|
||||||
|
# the host. Hand it back to whoever owns the mount point (the host user created
|
||||||
|
# that directory before mounting it).
|
||||||
|
fix_ownership() {
|
||||||
|
local dir="$1"
|
||||||
|
[ -d "$dir" ] || return 0
|
||||||
|
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
||||||
|
if [ "$owner" = ":" ]; then
|
||||||
|
owner=$(stat -c '%u:%g' "$dir")
|
||||||
|
fi
|
||||||
|
[ "$owner" = "0:0" ] && return 0
|
||||||
|
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Two tiers, because not every machine should get cluster tooling.
|
||||||
|
#
|
||||||
|
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
||||||
|
# creates one. Appropriate on a managed or corporate-issued machine
|
||||||
|
# where development tools are not wanted by default.
|
||||||
|
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
||||||
|
#
|
||||||
|
# The split exists because "install the toolchain" is not one decision: on a
|
||||||
|
# managed workspace the right answer is kubectl and nothing else.
|
||||||
|
CORE_TOOLS="kubectl jq"
|
||||||
|
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
||||||
|
# has ever invoked it. Add it back the day something actually needs a chart.
|
||||||
|
#
|
||||||
|
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
||||||
|
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
||||||
|
# because it is what wires a cluster to a local registry — without one, an
|
||||||
|
# unqualified image name resolves to docker.io/library/<name> and there is
|
||||||
|
# nothing structural stopping a push there.
|
||||||
|
#
|
||||||
|
# docker-compose is 'dev' for the same reason, and is here because the distro
|
||||||
|
# docker packages ship the daemon and CLI but frequently not the compose
|
||||||
|
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
||||||
|
# working Docker, and nothing about that message names the missing piece.
|
||||||
|
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
||||||
|
|
||||||
|
# ── what is already on this machine ───────────────────────────────────────
|
||||||
|
#
|
||||||
|
# A tool already on PATH at its pinned version is left where it is. Without
|
||||||
|
# this, install downloads a second copy into OUT_BIN and then reports the first
|
||||||
|
# one as shadowed — noise, and wrong, when both are the same version. That is
|
||||||
|
# the normal state of any machine someone set up by hand, whatever directory
|
||||||
|
# they happened to choose.
|
||||||
|
|
||||||
|
pin_of() {
|
||||||
|
case "$1" in
|
||||||
|
kubectl) echo "$KUBECTL_VERSION" ;;
|
||||||
|
jq) echo "$JQ_VERSION" ;;
|
||||||
|
kind) echo "$KIND_VERSION" ;;
|
||||||
|
tilt) echo "$TILT_VERSION" ;;
|
||||||
|
ctlptl) echo "$CTLPTL_VERSION" ;;
|
||||||
|
docker-compose) echo "$COMPOSE_VERSION" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The version string a binary reports. Each tool spells the question
|
||||||
|
# differently, and kubectl has to be told --client or it goes looking for a
|
||||||
|
# server to ask.
|
||||||
|
reported_version() {
|
||||||
|
local tool="$1" path="$2"
|
||||||
|
case "$tool" in
|
||||||
|
kubectl) "$path" version --client 2>/dev/null ;;
|
||||||
|
jq) "$path" --version 2>/dev/null ;;
|
||||||
|
*) "$path" version 2>/dev/null ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
||||||
|
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
||||||
|
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
||||||
|
#
|
||||||
|
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
||||||
|
# on every machine — some builds reject patterns that others accept — and a
|
||||||
|
# failed grep inside a count reads exactly like a zero.
|
||||||
|
version_matches() {
|
||||||
|
local tool="$1" path="$2" pin="$3" out v re
|
||||||
|
out=$(reported_version "$tool" "$path") || return 1
|
||||||
|
v="${pin#v}"
|
||||||
|
v="${v//./\\.}"
|
||||||
|
re="(^|[^0-9.])v?${v}([^0-9.]|\$)"
|
||||||
|
[[ $out =~ $re ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
||||||
|
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
||||||
|
# DIR" must not quietly skip something because this machine happens to have it.
|
||||||
|
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
||||||
|
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
||||||
|
|
||||||
|
# Every tool in the tier with its state, probed once and reported once. What
|
||||||
|
# still needs fetching is left in TOOLCHAIN_NEED for install() to act on.
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
detect_toolchain() {
|
||||||
|
local tier="${TIER:-dev}" b pin path found
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
echo
|
||||||
|
echo "toolchain (pinned, tier '$tier')"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
pin=$(pin_of "$b")
|
||||||
|
path=$(command -v "$b" 2>/dev/null || true)
|
||||||
|
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
||||||
|
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
||||||
|
# has no `docker-compose` to find — and probing only PATH would report it
|
||||||
|
# missing and re-download a copy that is already there. That is the exact
|
||||||
|
# noise the version-aware skip exists to prevent, so ask docker instead.
|
||||||
|
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
||||||
|
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
||||||
|
if [ "${found#v}" = "${pin#v}" ]; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
||||||
|
else
|
||||||
|
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
||||||
|
"$b" "$pin" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
elif version_matches "$b" "$path" "$pin"; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
||||||
|
else
|
||||||
|
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
||||||
|
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo " every pinned tool is already on PATH — nothing to fetch"
|
||||||
|
else
|
||||||
|
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) dest="$2"; shift 2 ;;
|
||||||
|
core|dev) tier="$1"; shift ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
dest="$(abspath "$dest")"
|
||||||
|
mkdir -p "$dest"
|
||||||
|
TIER="$tier"
|
||||||
|
|
||||||
|
if [ "$DEPS_SOURCE" = "baked" ]; then
|
||||||
|
echo "installing baked binaries from $BAKED_BIN"
|
||||||
|
cp -a "$BAKED_BIN"/. "$dest"/
|
||||||
|
fix_ownership "$dest"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${DEPS_ONLY:-}" ]; then
|
||||||
|
echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)"
|
||||||
|
else
|
||||||
|
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
||||||
|
fi
|
||||||
|
if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi
|
||||||
|
if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi
|
||||||
|
if [ "$tier" = "dev" ]; then
|
||||||
|
if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi
|
||||||
|
if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi
|
||||||
|
if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi
|
||||||
|
if want docker-compose; then
|
||||||
|
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
fix_ownership "$dest"
|
||||||
|
# kind writes the kubeconfig as root too; hand that back as well when it's
|
||||||
|
# a mounted host directory rather than container-local state.
|
||||||
|
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── install ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
report_manual() {
|
||||||
|
echo
|
||||||
|
if [ ${#MANUAL[@]} -eq 0 ]; then
|
||||||
|
echo "nothing left to do by hand."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
||||||
|
echo
|
||||||
|
local n=1
|
||||||
|
for m in "${MANUAL[@]}"; do
|
||||||
|
echo " $n. $m"
|
||||||
|
echo
|
||||||
|
n=$((n + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installing into a directory that sits early in PATH silently replaces whatever
|
||||||
|
# the machine was already using — which on a shared or client machine can break
|
||||||
|
# unrelated work (kubectl more than one minor away from a cluster is the common
|
||||||
|
# one). Say so; never decide it for them.
|
||||||
|
# Downloading a verified binary proves it is the right file, not that this
|
||||||
|
# machine can run it. On an old distro tilt fails here, with a linker error
|
||||||
|
# about a missing symbol, and finding that out now beats finding out during a
|
||||||
|
# first cluster build.
|
||||||
|
verify_tools() {
|
||||||
|
local tier="${1:-dev}" b bin out rc broke=0
|
||||||
|
echo "checking that each one actually runs"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
bin="$OUT_BIN/$b"
|
||||||
|
if [ ! -x "$bin" ]; then
|
||||||
|
printf ' %-14s not installed\n' "$b"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
||||||
|
# than one line gets SIGPIPE when head closes the pipe, and the
|
||||||
|
# pipeline reports 141 — so a working kubectl was announced as "does
|
||||||
|
# not run here", with its own correct version string as the evidence.
|
||||||
|
# Take the first line afterwards, from the string.
|
||||||
|
rc=0
|
||||||
|
case "$b" in
|
||||||
|
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
||||||
|
jq) out=$("$bin" --version 2>&1) || rc=$? ;;
|
||||||
|
*) out=$("$bin" version 2>&1) || rc=$? ;;
|
||||||
|
esac
|
||||||
|
out=${out%%$'\n'*}
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
printf ' %-14s %s\n' "$b" "$out"
|
||||||
|
else
|
||||||
|
printf ' ! %-12s does not run here: %s\n' "$b" "$out"
|
||||||
|
broke=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$broke" -eq 1 ]; then
|
||||||
|
echo
|
||||||
|
echo " A binary that downloads and verifies but will not start is almost"
|
||||||
|
echo " always this distro's libc being older than the release needs."
|
||||||
|
echo " 'detect' prints the glibc version. The core tier (kubectl + jq)"
|
||||||
|
echo " has no such dependency and will work regardless."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
echo "pinned, linux/amd64 only:"
|
||||||
|
printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' jq "$JQ_VERSION"
|
||||||
|
printf ' %-14s %s\n' kind "$KIND_VERSION"
|
||||||
|
printf ' %-14s %s\n' tilt "$TILT_VERSION"
|
||||||
|
printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION"
|
||||||
|
echo
|
||||||
|
echo " core = $CORE_TOOLS"
|
||||||
|
echo " dev = $CORE_TOOLS $DEV_TOOLS"
|
||||||
|
echo
|
||||||
|
echo "Checksums are pinned in the block at the top of this file. To bump one,"
|
||||||
|
echo "take the new checksum from the publisher's own release list — the header"
|
||||||
|
echo "comment has the exact commands."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
||||||
|
|
||||||
|
warn_shadowing() {
|
||||||
|
local b existing shadowed="" tier="${1:-dev}"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
[ -x "$OUT_BIN/$b" ] || continue
|
||||||
|
# Where would this resolve if OUT_BIN weren't in the way?
|
||||||
|
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
||||||
|
command -v "$b" 2>/dev/null || true)
|
||||||
|
[ -n "$existing" ] || continue
|
||||||
|
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
||||||
|
# The same version in both places is not a conflict: nothing changes for
|
||||||
|
# any other project whichever copy PATH happens to find first.
|
||||||
|
if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi
|
||||||
|
shadowed+=" $b $existing"$'\n'
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$shadowed" ] || return 0
|
||||||
|
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
||||||
|
printf '%s' "$shadowed"
|
||||||
|
echo " Other projects on this machine will pick up the new versions."
|
||||||
|
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
||||||
|
was just installed:
|
||||||
|
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
||||||
|
Or install somewhere private instead:
|
||||||
|
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
||||||
|
# retired v1 spelling; every compose file written in the last few years assumes
|
||||||
|
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
||||||
|
# So the binary is fetched like any other and then linked, in your own home —
|
||||||
|
# no root, and nothing outside it.
|
||||||
|
install_compose_plugin() {
|
||||||
|
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
||||||
|
[ -x "$src" ] || return 0
|
||||||
|
mkdir -p "$dir"
|
||||||
|
# Something else already owns that name — docker-desktop and some distro
|
||||||
|
# packages install a real file there. Overwriting it would take the plugin
|
||||||
|
# away from whatever put it there, so say so and let the user decide.
|
||||||
|
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
||||||
|
MANUAL+=("Something already installs the compose plugin at
|
||||||
|
$dir/docker-compose
|
||||||
|
To use rig's pinned build instead:
|
||||||
|
ln -sf $src $dir/docker-compose")
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ln -sfn "$src" "$dir/docker-compose"
|
||||||
|
echo " compose plugin -> $dir/docker-compose"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
local tier="${1:-dev}" b
|
||||||
|
TIER="$tier"
|
||||||
|
detect
|
||||||
|
|
||||||
|
# detect_toolchain has already probed PATH. Fetch only what it found missing
|
||||||
|
# or at the wrong version; a tool already present at its pin stays where it is.
|
||||||
|
if [ -n "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo
|
||||||
|
DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier"
|
||||||
|
echo
|
||||||
|
echo "installed to $OUT_BIN ($tier):"
|
||||||
|
for b in $TOOLCHAIN_NEED; do
|
||||||
|
if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi
|
||||||
|
done
|
||||||
|
if [ "$tier" = "core" ]; then
|
||||||
|
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||||
|
fi
|
||||||
|
# Only when compose was one of the things fetched: linking a binary
|
||||||
|
# that is already satisfied elsewhere on PATH would point the plugin at
|
||||||
|
# a copy rig did not install.
|
||||||
|
case " $TOOLCHAIN_NEED " in
|
||||||
|
*" docker-compose "*) install_compose_plugin ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Only worth saying when something actually landed in OUT_BIN. When every
|
||||||
|
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
||||||
|
# telling the user to add it would be advice to fix nothing.
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||||
|
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
warn_shadowing "$tier"
|
||||||
|
|
||||||
|
report_manual
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_linux
|
||||||
|
|
||||||
|
# Read the command, THEN shift — and shift only if there is something there.
|
||||||
|
# A bare `shift` with no positional parameters returns 1, and under `set -e`
|
||||||
|
# that ended the script before a single line was printed: running this with no
|
||||||
|
# arguments at all, the documented default, did nothing and said nothing.
|
||||||
|
cmd="${1:-install}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||||
|
need_downloads() {
|
||||||
|
require_amd64
|
||||||
|
if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi
|
||||||
|
pick_sha
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
detect) detect; report_manual ;;
|
||||||
|
list) list ;;
|
||||||
|
verify) verify_tools "${1:-dev}" ;;
|
||||||
|
fetch) need_downloads; fetch "$@" ;;
|
||||||
|
install) need_downloads; install "${1:-dev}" ;;
|
||||||
|
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
||||||
|
echo " install [core|dev] (default dev)" >&2
|
||||||
|
echo " fetch [core|dev] [--to DIR]" >&2
|
||||||
|
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -1,14 +1,112 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# How much memory this box will actually give you before something dies.
|
# GENERATED by make standalone — do not edit
|
||||||
#
|
#
|
||||||
# rig answers this for a machine it is installed on. This is the single file
|
# rigmini.sh for profile 'minimal', flattened from:
|
||||||
# version, for a machine rig is not going to: paste it onto a fresh AWS
|
# ctrl/mem.sh
|
||||||
# WorkSpace, an EC2 box or a container, run it, and get the same numbers in the
|
# ctrl/lib/config.sh
|
||||||
# same order so two machines can be read side by side.
|
# 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 'minimal' ──
|
||||||
|
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 -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 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_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx PROFILE_NAME="minimal"
|
||||||
|
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
|
# 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
|
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
||||||
# SURVIVE, by allocating until it stops.
|
# 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
|
# 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
|
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
||||||
@@ -16,15 +114,22 @@
|
|||||||
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
||||||
# on a box that OOMs at 2.
|
# on a box that OOMs at 2.
|
||||||
#
|
#
|
||||||
# Reports and instructs. It never raises a limit, frees anything, writes a
|
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
||||||
# config or installs a package — on a machine you are still evaluating, a probe
|
# allocation that can be raised, and the commonest failure is raising it without
|
||||||
# that changes what it is measuring is worse than no probe.
|
# 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:
|
# Usage:
|
||||||
# rigmini.sh status what it has, what caps it
|
# mem.sh status what it has, what caps it
|
||||||
# rigmini.sh push [--to GB] [--to-oom] climb until it stops
|
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
||||||
# rigmini.sh all [--budget GB] both, then the verdict
|
# mem.sh all [--budget GB] both, then the verdict
|
||||||
|
# mem.sh backup | restore .wslconfig, WSL only
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
# (sourced library inlined above)
|
||||||
|
|
||||||
# ── defaults ───────────────────────────────────────────────────────────────
|
# ── defaults ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -32,7 +137,7 @@ STEP_MB=0 # per allocation; 0 means scale it to the ceiling. See pus
|
|||||||
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
||||||
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
||||||
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
||||||
BUDGET_GB=6 # what the rig data profile is assumed to want; see all().
|
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.
|
BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess below.
|
||||||
|
|
||||||
# ── platform ───────────────────────────────────────────────────────────────
|
# ── platform ───────────────────────────────────────────────────────────────
|
||||||
@@ -289,7 +394,7 @@ status() {
|
|||||||
if [ -n "$shm" ]; then
|
if [ -n "$shm" ]; then
|
||||||
if [ "$shm" -le 64 ]; then
|
if [ "$shm" -le 64 ]; then
|
||||||
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
||||||
echo " is docker's default. Raise it with --shm-size when the cabinet fails."
|
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
||||||
else
|
else
|
||||||
echo " /dev/shm ${shm} MB"
|
echo " /dev/shm ${shm} MB"
|
||||||
fi
|
fi
|
||||||
@@ -334,7 +439,7 @@ status() {
|
|||||||
# not usefully apply — the change costs a full VM restart. Report it, and
|
# not usefully apply — the change costs a full VM restart. Report it, and
|
||||||
# report the commonest mistake, which is editing it and not restarting.
|
# report the commonest mistake, which is editing it and not restarting.
|
||||||
if is_wsl; then
|
if is_wsl; then
|
||||||
local cfg conf
|
local cfg conf conf_mb n
|
||||||
cfg=$(wslconfig_path)
|
cfg=$(wslconfig_path)
|
||||||
echo
|
echo
|
||||||
echo "wsl"
|
echo "wsl"
|
||||||
@@ -342,17 +447,32 @@ status() {
|
|||||||
echo " ! cannot tell which Windows profile owns .wslconfig"
|
echo " ! cannot tell which Windows profile owns .wslconfig"
|
||||||
else
|
else
|
||||||
echo " config $cfg"
|
echo " config $cfg"
|
||||||
conf=$(sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$cfg" 2>/dev/null \
|
conf=$(configured_memory "$cfg")
|
||||||
| tail -1 | tr -d '[:space:]')
|
|
||||||
if [ -n "$conf" ]; then
|
if [ -n "$conf" ]; then
|
||||||
echo " configured $conf (booted ${total} MB)"
|
conf_mb=$(to_mb "$conf")
|
||||||
echo " - if those disagree the edit has not been applied."
|
echo " configured $conf (${conf_mb} MB), booted ${total} MB"
|
||||||
echo " From a WINDOWS terminal: wsl --shutdown"
|
# 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
|
else
|
||||||
echo " configured no memory= set (WSL defaults to half the host RAM, or 8 GB,"
|
echo " configured no memory= set (WSL defaults to 50% of host RAM, or 8 GB,"
|
||||||
echo " whichever is less — which is where your Airflow ceiling comes from)"
|
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
|
||||||
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
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@@ -364,6 +484,103 @@ status() {
|
|||||||
return 0
|
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 ───────────────────────────────────────────────────────────────────
|
# ── push ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
STATE=""
|
STATE=""
|
||||||
@@ -560,28 +777,33 @@ all() {
|
|||||||
push
|
push
|
||||||
|
|
||||||
local got budget_mb ceiling
|
local got budget_mb ceiling
|
||||||
budget_mb=$(( BUDGET_GB * 1024 ))
|
load_config
|
||||||
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
|
budget_mb=$(( BUDGET_GB * 1024 ))
|
||||||
|
else
|
||||||
|
budget_mb=$(( NODES * NODE_MB ))
|
||||||
|
fi
|
||||||
ceiling=$(effective_ceiling_mb)
|
ceiling=$(effective_ceiling_mb)
|
||||||
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
||||||
[ -n "$got" ] || got=0
|
[ -n "$got" ] || got=0
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "verdict"
|
echo "verdict"
|
||||||
echo " budget ${BUDGET_GB} GB for kind + postgres + redis + airflow"
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
# Only worth explaining while it is still a guess. Once --budget is given
|
echo " budget ${budget_mb} MB (--budget)"
|
||||||
# the number came from somewhere better than this reasoning, and repeating
|
else
|
||||||
# the derivation would describe a figure that is no longer in use.
|
# rig's own figure for this profile: nodes times what one node costs.
|
||||||
if [ "$BUDGET_EXPLICIT" = no ]; then
|
# Addons carry no memory figure in rig yet, so this is the cluster alone
|
||||||
echo " - that is 2 GB per kind node, which is rig's own figure, plus about"
|
# and whatever you deploy comes on top. --budget once you know that too.
|
||||||
echo " 4 GB for the three cabinets. THE 4 GB IS AN ESTIMATE, not something"
|
echo " budget ${budget_mb} MB — profile ${PROFILE_NAME}: ${NODES} node(s) x ${NODE_MB} MB,"
|
||||||
echo " measured. Re-run with --budget once you have watched the real thing."
|
echo " the cluster alone; your workload comes on top (--budget GB)"
|
||||||
fi
|
fi
|
||||||
echo " measured ${got} MB handed over"
|
echo " measured ${got} MB handed over"
|
||||||
|
|
||||||
if [ "$got" -ge "$budget_mb" ]; then
|
if [ "$got" -ge "$budget_mb" ]; then
|
||||||
echo " fits, with $(( got - budget_mb )) MB spare."
|
echo " fits, with $(( got - budget_mb )) MB spare."
|
||||||
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
||||||
echo " - under 30% spare is thin for a scheduler. Airflow's memory use"
|
echo " - under 30% spare is thin once a workload runs on top: memory use"
|
||||||
echo " is spiky, and the spikes are what get killed."
|
echo " is spiky, and the spikes are what get killed."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -590,8 +812,8 @@ all() {
|
|||||||
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
||||||
echo " Free something, or read the caps section again."
|
echo " Free something, or read the caps section again."
|
||||||
else
|
else
|
||||||
echo " The box does not have it to give. A bigger bundle, or a smaller"
|
echo " The box does not have it to give. A bigger machine, or a profile"
|
||||||
echo " profile: PROFILE=minimal drops the cabinets entirely."
|
echo " with fewer nodes."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
@@ -628,7 +850,9 @@ case "$cmd" in
|
|||||||
status) parse_flags "$@"; status ;;
|
status) parse_flags "$@"; status ;;
|
||||||
push) parse_flags "$@"; push ;;
|
push) parse_flags "$@"; push ;;
|
||||||
all) parse_flags "$@"; all ;;
|
all) parse_flags "$@"; all ;;
|
||||||
*) echo "usage: $0 [status|push|all]" >&2
|
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 " push [--to GB] [--to-mb MB] [--step MB] [--to-oom]" >&2
|
||||||
echo " all [--budget GB]" >&2
|
echo " all [--budget GB]" >&2
|
||||||
exit 1 ;;
|
exit 1 ;;
|
||||||
23
rig/standalone/offline/Makefile
Normal file
23
rig/standalone/offline/Makefile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# 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)
|
||||||
958
rig/standalone/offline/rigdeps.sh
Executable file
958
rig/standalone/offline/rigdeps.sh
Executable file
@@ -0,0 +1,958 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GENERATED by make standalone — do not edit
|
||||||
|
#
|
||||||
|
# rigdeps.sh for profile 'offline', flattened from:
|
||||||
|
# ctrl/deps.sh
|
||||||
|
# ctrl/lib/config.sh
|
||||||
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
|
# ── from the libraries ──
|
||||||
|
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||||
|
_config_restore ()
|
||||||
|
{
|
||||||
|
local line;
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
eval "export $line";
|
||||||
|
fi;
|
||||||
|
done <<< "$1";
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
default_cluster_name ()
|
||||||
|
{
|
||||||
|
local n;
|
||||||
|
n=$(basename "$(cd .. && pwd)");
|
||||||
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
|
echo "${n:-rig}"
|
||||||
|
}
|
||||||
|
derive_port_base ()
|
||||||
|
{
|
||||||
|
local h;
|
||||||
|
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
||||||
|
echo $((20000 + (h % 200) * 10))
|
||||||
|
}
|
||||||
|
render_kind_config ()
|
||||||
|
{
|
||||||
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||||
|
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── configuration, frozen for profile 'offline' ──
|
||||||
|
load_config() {
|
||||||
|
local k saved=""
|
||||||
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
|
done
|
||||||
|
declare -gx ADDONS="metallb"
|
||||||
|
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||||
|
declare -g AUDIT="on"
|
||||||
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
|
declare -g CLUSTER="rig"
|
||||||
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
|
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
||||||
|
declare -gx COMPOSE_VERSION="5.5.1"
|
||||||
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
|
declare -gx DNS_MODE="hosts"
|
||||||
|
declare -g HTTPS_PORT="20311"
|
||||||
|
declare -g HTTP_PORT="20310"
|
||||||
|
declare -gx INGRESS_MODE="hostport"
|
||||||
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
|
declare -gx K8S_VERSION="v1_36"
|
||||||
|
declare -gx KIND_CONFIG="kind-config.audit.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_PATH="./k8s/kind-config.audit.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.audit.yaml.tpl"
|
||||||
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
|
declare -g KUBECONTEXT="kind-rig"
|
||||||
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
|
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||||
|
declare -gx METALLB_VERSION="v0.16.0"
|
||||||
|
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
|
declare -g NODES="1"
|
||||||
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
||||||
|
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
||||||
|
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||||
|
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -g NODE_MB="800"
|
||||||
|
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx PROFILE_NAME="offline"
|
||||||
|
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||||
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
|
declare -gx REGISTRY_MODE="local"
|
||||||
|
declare -g REGISTRY_PORT="20313"
|
||||||
|
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||||
|
declare -g TILT_PORT="20312"
|
||||||
|
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||||
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
|
declare -gx TILT_VERSION="0.37.6"
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
|
# ── ctrl/deps.sh ──
|
||||||
|
# Toolchain installer: detect the host, install a pinned toolchain onto it, then
|
||||||
|
# report what it could not do.
|
||||||
|
#
|
||||||
|
# It never runs the cluster, never uses sudo or apt, and writes only into
|
||||||
|
# $OUT_BIN (default ~/.local/bin). Everything that would touch the host proper —
|
||||||
|
# systemd, inotify limits, .wslconfig, docker group — is REPORTED for a human to
|
||||||
|
# decide on, never performed. That is what makes it safe to run on a machine that
|
||||||
|
# already has a working setup.
|
||||||
|
#
|
||||||
|
# Usage (normally via `make deps`, or directly):
|
||||||
|
# deps.sh detect # report host facts only, change nothing
|
||||||
|
# deps.sh list # the pinned versions
|
||||||
|
# deps.sh verify [core|dev] # run what is installed and see if it works
|
||||||
|
# deps.sh fetch [core|dev] [--to DIR] # download + verify into DIR
|
||||||
|
# deps.sh install [core|dev] # detect, fetch, install, report
|
||||||
|
#
|
||||||
|
# Tiers: 'core' is kubectl + jq (talk to a cluster); 'dev' adds kind and tilt
|
||||||
|
# Default is dev.
|
||||||
|
#
|
||||||
|
# Runs both inside the installer container and bare on a host. Inside the
|
||||||
|
# container, host files are read through $HOST_ROOT (mount / as :ro); bare, it
|
||||||
|
# falls back to /.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Keep the caller's cwd so a relative --to resolves where the user expects,
|
||||||
|
# not against ctrl/ once we've moved.
|
||||||
|
INVOKED_FROM="$PWD"
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Pins arrive through load_config like every other setting, not by sourcing
|
||||||
|
# versions.env here. That is what lets `make standalone` freeze them into a
|
||||||
|
# one-file installer: configuration has exactly one way in.
|
||||||
|
# (sourced library inlined above)
|
||||||
|
load_config
|
||||||
|
|
||||||
|
# Resolve a possibly-relative path against the caller's original directory.
|
||||||
|
abspath() {
|
||||||
|
case "$1" in
|
||||||
|
/*) echo "$1" ;;
|
||||||
|
*) echo "$INVOKED_FROM/$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
||||||
|
HOST_ROOT="${HOST_ROOT:-/}"
|
||||||
|
DEPS_SOURCE="${DEPS_SOURCE:-upstream}"
|
||||||
|
DEPS_ARTIFACTORY_URL="${DEPS_ARTIFACTORY_URL:-}"
|
||||||
|
BAKED_BIN="${BAKED_BIN:-/opt/rig/bin}"
|
||||||
|
|
||||||
|
# Collected by detect(), printed by report_manual() at the very end.
|
||||||
|
MANUAL=()
|
||||||
|
|
||||||
|
# Host FILES (/etc/..., /mnt/c/...) must be read through the mount. Kernel-level
|
||||||
|
# facts (kernel version, meminfo, inotify) are shared with the container, so the
|
||||||
|
# container's own view is already the host's.
|
||||||
|
# A /proc/meminfo field in MB, 0 if the field is absent. MEMINFO exists so the
|
||||||
|
# tight and does-not-fit branches can be exercised against a real machine's
|
||||||
|
# numbers from somewhere else; in normal use it is always /proc/meminfo.
|
||||||
|
mb_of() {
|
||||||
|
awk -v k="$1:" '$1 == k { printf "%d", $2 / 1024; found = 1 }
|
||||||
|
END { if (!found) printf "0" }' "${MEMINFO:-/proc/meminfo}"
|
||||||
|
}
|
||||||
|
|
||||||
|
host_file() {
|
||||||
|
local p="${1#/}"
|
||||||
|
if [ "$HOST_ROOT" != "/" ] && [ -e "$HOST_ROOT/$p" ]; then
|
||||||
|
echo "$HOST_ROOT/$p"
|
||||||
|
else
|
||||||
|
echo "/$p"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── the tools this script itself needs ─────────────────────────────────────
|
||||||
|
|
||||||
|
arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) echo amd64 ;;
|
||||||
|
aarch64|arm64) echo arm64 ;;
|
||||||
|
*) uname -m ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The pins above are amd64. Rather than download something that cannot execute
|
||||||
|
# and let it fail as "cannot execute binary file: Exec format error", say so
|
||||||
|
# here and hand over the commands that produce the right checksums.
|
||||||
|
require_amd64() {
|
||||||
|
local a; a=$(arch)
|
||||||
|
[ "$a" = "amd64" ] && return 0
|
||||||
|
cat >&2 <<EOF
|
||||||
|
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
||||||
|
|
||||||
|
Nothing here would run, so it does not download. To make an ${a} version, the
|
||||||
|
URLs need the ${a} artifact and the checksums need to come from each project's
|
||||||
|
own published list — not from these values, and not from a download you did:
|
||||||
|
|
||||||
|
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
||||||
|
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
||||||
|
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
||||||
|
|
||||||
|
Edit the pinned block at the top of this file with what those print.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DL=""
|
||||||
|
pick_downloader() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then DL=curl
|
||||||
|
elif command -v wget >/dev/null 2>&1; then DL=wget
|
||||||
|
else
|
||||||
|
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
||||||
|
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
local url="$1" out="$2"
|
||||||
|
case "$DL" in
|
||||||
|
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
||||||
|
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
SHA=""
|
||||||
|
pick_sha() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then SHA=sha256sum
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256"
|
||||||
|
else
|
||||||
|
echo "no sha256sum and no shasum — downloads could not be verified." >&2
|
||||||
|
echo "Refusing to install unverified binaries." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── package manager, for the instructions only ─────────────────────────────
|
||||||
|
# This never runs a package manager. It names one so the reported action is
|
||||||
|
# something you can paste, on the distro you are actually on — an apt line on
|
||||||
|
# Amazon Linux 2 is a wrong answer dressed up as help.
|
||||||
|
|
||||||
|
pkg_install_cmd() {
|
||||||
|
local pkg="$1"
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo "sudo apt-get update && sudo apt-get install -y $pkg"
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then echo "sudo dnf install -y $pkg"
|
||||||
|
elif command -v yum >/dev/null 2>&1; then echo "sudo yum install -y $pkg"
|
||||||
|
elif command -v zypper >/dev/null 2>&1; then echo "sudo zypper install -y $pkg"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then echo "sudo apk add $pkg"
|
||||||
|
else echo "install '$pkg' with this system's package manager"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_pkg() {
|
||||||
|
# Debian and Ubuntu call it docker.io; the RPM distros call it docker.
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then echo docker.io; else echo docker; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── detect ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
|
# the tooling. Detectable, so name it instead.
|
||||||
|
require_linux() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
|
|
||||||
|
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
wsl --install
|
||||||
|
|
||||||
|
That enables Windows features and needs a reboot, so it is not something this
|
||||||
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
|
this from there.
|
||||||
|
|
||||||
|
See "Starting from plain Windows" in README.md.
|
||||||
|
EOF
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
|
detect() {
|
||||||
|
echo "host"
|
||||||
|
echo " kernel $(uname -r)"
|
||||||
|
echo " arch $(arch) ($(uname -m))"
|
||||||
|
|
||||||
|
local osr; osr=$(host_file /etc/os-release)
|
||||||
|
[ -r "$osr" ] && echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' "$osr")"
|
||||||
|
|
||||||
|
# In MB. Whole gigabytes lose nearly half a GB on exactly the machines where
|
||||||
|
# it matters: 1874 MB available used to print as "1 GB". Facts only — whether
|
||||||
|
# that is enough depends on the profile, which check.sh knows and this does not.
|
||||||
|
local total_mb avail_mb swap_total_mb swap_used_mb om
|
||||||
|
total_mb=$(mb_of MemTotal)
|
||||||
|
avail_mb=$(mb_of MemAvailable)
|
||||||
|
swap_total_mb=$(mb_of SwapTotal)
|
||||||
|
swap_used_mb=$(( swap_total_mb - $(mb_of SwapFree) ))
|
||||||
|
printf " memory %d MB total, %d MB available\n" "$total_mb" "$avail_mb"
|
||||||
|
if [ "$swap_total_mb" -gt 0 ]; then
|
||||||
|
printf " swap %d MB used of %d MB\n" "$swap_used_mb" "$swap_total_mb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# How the kernel answers an allocation it cannot really satisfy. With 1 it
|
||||||
|
# always says yes and settles up later with the OOM killer, so a cluster that
|
||||||
|
# starts cleanly can still lose processes afterwards.
|
||||||
|
om=$(cat "${OVERCOMMIT_FILE:-/proc/sys/vm/overcommit_memory}" 2>/dev/null || echo '?')
|
||||||
|
case "$om" in
|
||||||
|
0) echo " overcommit 0 heuristic — allocations are granted on a guess" ;;
|
||||||
|
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit" ;;
|
||||||
|
2) echo " overcommit 2 strict — an allocation fails honestly instead of killing later" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo " install to $OUT_BIN"
|
||||||
|
detect_libc
|
||||||
|
detect_prereqs
|
||||||
|
detect_wsl
|
||||||
|
detect_filesystem
|
||||||
|
detect_docker
|
||||||
|
detect_inotify
|
||||||
|
detect_toolchain
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_wsl() {
|
||||||
|
if ! is_wsl; then
|
||||||
|
echo " platform native linux"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " platform WSL"
|
||||||
|
|
||||||
|
# systemd is off by default in WSL, and the ingress/DNS paths that use a
|
||||||
|
# host service need it. Enabling it requires a Windows-side restart, which
|
||||||
|
# cannot be issued from inside the distro.
|
||||||
|
local wc; wc=$(host_file /etc/wsl.conf)
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*systemd\s*=\s*true' "$wc"; then
|
||||||
|
echo " systemd enabled in wsl.conf"
|
||||||
|
else
|
||||||
|
echo " ! systemd not enabled in /etc/wsl.conf"
|
||||||
|
MANUAL+=("Enable systemd — add to /etc/wsl.conf:
|
||||||
|
[boot]
|
||||||
|
systemd=true
|
||||||
|
then from a WINDOWS terminal (not this shell): wsl --shutdown")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WSL regenerates /etc/resolv.conf on every boot, which silently reverts any
|
||||||
|
# local DNS setup.
|
||||||
|
if [ -r "$wc" ] && grep -qE '^\s*generateResolvConf\s*=\s*false' "$wc"; then
|
||||||
|
echo " resolv.conf pinned (generateResolvConf=false)"
|
||||||
|
else
|
||||||
|
echo " - resolv.conf is WSL-generated; DNS_MODE=dnsmasq would be reverted on reboot"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local wcfg
|
||||||
|
wcfg=$(ls "$HOST_ROOT"/mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
|
if [ -n "$wcfg" ] && grep -qE '^\s*memory\s*=' "$wcfg"; then
|
||||||
|
echo " wslconfig memory set: $(grep -E '^\s*memory\s*=' "$wcfg" | tr -d ' ')"
|
||||||
|
else
|
||||||
|
MANUAL+=("Cap/raise the WSL VM memory — see what is set versus what booted:
|
||||||
|
make mem status
|
||||||
|
It prints the edit to make and the command to apply it.")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not a path check: /mnt is an ordinary mount point and an ext4 disk mounted
|
||||||
|
# there is perfectly fine. What matters is the filesystem. The Windows drives
|
||||||
|
# arrive as 9p (WSL2) or drvfs (WSL1); network and fuse mounts behave the same
|
||||||
|
# way. None of them deliver inotify events, so anything watching files goes
|
||||||
|
# quiet without saying why.
|
||||||
|
watch_hostile_fs() {
|
||||||
|
local dir="$1" fstype
|
||||||
|
fstype=$(findmnt -no FSTYPE --target "$dir" 2>/dev/null || true)
|
||||||
|
[ -n "$fstype" ] || fstype=$(stat -f -c %T "$dir" 2>/dev/null || true)
|
||||||
|
case "$fstype" in
|
||||||
|
9p|v9fs|drvfs|cifs|smb3|nfs|nfs4|fuse.sshfs|fuseblk) echo "$fstype" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_filesystem() {
|
||||||
|
local root fstype
|
||||||
|
root=$(cd .. && pwd -P)
|
||||||
|
fstype=$(watch_hostile_fs "$root")
|
||||||
|
if [ -n "$fstype" ]; then
|
||||||
|
echo " ! this directory is on $fstype — file watching will not work"
|
||||||
|
MANUAL+=("Move this onto the local disk. Nothing watching files sees changes
|
||||||
|
on a $fstype mount, and everything else is slower:
|
||||||
|
cp -r \"$root\" ~/ && cd ~/$(basename "$root")")
|
||||||
|
else
|
||||||
|
echo " filesystem $root ($(findmnt -no FSTYPE --target "$root" 2>/dev/null || echo local))"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# tilt is the one binary here that needs a recent glibc. MEASURED, not guessed:
|
||||||
|
# tilt 0.37.6 on Amazon Linux 2 (glibc 2.26) fails with
|
||||||
|
#
|
||||||
|
# /lib64/libc.so.6: version `GLIBC_2.34' not found (required by .../tilt)
|
||||||
|
#
|
||||||
|
# which names a symbol rather than the problem. Amazon Linux 2 is a stock
|
||||||
|
# WorkSpaces bundle, so this is the likely case, not an exotic one. Report the
|
||||||
|
# version now; `verify` catches the actual failure after installing.
|
||||||
|
detect_libc() {
|
||||||
|
local v=""
|
||||||
|
if command -v ldd >/dev/null 2>&1; then
|
||||||
|
v=$(ldd --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+$' || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$v" ]; then
|
||||||
|
echo " libc unknown (no ldd) — 'verify' is the real test"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo " libc glibc $v"
|
||||||
|
if [ "$(printf '%s\n2.34\n' "$v" | sort -V | head -1)" != "2.34" ]; then
|
||||||
|
echo " ! older than glibc 2.34, which tilt needs. kubectl, kind, jq and"
|
||||||
|
echo " ctlptl are static or libc-only and work here; tilt will not start."
|
||||||
|
echo " Install the core tier, or run tilt from a container."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# What this script needs to do its own job. Reported here so `detect` answers
|
||||||
|
# "will install work?" instead of leaving you to find out one download in.
|
||||||
|
# Amazon Linux 2 ships without tar, which is exactly the surprise this catches.
|
||||||
|
detect_prereqs() {
|
||||||
|
local missing=""
|
||||||
|
if command -v curl >/dev/null 2>&1; then echo " download curl"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then echo " download wget"
|
||||||
|
else echo " ! no curl and no wget — nothing can be downloaded"; missing+=" curl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1; then
|
||||||
|
echo " checksums ok"
|
||||||
|
else
|
||||||
|
echo " ! no sha256sum or shasum — downloads could not be verified"
|
||||||
|
missing+=" coreutils"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
|
||||||
|
echo " archives tar + gzip"
|
||||||
|
else
|
||||||
|
echo " ! no tar/gzip — tilt and ctlptl ship as tarballs, so the dev tier"
|
||||||
|
echo " cannot be unpacked. The core tier is two bare binaries and is fine."
|
||||||
|
missing+=" tar gzip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
MANUAL+=("Install what this script needs to run at all:
|
||||||
|
$(pkg_install_cmd "${missing# }")")
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_docker() {
|
||||||
|
# Reachability of the daemon is the real question, and the CLI is only how
|
||||||
|
# we ask it. Note that when this runs inside the installer container, Docker
|
||||||
|
# necessarily exists on the host — otherwise nothing would be executing —
|
||||||
|
# so a missing CLI in here is an installer packaging bug, not a host problem.
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
echo " docker socket present (no cli in this context)"
|
||||||
|
else
|
||||||
|
echo " ! docker not found and no socket at /var/run/docker.sock"
|
||||||
|
MANUAL+=("Install Docker — the one true prerequisite, and the only thing here
|
||||||
|
that needs root:
|
||||||
|
$(pkg_install_cmd "$(docker_pkg)")
|
||||||
|
sudo systemctl enable --now docker
|
||||||
|
sudo usermod -aG docker \"\$USER\"
|
||||||
|
then log out and back in, so the new group applies to your shell.")
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
||||||
|
local n
|
||||||
|
n=$(docker ps --filter "label=io.x-k8s.kind.cluster" --format '{{.Names}}' 2>/dev/null | wc -l)
|
||||||
|
# Must be an `if`, not `[ ] && echo`: as the last statement in this
|
||||||
|
# function the latter returns 1 when the count is zero, and `set -e`
|
||||||
|
# then kills the caller. That is the fresh-machine case — no clusters
|
||||||
|
# yet — so the bug only ever shows up where it does most harm.
|
||||||
|
if [ "$n" -gt 0 ]; then
|
||||||
|
echo " - $n kind node container(s) already running; see 'make cluster list'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
|
MANUAL+=("Start Docker, or add yourself to the docker group:
|
||||||
|
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# kind and Tilt both watch large trees. WSL ships defaults (8192/128) far too low,
|
||||||
|
# and the failure mode is silent: Tilt simply stops noticing file changes.
|
||||||
|
detect_inotify() {
|
||||||
|
local w i
|
||||||
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
|
echo " inotify watches=$w instances=$i"
|
||||||
|
|
||||||
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
|
echo " ! inotify limits are low — Tilt will silently stop noticing file changes"
|
||||||
|
MANUAL+=("Raise inotify limits (needs root on the host):
|
||||||
|
echo -e 'fs.inotify.max_user_watches=524288\\nfs.inotify.max_user_instances=512' \\
|
||||||
|
| sudo tee /etc/sysctl.d/99-rig.conf
|
||||||
|
sudo sysctl --system")
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── fetch ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Resolve where a given artifact comes from, honouring DEPS_SOURCE.
|
||||||
|
resolve_url() {
|
||||||
|
local upstream="$1"
|
||||||
|
case "$DEPS_SOURCE" in
|
||||||
|
upstream) echo "$upstream" ;;
|
||||||
|
artifactory)
|
||||||
|
if [ -z "$DEPS_ARTIFACTORY_URL" ]; then
|
||||||
|
echo "DEPS_SOURCE=artifactory but DEPS_ARTIFACTORY_URL is empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "${DEPS_ARTIFACTORY_URL%/}/$(basename "$upstream")"
|
||||||
|
;;
|
||||||
|
*) echo "unsupported DEPS_SOURCE '$DEPS_SOURCE' for a download" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
local file="$1" want="$2" name="$3" got
|
||||||
|
got=$($SHA "$file" | awk '{print $1}')
|
||||||
|
if [ "$got" != "$want" ]; then
|
||||||
|
echo "checksum mismatch for $name" >&2
|
||||||
|
echo " expected $want" >&2
|
||||||
|
echo " got $got" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
||||||
|
fetch_bin() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4"
|
||||||
|
local tmp="$dest/.$name.tmp"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
mv "$tmp" "$dest/$name"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside-archive> <strip>
|
||||||
|
# Archive layouts differ — tilt's is flat (the binary at the root, strip=0),
|
||||||
|
# others nest it a directory down — so the caller says which.
|
||||||
|
fetch_tgz() {
|
||||||
|
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
||||||
|
local tmp="$dest/.$name.tgz"
|
||||||
|
echo " fetching $name"
|
||||||
|
download "$(resolve_url "$url")" "$tmp"
|
||||||
|
verify "$tmp" "$sha" "$name"
|
||||||
|
# --no-same-owner: extracting as root would otherwise restore the uid/gid
|
||||||
|
# baked into the archive (some ship as uid 1001), leaving a binary the host
|
||||||
|
# user does not own.
|
||||||
|
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
||||||
|
rm -f "$tmp"
|
||||||
|
chmod +x "$dest/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The installer runs as root so it can reach the docker socket, which means
|
||||||
|
# everything it writes into a mounted volume lands root-owned and unusable from
|
||||||
|
# the host. Hand it back to whoever owns the mount point (the host user created
|
||||||
|
# that directory before mounting it).
|
||||||
|
fix_ownership() {
|
||||||
|
local dir="$1"
|
||||||
|
[ -d "$dir" ] || return 0
|
||||||
|
local owner="${HOST_UID:-}:${HOST_GID:-}"
|
||||||
|
if [ "$owner" = ":" ]; then
|
||||||
|
owner=$(stat -c '%u:%g' "$dir")
|
||||||
|
fi
|
||||||
|
[ "$owner" = "0:0" ] && return 0
|
||||||
|
chown -R "$owner" "$dir" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Two tiers, because not every machine should get cluster tooling.
|
||||||
|
#
|
||||||
|
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
||||||
|
# creates one. Appropriate on a managed or corporate-issued machine
|
||||||
|
# where development tools are not wanted by default.
|
||||||
|
# dev core plus kind and tilt — build clusters and hot-reload into them.
|
||||||
|
#
|
||||||
|
# The split exists because "install the toolchain" is not one decision: on a
|
||||||
|
# managed workspace the right answer is kubectl and nothing else.
|
||||||
|
CORE_TOOLS="kubectl jq"
|
||||||
|
# No helm: every addon installs with `kubectl apply -f <url>`, so nothing here
|
||||||
|
# has ever invoked it. Add it back the day something actually needs a chart.
|
||||||
|
#
|
||||||
|
# ctlptl is 'dev' rather than 'core' for the same reason kind is: core is "talk
|
||||||
|
# to a cluster someone else runs", and ctlptl builds them. It earns its place
|
||||||
|
# because it is what wires a cluster to a local registry — without one, an
|
||||||
|
# unqualified image name resolves to docker.io/library/<name> and there is
|
||||||
|
# nothing structural stopping a push there.
|
||||||
|
#
|
||||||
|
# docker-compose is 'dev' for the same reason, and is here because the distro
|
||||||
|
# docker packages ship the daemon and CLI but frequently not the compose
|
||||||
|
# plugin — so `docker compose up` fails with "unknown command" on an otherwise
|
||||||
|
# working Docker, and nothing about that message names the missing piece.
|
||||||
|
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
||||||
|
|
||||||
|
# ── what is already on this machine ───────────────────────────────────────
|
||||||
|
#
|
||||||
|
# A tool already on PATH at its pinned version is left where it is. Without
|
||||||
|
# this, install downloads a second copy into OUT_BIN and then reports the first
|
||||||
|
# one as shadowed — noise, and wrong, when both are the same version. That is
|
||||||
|
# the normal state of any machine someone set up by hand, whatever directory
|
||||||
|
# they happened to choose.
|
||||||
|
|
||||||
|
pin_of() {
|
||||||
|
case "$1" in
|
||||||
|
kubectl) echo "$KUBECTL_VERSION" ;;
|
||||||
|
jq) echo "$JQ_VERSION" ;;
|
||||||
|
kind) echo "$KIND_VERSION" ;;
|
||||||
|
tilt) echo "$TILT_VERSION" ;;
|
||||||
|
ctlptl) echo "$CTLPTL_VERSION" ;;
|
||||||
|
docker-compose) echo "$COMPOSE_VERSION" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# The version string a binary reports. Each tool spells the question
|
||||||
|
# differently, and kubectl has to be told --client or it goes looking for a
|
||||||
|
# server to ask.
|
||||||
|
reported_version() {
|
||||||
|
local tool="$1" path="$2"
|
||||||
|
case "$tool" in
|
||||||
|
kubectl) "$path" version --client 2>/dev/null ;;
|
||||||
|
jq) "$path" --version 2>/dev/null ;;
|
||||||
|
*) "$path" version 2>/dev/null ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Does the binary at PATH report PIN? Matched as a whole version token, so
|
||||||
|
# 0.37.6 never matches 10.37.60, with the leading v optional either side: kind
|
||||||
|
# says v0.32.0, jq says jq-1.8.2, and tilt says v0.37.6 against a pin of 0.37.6.
|
||||||
|
#
|
||||||
|
# Bash's own regex rather than grep, deliberately. grep is not the same program
|
||||||
|
# on every machine — some builds reject patterns that others accept — and a
|
||||||
|
# failed grep inside a count reads exactly like a zero.
|
||||||
|
version_matches() {
|
||||||
|
local tool="$1" path="$2" pin="$3" out v re
|
||||||
|
out=$(reported_version "$tool" "$path") || return 1
|
||||||
|
v="${pin#v}"
|
||||||
|
v="${v//./\\.}"
|
||||||
|
re="(^|[^0-9.])v?${v}([^0-9.]|\$)"
|
||||||
|
[[ $out =~ $re ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# DEPS_ONLY narrows a fetch to the tools it names. Unset means the whole tier,
|
||||||
|
# which is what an explicit `deps.sh fetch` always gets: "download these into
|
||||||
|
# DIR" must not quietly skip something because this machine happens to have it.
|
||||||
|
# Only install() sets it, to what detect_toolchain found missing or mismatched.
|
||||||
|
want() { [ -z "${DEPS_ONLY:-}" ] || [[ " $DEPS_ONLY " == *" $1 "* ]]; }
|
||||||
|
|
||||||
|
# Every tool in the tier with its state, probed once and reported once. What
|
||||||
|
# still needs fetching is left in TOOLCHAIN_NEED for install() to act on.
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
detect_toolchain() {
|
||||||
|
local tier="${TIER:-dev}" b pin path found
|
||||||
|
TOOLCHAIN_NEED=""
|
||||||
|
echo
|
||||||
|
echo "toolchain (pinned, tier '$tier')"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
pin=$(pin_of "$b")
|
||||||
|
path=$(command -v "$b" 2>/dev/null || true)
|
||||||
|
# compose is the one tool that is normally NOT a binary on PATH. It is a
|
||||||
|
# docker CLI plugin, so a machine where `docker compose` works perfectly
|
||||||
|
# has no `docker-compose` to find — and probing only PATH would report it
|
||||||
|
# missing and re-download a copy that is already there. That is the exact
|
||||||
|
# noise the version-aware skip exists to prevent, so ask docker instead.
|
||||||
|
if [ "$b" = docker-compose ] && [ -z "$path" ]; then
|
||||||
|
if found=$(docker compose version --short 2>/dev/null) && [ -n "$found" ]; then
|
||||||
|
if [ "${found#v}" = "${pin#v}" ]; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "docker cli plugin"
|
||||||
|
else
|
||||||
|
printf " ! %-8s wants %s, the docker cli plugin reports '%s'\n" \
|
||||||
|
"$b" "$pin" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "$path" ]; then
|
||||||
|
printf " - %-8s %-9s not found\n" "$b" "$pin"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
elif version_matches "$b" "$path" "$pin"; then
|
||||||
|
printf " %-8s %-9s %s\n" "$b" "$pin" "$path"
|
||||||
|
else
|
||||||
|
found=$(reported_version "$b" "$path" 2>/dev/null | head -1 || true)
|
||||||
|
printf " ! %-8s wants %s, %s reports '%s'\n" "$b" "$pin" "$path" "$found"
|
||||||
|
TOOLCHAIN_NEED+="$b "
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -z "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo " every pinned tool is already on PATH — nothing to fetch"
|
||||||
|
else
|
||||||
|
echo " 'make deps' fetches only: ${TOOLCHAIN_NEED% }"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
local dest="$OUT_BIN" tier="${TIER:-dev}"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) dest="$2"; shift 2 ;;
|
||||||
|
core|dev) tier="$1"; shift ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
dest="$(abspath "$dest")"
|
||||||
|
mkdir -p "$dest"
|
||||||
|
TIER="$tier"
|
||||||
|
|
||||||
|
if [ "$DEPS_SOURCE" = "baked" ]; then
|
||||||
|
echo "installing baked binaries from $BAKED_BIN"
|
||||||
|
cp -a "$BAKED_BIN"/. "$dest"/
|
||||||
|
fix_ownership "$dest"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${DEPS_ONLY:-}" ]; then
|
||||||
|
echo "fetching ${DEPS_ONLY% } (source: $DEPS_SOURCE)"
|
||||||
|
else
|
||||||
|
echo "fetching '$tier' toolchain (source: $DEPS_SOURCE)"
|
||||||
|
fi
|
||||||
|
if want kubectl; then fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"; fi
|
||||||
|
if want jq; then fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"; fi
|
||||||
|
if [ "$tier" = "dev" ]; then
|
||||||
|
if want kind; then fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"; fi
|
||||||
|
if want tilt; then fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0; fi
|
||||||
|
if want ctlptl; then fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0; fi
|
||||||
|
if want docker-compose; then
|
||||||
|
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
fix_ownership "$dest"
|
||||||
|
# kind writes the kubeconfig as root too; hand that back as well when it's
|
||||||
|
# a mounted host directory rather than container-local state.
|
||||||
|
fix_ownership "${KUBE_DIR:-/out/kube}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── install ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
report_manual() {
|
||||||
|
echo
|
||||||
|
if [ ${#MANUAL[@]} -eq 0 ]; then
|
||||||
|
echo "nothing left to do by hand."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
||||||
|
echo
|
||||||
|
local n=1
|
||||||
|
for m in "${MANUAL[@]}"; do
|
||||||
|
echo " $n. $m"
|
||||||
|
echo
|
||||||
|
n=$((n + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installing into a directory that sits early in PATH silently replaces whatever
|
||||||
|
# the machine was already using — which on a shared or client machine can break
|
||||||
|
# unrelated work (kubectl more than one minor away from a cluster is the common
|
||||||
|
# one). Say so; never decide it for them.
|
||||||
|
# Downloading a verified binary proves it is the right file, not that this
|
||||||
|
# machine can run it. On an old distro tilt fails here, with a linker error
|
||||||
|
# about a missing symbol, and finding that out now beats finding out during a
|
||||||
|
# first cluster build.
|
||||||
|
verify_tools() {
|
||||||
|
local tier="${1:-dev}" b bin out rc broke=0
|
||||||
|
echo "checking that each one actually runs"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
bin="$OUT_BIN/$b"
|
||||||
|
if [ ! -x "$bin" ]; then
|
||||||
|
printf ' %-14s not installed\n' "$b"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Not piped into `head`. With `pipefail` set, a tool that prints more
|
||||||
|
# than one line gets SIGPIPE when head closes the pipe, and the
|
||||||
|
# pipeline reports 141 — so a working kubectl was announced as "does
|
||||||
|
# not run here", with its own correct version string as the evidence.
|
||||||
|
# Take the first line afterwards, from the string.
|
||||||
|
rc=0
|
||||||
|
case "$b" in
|
||||||
|
kubectl) out=$("$bin" version --client 2>&1) || rc=$? ;;
|
||||||
|
jq) out=$("$bin" --version 2>&1) || rc=$? ;;
|
||||||
|
*) out=$("$bin" version 2>&1) || rc=$? ;;
|
||||||
|
esac
|
||||||
|
out=${out%%$'\n'*}
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
printf ' %-14s %s\n' "$b" "$out"
|
||||||
|
else
|
||||||
|
printf ' ! %-12s does not run here: %s\n' "$b" "$out"
|
||||||
|
broke=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$broke" -eq 1 ]; then
|
||||||
|
echo
|
||||||
|
echo " A binary that downloads and verifies but will not start is almost"
|
||||||
|
echo " always this distro's libc being older than the release needs."
|
||||||
|
echo " 'detect' prints the glibc version. The core tier (kubectl + jq)"
|
||||||
|
echo " has no such dependency and will work regardless."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
echo "pinned, linux/amd64 only:"
|
||||||
|
printf ' %-14s %s\n' kubectl "$KUBECTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' jq "$JQ_VERSION"
|
||||||
|
printf ' %-14s %s\n' kind "$KIND_VERSION"
|
||||||
|
printf ' %-14s %s\n' tilt "$TILT_VERSION"
|
||||||
|
printf ' %-14s %s\n' ctlptl "$CTLPTL_VERSION"
|
||||||
|
printf ' %-14s %s\n' docker-compose "$COMPOSE_VERSION"
|
||||||
|
echo
|
||||||
|
echo " core = $CORE_TOOLS"
|
||||||
|
echo " dev = $CORE_TOOLS $DEV_TOOLS"
|
||||||
|
echo
|
||||||
|
echo "Checksums are pinned in the block at the top of this file. To bump one,"
|
||||||
|
echo "take the new checksum from the publisher's own release list — the header"
|
||||||
|
echo "comment has the exact commands."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
||||||
|
|
||||||
|
warn_shadowing() {
|
||||||
|
local b existing shadowed="" tier="${1:-dev}"
|
||||||
|
for b in $(tier_tools "$tier"); do
|
||||||
|
[ -x "$OUT_BIN/$b" ] || continue
|
||||||
|
# Where would this resolve if OUT_BIN weren't in the way?
|
||||||
|
existing=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -vx "$OUT_BIN" | paste -sd:) \
|
||||||
|
command -v "$b" 2>/dev/null || true)
|
||||||
|
[ -n "$existing" ] || continue
|
||||||
|
[ "$existing" = "$OUT_BIN/$b" ] && continue
|
||||||
|
# The same version in both places is not a conflict: nothing changes for
|
||||||
|
# any other project whichever copy PATH happens to find first.
|
||||||
|
if version_matches "$b" "$existing" "$(pin_of "$b")"; then continue; fi
|
||||||
|
shadowed+=" $b $existing"$'\n'
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "$shadowed" ] || return 0
|
||||||
|
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) return 0 ;; # not on PATH yet, so nothing is being shadowed
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " ! these were already installed elsewhere and are now shadowed by $OUT_BIN:"
|
||||||
|
printf '%s' "$shadowed"
|
||||||
|
echo " Other projects on this machine will pick up the new versions."
|
||||||
|
MANUAL+=("Decide which toolchain wins. To keep the previous one, remove what
|
||||||
|
was just installed:
|
||||||
|
rm -f $(for b in $(tier_tools "$tier"); do printf '%s ' "$OUT_BIN/$b"; done)
|
||||||
|
Or install somewhere private instead:
|
||||||
|
OUT_BIN=\$PWD/def/bin make deps # then put that dir first in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
# A copy in OUT_BIN only gives you `docker-compose`. That hyphenated form is the
|
||||||
|
# retired v1 spelling; every compose file written in the last few years assumes
|
||||||
|
# `docker compose`, which resolves plugins BY NAME out of a plugin directory.
|
||||||
|
# So the binary is fetched like any other and then linked, in your own home —
|
||||||
|
# no root, and nothing outside it.
|
||||||
|
install_compose_plugin() {
|
||||||
|
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
||||||
|
[ -x "$src" ] || return 0
|
||||||
|
mkdir -p "$dir"
|
||||||
|
# Something else already owns that name — docker-desktop and some distro
|
||||||
|
# packages install a real file there. Overwriting it would take the plugin
|
||||||
|
# away from whatever put it there, so say so and let the user decide.
|
||||||
|
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
||||||
|
MANUAL+=("Something already installs the compose plugin at
|
||||||
|
$dir/docker-compose
|
||||||
|
To use rig's pinned build instead:
|
||||||
|
ln -sf $src $dir/docker-compose")
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
ln -sfn "$src" "$dir/docker-compose"
|
||||||
|
echo " compose plugin -> $dir/docker-compose"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
local tier="${1:-dev}" b
|
||||||
|
TIER="$tier"
|
||||||
|
detect
|
||||||
|
|
||||||
|
# detect_toolchain has already probed PATH. Fetch only what it found missing
|
||||||
|
# or at the wrong version; a tool already present at its pin stays where it is.
|
||||||
|
if [ -n "$TOOLCHAIN_NEED" ]; then
|
||||||
|
echo
|
||||||
|
DEPS_ONLY="$TOOLCHAIN_NEED" fetch "$tier"
|
||||||
|
echo
|
||||||
|
echo "installed to $OUT_BIN ($tier):"
|
||||||
|
for b in $TOOLCHAIN_NEED; do
|
||||||
|
if [ -x "$OUT_BIN/$b" ]; then echo " $b"; fi
|
||||||
|
done
|
||||||
|
if [ "$tier" = "core" ]; then
|
||||||
|
echo " (no kind/tilt — 'make deps dev' adds them)"
|
||||||
|
fi
|
||||||
|
# Only when compose was one of the things fetched: linking a binary
|
||||||
|
# that is already satisfied elsewhere on PATH would point the plugin at
|
||||||
|
# a copy rig did not install.
|
||||||
|
case " $TOOLCHAIN_NEED " in
|
||||||
|
*" docker-compose "*) install_compose_plugin ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Only worth saying when something actually landed in OUT_BIN. When every
|
||||||
|
# tool was satisfied elsewhere, OUT_BIN may reasonably be off PATH, and
|
||||||
|
# telling the user to add it would be advice to fix nothing.
|
||||||
|
case ":${PATH}:" in
|
||||||
|
*":$OUT_BIN:"*) ;;
|
||||||
|
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
||||||
|
export PATH=\"${OUT_BIN}:\$PATH\"") ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
warn_shadowing "$tier"
|
||||||
|
|
||||||
|
report_manual
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_linux
|
||||||
|
|
||||||
|
# Read the command, THEN shift — and shift only if there is something there.
|
||||||
|
# A bare `shift` with no positional parameters returns 1, and under `set -e`
|
||||||
|
# that ended the script before a single line was printed: running this with no
|
||||||
|
# arguments at all, the documented default, did nothing and said nothing.
|
||||||
|
cmd="${1:-install}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
# Baked mode copies binaries already in the image, so it needs no downloader.
|
||||||
|
need_downloads() {
|
||||||
|
require_amd64
|
||||||
|
if [ "$DEPS_SOURCE" != baked ]; then pick_downloader; fi
|
||||||
|
pick_sha
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
detect) detect; report_manual ;;
|
||||||
|
list) list ;;
|
||||||
|
verify) verify_tools "${1:-dev}" ;;
|
||||||
|
fetch) need_downloads; fetch "$@" ;;
|
||||||
|
install) need_downloads; install "${1:-dev}" ;;
|
||||||
|
*) echo "usage: $0 [detect|list|verify|fetch|install]" >&2
|
||||||
|
echo " install [core|dev] (default dev)" >&2
|
||||||
|
echo " fetch [core|dev] [--to DIR]" >&2
|
||||||
|
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
859
rig/standalone/offline/rigmini.sh
Executable file
859
rig/standalone/offline/rigmini.sh
Executable file
@@ -0,0 +1,859 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GENERATED by make standalone — do not edit
|
||||||
|
#
|
||||||
|
# rigmini.sh for profile 'offline', flattened from:
|
||||||
|
# ctrl/mem.sh
|
||||||
|
# ctrl/lib/config.sh
|
||||||
|
# Edit those and run `make standalone`. Changes made here are lost, and
|
||||||
|
# `make selftest` fails while this file differs from what rig generates.
|
||||||
|
|
||||||
|
# ── from the libraries ──
|
||||||
|
declare -- CONFIG_OVERRIDABLE=$'PROFILE CLUSTER K8S_VERSION KIND_CONFIG ADDONS\n REGISTRY_MODE INGRESS_MODE DNS_MODE TILT_PORT\n SOURCE ARCH DEPS_SOURCE HTTP_PORT HTTPS_PORT\n REGISTRY_PORT MANIFESTS_DIR'
|
||||||
|
_config_restore ()
|
||||||
|
{
|
||||||
|
local line;
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
eval "export $line";
|
||||||
|
fi;
|
||||||
|
done <<< "$1";
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
default_cluster_name ()
|
||||||
|
{
|
||||||
|
local n;
|
||||||
|
n=$(basename "$(cd .. && pwd)");
|
||||||
|
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-');
|
||||||
|
n=$(echo "$n" | sed 's/^-*//; s/-*$//');
|
||||||
|
echo "${n:-rig}"
|
||||||
|
}
|
||||||
|
derive_port_base ()
|
||||||
|
{
|
||||||
|
local h;
|
||||||
|
h=$(printf '%s' "$1" | cksum | awk '{print $1}');
|
||||||
|
echo $((20000 + (h % 200) * 10))
|
||||||
|
}
|
||||||
|
render_kind_config ()
|
||||||
|
{
|
||||||
|
local host_workdir="${HOST_WORKDIR:-$(cd .. && pwd)}";
|
||||||
|
sed -e "s|\${CLUSTER}|${CLUSTER}|g" -e "s|\${NODE_IMAGE}|${NODE_IMAGE}|g" -e "s|\${HTTP_PORT}|${HTTP_PORT}|g" -e "s|\${HOST_WORKDIR}|${host_workdir}|g" "$KIND_CONFIG_PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── configuration, frozen for profile 'offline' ──
|
||||||
|
load_config() {
|
||||||
|
local k saved=""
|
||||||
|
for k in $CONFIG_OVERRIDABLE; do
|
||||||
|
if [ -n "${!k+x}" ]; then saved+="$k=$(printf '%q' "${!k}")"$'\n'; fi
|
||||||
|
done
|
||||||
|
declare -gx ADDONS="metallb"
|
||||||
|
declare -gx AIRFLOW_IMAGE="apache/airflow:2.10.4"
|
||||||
|
declare -g AUDIT="on"
|
||||||
|
declare -gx CERT_MANAGER_VERSION="v1.21.1"
|
||||||
|
declare -g CLUSTER="rig"
|
||||||
|
declare -gx COMPOSE_SHA256="db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576"
|
||||||
|
declare -gx COMPOSE_URL="https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-linux-x86_64"
|
||||||
|
declare -gx COMPOSE_VERSION="5.5.1"
|
||||||
|
declare -gx CTLPTL_SHA256="c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e"
|
||||||
|
declare -gx CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v0.9.4/ctlptl.0.9.4.linux.x86_64.tar.gz"
|
||||||
|
declare -gx CTLPTL_VERSION="0.9.4"
|
||||||
|
declare -gx DNS_MODE="hosts"
|
||||||
|
declare -g HTTPS_PORT="20311"
|
||||||
|
declare -g HTTP_PORT="20310"
|
||||||
|
declare -gx INGRESS_MODE="hostport"
|
||||||
|
declare -gx JQ_SHA256="b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f"
|
||||||
|
declare -gx JQ_URL="https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||||
|
declare -gx JQ_VERSION="1.8.2"
|
||||||
|
declare -gx K8S_VERSION="v1_36"
|
||||||
|
declare -gx KIND_CONFIG="kind-config.audit.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_PATH="./k8s/kind-config.audit.yaml.tpl"
|
||||||
|
declare -g KIND_CONFIG_SHOWN="ctrl/k8s/kind-config.audit.yaml.tpl"
|
||||||
|
declare -gx KIND_SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54"
|
||||||
|
declare -gx KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/v0.32.0/kind-linux-amd64"
|
||||||
|
declare -gx KIND_VERSION="v0.32.0"
|
||||||
|
declare -g KUBECONTEXT="kind-rig"
|
||||||
|
declare -gx KUBECTL_SHA256="ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336"
|
||||||
|
declare -gx KUBECTL_URL="https://dl.k8s.io/release/v1.36.3/bin/linux/amd64/kubectl"
|
||||||
|
declare -gx KUBECTL_VERSION="v1.36.3"
|
||||||
|
declare -g MANIFESTS_DIR="ctrl/k8s/overlays/dev"
|
||||||
|
declare -gx METALLB_VERSION="v0.16.0"
|
||||||
|
declare -gx METRICS_SERVER_VERSION="v0.9.0"
|
||||||
|
declare -g NODES="1"
|
||||||
|
declare -g NODE_IMAGE="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -gx NODE_IMAGE_v1_33="kindest/node:v1.33.12@sha256:3f5c8443c620245e4d355cfe09e96a91ead32ceaa569d3f1ca9edf0cb2fe2ff4"
|
||||||
|
declare -gx NODE_IMAGE_v1_34="kindest/node:v1.34.8@sha256:02722c2dedddcfc00febf5d27fbeb9b7b2c14294c82109ff4a85d89ac9ba3256"
|
||||||
|
declare -gx NODE_IMAGE_v1_35="kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
|
||||||
|
declare -gx NODE_IMAGE_v1_36="kindest/node:v1.36.1@sha256:3489c7674813ba5d8b1a9977baea8a6e553784dab7b84759d1014dbd78f7ebd5"
|
||||||
|
declare -g NODE_MB="800"
|
||||||
|
declare -gx POSTGRES_IMAGE="postgres:16-alpine"
|
||||||
|
declare -gx PROFILE_NAME="offline"
|
||||||
|
declare -gx REDIS_IMAGE="redis:7-alpine"
|
||||||
|
declare -gx REGISTRY_IMAGE="registry:2"
|
||||||
|
declare -gx REGISTRY_MODE="local"
|
||||||
|
declare -g REGISTRY_PORT="20313"
|
||||||
|
declare -gx STUB_IMAGE="python:3.12-slim"
|
||||||
|
declare -g TILT_PORT="20312"
|
||||||
|
declare -gx TILT_SHA256="e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6"
|
||||||
|
declare -gx TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v0.37.6/tilt.0.37.6.linux.x86_64.tar.gz"
|
||||||
|
declare -gx TILT_VERSION="0.37.6"
|
||||||
|
_config_restore "$saved"
|
||||||
|
}
|
||||||
|
# ── end of frozen configuration ──
|
||||||
|
|
||||||
|
# ── ctrl/mem.sh ──
|
||||||
|
# How much memory this machine will actually give you before something dies —
|
||||||
|
# rig's memory tool, and (generated from this file) the standalone rigmini.sh.
|
||||||
|
#
|
||||||
|
# There are two numbers and they are rarely the same. `status` reports what the
|
||||||
|
# machine ADVERTISES and what is quietly capping it. `push` finds what it will
|
||||||
|
# SURVIVE, by allocating until it stops. `all` does both and weighs the result
|
||||||
|
# against what this profile's cluster needs.
|
||||||
|
#
|
||||||
|
# The gap between them is the whole reason this exists. Under WSL the cap lives
|
||||||
|
# in .wslconfig; in a container or a managed workspace it is a cgroup limit, and
|
||||||
|
# there /proc/meminfo reports the HOST's memory while the kernel kills you at a
|
||||||
|
# fraction of it. A script that only read MemTotal would confidently report 32 GB
|
||||||
|
# on a box that OOMs at 2.
|
||||||
|
#
|
||||||
|
# Runs on native Linux and under WSL. On WSL the memory you see is a VM
|
||||||
|
# allocation that can be raised, and the commonest failure is raising it without
|
||||||
|
# restarting — so status compares what .wslconfig says with what actually booted.
|
||||||
|
#
|
||||||
|
# Reports and instructs. It never raises a limit, frees anything or installs a
|
||||||
|
# package. The one write it can make is `backup`, which copies .wslconfig beside
|
||||||
|
# itself, so that `restore` has something to put back after a hand edit.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# mem.sh status what it has, what caps it
|
||||||
|
# mem.sh push [--to GB] [--to-oom] climb until it stops
|
||||||
|
# mem.sh all [--budget GB] both, then the verdict
|
||||||
|
# mem.sh backup | restore .wslconfig, WSL only
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
# (sourced library inlined above)
|
||||||
|
|
||||||
|
# ── defaults ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
STEP_MB=0 # per allocation; 0 means scale it to the ceiling. See push().
|
||||||
|
STEP_EXPLICIT=no # whether --step was given, which turns the scaling off.
|
||||||
|
TO_MB="" # --to: stop here regardless. Empty means no hard cap.
|
||||||
|
TO_OOM=no # --to-oom: opt in to running until the kernel intervenes.
|
||||||
|
BUDGET_GB="" # --budget; empty means what this profile's cluster needs, from rig.
|
||||||
|
BUDGET_EXPLICIT=no # whether --budget was given, which retires the guess below.
|
||||||
|
|
||||||
|
# ── platform ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Windows outside WSL — Git Bash, MSYS, Cygwin — looks close enough to work and
|
||||||
|
# then fails in a pile of confusing ways: no /proc, no docker socket, none of
|
||||||
|
# the tooling. Detectable, so name it instead.
|
||||||
|
require_linux() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
This has to run inside WSL, not Git Bash / MSYS / Cygwin.
|
||||||
|
|
||||||
|
If WSL is not installed yet, from an elevated PowerShell or Command Prompt:
|
||||||
|
|
||||||
|
wsl --install
|
||||||
|
|
||||||
|
That enables Windows features and needs a reboot, so it is not something this
|
||||||
|
script will do for you. Afterwards, open the Linux shell it installs and run
|
||||||
|
this from there.
|
||||||
|
EOF
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Everything below reads /proc. Without it there is nothing to measure, and
|
||||||
|
# failing here beats printing a page of empty fields.
|
||||||
|
if [ ! -r /proc/meminfo ]; then
|
||||||
|
echo "no readable /proc/meminfo — this needs a Linux kernel." >&2
|
||||||
|
echo "On macOS or a BSD none of the numbers below exist." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
||||||
|
|
||||||
|
is_container() {
|
||||||
|
[ -f /.dockerenv ] && return 0
|
||||||
|
grep -qE '(docker|containerd|kubepods|lxc|podman)' /proc/1/cgroup 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
platform() {
|
||||||
|
if is_wsl; then echo WSL
|
||||||
|
elif is_container; then echo container
|
||||||
|
else echo "native linux"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── reading memory ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
mb() { echo $(( $(awk "/^$1:/{print \$2}" /proc/meminfo) / 1024 )); }
|
||||||
|
|
||||||
|
# MemAvailable arrived in kernel 3.14. Older kernels — and they turn up on
|
||||||
|
# corporate images — need the estimate it replaced, which is worse but not wrong.
|
||||||
|
avail_meminfo_mb() {
|
||||||
|
if grep -q '^MemAvailable:' /proc/meminfo; then
|
||||||
|
mb MemAvailable
|
||||||
|
else
|
||||||
|
awk '/^(MemFree|Buffers|Cached):/{t+=$2} END{print int(t/1024)}' /proc/meminfo
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Where a cgroup records this cgroup's own limit and usage. Set once by
|
||||||
|
# find_cgroup, because every later reading needs both and hunting for the files
|
||||||
|
# on each call would be the slow part of the poll loop.
|
||||||
|
CG_MAX_FILE=""
|
||||||
|
CG_CUR_FILE=""
|
||||||
|
CG_VERSION=""
|
||||||
|
|
||||||
|
find_cgroup() {
|
||||||
|
local rel
|
||||||
|
|
||||||
|
# Inside a container the cgroup namespace makes the top of the tree BE the
|
||||||
|
# container's own cgroup, so the unqualified path is already the right one.
|
||||||
|
# On a host it is the root cgroup, which is never limited — hence the second
|
||||||
|
# attempt via /proc/self/cgroup, which names the slice this shell is in.
|
||||||
|
if [ -r /sys/fs/cgroup/memory.max ]; then
|
||||||
|
CG_VERSION=v2
|
||||||
|
CG_MAX_FILE=/sys/fs/cgroup/memory.max
|
||||||
|
CG_CUR_FILE=/sys/fs/cgroup/memory.current
|
||||||
|
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
|
||||||
|
CG_VERSION=v1
|
||||||
|
CG_MAX_FILE=/sys/fs/cgroup/memory/memory.limit_in_bytes
|
||||||
|
CG_CUR_FILE=/sys/fs/cgroup/memory/memory.usage_in_bytes
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel=$(awk -F: '$1=="0"{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
||||||
|
if [ -n "$rel" ] && [ "$rel" != "/" ] && [ -r "/sys/fs/cgroup${rel}/memory.max" ]; then
|
||||||
|
CG_VERSION=v2
|
||||||
|
CG_MAX_FILE="/sys/fs/cgroup${rel}/memory.max"
|
||||||
|
CG_CUR_FILE="/sys/fs/cgroup${rel}/memory.current"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rel=$(awk -F: '$2 ~ /(^|,)memory(,|$)/{print $3; exit}' /proc/self/cgroup 2>/dev/null || true)
|
||||||
|
if [ -n "$rel" ] && [ "$rel" != "/" ] \
|
||||||
|
&& [ -r "/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes" ]; then
|
||||||
|
CG_VERSION=v1
|
||||||
|
CG_MAX_FILE="/sys/fs/cgroup/memory${rel}/memory.limit_in_bytes"
|
||||||
|
CG_CUR_FILE="/sys/fs/cgroup/memory${rel}/memory.usage_in_bytes"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The cap in MB, or "" when there is none worth reporting. v2 spells unlimited
|
||||||
|
# "max"; v1 spells it as a number near 2^63, which is why this compares against
|
||||||
|
# MemTotal rather than testing for a magic value — a "limit" above the machine's
|
||||||
|
# own memory is not a limit, however it is written.
|
||||||
|
cgroup_cap_mb() {
|
||||||
|
local raw cap
|
||||||
|
[ -n "$CG_MAX_FILE" ] && [ -r "$CG_MAX_FILE" ] || { echo ""; return 0; }
|
||||||
|
raw=$(cat "$CG_MAX_FILE" 2>/dev/null || echo max)
|
||||||
|
[ "$raw" = "max" ] && { echo ""; return 0; }
|
||||||
|
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
cap=$((raw / 1024 / 1024))
|
||||||
|
[ "$cap" -ge "$(mb MemTotal)" ] && { echo ""; return 0; }
|
||||||
|
echo "$cap"
|
||||||
|
}
|
||||||
|
|
||||||
|
cgroup_used_mb() {
|
||||||
|
local raw
|
||||||
|
[ -n "$CG_CUR_FILE" ] && [ -r "$CG_CUR_FILE" ] || { echo ""; return 0; }
|
||||||
|
raw=$(cat "$CG_CUR_FILE" 2>/dev/null || echo "")
|
||||||
|
case "$raw" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
echo $((raw / 1024 / 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
# ulimit -v is a per-process address-space cap. It stops YOU long before the box
|
||||||
|
# does, and because it is inherited from a login shell it is easy to hit without
|
||||||
|
# knowing it is set.
|
||||||
|
ulimit_v_mb() {
|
||||||
|
local v; v=$(ulimit -v 2>/dev/null || echo unlimited)
|
||||||
|
[ "$v" = "unlimited" ] && { echo ""; return 0; }
|
||||||
|
case "$v" in ''|*[!0-9]*) echo ""; return 0 ;; esac
|
||||||
|
echo $((v / 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
# The number everything else is about: the lowest of the things that can stop
|
||||||
|
# you. Printed at the end of `status` and used as the sanity bound in `push`.
|
||||||
|
effective_ceiling_mb() {
|
||||||
|
local c; c=$(mb MemTotal)
|
||||||
|
local cap; cap=$(cgroup_cap_mb)
|
||||||
|
local ul; ul=$(ulimit_v_mb)
|
||||||
|
[ -n "$cap" ] && [ "$cap" -lt "$c" ] && c="$cap"
|
||||||
|
[ -n "$ul" ] && [ "$ul" -lt "$c" ] && c="$ul"
|
||||||
|
echo "$c"
|
||||||
|
}
|
||||||
|
|
||||||
|
# How much room is left RIGHT NOW, from whichever accounting actually governs.
|
||||||
|
# In a capped container /proc/meminfo describes the host and is worse than
|
||||||
|
# useless for this — it would report tens of gigabytes free on a box that is one
|
||||||
|
# allocation from being killed.
|
||||||
|
headroom_mb() {
|
||||||
|
local cap used
|
||||||
|
cap=$(cgroup_cap_mb)
|
||||||
|
used=$(cgroup_used_mb)
|
||||||
|
if [ -n "$cap" ] && [ -n "$used" ]; then
|
||||||
|
echo $(( cap - used ))
|
||||||
|
else
|
||||||
|
avail_meminfo_mb
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── status ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# /mnt/c/Users can hold several real accounts — a renamed login leaves the old
|
||||||
|
# directory behind — so picking the first alphabetically is a coin toss. Ask
|
||||||
|
# Windows, then fall back to whichever profile actually owns a config.
|
||||||
|
wslconfig_path() {
|
||||||
|
local profile winpath found
|
||||||
|
profile=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d "\r\n" || true)
|
||||||
|
case "$profile" in
|
||||||
|
""|*%*) ;;
|
||||||
|
*) winpath=$(wslpath -u "$profile" 2>/dev/null || true)
|
||||||
|
if [ -n "$winpath" ] && [ -d "$winpath" ]; then
|
||||||
|
echo "$winpath/.wslconfig"; return 0
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
found=$(ls -d /mnt/c/Users/*/.wslconfig 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$found" ] && echo "$found"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
hogs() {
|
||||||
|
echo " holding the most:"
|
||||||
|
ps -eo rss,comm --sort=-rss 2>/dev/null \
|
||||||
|
| awk 'NR>1 && NR<=6 {printf " %6.0f MB %s\n", $1/1024, $2}'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
local total avail swap_total swap_free cap ul cur
|
||||||
|
|
||||||
|
echo "host"
|
||||||
|
echo " platform $(platform)"
|
||||||
|
echo " kernel $(uname -r)"
|
||||||
|
[ -r /etc/os-release ] && \
|
||||||
|
echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' /etc/os-release)"
|
||||||
|
echo " cpu $(getconf _NPROCESSORS_ONLN 2>/dev/null || echo '?') online, load $(cut -d' ' -f1-3 /proc/loadavg)"
|
||||||
|
|
||||||
|
# ── the caps first, because they decide what the totals below are worth ──
|
||||||
|
echo
|
||||||
|
echo "caps"
|
||||||
|
cap=$(cgroup_cap_mb)
|
||||||
|
if [ -n "$cap" ]; then
|
||||||
|
cur=$(cgroup_used_mb)
|
||||||
|
echo " cgroup ${cap} MB (${CG_VERSION}, ${CG_CUR_FILE##*/} says ${cur:-?} MB used)"
|
||||||
|
echo " ! /proc/meminfo below describes the HOST, not this cgroup."
|
||||||
|
echo " $(mb MemTotal) MB total is not yours; ${cap} MB is."
|
||||||
|
elif [ -n "$CG_VERSION" ]; then
|
||||||
|
echo " cgroup none (${CG_VERSION} present, no memory limit set)"
|
||||||
|
else
|
||||||
|
echo " cgroup no memory controller found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ul=$(ulimit_v_mb)
|
||||||
|
if [ -n "$ul" ]; then
|
||||||
|
echo " ! ulimit -v ${ul} MB — a per-process cap, inherited from your shell"
|
||||||
|
echo " it stops this process long before the machine runs out"
|
||||||
|
else
|
||||||
|
echo " ulimit -v unlimited"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# overcommit_memory=0 is the default heuristic: a large allocation is
|
||||||
|
# granted on a guess, and the reckoning arrives later as an OOM kill rather
|
||||||
|
# than as a failed malloc. It is why `push` touches every page it asks for.
|
||||||
|
local om or_
|
||||||
|
om=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null || echo '?')
|
||||||
|
or_=$(cat /proc/sys/vm/overcommit_ratio 2>/dev/null || echo '?')
|
||||||
|
case "$om" in
|
||||||
|
0) echo " overcommit 0 heuristic — allocations are granted on a guess," ;;
|
||||||
|
1) echo " overcommit 1 always — every allocation succeeds; the OOM killer is the only limit," ;;
|
||||||
|
2) echo " overcommit 2 strict (ratio ${or_}%) — allocation fails honestly instead of killing later," ;;
|
||||||
|
*) echo " overcommit ${om}" ;;
|
||||||
|
esac
|
||||||
|
[ "$om" != "?" ] && echo " so RSS is the number to trust, not what a process asked for"
|
||||||
|
|
||||||
|
# ── what it says it has ──
|
||||||
|
total=$(mb MemTotal); avail=$(avail_meminfo_mb)
|
||||||
|
swap_total=$(mb SwapTotal); swap_free=$(mb SwapFree)
|
||||||
|
echo
|
||||||
|
echo "memory"
|
||||||
|
echo " total ${total} MB"
|
||||||
|
echo " available ${avail} MB"
|
||||||
|
echo " swap ${swap_total} MB ($(( swap_total - swap_free )) MB used)"
|
||||||
|
if [ "$swap_total" -eq 0 ]; then
|
||||||
|
echo " - no swap: this box has no cushion. It goes from fine to OOM-killed"
|
||||||
|
echo " with nothing in between, which is the abrupt failure you get in a VM."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# postgres puts its shared buffers in /dev/shm. Docker's default is 64 MB,
|
||||||
|
# and the resulting failure names neither shm nor the size.
|
||||||
|
if [ -d /dev/shm ]; then
|
||||||
|
local shm; shm=$(df -Pm /dev/shm 2>/dev/null | awk 'NR==2{print $2}')
|
||||||
|
if [ -n "$shm" ]; then
|
||||||
|
if [ "$shm" -le 64 ]; then
|
||||||
|
echo " ! /dev/shm ${shm} MB — postgres puts shared memory here and 64 MB"
|
||||||
|
echo " is docker's default. Raise it with --shm-size when postgres fails."
|
||||||
|
else
|
||||||
|
echo " /dev/shm ${shm} MB"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "disk"
|
||||||
|
local d
|
||||||
|
for d in / /tmp /var/lib/docker; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
df -Pm "$d" 2>/dev/null | awk -v p="$d" 'NR==2{printf " %-12s %s MB free of %s MB\n", p, $4, $2}'
|
||||||
|
done
|
||||||
|
|
||||||
|
# kind and Tilt both watch large trees, and the failure mode is silent:
|
||||||
|
# they simply stop noticing file changes. Cheap to report while we are here.
|
||||||
|
local w i
|
||||||
|
w=$(cat /proc/sys/fs/inotify/max_user_watches 2>/dev/null || echo 0)
|
||||||
|
i=$(cat /proc/sys/fs/inotify/max_user_instances 2>/dev/null || echo 0)
|
||||||
|
echo
|
||||||
|
echo "tooling"
|
||||||
|
echo " inotify watches=$w instances=$i"
|
||||||
|
if [ "$w" -lt 524288 ] || [ "$i" -lt 512 ]; then
|
||||||
|
echo " ! low — anything watching files will silently stop seeing changes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
echo " docker socket present, no cli"
|
||||||
|
else
|
||||||
|
echo " docker not installed"
|
||||||
|
fi
|
||||||
|
elif docker info >/dev/null 2>&1; then
|
||||||
|
local n
|
||||||
|
n=$(docker ps -q 2>/dev/null | wc -l)
|
||||||
|
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null), ${n} container(s) running"
|
||||||
|
else
|
||||||
|
echo " ! docker cli present but the daemon is unreachable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# WSL keeps its cap on the Windows side, in a file this shell can read but
|
||||||
|
# not usefully apply — the change costs a full VM restart. Report it, and
|
||||||
|
# report the commonest mistake, which is editing it and not restarting.
|
||||||
|
if is_wsl; then
|
||||||
|
local cfg conf conf_mb n
|
||||||
|
cfg=$(wslconfig_path)
|
||||||
|
echo
|
||||||
|
echo "wsl"
|
||||||
|
if [ -z "$cfg" ]; then
|
||||||
|
echo " ! cannot tell which Windows profile owns .wslconfig"
|
||||||
|
else
|
||||||
|
echo " config $cfg"
|
||||||
|
conf=$(configured_memory "$cfg")
|
||||||
|
if [ -n "$conf" ]; then
|
||||||
|
conf_mb=$(to_mb "$conf")
|
||||||
|
echo " configured $conf (${conf_mb} MB), booted ${total} MB"
|
||||||
|
# The VM reports a little less than allocated; 15% covers the
|
||||||
|
# kernel without calling every healthy machine a mismatch.
|
||||||
|
if [ -n "$conf_mb" ] && [ "$total" -lt $(( conf_mb * 85 / 100 )) ]; then
|
||||||
|
echo " ! configured ${conf_mb} MB but booted ${total} MB — not applied yet."
|
||||||
|
echo " From a WINDOWS terminal: wsl --shutdown then start the distro again."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " configured no memory= set (WSL defaults to 50% of host RAM, or 8 GB,"
|
||||||
|
echo " whichever is less). To raise it, add on the Windows side:"
|
||||||
|
echo " [wsl2]"
|
||||||
|
echo " memory=8GB"
|
||||||
|
echo " then from a WINDOWS terminal: wsl --shutdown"
|
||||||
|
fi
|
||||||
|
n=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
|
if [ "$n" -gt 0 ]; then
|
||||||
|
echo " backups $n (newest: $(ls -t "$cfg".*.bak 2>/dev/null | head -1))"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
echo " - native linux: no VM allocation to raise. If memory is tight the levers"
|
||||||
|
echo " are freeing something or adding swap."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "effective ceiling $(effective_ceiling_mb) MB"
|
||||||
|
echo " the lowest of MemTotal, the cgroup cap and ulimit -v. What the box"
|
||||||
|
echo " claims. 'push' measures what it will actually hand over."
|
||||||
|
|
||||||
|
[ "$avail" -lt $(( total / 5 )) ] && { echo; hogs; }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── .wslconfig ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
require_wsl() {
|
||||||
|
if ! is_wsl; then
|
||||||
|
echo "$1 acts on .wslconfig, which only exists under WSL." >&2
|
||||||
|
echo "This is native Linux — there is no VM allocation to save or roll back." >&2
|
||||||
|
echo "Use 'status' to see what the machine actually has." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# backup and restore act on the file, so unlike status they must not guess.
|
||||||
|
wslconfig_required() {
|
||||||
|
local cfg; cfg=$(wslconfig_required)
|
||||||
|
if [ -z "$cfg" ]; then
|
||||||
|
echo "cannot tell which Windows profile owns .wslconfig. Candidates:" >&2
|
||||||
|
ls -d /mnt/c/Users/*/ 2>/dev/null \
|
||||||
|
| grep -viE "/(All Users|Default|Default User|Public)/$" | sed "s/^/ /" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$cfg"
|
||||||
|
}
|
||||||
|
|
||||||
|
configured_memory() {
|
||||||
|
[ -r "$1" ] || { echo ""; return; }
|
||||||
|
sed -n 's/^[[:space:]]*memory[[:space:]]*=[[:space:]]*//p' "$1" | tail -1 | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
# "9GB" / "8192MB" / "9G" -> MB, so it can be compared with /proc/meminfo.
|
||||||
|
to_mb() {
|
||||||
|
local v="${1^^}" n
|
||||||
|
n=$(echo "$v" | tr -dc '0-9')
|
||||||
|
[ -n "$n" ] || { echo ""; return; }
|
||||||
|
case "$v" in
|
||||||
|
*GB|*G) echo $(( n * 1024 )) ;;
|
||||||
|
*MB|*M) echo "$n" ;;
|
||||||
|
*) echo $(( n / 1024 / 1024 )) ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
backup() {
|
||||||
|
require_wsl backup
|
||||||
|
local cfg dest
|
||||||
|
cfg=$(wslconfig_required)
|
||||||
|
[ -r "$cfg" ] || { echo "nothing to back up: $cfg does not exist" >&2; exit 1; }
|
||||||
|
# Timestamped and never overwritten: a backup that can destroy itself on a
|
||||||
|
# second run is not a backup.
|
||||||
|
dest="${cfg}.$(date +%Y%m%d-%H%M%S).bak"
|
||||||
|
cp "$cfg" "$dest"
|
||||||
|
echo "backed up $dest"
|
||||||
|
echo
|
||||||
|
echo "Edit $cfg by hand, then from a WINDOWS terminal: wsl --shutdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
require_wsl restore
|
||||||
|
local cfg newest count
|
||||||
|
cfg=$(wslconfig_required)
|
||||||
|
newest=$(ls -t "$cfg".*.bak 2>/dev/null | head -1 || true)
|
||||||
|
[ -n "$newest" ] || { echo "no backups found beside $cfg" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "restoring $newest"
|
||||||
|
echo " -> $cfg"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Newest is the right default — undo the last edit — but if you backed up
|
||||||
|
# *after* editing, the state you want is older. Show the rest so a no-op
|
||||||
|
# restore is obviously a no-op rather than a mystery.
|
||||||
|
count=$(ls "$cfg".*.bak 2>/dev/null | wc -l)
|
||||||
|
if [ "$count" -gt 1 ]; then
|
||||||
|
echo "$count backups exist, newest first:"
|
||||||
|
ls -t "$cfg".*.bak | sed 's/^/ /'
|
||||||
|
echo " (restoring the newest; copy another by hand to pick an older one)"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -r "$cfg" ]; then
|
||||||
|
echo "what changes:"
|
||||||
|
if diff "$cfg" "$newest" > /tmp/mem.diff 2>&1 && [ ! -s /tmp/mem.diff ]; then
|
||||||
|
echo " nothing — that backup is identical to the current config"
|
||||||
|
else
|
||||||
|
sed 's/^/ /' /tmp/mem.diff
|
||||||
|
fi
|
||||||
|
rm -f /tmp/mem.diff
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "proceed? [y/N] "
|
||||||
|
read -r reply
|
||||||
|
case "$reply" in
|
||||||
|
y|Y|yes|Yes) ;;
|
||||||
|
*) echo "left alone"; return 0 ;;
|
||||||
|
esac
|
||||||
|
cp "$newest" "$cfg"
|
||||||
|
echo "restored. From a WINDOWS terminal: wsl --shutdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── push ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
STATE=""
|
||||||
|
CHILD=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ -n "$CHILD" ] && kill -0 "$CHILD" 2>/dev/null; then
|
||||||
|
kill -KILL "$CHILD" 2>/dev/null || true
|
||||||
|
wait "$CHILD" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
[ -n "$STATE" ] && rm -f "$STATE"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# The child allocates and stops itself; the parent only watches. That split is
|
||||||
|
# the point: under --to-oom the allocating process is expected to be killed, and
|
||||||
|
# something has to survive to say how far it got.
|
||||||
|
allocator() {
|
||||||
|
# Raise our own OOM score to the maximum so the kernel picks THIS process
|
||||||
|
# first. Raising needs no privilege (only lowering does). Without it, the
|
||||||
|
# kernel is free to choose your shell, your ssh session or dockerd — on a
|
||||||
|
# box you are still using, that is not an acceptable coin toss.
|
||||||
|
echo 1000 > "/proc/$BASHPID/oom_score_adj" 2>/dev/null || true
|
||||||
|
|
||||||
|
local arr=() held=0 i=0 rss swapped avail first_swap=0
|
||||||
|
local bytes=$((STEP_MB * 1024 * 1024))
|
||||||
|
local swap_used_start
|
||||||
|
swap_used_start=$(( $(mb SwapTotal) - $(mb SwapFree) ))
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
# Written STRAIGHT INTO the array element. The obvious spelling —
|
||||||
|
# build one chunk and `arr+=("$chunk")` — costs three copies per step,
|
||||||
|
# not one: the template stays resident, expanding "$chunk" makes a
|
||||||
|
# temporary word, and the append makes the element. A 128 MB step then
|
||||||
|
# needs 384 MB transiently, and on a small box it is killed on the
|
||||||
|
# first append while reporting a third of the true ceiling.
|
||||||
|
#
|
||||||
|
# printf -v into a subscript also means every page is written, so it is
|
||||||
|
# resident rather than merely promised — the only kind of allocation
|
||||||
|
# that measures anything under heuristic overcommit.
|
||||||
|
printf -v "arr[$i]" '%*s' "$bytes" ''
|
||||||
|
i=$((i + 1)); held=$((held + STEP_MB))
|
||||||
|
|
||||||
|
rss=$(awk '/^VmRSS:/{print int($2/1024)}' "/proc/$BASHPID/status" 2>/dev/null || echo 0)
|
||||||
|
avail=$(headroom_mb)
|
||||||
|
swapped=$(( $(mb SwapTotal) - $(mb SwapFree) - swap_used_start ))
|
||||||
|
[ "$swapped" -lt 0 ] && swapped=0
|
||||||
|
|
||||||
|
printf '%8s MB held rss %7s MB headroom %7s MB swap +%s MB\n' \
|
||||||
|
"$held" "$rss" "$avail" "$swapped"
|
||||||
|
printf '%s %s %s %s\n' "$held" "$rss" "$avail" "$swapped" >> "$STATE"
|
||||||
|
|
||||||
|
# Worth calling out separately from the ceiling: this is where the box
|
||||||
|
# stops being fast and starts being unusable, which for a scheduler is
|
||||||
|
# a different and earlier problem than being killed.
|
||||||
|
if [ "$swapped" -gt 0 ] && [ "$first_swap" -eq 0 ]; then
|
||||||
|
first_swap=$held
|
||||||
|
echo " - first swap page at ${held} MB — past here it works but crawls"
|
||||||
|
echo "swapat $held" >> "$STATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$TO_MB" ] && [ "$held" -ge "$TO_MB" ]; then
|
||||||
|
echo "stop reached-the-cap" >> "$STATE"; return 0
|
||||||
|
fi
|
||||||
|
if [ "$TO_OOM" = no ] && [ "$avail" -lt "$FLOOR_MB" ]; then
|
||||||
|
echo "stop floor" >> "$STATE"; return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
push() {
|
||||||
|
local total ceiling rc=0 last held rss swapat stop
|
||||||
|
total=$(mb MemTotal)
|
||||||
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
|
||||||
|
# A step is worth about a sixty-fourth of the ceiling: enough resolution to
|
||||||
|
# find the edge, few enough lines to read, and small enough that the
|
||||||
|
# transient cost of one allocation never dominates a small box. A fixed
|
||||||
|
# size cannot do all three — 128 MB is fine on 16 GB and absurd on 512 MB.
|
||||||
|
if [ "$STEP_EXPLICIT" = no ]; then
|
||||||
|
STEP_MB=$(( ceiling / 64 ))
|
||||||
|
[ "$STEP_MB" -lt 4 ] && STEP_MB=4
|
||||||
|
[ "$STEP_MB" -gt 256 ] && STEP_MB=256
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop with a cushion rather than riding it to the kill. How big a cushion
|
||||||
|
# depends on what it is protecting. Under a cgroup cap, running out kills
|
||||||
|
# only this container's own processes, so it need cover no more than the
|
||||||
|
# shell that prints the result — and a 512 MB cushion on a 1 GB box would
|
||||||
|
# halve the answer. On a host there is everything else to protect, and the
|
||||||
|
# OOM killer does not promise to pick the process that caused the problem.
|
||||||
|
if [ -n "$(cgroup_cap_mb)" ]; then FLOOR_MB=64; else FLOOR_MB=512; fi
|
||||||
|
[ $(( ceiling / 20 )) -gt "$FLOOR_MB" ] && FLOOR_MB=$(( ceiling / 20 ))
|
||||||
|
|
||||||
|
STATE=$(mktemp "${TMPDIR:-/tmp}/rigmini.XXXXXX")
|
||||||
|
trap cleanup EXIT
|
||||||
|
# INT kills the child and lets the summary below print anyway, so an
|
||||||
|
# impatient Ctrl-C still tells you how far it got — and, more importantly,
|
||||||
|
# still gives the memory back.
|
||||||
|
trap 'echo; echo " interrupted"; echo "stop interrupted" >> "$STATE"; [ -n "$CHILD" ] && kill -KILL "$CHILD" 2>/dev/null || true' INT
|
||||||
|
|
||||||
|
echo "push"
|
||||||
|
echo " step ${STEP_MB} MB per allocation, every page touched"
|
||||||
|
echo " ceiling ${ceiling} MB claimed"
|
||||||
|
if [ -n "$TO_MB" ]; then
|
||||||
|
echo " stopping at ${TO_MB} MB (--to)"
|
||||||
|
elif [ "$TO_OOM" = yes ]; then
|
||||||
|
echo " ! stopping only when the kernel stops it (--to-oom)"
|
||||||
|
echo " the allocating child is marked as the preferred OOM victim,"
|
||||||
|
echo " but nothing about an OOM kill is entirely polite. Not on a box"
|
||||||
|
echo " running anything you mind losing."
|
||||||
|
else
|
||||||
|
echo " stopping when headroom drops below ${FLOOR_MB} MB"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
allocator &
|
||||||
|
CHILD=$!
|
||||||
|
wait "$CHILD" || rc=$?
|
||||||
|
CHILD=""
|
||||||
|
trap - INT
|
||||||
|
|
||||||
|
last=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 || true)
|
||||||
|
held=$(echo "$last" | awk '{print $1}')
|
||||||
|
rss=$(echo "$last" | awk '{print $2}')
|
||||||
|
swapat=$(awk '/^swapat/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
||||||
|
stop=$(awk '/^stop/{print $2}' "$STATE" 2>/dev/null | head -1 || true)
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ -z "$held" ]; then
|
||||||
|
echo " ! nothing was allocated. Even one ${STEP_MB} MB chunk failed —"
|
||||||
|
echo " try a smaller --step, or check ulimit -v in 'status'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " reached ${rss:-$held} MB resident"
|
||||||
|
[ -n "$swapat" ] && echo " swapping from ${swapat} MB"
|
||||||
|
|
||||||
|
case "$stop" in
|
||||||
|
reached-the-cap)
|
||||||
|
echo " outcome stopped at the --to cap, not at a limit."
|
||||||
|
echo " The box held ${TO_MB} MB without complaint; there is more." ;;
|
||||||
|
floor)
|
||||||
|
echo " outcome stopped with a cushion intact, by choice."
|
||||||
|
echo " The real ceiling is higher — --to-oom finds it, at the"
|
||||||
|
echo " cost of an actual OOM kill." ;;
|
||||||
|
interrupted)
|
||||||
|
echo " outcome interrupted at ${rss:-$held} MB — where you stopped it,"
|
||||||
|
echo " not where the box did." ;;
|
||||||
|
*)
|
||||||
|
# No stop line means the child did not decide to stop: it was ended.
|
||||||
|
if [ "$rc" -ge 128 ]; then
|
||||||
|
echo " outcome the child was killed (signal $((rc - 128))) at ${rss:-$held} MB."
|
||||||
|
elif [ "$rc" -ne 0 ]; then
|
||||||
|
echo " outcome the allocation failed at ${rss:-$held} MB (exit ${rc})."
|
||||||
|
echo " bash could not get the next chunk — an honest malloc"
|
||||||
|
echo " failure rather than a kill. That is the strict-overcommit"
|
||||||
|
echo " or ulimit path."
|
||||||
|
else
|
||||||
|
echo " outcome ended at ${rss:-$held} MB."
|
||||||
|
fi
|
||||||
|
local ev
|
||||||
|
ev=$(dmesg 2>/dev/null | tail -80 | grep -iE 'oom-kill|killed process' | tail -1 || true)
|
||||||
|
if [ -n "$ev" ]; then
|
||||||
|
echo " kernel ${ev#*] }"
|
||||||
|
else
|
||||||
|
echo " - dmesg is unreadable here (dmesg_restrict, or no privilege),"
|
||||||
|
echo " so the kill cannot be confirmed from this side. The number stands."
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The gap between the claim and the measurement is the finding — but only
|
||||||
|
# when the BOX chose where to stop. An empty $stop means the child was ended
|
||||||
|
# rather than deciding to end; anything else (--to, the floor) is a stop we
|
||||||
|
# asked for, and flagging those as short of the ceiling would put a warning
|
||||||
|
# on every deliberately small run.
|
||||||
|
local got="${rss:-$held}"
|
||||||
|
echo
|
||||||
|
if [ -z "$stop" ] && [ "$got" -lt $(( ceiling * 70 / 100 )) ]; then
|
||||||
|
echo " ! claimed ${ceiling} MB, gave up ${got} MB — under 70% of it."
|
||||||
|
echo " Something is taking the difference. 'status' names the candidates:"
|
||||||
|
echo " a cgroup cap, ulimit -v, or memory already resident."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── all ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
all() {
|
||||||
|
status
|
||||||
|
echo
|
||||||
|
echo "────────────────────────────────────────────────────────────"
|
||||||
|
echo
|
||||||
|
push
|
||||||
|
|
||||||
|
local got budget_mb ceiling
|
||||||
|
load_config
|
||||||
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
|
budget_mb=$(( BUDGET_GB * 1024 ))
|
||||||
|
else
|
||||||
|
budget_mb=$(( NODES * NODE_MB ))
|
||||||
|
fi
|
||||||
|
ceiling=$(effective_ceiling_mb)
|
||||||
|
got=$(grep -E '^[0-9]' "$STATE" 2>/dev/null | tail -1 | awk '{print $2}' || true)
|
||||||
|
[ -n "$got" ] || got=0
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "verdict"
|
||||||
|
if [ -n "$BUDGET_GB" ]; then
|
||||||
|
echo " budget ${budget_mb} MB (--budget)"
|
||||||
|
else
|
||||||
|
# rig's own figure for this profile: nodes times what one node costs.
|
||||||
|
# Addons carry no memory figure in rig yet, so this is the cluster alone
|
||||||
|
# and whatever you deploy comes on top. --budget once you know that too.
|
||||||
|
echo " budget ${budget_mb} MB — profile ${PROFILE_NAME}: ${NODES} node(s) x ${NODE_MB} MB,"
|
||||||
|
echo " the cluster alone; your workload comes on top (--budget GB)"
|
||||||
|
fi
|
||||||
|
echo " measured ${got} MB handed over"
|
||||||
|
|
||||||
|
if [ "$got" -ge "$budget_mb" ]; then
|
||||||
|
echo " fits, with $(( got - budget_mb )) MB spare."
|
||||||
|
if [ "$got" -lt $(( budget_mb * 130 / 100 )) ]; then
|
||||||
|
echo " - under 30% spare is thin once a workload runs on top: memory use"
|
||||||
|
echo " is spiky, and the spikes are what get killed."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ! short by $(( budget_mb - got )) MB."
|
||||||
|
if [ "$ceiling" -ge "$budget_mb" ]; then
|
||||||
|
echo " The box CLAIMS enough (${ceiling} MB) but did not deliver it."
|
||||||
|
echo " Free something, or read the caps section again."
|
||||||
|
else
|
||||||
|
echo " The box does not have it to give. A bigger machine, or a profile"
|
||||||
|
echo " with fewer nodes."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
parse_flags() {
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) TO_MB=$(( ${2:?--to needs a value in GB} * 1024 )); shift 2 ;;
|
||||||
|
--to-mb) TO_MB="${2:?--to-mb needs a value in MB}"; shift 2 ;;
|
||||||
|
--step) STEP_MB="${2:?--step needs a value in MB}"; STEP_EXPLICIT=yes; shift 2 ;;
|
||||||
|
--to-oom) TO_OOM=yes; shift ;;
|
||||||
|
--budget) BUDGET_GB="${2:?--budget needs a value in GB}"; BUDGET_EXPLICIT=yes; shift 2 ;;
|
||||||
|
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ "$TO_OOM" = yes ] && [ -n "$TO_MB" ]; then
|
||||||
|
echo "--to and --to-oom contradict each other: one stops early, the other" >&2
|
||||||
|
echo "refuses to stop at all. Pick one." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
require_linux
|
||||||
|
find_cgroup
|
||||||
|
|
||||||
|
cmd="${1:-status}"
|
||||||
|
[ $# -gt 0 ] && shift
|
||||||
|
|
||||||
|
case "$cmd" in
|
||||||
|
status) parse_flags "$@"; status ;;
|
||||||
|
push) parse_flags "$@"; push ;;
|
||||||
|
all) parse_flags "$@"; all ;;
|
||||||
|
backup) backup ;;
|
||||||
|
restore) restore ;;
|
||||||
|
*) echo "usage: $0 [status|push|all|backup|restore]" >&2
|
||||||
|
echo " push [--to GB] [--to-mb MB] [--step MB] [--to-oom]" >&2
|
||||||
|
echo " all [--budget GB]" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -1,637 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Put kind, tilt and kubectl on a machine that has none of them.
|
|
||||||
#
|
|
||||||
# The single file companion to rigmini.sh, for the same reason: rig installs its
|
|
||||||
# toolchain from ctrl/deps.sh reading ctrl/versions.env, and neither of those is
|
|
||||||
# going to a fresh AWS WorkSpace. The pins live inline here instead.
|
|
||||||
#
|
|
||||||
# What it will not do, deliberately:
|
|
||||||
#
|
|
||||||
# * no sudo, no apt, no yum. It writes into $OUT_BIN (default ~/.local/bin)
|
|
||||||
# and, for compose only, a symlink under ~/.docker/cli-plugins — both in
|
|
||||||
# your own home. Everything needing root — installing Docker, joining the
|
|
||||||
# docker group, raising inotify limits — is REPORTED for you to decide on.
|
|
||||||
# That is what makes it safe to run on a machine that already works.
|
|
||||||
# * no unverified download. Every artifact is checked against a SHA256 taken
|
|
||||||
# from the publisher's own release list. A mismatch aborts.
|
|
||||||
# * no guessing at another architecture. See ARCHITECTURE below.
|
|
||||||
#
|
|
||||||
# Two tiers, because "install the toolchain" is not one decision:
|
|
||||||
#
|
|
||||||
# core kubectl, jq — talk to a cluster someone else runs. Nothing that
|
|
||||||
# creates one. The right answer on a managed or corporate machine.
|
|
||||||
# dev core plus kind, tilt and ctlptl — build clusters and hot-reload
|
|
||||||
# into them. The default, and what you want on a workspace of your own.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# rigdeps.sh detect report the host, change nothing
|
|
||||||
# rigdeps.sh list the pinned versions and where they come from
|
|
||||||
# rigdeps.sh install [core|dev] detect, download, verify, install, report
|
|
||||||
# rigdeps.sh fetch [core|dev] [--to DIR] download + verify only
|
|
||||||
# rigdeps.sh verify run what is installed and see if it works
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUT_BIN="${OUT_BIN:-$HOME/.local/bin}"
|
|
||||||
|
|
||||||
# ── the pinned toolchain ───────────────────────────────────────────────────
|
|
||||||
#
|
|
||||||
# ARCHITECTURE. These checksums are the upstream-published SHA256 of the
|
|
||||||
# **linux/amd64** artifact and of nothing else. An arm64 WorkSpace bundle needs
|
|
||||||
# a different binary with a different checksum, and this script refuses rather
|
|
||||||
# than reusing these — a checksum that is merely plausible is worse than none,
|
|
||||||
# because it turns a verified download into a ceremony.
|
|
||||||
#
|
|
||||||
# To bump a version, or to add arm64: take the checksum from the release's own
|
|
||||||
# published list, never from a download you did.
|
|
||||||
#
|
|
||||||
# curl -sSL https://github.com/<org>/<repo>/releases/download/<tag>/checksums.txt
|
|
||||||
#
|
|
||||||
# kubectl publishes its own instead, at <KUBECTL_URL>.sha256.
|
|
||||||
|
|
||||||
KIND_VERSION=v0.32.0
|
|
||||||
KIND_SHA256=50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54
|
|
||||||
KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64"
|
|
||||||
|
|
||||||
KUBECTL_VERSION=v1.36.3
|
|
||||||
KUBECTL_SHA256=ebbd080e7c2e275093b55915722043257eb24004363e20acb3c4d71919f88336
|
|
||||||
KUBECTL_URL="https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
|
||||||
|
|
||||||
TILT_VERSION=0.37.6
|
|
||||||
TILT_SHA256=e9672b8a18d43501f35dcfe98465969a7db0e436b36cf0c50c7e6f8d40de5fe6
|
|
||||||
TILT_URL="https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.x86_64.tar.gz"
|
|
||||||
|
|
||||||
# ctlptl creates a kind cluster WITH a local registry wired in, which is what
|
|
||||||
# keeps images off docker.io — an unqualified image name resolves to
|
|
||||||
# docker.io/library/<name>, and there is nothing structural stopping a push there.
|
|
||||||
CTLPTL_VERSION=0.9.4
|
|
||||||
CTLPTL_SHA256=c63a1ec28e60bc3faf6becb76f53355c5cf5e0143dafdd27ad85db5584fa6b1e
|
|
||||||
CTLPTL_URL="https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/ctlptl.${CTLPTL_VERSION}.linux.x86_64.tar.gz"
|
|
||||||
|
|
||||||
# Upstream's static build. Debian's jq is linked against libjq/libonig, which is
|
|
||||||
# fine on Debian and not portable anywhere else.
|
|
||||||
JQ_VERSION=1.8.2
|
|
||||||
JQ_SHA256=b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f
|
|
||||||
JQ_URL="https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64"
|
|
||||||
|
|
||||||
# Distro docker packages ship the daemon and CLI but frequently not this, so
|
|
||||||
# `docker compose up` fails with "unknown command" on an otherwise working
|
|
||||||
# Docker. It is a CLI plugin: the binary is found by name in a plugin directory,
|
|
||||||
# which is why install_compose_plugin links it into ~/.docker/cli-plugins.
|
|
||||||
COMPOSE_VERSION=5.5.1
|
|
||||||
COMPOSE_SHA256=db1889184726840f75c4f9c001048430d4f25b3be3cb084d3ddd762bc0aed576
|
|
||||||
COMPOSE_URL="https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-x86_64"
|
|
||||||
|
|
||||||
CORE_TOOLS="kubectl jq"
|
|
||||||
DEV_TOOLS="kind tilt ctlptl docker-compose"
|
|
||||||
|
|
||||||
# No helm: every rig addon installs with `kubectl apply -f`, so nothing has ever
|
|
||||||
# invoked it. Add it the day something actually needs a chart.
|
|
||||||
|
|
||||||
# Collected as we go, printed by report_manual() at the very end. Anything that
|
|
||||||
# needs root or a decision lands here instead of being done.
|
|
||||||
MANUAL=()
|
|
||||||
|
|
||||||
# ── 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 ;;
|
|
||||||
Linux) ;;
|
|
||||||
*) echo "$(uname -s) is not Linux. These are linux binaries; nothing here" >&2
|
|
||||||
echo "would run even if it downloaded." >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) echo amd64 ;;
|
|
||||||
aarch64|arm64) echo arm64 ;;
|
|
||||||
*) uname -m ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The pins above are amd64. Rather than download something that cannot execute
|
|
||||||
# and let it fail as "cannot execute binary file: Exec format error", say so
|
|
||||||
# here and hand over the commands that produce the right checksums.
|
|
||||||
require_amd64() {
|
|
||||||
local a; a=$(arch)
|
|
||||||
[ "$a" = "amd64" ] && return 0
|
|
||||||
cat >&2 <<EOF
|
|
||||||
This machine is ${a} ($(uname -m)); every pin in this script is linux/amd64.
|
|
||||||
|
|
||||||
Nothing here would run, so it does not download. To make an ${a} version, the
|
|
||||||
URLs need the ${a} artifact and the checksums need to come from each project's
|
|
||||||
own published list — not from these values, and not from a download you did:
|
|
||||||
|
|
||||||
curl -sSL https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${a}/kubectl.sha256
|
|
||||||
curl -sSL https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}/checksums.txt
|
|
||||||
curl -sSL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/sha256sum.txt
|
|
||||||
|
|
||||||
Edit the pinned block at the top of this file with what those print.
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
is_wsl() { grep -qi microsoft /proc/version 2>/dev/null; }
|
|
||||||
|
|
||||||
# ── the tools this script itself needs ─────────────────────────────────────
|
|
||||||
|
|
||||||
# A fresh minimal image may genuinely have neither curl nor wget. Find out once,
|
|
||||||
# up front, rather than half way through the first download.
|
|
||||||
DL=""
|
|
||||||
pick_downloader() {
|
|
||||||
if command -v curl >/dev/null 2>&1; then DL=curl
|
|
||||||
elif command -v wget >/dev/null 2>&1; then DL=wget
|
|
||||||
else
|
|
||||||
echo "neither curl nor wget is installed, so nothing can be downloaded." >&2
|
|
||||||
echo "Install one first: $(pkg_install_cmd curl)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
download() {
|
|
||||||
local url="$1" out="$2"
|
|
||||||
case "$DL" in
|
|
||||||
curl) curl -fsSL --retry 3 -o "$out" "$url" ;;
|
|
||||||
wget) wget -q --tries=3 -O "$out" "$url" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# sha256sum is coreutils; shasum is the perl one that turns up on stripped
|
|
||||||
# images. Verification is not optional, so if neither exists that is fatal.
|
|
||||||
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 ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
detect() {
|
|
||||||
echo "host"
|
|
||||||
echo " kernel $(uname -r)"
|
|
||||||
echo " arch $(arch) ($(uname -m))"
|
|
||||||
[ -r /etc/os-release ] && \
|
|
||||||
echo " distro $(sed -n 's/^PRETTY_NAME="\(.*\)"/\1/p' /etc/os-release)"
|
|
||||||
if is_wsl; then echo " platform WSL"; else echo " platform native linux"; fi
|
|
||||||
|
|
||||||
local total_kb avail_kb
|
|
||||||
total_kb=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
|
|
||||||
avail_kb=$(awk '/^MemAvailable:/{print $2}' /proc/meminfo)
|
|
||||||
printf " memory %d GB total, %d GB available\n" \
|
|
||||||
$((total_kb / 1024 / 1024)) $((avail_kb / 1024 / 1024))
|
|
||||||
if [ $((avail_kb / 1024 / 1024)) -lt 4 ]; then
|
|
||||||
echo " ! under 4 GB available — a cluster will struggle here."
|
|
||||||
echo " rigmini.sh says how much this box will actually give you."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " install to $OUT_BIN"
|
|
||||||
detect_libc
|
|
||||||
detect_prereqs
|
|
||||||
detect_docker
|
|
||||||
detect_inotify
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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() {
|
|
||||||
# kind builds a cluster out of containers. Without a reachable daemon,
|
|
||||||
# everything here installs perfectly and then does nothing.
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
if [ -S /var/run/docker.sock ]; then
|
|
||||||
echo " docker socket present, no cli"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo " ! docker not installed — kind has nothing to build a cluster in"
|
|
||||||
MANUAL+=("Install Docker. It is the one real 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.")
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if docker info >/dev/null 2>&1; then
|
|
||||||
echo " docker $(docker version --format '{{.Server.Version}}' 2>/dev/null)"
|
|
||||||
# Distro packages routinely omit the compose plugin, so a working
|
|
||||||
# daemon says nothing about whether `docker compose up` will run.
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
echo " compose $(docker compose version --short 2>/dev/null)"
|
|
||||||
else
|
|
||||||
echo " ! no 'docker compose' plugin — compose files will not start."
|
|
||||||
echo " The dev tier installs one; no root needed."
|
|
||||||
fi
|
|
||||||
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 here the
|
|
||||||
# latter returns 1 when the count is zero, and `set -e` kills the
|
|
||||||
# caller. That is the fresh-machine case, where it does most harm.
|
|
||||||
if [ "$n" -gt 0 ]; then
|
|
||||||
echo " - $n kind node container(s) already running"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ! docker cli present but the daemon is unreachable"
|
|
||||||
MANUAL+=("Start Docker, or add yourself to the docker group:
|
|
||||||
sudo systemctl enable --now docker
|
|
||||||
sudo usermod -aG docker \"\$USER\" # then log out and back in")
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# kind and tilt both watch large trees. Distro defaults are far too low and the
|
|
||||||
# failure mode is silent: tilt simply stops noticing that files changed.
|
|
||||||
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 " ! low — tilt will silently stop seeing file changes"
|
|
||||||
MANUAL+=("Raise the inotify limits (needs root):
|
|
||||||
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
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── fetch ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
verify_sha() {
|
|
||||||
local file="$1" want="$2" name="$3" got
|
|
||||||
got=$($SHA "$file" | awk '{print $1}')
|
|
||||||
if [ "$got" != "$want" ]; then
|
|
||||||
echo >&2
|
|
||||||
echo "CHECKSUM MISMATCH for $name — not installing it." >&2
|
|
||||||
echo " expected $want" >&2
|
|
||||||
echo " got $got" >&2
|
|
||||||
echo >&2
|
|
||||||
echo "Either the pin in this script is stale, or what arrived is not what" >&2
|
|
||||||
echo "the publisher released. Neither is worth guessing about." >&2
|
|
||||||
rm -f "$file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_bin <name> <url> <sha256> <dest-dir> — a bare binary
|
|
||||||
fetch_bin() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4"
|
|
||||||
local tmp="$dest/.$name.tmp"
|
|
||||||
printf ' %-8s ' "$name"
|
|
||||||
download "$url" "$tmp"
|
|
||||||
verify_sha "$tmp" "$sha" "$name"
|
|
||||||
mv "$tmp" "$dest/$name"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
echo "ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
# fetch_tgz <name> <url> <sha256> <dest-dir> <path-inside> <strip>
|
|
||||||
# Archive layouts differ, so the caller says which. tilt and ctlptl both ship
|
|
||||||
# the binary at the archive root, hence strip=0.
|
|
||||||
fetch_tgz() {
|
|
||||||
local name="$1" url="$2" sha="$3" dest="$4" inner="$5" strip="$6"
|
|
||||||
local tmp="$dest/.$name.tgz"
|
|
||||||
printf ' %-8s ' "$name"
|
|
||||||
download "$url" "$tmp"
|
|
||||||
verify_sha "$tmp" "$sha" "$name"
|
|
||||||
# --no-same-owner: some archives ship as uid 1001, and extracting as root
|
|
||||||
# would otherwise restore an owner that is not you.
|
|
||||||
tar -xzf "$tmp" -C "$dest" --strip-components="$strip" --no-same-owner "$inner"
|
|
||||||
rm -f "$tmp"
|
|
||||||
chmod +x "$dest/$name"
|
|
||||||
echo "ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch() {
|
|
||||||
local dest="$OUT_BIN" tier="dev"
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--to) dest="${2:?--to needs a directory}"; shift 2 ;;
|
|
||||||
core|dev) tier="$1"; shift ;;
|
|
||||||
*) echo "unknown argument: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
mkdir -p "$dest"
|
|
||||||
|
|
||||||
if ! command -v tar >/dev/null 2>&1 && [ "$tier" = "dev" ]; then
|
|
||||||
echo "tar is missing, and tilt and ctlptl ship as tarballs." >&2
|
|
||||||
echo " $(pkg_install_cmd tar)" >&2
|
|
||||||
echo "Or install the core tier, which is two bare binaries: $0 install core" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "fetching '$tier' into $dest (verifying every checksum)"
|
|
||||||
fetch_bin kubectl "$KUBECTL_URL" "$KUBECTL_SHA256" "$dest"
|
|
||||||
fetch_bin jq "$JQ_URL" "$JQ_SHA256" "$dest"
|
|
||||||
if [ "$tier" = "dev" ]; then
|
|
||||||
fetch_bin kind "$KIND_URL" "$KIND_SHA256" "$dest"
|
|
||||||
fetch_tgz tilt "$TILT_URL" "$TILT_SHA256" "$dest" tilt 0
|
|
||||||
fetch_tgz ctlptl "$CTLPTL_URL" "$CTLPTL_SHA256" "$dest" ctlptl 0
|
|
||||||
fetch_bin docker-compose "$COMPOSE_URL" "$COMPOSE_SHA256" "$dest"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# A copy in OUT_BIN only gives you `docker-compose`. The hyphenated form is the
|
|
||||||
# retired v1 spelling; every compose file written in the last few years assumes
|
|
||||||
# `docker compose`, and that resolves plugins by name from this directory.
|
|
||||||
install_compose_plugin() {
|
|
||||||
local src="$OUT_BIN/docker-compose" dir="$HOME/.docker/cli-plugins"
|
|
||||||
[ -x "$src" ] || return 0
|
|
||||||
mkdir -p "$dir"
|
|
||||||
if [ -e "$dir/docker-compose" ] && [ ! -L "$dir/docker-compose" ]; then
|
|
||||||
echo
|
|
||||||
echo " ! $dir/docker-compose exists and is not a symlink — left alone"
|
|
||||||
MANUAL+=("Something already installs the compose plugin at
|
|
||||||
$dir/docker-compose
|
|
||||||
To use the pinned build instead:
|
|
||||||
ln -sf $src $dir/docker-compose")
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
ln -sfn "$src" "$dir/docker-compose"
|
|
||||||
echo
|
|
||||||
echo " compose plugin linked into $dir"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── verify ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
tier_tools() { [ "$1" = "core" ] && echo "$CORE_TOOLS" || echo "$CORE_TOOLS $DEV_TOOLS"; }
|
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── install ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Installing into a directory early in PATH silently replaces whatever the
|
|
||||||
# machine was already using, which on a shared or corporate machine can break
|
|
||||||
# unrelated work — kubectl more than one minor away from its cluster is the
|
|
||||||
# common one. Say so; never decide it.
|
|
||||||
warn_shadowing() {
|
|
||||||
local b existing shadowed="" paths="" tier="${1:-dev}"
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) return 0 ;; # not on PATH, so nothing is being shadowed yet
|
|
||||||
esac
|
|
||||||
for b in $(tier_tools "$tier"); do
|
|
||||||
[ -x "$OUT_BIN/$b" ] || continue
|
|
||||||
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
|
|
||||||
shadowed+=$(printf ' %-14s %s' "$b" "$existing")$'\n'
|
|
||||||
# Only the shadowing copies are the user's to remove. Listing the whole
|
|
||||||
# tier would delete tools that shadow nothing and are the only copy.
|
|
||||||
paths+="$OUT_BIN/$b "
|
|
||||||
done
|
|
||||||
[ -n "$shadowed" ] || return 0
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo " ! these were already installed elsewhere and are now shadowed:"
|
|
||||||
printf '%s' "$shadowed"
|
|
||||||
MANUAL+=("Decide which toolchain wins. To keep the previous one:
|
|
||||||
rm -f ${paths% }
|
|
||||||
Or keep both, and let the existing one win by putting $OUT_BIN
|
|
||||||
last on PATH instead of first:
|
|
||||||
export PATH=\"\$PATH:$OUT_BIN\"")
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
report_manual() {
|
|
||||||
echo
|
|
||||||
if [ ${#MANUAL[@]} -eq 0 ]; then
|
|
||||||
echo "nothing left to do by hand."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "host actions this cannot perform (${#MANUAL[@]}):"
|
|
||||||
echo
|
|
||||||
local n=1 m
|
|
||||||
for m in "${MANUAL[@]}"; do
|
|
||||||
echo " $n. $m"
|
|
||||||
echo
|
|
||||||
n=$((n + 1))
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
install() {
|
|
||||||
local tier="${1:-dev}"
|
|
||||||
detect
|
|
||||||
echo
|
|
||||||
fetch "$tier"
|
|
||||||
# An `if`, not `[ ] && ...`: on the core tier the test fails, and under
|
|
||||||
# `set -e` a bare failing test here would end the run silently.
|
|
||||||
if [ "$tier" = "dev" ]; then
|
|
||||||
install_compose_plugin
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
verify_tools "$tier"
|
|
||||||
warn_shadowing "$tier"
|
|
||||||
|
|
||||||
if [ "$tier" = "core" ]; then
|
|
||||||
echo
|
|
||||||
echo " core tier: no kind, tilt, ctlptl or compose. '$0 install dev' adds them."
|
|
||||||
fi
|
|
||||||
|
|
||||||
case ":${PATH}:" in
|
|
||||||
*":$OUT_BIN:"*) ;;
|
|
||||||
*) MANUAL+=("Put the toolchain on your PATH — add to ~/.bashrc:
|
|
||||||
export PATH=\"${OUT_BIN}:\$PATH\"
|
|
||||||
then: source ~/.bashrc") ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
report_manual
|
|
||||||
|
|
||||||
if [ "$tier" = "dev" ]; then
|
|
||||||
echo "Once Docker is reachable and this is on PATH:"
|
|
||||||
echo
|
|
||||||
echo " kind create cluster --name scratch"
|
|
||||||
echo " kubectl cluster-info --context kind-scratch"
|
|
||||||
echo " kind delete cluster --name scratch"
|
|
||||||
echo
|
|
||||||
echo "That round trip is the real test that this machine can host a rig."
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── 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
|
|
||||||
|
|
||||||
case "$cmd" in
|
|
||||||
detect) detect; report_manual ;;
|
|
||||||
list) list ;;
|
|
||||||
verify) verify_tools "${1:-dev}" ;;
|
|
||||||
fetch) require_amd64; pick_downloader; pick_sha; fetch "$@" ;;
|
|
||||||
install) require_amd64; pick_downloader; pick_sha; install "${1:-dev}" ;;
|
|
||||||
*) echo "usage: $0 [detect|list|install|fetch|verify]" >&2
|
|
||||||
echo " install [core|dev] (default dev)" >&2
|
|
||||||
echo " fetch [core|dev] [--to DIR]" >&2
|
|
||||||
echo " OUT_BIN=<dir> overrides the install directory" >&2
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
Reference in New Issue
Block a user