Files
Vantz Stockwell 651a35d4be
All checks were successful
CI / backend-types (push) Successful in 10s
CI / frontend-build (push) Successful in 15s
CI / agent-tests (push) Successful in 39s
CI / integration (push) Successful in 22s
docs(reference): import Dune: Awakening server-manager references
Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.

- icehunter/    dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
                closest analog to our agent's Dune docker control plane (compose
                lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/  Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
                daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
                Hyper-V self-host path + game-config schema

See docs/reference-repos/README.md for the full index + how we use each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:08:05 -04:00

354 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
# install.sh — install the dune-admin binary on an Ubuntu host
#
# This script ONLY installs the binary. It does not configure it. To configure
# dune-admin after installing, run the built-in setup wizard:
#
# /opt/dune-admin/dune-admin -setup
#
# The wizard asks you to pick a control plane (kubectl / docker / local / amp)
# and writes ~/.dune-admin/config.yaml.
#
# Prerequisites (this script does NOT install these for you):
# - Ubuntu 22.04 or 24.04 with passwordless sudo
# - Whatever your control plane needs already running:
# * amp → AMP installed with a running Dune instance
# (verify with: sudo -u amp ampinstmgr -l)
# * kubectl → kubeconfig pointing at a k3s/k8s cluster with the
# dune workloads deployed
# * docker → docker daemon + named containers for dune services
# * local → game-server processes reachable on localhost
# - PostgreSQL reachable (typically 127.0.0.1:15432 with the AMP module)
# - Outbound internet (to fetch Go, Node, pnpm, the source repo)
#
# What this script does:
# 1. Installs build toolchain: Go 1.26.3, Node 22 LTS, pnpm 10.28.1, build-essential
# 2. Clones dune-admin source into $SOURCE_DIR (default: ~/src/dune-admin)
# 3. Builds the Linux binary + frontend assets
# 4. Copies the binary, SPA, and data files into $INSTALL_DIR (default: /opt/dune-admin)
# 5. Writes the systemd unit (Restart=always) — but does not enable/start it
# 6. Prints next steps: setup wizard, sudoers entry, service enable/start
#
# What this script does NOT do:
# - Install AMP, k3s, Docker, or set up game services — those are prerequisites
# - Run the setup wizard (interactive; you do that, see above)
# - Apply sudoers grants (security-sensitive; you review and apply)
# - Enable or start the systemd unit (it writes the unit, but you enable/start
# it after running the setup wizard)
#
# Re-running this script is safe and idempotent. If a toolchain version is
# already correct, it's skipped. If source is already cloned, it's fetched
# and reset to the target branch. If artifacts already exist in $INSTALL_DIR,
# they are replaced atomically with a `.prev` backup left in place.
#
# Local patches:
# If a "patches" directory exists next to this script (or one is specified
# with --patches-dir), every *.patch file in it is applied with `git apply`
# after the source checkout. Use this to layer in unmerged fixes or local
# modifications without forking the repo. Pass --no-patches to skip.
#
# Usage:
# ./install.sh # main branch, /opt/dune-admin, current user
# ./install.sh --branch chore/phase-0-bug-fixes
# ./install.sh --install-dir /opt/dune-admin --service-user dune-admin
# ./install.sh --patches-dir ./my-patches
# ./install.sh --no-patches
# ./install.sh --help
# Re-exec under bash when started by another shell (`sh install.sh`, `sh -c …`,
# `zsh install.sh`) so the bash-only features below — arrays, [[ … ]],
# `set -o pipefail`, ${BASH_SOURCE} — work regardless of the caller's shell (#76).
# POSIX-safe test, and placed after the comment header so it stays out of the
# `sed -n '2,30p'` range that usage() prints. The README's `curl … | bash`
# already runs under bash, so the guard is a no-op there.
if [ -z "${BASH_VERSION:-}" ]; then
exec bash "$0" "$@"
fi
set -euo pipefail
# ── Defaults (override via flags) ─────────────────────────────────────────────
REPO_URL="https://github.com/Icehunter/dune-admin.git"
BRANCH="main"
SOURCE_DIR="${HOME}/src/dune-admin"
INSTALL_DIR="/opt/dune-admin"
SERVICE_USER="${USER}"
SKIP_TOOLCHAIN=0
# Patches directory: defaults to ./patches alongside this script. Empty/missing
# is fine — just means no patches will be applied.
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PATCHES_DIR="${SCRIPT_DIR}/patches"
APPLY_PATCHES=1
GO_VERSION="1.26.3"
NODE_MAJOR="22"
PNPM_VERSION="10.28.1"
# ── Helpers ──────────────────────────────────────────────────────────────────
log() { printf '\033[1;34m[install]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[ ok ]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn ]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[fail ]\033[0m %s\n' "$*" >&2; exit 1; }
usage() {
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# ── Argument parsing ─────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--repo) REPO_URL="$2"; shift 2 ;;
--branch) BRANCH="$2"; shift 2 ;;
--source-dir) SOURCE_DIR="$2"; shift 2 ;;
--install-dir) INSTALL_DIR="$2"; shift 2 ;;
--service-user) SERVICE_USER="$2"; shift 2 ;;
--skip-toolchain) SKIP_TOOLCHAIN=1; shift ;;
--patches-dir) PATCHES_DIR="$2"; shift 2 ;;
--no-patches) APPLY_PATCHES=0; shift ;;
-h|--help) usage ;;
*) die "unknown flag: $1 (try --help)" ;;
esac
done
log "config:"
log " repo: $REPO_URL"
log " branch: $BRANCH"
log " source dir: $SOURCE_DIR"
log " install dir: $INSTALL_DIR"
log " service user: $SERVICE_USER"
log ""
# ── Sanity checks ────────────────────────────────────────────────────────────
[[ "$(id -u)" -eq 0 ]] && die "run this as a normal user with sudo, not as root directly"
sudo -n true 2>/dev/null || die "this user needs passwordless sudo (or you need to authenticate sudo first)"
id "$SERVICE_USER" >/dev/null 2>&1 || die "service user '$SERVICE_USER' does not exist"
command -v sudo >/dev/null || die "sudo is required"
# Don't try to migrate a running service silently — make the operator stop it first.
if systemctl is-active --quiet dune-admin 2>/dev/null; then
warn "dune-admin.service is currently active. Stop it before re-running this script:"
warn " sudo systemctl stop dune-admin"
die "refusing to swap binary under a running service"
fi
# ── 1. Toolchain ─────────────────────────────────────────────────────────────
if [[ "$SKIP_TOOLCHAIN" -eq 0 ]]; then
log "installing build toolchain (apt: build-essential, git, curl, ca-certificates)…"
sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
build-essential git curl ca-certificates
# ── Go ─────────────────────────────────────────────────────────────────────
if /usr/local/go/bin/go version 2>/dev/null | grep -q "go${GO_VERSION}"; then
ok "go ${GO_VERSION} already installed"
else
log "installing go ${GO_VERSION} to /usr/local/go…"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" -o "$tmp/go.tgz"
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf "$tmp/go.tgz"
ok "go $(/usr/local/go/bin/go version | awk '{print $3}') installed"
fi
export PATH="/usr/local/go/bin:${PATH}"
# ── Node.js ────────────────────────────────────────────────────────────────
if command -v node >/dev/null && node -v | grep -q "^v${NODE_MAJOR}\."; then
ok "node $(node -v) already installed"
else
log "installing Node ${NODE_MAJOR} LTS from NodeSource…"
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | sudo -E bash -
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nodejs
ok "node $(node -v) installed"
fi
# ── pnpm ───────────────────────────────────────────────────────────────────
if command -v pnpm >/dev/null && pnpm -v 2>/dev/null | grep -q "^${PNPM_VERSION}$"; then
ok "pnpm ${PNPM_VERSION} already installed"
else
log "installing pnpm ${PNPM_VERSION}"
sudo npm install -g "pnpm@${PNPM_VERSION}"
ok "pnpm $(pnpm -v) installed"
fi
else
log "skipping toolchain installation (--skip-toolchain)"
export PATH="/usr/local/go/bin:${PATH}"
fi
log ""
# ── 2. Source ────────────────────────────────────────────────────────────────
log "syncing source into $SOURCE_DIR @ branch $BRANCH"
mkdir -p "$(dirname "$SOURCE_DIR")"
if [[ -d "$SOURCE_DIR/.git" ]]; then
git -C "$SOURCE_DIR" fetch --quiet origin
else
git clone --quiet "$REPO_URL" "$SOURCE_DIR"
fi
git -C "$SOURCE_DIR" checkout --quiet "$BRANCH" 2>/dev/null \
|| git -C "$SOURCE_DIR" checkout --quiet -B "$BRANCH" "origin/$BRANCH"
git -C "$SOURCE_DIR" reset --hard --quiet "origin/$BRANCH"
ok "checked out $(git -C "$SOURCE_DIR" rev-parse --short HEAD) ($(git -C "$SOURCE_DIR" log -1 --format=%s))"
log ""
# ── 2b. Local patches ────────────────────────────────────────────────────────
# Apply *.patch files (in lexical order) from $PATCHES_DIR. This lets you
# layer in unmerged fixes (e.g. our spaHandler restoration while the upstream
# PR is in review) without maintaining a fork.
if [[ "$APPLY_PATCHES" -eq 1 && -d "$PATCHES_DIR" ]]; then
shopt -s nullglob
patches=( "$PATCHES_DIR"/*.patch )
shopt -u nullglob
if [[ ${#patches[@]} -gt 0 ]]; then
log "applying ${#patches[@]} local patch(es) from $PATCHES_DIR"
for p in "${patches[@]}"; do
if git -C "$SOURCE_DIR" apply --check "$p" 2>/dev/null; then
git -C "$SOURCE_DIR" apply "$p"
ok " applied $(basename "$p")"
else
# Already applied? Skip silently if reverse-apply works (idempotent re-runs).
if git -C "$SOURCE_DIR" apply --reverse --check "$p" 2>/dev/null; then
ok " $(basename "$p") already applied (skipped)"
else
die " $(basename "$p") does not apply cleanly. Inspect the patch and the target file:\n git -C $SOURCE_DIR apply --check $p"
fi
fi
done
else
log "no *.patch files in $PATCHES_DIR (skipping)"
fi
elif [[ "$APPLY_PATCHES" -eq 0 ]]; then
log "patch application disabled (--no-patches)"
else
log "no patches dir at $PATCHES_DIR (skipping)"
fi
log ""
# ── 3. Build ─────────────────────────────────────────────────────────────────
log "building frontend (pnpm)…"
# Workaround for pnpm + rolldown native bindings on Linux/Windows: hoist
# node_modules so @rolldown/binding-* is resolvable from the top level.
echo 'node-linker=hoisted' > "$SOURCE_DIR/web/.npmrc"
(cd "$SOURCE_DIR/web" && pnpm install --frozen-lockfile && pnpm build) >/dev/null
[[ -f "$SOURCE_DIR/web/dist/index.html" ]] || die "frontend build produced no dist/index.html"
ok "frontend built ($(du -sh "$SOURCE_DIR/web/dist" | awk '{print $1}'))"
log "building backend (go)…"
(cd "$SOURCE_DIR" && make linux) >/dev/null
[[ -f "$SOURCE_DIR/dune-admin-linux" ]] || die "backend build produced no dune-admin-linux"
ok "backend built ($(du -sh "$SOURCE_DIR/dune-admin-linux" | awk '{print $1}'))"
log ""
# ── 4. Install into $INSTALL_DIR ─────────────────────────────────────────────
log "installing into $INSTALL_DIR (as service user '$SERVICE_USER')…"
sudo mkdir -p "$INSTALL_DIR"
sudo chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
# Backup existing binary (move to .prev for one-step rollback).
if [[ -f "$INSTALL_DIR/dune-admin" ]]; then
sudo cp -f "$INSTALL_DIR/dune-admin" "$INSTALL_DIR/dune-admin.prev"
fi
sudo install -m 0755 -o "$SERVICE_USER" -g "$SERVICE_USER" \
"$SOURCE_DIR/dune-admin-linux" "$INSTALL_DIR/dune-admin"
# Backup existing dist (move to dist.prev for one-step rollback).
if [[ -d "$INSTALL_DIR/dist" ]]; then
sudo rm -rf "$INSTALL_DIR/dist.prev"
sudo mv "$INSTALL_DIR/dist" "$INSTALL_DIR/dist.prev"
fi
sudo cp -r "$SOURCE_DIR/web/dist" "$INSTALL_DIR/dist"
sudo chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/dist"
# Data files (only copy if newer or missing — these change less often).
for f in item-data.json quality-data.json tags-data.json; do
if [[ -f "$SOURCE_DIR/$f" ]]; then
sudo install -m 0644 -o "$SERVICE_USER" -g "$SERVICE_USER" \
"$SOURCE_DIR/$f" "$INSTALL_DIR/$f"
fi
done
ok "installed: $(ls -la "$INSTALL_DIR/dune-admin" | awk '{print $NF, $5, "bytes"}')"
log ""
# ── 4b. systemd unit ─────────────────────────────────────────────────────────
# Write (or repair) the unit with Restart=always. This is REQUIRED for in-app
# self-update: after swapping the binary the process re-execs/exits, and only
# Restart=always reliably brings it back on a clean exit (a hand-made unit with
# Restart=on-failure leaves the service down after an update). We deliberately
# do NOT enable/start here — the service needs config.yaml from the setup
# wizard first — but we DO restart it if it is already enabled (re-install).
UNIT_PATH="/etc/systemd/system/dune-admin.service"
log "writing systemd unit $UNIT_PATH (Restart=always)…"
sudo tee "$UNIT_PATH" >/dev/null <<UNIT
[Unit]
Description=Dune Admin
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$SERVICE_USER
Group=$SERVICE_USER
WorkingDirectory=$INSTALL_DIR
ExecStart=$INSTALL_DIR/dune-admin
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
if systemctl is-enabled --quiet dune-admin 2>/dev/null; then
log "existing service detected — restarting onto the new binary…"
sudo systemctl restart dune-admin || warn "restart failed; check: sudo journalctl -u dune-admin -e"
fi
ok "systemd unit installed (Restart=always)"
log ""
# ── 5. Next steps ────────────────────────────────────────────────────────────
cat <<EOF
──────────────────────────────────────────────────────────────────────────────
install complete. dune-admin binary + SPA are in $INSTALL_DIR.
NEXT STEPS (each is intentionally manual so you can review):
1) RUN THE SETUP WIZARD to generate ~/.dune-admin/config.yaml
cd $INSTALL_DIR
./dune-admin -setup
Select 'amp' as the control plane. Have these handy:
- AMP instance name (e.g. DuneAwakening01) — run \`sudo -u amp ampinstmgr -l\`
- OS user that runs AMP (typically 'amp')
- PostgreSQL password (AMP module sets this during instance creation)
2) APPLY SUDOERS GRANTS — the wizard prints an example at the end.
Save it to /etc/sudoers.d/dune-admin and validate:
sudo visudo -c
Without this, the Server Settings tab cannot write the INI files.
3) START THE SERVICE — the systemd unit is already installed at
/etc/systemd/system/dune-admin.service (Restart=always, User=$SERVICE_USER).
After the setup wizard has written the config, enable and start it:
sudo systemctl enable --now dune-admin
sudo journalctl -u dune-admin -f # tail logs
Browse to http://<this-host>:9090 (or whatever listen_addr you chose).
NOTE: this installer writes the unit with Restart=always, which is required
for in-app self-update (Settings → Check for Updates) to restart cleanly.
ROLLBACK (if something is wrong):
sudo systemctl stop dune-admin
sudo cp $INSTALL_DIR/dune-admin.prev $INSTALL_DIR/dune-admin
sudo rm -rf $INSTALL_DIR/dist && sudo mv $INSTALL_DIR/dist.prev $INSTALL_DIR/dist
sudo systemctl start dune-admin
──────────────────────────────────────────────────────────────────────────────
EOF