rig/commands/coolify-install.sh
dan-claude-bot 1845468765 feat(bootstrap)!: machine roles carry a -server suffix; staging-server restored
rig builds two kinds of thing on opposite sides of a trust boundary --
tailnet machines it converges, and guests a box mints -- and both families
lived in one flat namespace with nothing in a role name saying which you
meant. `staging` is where that stopped being cosmetic: the word names the
metal that hosts guests and the guests on it, only one could have it, and
#31 gave it to the guests. The VM-host shape was left nameless, spelled
`custom --class server --host yes --join authkey`, which is what every
refusal recited at an operator who had confused the two.

The suffix now names the family: control-plane-server, workload-server,
runner-server, dev-server, plus the restored staging-server (class=server
host=yes join=authkey). host=yes already installs the box CLI and runs box's
setup-host, so staging-server is a table row, not new machinery. It stays
OUT of the tag:server allow-list deliberately -- a host is never managed by
the control plane, its guests are -- so its key is minted tag:local.

custom and workstation keep bare names as the rule, not an exception to it:
custom presets nothing and can be any shape including a guest, so a family
claim is one it cannot make; a workstation is somebody's own device, joined
by interactive login, user-owned and untagged, never tailnet-managed.

Hard cut, no aliases -- old names are refused as unknown. Two consequences
this reaches beyond the CLI surface. TS_HOSTNAME defaults to the role name,
so a box taking the default now comes up control-plane-server. And the two
coolify commands match the ROLE NAME in /etc/rig/role, not the traits, so
they now look for role=control-plane-server; a pre-rename control plane
takes their warning branch, which is advisory and never a gate, so the run
proceeds and the message names the repair.

dev-server is class=human, which reads like a contradiction and is not: the
suffix names the family, the class names the root-SSH door policy. The two
axes share the word "server", which is a real wart -- #77 renames the class
trait to what it controls, kept separate because it reaches markers on live
machines that guard root SSH.

Tests cover both directions of the cut: every new name resolves, every old
name is refused as unknown, and the two deliberately-bare roles are proven
NOT to have been swept up -- the inverse error, which would otherwise only
surface at somebody's laptop.

Closes #76 (machine-role half)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:36:00 +00:00

75 lines
3.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# rig coolify install — pinned Coolify install; AUTOUPDATE=false so the
# platform never self-updates underneath its operators. Upgrades are an
# explicit act.
set -euo pipefail
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
# shellcheck source=SCRIPTDIR/lib/users-config.sh
. "$HERE/lib/users-config.sh" # read_role_marker — the traits line bootstrap wrote
log() { printf 'rig-coolify: %s\n' "$*"; }
warn() { printf 'rig-coolify: WARNING: %s\n' "$*" >&2; }
die() { printf 'rig-coolify: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
usage() {
cat <<'EOF'
usage: rig coolify install --version <pin>
Installs Coolify at exactly <pin> (e.g. 4.1.2) with AUTOUPDATE=false.
Control-plane box only. The version pin is required — you state the floor
your tooling is verified against; there is no default.
EOF
}
VERSION=""
while [ $# -gt 0 ]; do
case "$1" in
--version)
[ $# -ge 2 ] || die "--version needs a value" 2
VERSION="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown flag: $1" 2 ;;
esac
done
if [ -z "$VERSION" ]; then
usage >&2
die "--version <pin> is required" 2
fi
# --- role-marker sanity (issue #25) ------------------------------------------
# Coolify belongs on the control-plane box and nowhere else — but the marker is
# ADVISORY, never a gate. It may legitimately be absent (a box bootstrapped
# before rig wrote markers, or a hand-built one), and rig refuses to guess from
# silence. When the marker EXISTS and names another role, the likeliest story
# is an operator in the wrong SSH session about to put a control plane on a
# workload box — so say it loudly. But WARN, never die: the operator may also
# be deliberately repurposing the box, and an advisory file must never outrank
# the human running the command (contrast close-root, where the marker IS the
# gate — shutting the root door blind is irreversible in a way an extra
# Coolify is not). Placed BEFORE the root check for the same reason arg errors
# are: the harness proves it non-root, and reading a 0644 file needs no
# privilege. RIG_ROLE_MARKER overrides the path so tests point it at fixtures
# (repo precedent: users-apply, users-close-root).
#
# This match is on the ROLE NAME, which #76's rename therefore reaches: a box
# bootstrapped before the rename carries 'role=control-plane' and now takes the
# warning branch. That is the hard cut behaving as designed — the marker is
# advisory, the run still proceeds, and the warning names the re-bootstrap that
# makes the marker true again. Nothing here is load-bearing enough to justify
# carrying the old name forever.
MARKER_LINE="$(read_role_marker "${RIG_ROLE_MARKER:-/etc/rig/role}")"
case "$MARKER_LINE" in
""|"role=control-plane-server"|"role=control-plane-server "*) ;;
*) warn "this box's role marker says '${MARKER_LINE}' — not a control-plane box. Coolify belongs on role control-plane-server; if this is the wrong box, stop here and re-check your SSH session. Repurposing it on purpose? Re-run 'rig bootstrap control-plane-server' first so the marker tells the truth." ;;
esac
[ "$(id -u)" -eq 0 ] || die "must run as root"
export AUTOUPDATE=false
log "installing coolify ${VERSION} (AUTOUPDATE=false)"
curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o /tmp/coolify-install.sh
bash /tmp/coolify-install.sh "$VERSION"
log "coolify ${VERSION} installed with AUTOUPDATE=false"
log "next: rig coolify backup install (nightly control-plane dump — do this before the box holds anything)"
log "then: your bootstrap runbook (admin user, API token, GitHub App, S3 destination)"