317 lines
12 KiB
Bash
Executable File
317 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Create a disposable Linux environment to validate the installer from a
|
|
# genuinely clean slate — one that can be thrown away without touching the
|
|
# environment you actually work in.
|
|
#
|
|
# This is the ONLY host-aware file in the tree. Everything else needs just a
|
|
# Linux with Docker, which is what keeps other host types a later addition
|
|
# rather than a rewrite.
|
|
#
|
|
# On WSL it creates a second distro. There is no .bat and no PowerShell script:
|
|
# wsl.exe is callable from inside WSL, and wslpath converts the paths it wants.
|
|
# A machine with no WSL at all needs `wsl --install` run once by hand first —
|
|
# scripting a reboot-requiring Windows feature install is not worth it.
|
|
#
|
|
# Docker: borrowed by default, never installed twice
|
|
# --------------------------------------------------
|
|
# WSL2 distros share one kernel and one network stack, so two dockerd instances
|
|
# contend over docker0 and iptables and can disturb the daemon you depend on.
|
|
# (That is why Docker Desktop runs one daemon in a dedicated distro and shares
|
|
# its socket rather than installing one per distro.)
|
|
#
|
|
# REUSE_DOCKER=1 (default) borrow the host distro's daemon over /mnt/wsl.
|
|
# Nothing is installed; nothing can conflict.
|
|
# Requires `ctrl/dockerhost.sh share` once on the
|
|
# distro that owns Docker.
|
|
# REUSE_DOCKER=0 install a second daemon in the new distro. Only
|
|
# if you specifically want to test a from-scratch
|
|
# Docker install, and not on a machine you need.
|
|
#
|
|
# Borrowing is also the more honest test: rig never installs Docker anyway — it
|
|
# is the documented prerequisite — so a clean box does not need its own to
|
|
# exercise everything rig actually does.
|
|
#
|
|
# Usage: newbox.sh create | destroy [--purge] | status | shell
|
|
set -euo pipefail
|
|
cd "$(dirname "$0")"
|
|
|
|
source ./lib/config.sh
|
|
load_config
|
|
|
|
REPO="$(cd .. && pwd)"
|
|
|
|
# The distro is named after this environment, and that derived name is the ONLY
|
|
# thing this script will ever destroy. See guard_name().
|
|
BOX="${BOX:-${CLUSTER}box}"
|
|
BOX_USER="${BOX_USER:-dev}"
|
|
|
|
# Borrow the host distro's Docker rather than installing a second daemon.
|
|
REUSE_DOCKER="${REUSE_DOCKER:-1}"
|
|
SHARED_SOCK=/mnt/wsl/shared-docker/docker.sock
|
|
|
|
WSL_EXE=/mnt/c/Windows/System32/wsl.exe
|
|
|
|
# ── host detection ─────────────────────────────────────────────────────────
|
|
|
|
require_wsl() {
|
|
if ! grep -qi microsoft /proc/version 2>/dev/null; then
|
|
cat >&2 <<'EOF'
|
|
newbox is WSL-only for now.
|
|
|
|
If WSL is not installed, run `wsl --install` from an elevated Windows prompt
|
|
first — see "Starting from plain Windows" in README.md.
|
|
|
|
On native Linux you do not need it: rig already isolates environments by
|
|
directory (own cluster, context, images and port block), so a second copy in a
|
|
second directory is the clean slate. To validate the installer itself against a
|
|
bare system, run ctrl/deps.sh against a stock Debian container instead.
|
|
EOF
|
|
exit 1
|
|
fi
|
|
if [ ! -x "$WSL_EXE" ]; then
|
|
echo "wsl.exe not found at $WSL_EXE" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
wsl_list() { "$WSL_EXE" -l -q 2>/dev/null | tr -d '\0\r'; }
|
|
box_exists() { wsl_list | grep -qx "$BOX"; }
|
|
|
|
# `wsl --unregister` permanently deletes a distro's filesystem. The whole safety
|
|
# story is this function: only the name derived from this directory can ever be
|
|
# a target, so a typo or a stray argument cannot destroy the distro you work in.
|
|
guard_name() {
|
|
local derived="${CLUSTER}box"
|
|
if [ "$BOX" != "$derived" ]; then
|
|
echo "refusing: BOX='$BOX' is not the name derived from this directory ('$derived')." >&2
|
|
echo "That guard exists because --unregister is irreversible." >&2
|
|
exit 1
|
|
fi
|
|
if [ -z "$CLUSTER" ] || [ "$BOX" = "box" ]; then
|
|
echo "refusing: empty environment name" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ── create ─────────────────────────────────────────────────────────────────
|
|
|
|
rootfs_path() {
|
|
local win_home; win_home=$(wslpath "$("$WSL_EXE" -d "$(wsl_list | head -1)" -e printf '%s' "$USERPROFILE" 2>/dev/null || true)" 2>/dev/null || true)
|
|
# Simpler and reliable: use the current user's Windows home via /mnt/c.
|
|
ls -d /mnt/c/Users/*/ 2>/dev/null | grep -viE '/(All Users|Default|Default User|Public)/$' | head -1
|
|
}
|
|
|
|
build_rootfs() {
|
|
local tar="$1"
|
|
if [ -f "$tar" ]; then
|
|
echo " rootfs cached: $(basename "$tar")"
|
|
return
|
|
fi
|
|
echo " exporting a stock Debian rootfs (cached for next time)"
|
|
local cid; cid=$(docker create debian:trixie-slim)
|
|
docker export "$cid" > "$tar"
|
|
docker rm -f "$cid" >/dev/null
|
|
}
|
|
|
|
provision() {
|
|
echo " provisioning (root)"
|
|
local hosts_block
|
|
hosts_block=$(CLUSTER="$CLUSTER" HTTP_PORT="$HTTP_PORT" \
|
|
envsubst < ./hosts.tmpl 2>/dev/null || sed "s/\${CLUSTER}/$CLUSTER/g" ./hosts.tmpl)
|
|
|
|
# Piped as stdin rather than a second script file, the same shape as any
|
|
# remote provisioning heredoc. Everything here is idempotent so a failed run
|
|
# can simply be repeated.
|
|
"$WSL_EXE" -d "$BOX" -u root -- bash -s <<PROVISION
|
|
set -euo pipefail
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq ca-certificates curl gnupg sudo >/dev/null
|
|
|
|
if [ "$REUSE_DOCKER" = "1" ]; then
|
|
# Borrow the host distro's daemon: CLI only, no dockerd, nothing to
|
|
# conflict with. The GID must match the owner's or the shared socket is
|
|
# unreadable here even though it is visible.
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
if [ ! -f /etc/apt/keyrings/docker.asc ]; then
|
|
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
|
chmod a+r /etc/apt/keyrings/docker.asc
|
|
fi
|
|
echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable" \
|
|
> /etc/apt/sources.list.d/docker.list
|
|
apt-get update -qq
|
|
apt-get install -y -qq docker-ce-cli >/dev/null
|
|
|
|
echo "export DOCKER_HOST=unix://$SHARED_SOCK" > /etc/profile.d/rig-docker-host.sh
|
|
|
|
if [ -f /mnt/wsl/shared-docker/OWNER ]; then
|
|
gid=\$(awk '/docker gid:/ {print \$3}' /mnt/wsl/shared-docker/OWNER)
|
|
if [ -n "\$gid" ]; then
|
|
getent group docker >/dev/null && groupmod -g "\$gid" docker || groupadd -g "\$gid" docker
|
|
fi
|
|
fi
|
|
else
|
|
# A second daemon. Only when deliberately testing a from-scratch install.
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
if [ ! -f /etc/apt/keyrings/docker.asc ]; then
|
|
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
|
chmod a+r /etc/apt/keyrings/docker.asc
|
|
fi
|
|
echo "deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \$(. /etc/os-release && echo \$VERSION_CODENAME) stable" \
|
|
> /etc/apt/sources.list.d/docker.list
|
|
apt-get update -qq
|
|
apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null
|
|
fi
|
|
|
|
id -u "$BOX_USER" >/dev/null 2>&1 || useradd -m -s /bin/bash "$BOX_USER"
|
|
usermod -aG sudo,docker "$BOX_USER"
|
|
echo "$BOX_USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-$BOX_USER
|
|
chmod 0440 /etc/sudoers.d/90-$BOX_USER
|
|
|
|
# systemd is off by default in WSL, and Docker needs it. Takes effect on the
|
|
# next start of this distro, which is why create() terminates it below.
|
|
cat > /etc/wsl.conf <<WSLCONF
|
|
[boot]
|
|
systemd=true
|
|
|
|
[user]
|
|
default=$BOX_USER
|
|
WSLCONF
|
|
|
|
# The default inotify limits are low enough that file watching silently stops
|
|
# working — no error, changes just stop being noticed. Fix it before it bites.
|
|
cat > /etc/sysctl.d/99-rig.conf <<SYSCTL
|
|
fs.inotify.max_user_watches=524288
|
|
fs.inotify.max_user_instances=512
|
|
SYSCTL
|
|
|
|
if ! grep -q 'rig environment' /etc/hosts 2>/dev/null; then
|
|
{ echo ""; echo "# rig environment"; cat <<'HOSTS'
|
|
$hosts_block
|
|
HOSTS
|
|
} >> /etc/hosts
|
|
fi
|
|
|
|
touch /etc/rig-provisioned
|
|
PROVISION
|
|
}
|
|
|
|
create() {
|
|
require_wsl
|
|
guard_name
|
|
|
|
local winhome; winhome=$(rootfs_path)
|
|
[ -n "$winhome" ] || { echo "could not locate the Windows user directory" >&2; exit 1; }
|
|
local tar="${winhome}rig-rootfs.tar"
|
|
local installdir="${winhome}WSL/${BOX}"
|
|
|
|
echo "creating '$BOX'"
|
|
if [ "$REUSE_DOCKER" = "1" ]; then
|
|
echo " docker: borrowing the host distro's daemon (nothing installed)"
|
|
if [ ! -S "$SHARED_SOCK" ]; then
|
|
echo
|
|
echo " No shared socket yet. In the distro that owns Docker, run once:"
|
|
echo " sudo bash ctrl/dockerhost.sh share"
|
|
echo " That adds one systemd drop-in and nothing else; undo with 'unshare'."
|
|
echo " Continuing — the box will be created, but Docker won't work in it"
|
|
echo " until you do that."
|
|
fi
|
|
else
|
|
echo
|
|
echo " REUSE_DOCKER=0: installing a SECOND Docker daemon."
|
|
echo " WSL distros share a network stack, so this can disturb Docker in"
|
|
echo " the distro you work in. Ctrl-C now if that is a bad trade today."
|
|
echo
|
|
sleep 4
|
|
fi
|
|
echo
|
|
|
|
if box_exists; then
|
|
echo " distro already registered"
|
|
else
|
|
build_rootfs "$tar"
|
|
mkdir -p "$installdir"
|
|
"$WSL_EXE" --import "$BOX" "$(wslpath -w "$installdir")" "$(wslpath -w "$tar")" --version 2
|
|
fi
|
|
|
|
# Resumable: a partially-created box is finished rather than restarted.
|
|
if "$WSL_EXE" -d "$BOX" -u root -- test -f /etc/rig-provisioned 2>/dev/null; then
|
|
echo " already provisioned"
|
|
else
|
|
provision
|
|
echo " restarting the distro so systemd and group membership apply"
|
|
"$WSL_EXE" --terminate "$BOX" # ONLY this distro; never --shutdown
|
|
fi
|
|
|
|
echo " copying rig in"
|
|
tar c -C "$REPO" --exclude=def --exclude=.git --exclude=ctrl/.env . \
|
|
| "$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc "mkdir -p ~/rig && tar x -C ~/rig"
|
|
|
|
echo
|
|
echo " docker: $("$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc 'systemctl is-active docker 2>/dev/null || echo inactive')"
|
|
echo
|
|
echo "next:"
|
|
echo " make newbox shell # a shell inside it"
|
|
echo " then: cd ~/rig && make check && make deps && make cluster up"
|
|
echo
|
|
echo "For a browser on Windows to resolve the hostnames, paste this into"
|
|
echo "C:\\Windows\\System32\\drivers\\etc\\hosts (it has no wildcard support):"
|
|
CLUSTER="$CLUSTER" envsubst < ./hosts.tmpl 2>/dev/null | grep -v '^#' | grep -v '^$' | sed 's/^/ /'
|
|
}
|
|
|
|
# ── the rest ───────────────────────────────────────────────────────────────
|
|
|
|
destroy() {
|
|
require_wsl
|
|
guard_name
|
|
|
|
if ! box_exists; then
|
|
echo "no distro '$BOX' to remove"
|
|
else
|
|
echo "about to PERMANENTLY delete the distro '$BOX' and its filesystem."
|
|
"$WSL_EXE" --terminate "$BOX" 2>/dev/null || true
|
|
"$WSL_EXE" --unregister "$BOX"
|
|
echo " unregistered"
|
|
fi
|
|
|
|
local winhome; winhome=$(rootfs_path)
|
|
rm -rf "${winhome}WSL/${BOX}" 2>/dev/null || true
|
|
|
|
if [ "${1:-}" = "--purge" ]; then
|
|
rm -f "${winhome}rig-rootfs.tar"
|
|
echo " cached rootfs removed"
|
|
fi
|
|
}
|
|
|
|
status() {
|
|
require_wsl
|
|
echo "environment $CLUSTER"
|
|
echo "distro $BOX"
|
|
if box_exists; then
|
|
echo "registered yes"
|
|
echo "provisioned $("$WSL_EXE" -d "$BOX" -u root -- test -f /etc/rig-provisioned 2>/dev/null && echo yes || echo no)"
|
|
echo "docker $("$WSL_EXE" -d "$BOX" -u root -- bash -lc 'systemctl is-active docker 2>/dev/null' || echo unknown)"
|
|
echo "rig copied $("$WSL_EXE" -d "$BOX" -u "$BOX_USER" -- bash -lc 'test -f ~/rig/Makefile && echo yes || echo no' 2>/dev/null)"
|
|
else
|
|
echo "registered no"
|
|
fi
|
|
echo
|
|
echo "all distros (this one is never touched unless it is '$BOX'):"
|
|
wsl_list | sed 's/^/ /'
|
|
}
|
|
|
|
shell() {
|
|
require_wsl
|
|
box_exists || { echo "no distro '$BOX' — run 'make newbox' first" >&2; exit 1; }
|
|
"$WSL_EXE" -d "$BOX" -u "$BOX_USER" --cd '~'
|
|
}
|
|
|
|
case "${1:-status}" in
|
|
create) create ;;
|
|
destroy) shift; destroy "${1:-}" ;;
|
|
status) status ;;
|
|
shell) shell ;;
|
|
*) echo "usage: $0 [create|destroy [--purge]|status|shell]" >&2; exit 1 ;;
|
|
esac
|