panel public release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+507
@@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env bash
|
||||
# Panel one-line installer — controller + agent + Postgres on a single Linux box.
|
||||
#
|
||||
# curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash
|
||||
#
|
||||
# Non-interactive knobs (env vars, all optional):
|
||||
# PANEL_REPO_URL git/https base of the panel source (default: placeholder, see below)
|
||||
# PANEL_PREBUILT_URL tarball with prebuilt bin/{controller,agent,panelctl} + modules/ (skips Go build)
|
||||
# (default: the published v0.9.2 linux/amd64 release — so
|
||||
# curl|bash needs no Go. Set it EMPTY to force a source build.)
|
||||
# PANEL_VERSION version string baked into the binaries (default: git describe || dev)
|
||||
# PANEL_DIR install prefix (default: /opt/panel)
|
||||
# PANEL_DATA_DIR data directory (default: $PANEL_DIR/data)
|
||||
# PANEL_HTTP_PORT dashboard port (default: 8080)
|
||||
# PANEL_GRPC_PORT agent gRPC port (default: 8443)
|
||||
# PANEL_ADMIN_PASSWORD stable first-boot admin password (default: generated by the controller)
|
||||
# PANEL_DB_URL use an EXISTING Postgres instead of the managed container
|
||||
# PANEL_DB_PASSWORD password for the managed Postgres container (default: generated)
|
||||
# PANEL_DB_CONTAINER name of the managed Postgres container (default: panel-postgres)
|
||||
# PANEL_DB_PORT host port to bind Postgres on (127.0.0.1) (default: 5432)
|
||||
# PANEL_DB_VOLUME docker volume for Postgres data (default: panel_pgdata)
|
||||
# PANEL_SKIP_AGENT=1 install controller only (add agents on other boxes later)
|
||||
#
|
||||
# Run TWO independent panels on one host by giving each install distinct
|
||||
# PANEL_DIR, PANEL_HTTP_PORT, PANEL_GRPC_PORT, PANEL_DB_CONTAINER,
|
||||
# PANEL_DB_PORT and PANEL_DB_VOLUME values so nothing collides.
|
||||
#
|
||||
# Idempotent: safe to re-run — existing containers, units, CA, DB and
|
||||
# passwords are kept; binaries and modules are refreshed.
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_REPO_URL="${PANEL_REPO_URL:-https://git.pdxtechs.com/dbledeez/panel}"
|
||||
# Default to the published prebuilt release so `curl … | sudo bash` works with no
|
||||
# Go toolchain and no env. Explicitly set PANEL_PREBUILT_URL= (empty) to force the
|
||||
# git-clone + Go source-build fallback instead. A local checkout (running
|
||||
# ./install.sh from inside the repo) always wins over this default — see §4.
|
||||
# Multi-arch: map the running machine's arch to the release naming (…-linux-<arch>).
|
||||
# Only linux/amd64 has a published prebuilt release asset today — so on any other
|
||||
# arch we DON'T point at a nonexistent tarball. We leave PANEL_PREBUILT_DEFAULT
|
||||
# empty for non-amd64, which makes the default resolve to a source build (needs Go).
|
||||
PANEL_ARCH_RAW="$(uname -m 2>/dev/null || echo unknown)"
|
||||
case "$PANEL_ARCH_RAW" in
|
||||
x86_64|amd64) PANEL_ARCH=amd64 ;;
|
||||
aarch64|arm64) PANEL_ARCH=arm64 ;;
|
||||
*) PANEL_ARCH="$PANEL_ARCH_RAW" ;;
|
||||
esac
|
||||
if [ "$PANEL_ARCH" = amd64 ]; then
|
||||
PANEL_PREBUILT_DEFAULT="https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.2/panel-v0.9.2-linux-amd64.tar.gz"
|
||||
else
|
||||
# No prebuilt release asset exists for $PANEL_ARCH yet. Falling back to a
|
||||
# source build (requires Go) rather than fetching the wrong-arch amd64 binary.
|
||||
PANEL_PREBUILT_DEFAULT=""
|
||||
fi
|
||||
# Was the var set in the environment at all (even to empty)? Distinguishes an
|
||||
# operator who explicitly wants prebuilt from the plain default.
|
||||
if [ "${PANEL_PREBUILT_URL+set}" = set ]; then PANEL_PREBUILT_URL_EXPLICIT=1; else PANEL_PREBUILT_URL_EXPLICIT=""; fi
|
||||
PANEL_PREBUILT_URL="${PANEL_PREBUILT_URL-$PANEL_PREBUILT_DEFAULT}"
|
||||
PANEL_DIR="${PANEL_DIR:-/opt/panel}"
|
||||
PANEL_DATA_DIR="${PANEL_DATA_DIR:-$PANEL_DIR/data}"
|
||||
PANEL_HTTP_PORT="${PANEL_HTTP_PORT:-8080}"
|
||||
PANEL_GRPC_PORT="${PANEL_GRPC_PORT:-8443}"
|
||||
PANEL_ADMIN_EMAIL="${PANEL_ADMIN_EMAIL:-}"
|
||||
PANEL_ADMIN_PASSWORD="${PANEL_ADMIN_PASSWORD:-}"
|
||||
# Public HTTPS URL when behind a reverse proxy — required for Steam login to
|
||||
# work through the proxy (e.g. https://panel.example.com). Leave empty for
|
||||
# direct LAN/IP access.
|
||||
PANEL_PUBLIC_URL="${PANEL_PUBLIC_URL:-}"
|
||||
PANEL_DB_URL="${PANEL_DB_URL:-}"
|
||||
PANEL_DB_PASSWORD="${PANEL_DB_PASSWORD:-}"
|
||||
PANEL_DB_CONTAINER="${PANEL_DB_CONTAINER:-panel-postgres}"
|
||||
PANEL_DB_PORT="${PANEL_DB_PORT:-5432}"
|
||||
PANEL_DB_VOLUME="${PANEL_DB_VOLUME:-panel_pgdata}"
|
||||
PANEL_SKIP_AGENT="${PANEL_SKIP_AGENT:-0}"
|
||||
PANEL_USER=panel
|
||||
PG_CONTAINER="$PANEL_DB_CONTAINER"
|
||||
|
||||
# Legacy escape hatch (kept for compatibility): a release asset tried only when
|
||||
# no prebuilt URL, no local checkout and no Go are available. Now that
|
||||
# PANEL_PREBUILT_URL defaults to the published release above, this is rarely hit —
|
||||
# it only matters if someone explicitly emptied PANEL_PREBUILT_URL but still has
|
||||
# no Go and no checkout.
|
||||
PANEL_RELEASE_TARBALL_URL="${PANEL_RELEASE_TARBALL_URL:-$PANEL_PREBUILT_DEFAULT}"
|
||||
|
||||
log() { printf '\033[1;32m[panel]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[panel]\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31m[panel]\033[0m ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" = 0 ] || die "run as root (sudo bash install.sh)"
|
||||
command -v curl >/dev/null || die "curl is required"
|
||||
|
||||
# ---- Arch note -----------------------------------------------------------
|
||||
if [ "$PANEL_ARCH" = amd64 ]; then
|
||||
log "arch: $PANEL_ARCH_RAW ($PANEL_ARCH) — using prebuilt release by default"
|
||||
elif [ -n "$PANEL_PREBUILT_URL_EXPLICIT" ] && [ -n "$PANEL_PREBUILT_URL" ]; then
|
||||
log "arch: $PANEL_ARCH_RAW ($PANEL_ARCH) — using operator-supplied PANEL_PREBUILT_URL"
|
||||
else
|
||||
log "arch: $PANEL_ARCH_RAW ($PANEL_ARCH) — no prebuilt for $PANEL_ARCH, building from source (needs Go)"
|
||||
fi
|
||||
|
||||
# ---- Port pre-flight -----------------------------------------------------
|
||||
# Fail early and loudly if any port we're about to bind is already taken,
|
||||
# instead of letting the controller or the Postgres container crash mid-install
|
||||
# with an opaque "address already in use". Uses whichever probe tool exists.
|
||||
port_in_use() {
|
||||
# $1 = port. Returns 0 (true) if something is LISTENing on it.
|
||||
_p="$1"
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${_p}\$"
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
netstat -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${_p}\$"
|
||||
elif command -v lsof >/dev/null 2>&1; then
|
||||
lsof -iTCP:"${_p}" -sTCP:LISTEN -Pn >/dev/null 2>&1
|
||||
else
|
||||
warn "no ss/netstat/lsof available — skipping port pre-flight for :${_p}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
_preflight_port() {
|
||||
# $1 = port, $2 = human label, $3 = env var name to override
|
||||
if port_in_use "$1"; then
|
||||
die "port $1 ($2) is already in use.
|
||||
Free it, or choose another with: $3=<port> (re-run the installer).
|
||||
e.g. $3=$(( $1 + 1 )) sudo bash install.sh"
|
||||
fi
|
||||
}
|
||||
_preflight_port "$PANEL_HTTP_PORT" "dashboard HTTP" "PANEL_HTTP_PORT"
|
||||
_preflight_port "$PANEL_GRPC_PORT" "agent gRPC" "PANEL_GRPC_PORT"
|
||||
# The managed Postgres binds 127.0.0.1:$PANEL_DB_PORT. Skip this check when an
|
||||
# external DB is supplied (we don't start a container then) OR when the managed
|
||||
# container already exists (a re-run legitimately re-binds its own port).
|
||||
if [ -z "$PANEL_DB_URL" ] && ! docker inspect "$PG_CONTAINER" >/dev/null 2>&1; then
|
||||
_preflight_port "$PANEL_DB_PORT" "Postgres" "PANEL_DB_PORT"
|
||||
fi
|
||||
|
||||
# ---- 0. Admin account: prompt when interactive ---------------------------
|
||||
# When a real person runs this (a TTY is available), ask them to choose the
|
||||
# admin login + password up front — no digging a generated password out of
|
||||
# the log afterwards. When piped non-interactively (curl | bash with no TTY,
|
||||
# CI, cloud-init) we skip the prompt: any PANEL_ADMIN_* env values are used,
|
||||
# and a missing password is generated by the controller as before.
|
||||
if [ -z "$PANEL_ADMIN_PASSWORD" ] && [ -r /dev/tty ] && { [ -t 0 ] || [ -t 1 ]; }; then
|
||||
printf '\n\033[1;36m── Set up your panel admin login ──\033[0m\n' > /dev/tty
|
||||
if [ -z "$PANEL_ADMIN_EMAIL" ]; then
|
||||
printf 'Admin email [admin@panel.local]: ' > /dev/tty
|
||||
read -r _in_email < /dev/tty || _in_email=""
|
||||
PANEL_ADMIN_EMAIL="${_in_email:-}"
|
||||
fi
|
||||
while [ -z "$PANEL_ADMIN_PASSWORD" ]; do
|
||||
printf 'Admin password (min 8 chars, blank = auto-generate): ' > /dev/tty
|
||||
stty -echo 2>/dev/null < /dev/tty
|
||||
read -r _pw1 < /dev/tty || _pw1=""
|
||||
stty echo 2>/dev/null < /dev/tty; printf '\n' > /dev/tty
|
||||
if [ -z "$_pw1" ]; then
|
||||
warn "no password entered — the controller will generate one and print it."
|
||||
break
|
||||
fi
|
||||
if [ "${#_pw1}" -lt 8 ]; then
|
||||
printf ' password too short (need 8+); try again.\n' > /dev/tty
|
||||
continue
|
||||
fi
|
||||
printf 'Confirm password: ' > /dev/tty
|
||||
stty -echo 2>/dev/null < /dev/tty
|
||||
read -r _pw2 < /dev/tty || _pw2=""
|
||||
stty echo 2>/dev/null < /dev/tty; printf '\n' > /dev/tty
|
||||
if [ "$_pw1" != "$_pw2" ]; then
|
||||
printf ' passwords did not match; try again.\n' > /dev/tty
|
||||
continue
|
||||
fi
|
||||
PANEL_ADMIN_PASSWORD="$_pw1"
|
||||
done
|
||||
unset _in_email _pw1 _pw2
|
||||
fi
|
||||
|
||||
# ---- 1. Distro detection -------------------------------------------------
|
||||
DISTRO=unknown
|
||||
if [ -r /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
DISTRO="${ID:-unknown}"
|
||||
fi
|
||||
log "distro: $DISTRO"
|
||||
|
||||
# ---- 2. Docker -----------------------------------------------------------
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
log "docker present: $(docker --version)"
|
||||
else
|
||||
case "$DISTRO" in
|
||||
ubuntu|debian|fedora|centos|rhel|raspbian)
|
||||
log "installing Docker via get.docker.com"
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
;;
|
||||
*)
|
||||
die "Docker not found and no auto-install path for '$DISTRO'.
|
||||
Install Docker Engine first: https://docs.docker.com/engine/install/ then re-run."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
systemctl enable --now docker >/dev/null 2>&1 || true
|
||||
docker info >/dev/null 2>&1 || die "docker daemon is not running/reachable"
|
||||
|
||||
# ---- 3. Postgres ---------------------------------------------------------
|
||||
mkdir -p "$PANEL_DIR/bin" "$PANEL_DATA_DIR"
|
||||
ENV_FILE="$PANEL_DIR/panel.env"
|
||||
|
||||
if [ -n "$PANEL_DB_URL" ]; then
|
||||
log "using external Postgres from \$PANEL_DB_URL"
|
||||
else
|
||||
if docker inspect "$PG_CONTAINER" >/dev/null 2>&1; then
|
||||
log "postgres container '$PG_CONTAINER' already exists — starting"
|
||||
docker start "$PG_CONTAINER" >/dev/null
|
||||
# Recover the password persisted by a previous run.
|
||||
if [ -z "$PANEL_DB_PASSWORD" ] && [ -f "$ENV_FILE" ]; then
|
||||
PANEL_DB_PASSWORD="$(sed -n 's/^PANEL_DB_PASSWORD=//p' "$ENV_FILE" | head -n1)"
|
||||
fi
|
||||
[ -n "$PANEL_DB_PASSWORD" ] || die "existing $PG_CONTAINER but no PANEL_DB_PASSWORD (set it, or remove the container to start fresh)"
|
||||
else
|
||||
if [ -z "$PANEL_DB_PASSWORD" ]; then
|
||||
PANEL_DB_PASSWORD="$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||||
fi
|
||||
log "starting postgres container '$PG_CONTAINER' on 127.0.0.1:$PANEL_DB_PORT (volume $PANEL_DB_VOLUME)"
|
||||
docker run -d --name "$PG_CONTAINER" --restart unless-stopped \
|
||||
-e POSTGRES_USER=panel -e POSTGRES_PASSWORD="$PANEL_DB_PASSWORD" -e POSTGRES_DB=panel \
|
||||
-p "127.0.0.1:$PANEL_DB_PORT:5432" \
|
||||
-v "$PANEL_DB_VOLUME:/var/lib/postgresql/data" \
|
||||
postgres:17-alpine >/dev/null
|
||||
fi
|
||||
PANEL_DB_URL="postgres://panel:${PANEL_DB_PASSWORD}@127.0.0.1:$PANEL_DB_PORT/panel?sslmode=disable"
|
||||
log "waiting for postgres to accept connections"
|
||||
# First-boot does a full initdb + CREATE DATABASE + restart; on slow disks or
|
||||
# the vfs storage driver this can take well over 30s, so budget 120s.
|
||||
for _ in $(seq 1 120); do
|
||||
if docker exec "$PG_CONTAINER" pg_isready -U panel -d panel >/dev/null 2>&1; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
docker exec "$PG_CONTAINER" pg_isready -U panel -d panel >/dev/null 2>&1 || die "postgres did not become ready"
|
||||
fi
|
||||
|
||||
# ---- 4. Source / binaries ------------------------------------------------
|
||||
SRC=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd || true)"
|
||||
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/go.work" ] && [ -d "$SCRIPT_DIR/controller" ]; then
|
||||
SRC="$SCRIPT_DIR"
|
||||
log "using local checkout at $SRC"
|
||||
# A local checkout means the operator wants to build THIS tree. Don't silently
|
||||
# override it with the prebuilt release default. (An explicit PANEL_PREBUILT_URL
|
||||
# env still forces prebuilt — it survives because it was exported, not defaulted.)
|
||||
if [ -z "${PANEL_PREBUILT_URL_EXPLICIT:-}" ] && [ "$PANEL_PREBUILT_URL" = "$PANEL_PREBUILT_DEFAULT" ]; then
|
||||
PANEL_PREBUILT_URL=""
|
||||
fi
|
||||
fi
|
||||
|
||||
# If no prebuilt URL, no local checkout and no Go, but a release tarball URL is
|
||||
# known, auto-fall-back to the published release so the one-liner still works.
|
||||
if [ -z "$PANEL_PREBUILT_URL" ] && [ -z "$SRC" ] \
|
||||
&& ! command -v go >/dev/null 2>&1 && [ -n "$PANEL_RELEASE_TARBALL_URL" ]; then
|
||||
log "no Go toolchain — falling back to the published release tarball"
|
||||
PANEL_PREBUILT_URL="$PANEL_RELEASE_TARBALL_URL"
|
||||
fi
|
||||
|
||||
if [ -n "$PANEL_PREBUILT_URL" ]; then
|
||||
log "downloading prebuilt binaries: $PANEL_PREBUILT_URL"
|
||||
TMP_TGZ="$(mktemp)"
|
||||
curl -fsSL "$PANEL_PREBUILT_URL" -o "$TMP_TGZ"
|
||||
tar -xzf "$TMP_TGZ" -C "$PANEL_DIR"
|
||||
rm -f "$TMP_TGZ"
|
||||
[ -x "$PANEL_DIR/bin/controller" ] || die "prebuilt tarball did not contain bin/controller"
|
||||
else
|
||||
if [ -z "$SRC" ]; then
|
||||
command -v git >/dev/null || die "git is required to fetch sources (or set PANEL_PREBUILT_URL)"
|
||||
SRC="$PANEL_DIR/src"
|
||||
# BUG-4: the src tree is cloned as root then chowned to $PANEL_USER; on a
|
||||
# re-run git would refuse with "detected dubious ownership". Mark it safe
|
||||
# for whatever user runs git (root here) so re-runs are idempotent.
|
||||
git config --global --add safe.directory "$SRC" 2>/dev/null || true
|
||||
if [ -d "$SRC/.git" ]; then
|
||||
log "updating source checkout at $SRC"
|
||||
git -C "$SRC" pull --ff-only
|
||||
else
|
||||
log "cloning $PANEL_REPO_URL"
|
||||
git clone --depth 1 "$PANEL_REPO_URL" "$SRC"
|
||||
fi
|
||||
fi
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
die "Go toolchain not found. The one-line installer normally uses prebuilt
|
||||
binaries; falling back to a source build requires Go >= 1.26 (https://go.dev/dl/).
|
||||
Alternatively set PANEL_PREBUILT_URL to a prebuilt tarball, or
|
||||
PANEL_RELEASE_TARBALL_URL to a published release asset."
|
||||
fi
|
||||
VERSION="${PANEL_VERSION:-$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo dev)}"
|
||||
LDFLAGS="-s -w -X github.com/dbledeez/panel/pkg/version.Version=$VERSION"
|
||||
log "building panel $VERSION"
|
||||
(cd "$SRC" && \
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o "$PANEL_DIR/bin/controller" ./controller/cmd/controller && \
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o "$PANEL_DIR/bin/agent" ./agent/cmd/agent && \
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o "$PANEL_DIR/bin/panelctl" ./controller/cmd/panelctl)
|
||||
log "syncing game modules"
|
||||
rm -rf "$PANEL_DIR/modules.new"
|
||||
cp -r "$SRC/modules" "$PANEL_DIR/modules.new"
|
||||
rm -rf "$PANEL_DIR/modules"
|
||||
mv "$PANEL_DIR/modules.new" "$PANEL_DIR/modules"
|
||||
fi
|
||||
|
||||
# ---- 5. User + env file + systemd units ----------------------------------
|
||||
if ! id "$PANEL_USER" >/dev/null 2>&1; then
|
||||
useradd --system --home-dir "$PANEL_DIR" --shell /usr/sbin/nologin "$PANEL_USER"
|
||||
fi
|
||||
usermod -aG docker "$PANEL_USER" 2>/dev/null || true
|
||||
|
||||
umask 077
|
||||
{
|
||||
echo "PANEL_DB_URL=$PANEL_DB_URL"
|
||||
[ -n "$PANEL_DB_PASSWORD" ] && echo "PANEL_DB_PASSWORD=$PANEL_DB_PASSWORD"
|
||||
[ -n "$PANEL_ADMIN_EMAIL" ] && echo "PANEL_ADMIN_EMAIL=$PANEL_ADMIN_EMAIL"
|
||||
[ -n "$PANEL_ADMIN_PASSWORD" ] && echo "PANEL_ADMIN_PASSWORD=$PANEL_ADMIN_PASSWORD"
|
||||
[ -n "$PANEL_PUBLIC_URL" ] && echo "PANEL_PUBLIC_URL=$PANEL_PUBLIC_URL"
|
||||
echo "PANEL_HTTP_PORT=$PANEL_HTTP_PORT"
|
||||
echo "PANEL_GRPC_PORT=$PANEL_GRPC_PORT"
|
||||
} > "$ENV_FILE"
|
||||
umask 022
|
||||
chown -R "$PANEL_USER:$PANEL_USER" "$PANEL_DIR"
|
||||
chmod 600 "$ENV_FILE"
|
||||
|
||||
# BUG-2/3/5: do NOT install deploy/systemd/*.service verbatim — those hardcode
|
||||
# /opt/panel, ProtectHome=true and stale :8080/:8443 in the Description. Instead
|
||||
# GENERATE both units here, substituting the REAL install parameters.
|
||||
#
|
||||
# BUG-3: systemd ProtectHome=true makes any binary under /home unreadable to the
|
||||
# service (203/EXEC). When PANEL_DIR is under /home we emit the unit WITHOUT
|
||||
# ProtectHome so it actually runs (reduced hardening, noted inline). The default
|
||||
# /opt/panel keeps ProtectHome=true.
|
||||
case "$PANEL_DIR/" in
|
||||
/home/*) PROTECT_HOME=""; log "PANEL_DIR is under /home — generating units without ProtectHome (reduced hardening)";;
|
||||
*) PROTECT_HOME="ProtectHome=true";;
|
||||
esac
|
||||
|
||||
write_unit() {
|
||||
# $1 = destination path
|
||||
cat > "$1"
|
||||
chmod 0644 "$1"
|
||||
}
|
||||
|
||||
write_unit /etc/systemd/system/panel-controller.service <<EOF
|
||||
[Unit]
|
||||
Description=Panel controller (HTTP dashboard :${PANEL_HTTP_PORT} + agent gRPC :${PANEL_GRPC_PORT})
|
||||
Documentation=https://github.com/dbledeez/panel
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${PANEL_USER}
|
||||
Group=${PANEL_USER}
|
||||
WorkingDirectory=${PANEL_DIR}
|
||||
Environment=PANEL_HTTP_PORT=${PANEL_HTTP_PORT}
|
||||
Environment=PANEL_GRPC_PORT=${PANEL_GRPC_PORT}
|
||||
EnvironmentFile=-${ENV_FILE}
|
||||
ExecStart=${PANEL_DIR}/bin/controller \\
|
||||
-http :\${PANEL_HTTP_PORT} \\
|
||||
-grpc :\${PANEL_GRPC_PORT} \\
|
||||
-ca-dir ./data/ca
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
# Hardening — the controller only needs its own tree + the network.
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
${PROTECT_HOME}
|
||||
ReadWritePaths=${PANEL_DIR}
|
||||
PrivateTmp=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
write_unit /etc/systemd/system/panel-agent.service <<EOF
|
||||
[Unit]
|
||||
Description=Panel agent (Docker runtime for game-server instances)
|
||||
Documentation=https://github.com/dbledeez/panel
|
||||
After=network-online.target docker.service panel-controller.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${PANEL_USER}
|
||||
Group=${PANEL_USER}
|
||||
SupplementaryGroups=docker
|
||||
WorkingDirectory=${PANEL_DIR}
|
||||
Environment=PANEL_GRPC_PORT=${PANEL_GRPC_PORT}
|
||||
EnvironmentFile=-${ENV_FILE}
|
||||
ExecStart=${PANEL_DIR}/bin/agent \\
|
||||
--controller localhost:\${PANEL_GRPC_PORT} \\
|
||||
--modules-dir ./modules \\
|
||||
--data-root ./data/instances \\
|
||||
--meta-dir ./data/instance-meta \\
|
||||
--backup-dir ./data/backups \\
|
||||
--cert-dir ./data/certs
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
# Hardening is intentionally lighter than the controller's: the agent manages
|
||||
# Docker containers and bind-mounts instance data.
|
||||
NoNewPrivileges=true
|
||||
${PROTECT_HOME}
|
||||
ReadWritePaths=${PANEL_DIR}
|
||||
PrivateTmp=true
|
||||
LockPersonality=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# ---- 6. Controller first boot (CA + admin bootstrap) ----------------------
|
||||
FIRST_BOOT=1
|
||||
if [ -f "$PANEL_DATA_DIR/ca/root.crt" ] || [ -d "$PANEL_DATA_DIR/ca" ]; then
|
||||
FIRST_BOOT=0
|
||||
fi
|
||||
BOOT_MARK="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
systemctl enable --now panel-controller >/dev/null
|
||||
systemctl restart panel-controller
|
||||
|
||||
log "waiting for the controller on :$PANEL_HTTP_PORT"
|
||||
for _ in $(seq 1 60); do
|
||||
if curl -fsS "http://127.0.0.1:$PANEL_HTTP_PORT/login" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
curl -fsS "http://127.0.0.1:$PANEL_HTTP_PORT/login" >/dev/null 2>&1 || die "controller did not come up — check: journalctl -u panel-controller"
|
||||
|
||||
ADMIN_EMAIL="${PANEL_ADMIN_EMAIL:-admin@panel.local}"
|
||||
# BUG-5b: always give the operator a path to the password. If they supplied/chose
|
||||
# one, say so and point at the env file; otherwise scrape the generated one below.
|
||||
ADMIN_PW_SHOWN="(the password you set at install; also in $ENV_FILE)"
|
||||
if [ -z "$PANEL_ADMIN_PASSWORD" ]; then
|
||||
if [ "$FIRST_BOOT" = 1 ]; then
|
||||
# The controller prints the generated password once in its log.
|
||||
GEN_PW="$(journalctl -u panel-controller --since "$BOOT_MARK" --no-pager 2>/dev/null \
|
||||
| sed -n 's/.*password: \([a-f0-9]\{16\}\).*/\1/p' | head -n1 || true)"
|
||||
if [ -n "$GEN_PW" ]; then
|
||||
ADMIN_PW_SHOWN="$GEN_PW"
|
||||
PANEL_ADMIN_PASSWORD="$GEN_PW"
|
||||
else
|
||||
ADMIN_PW_SHOWN="(see: journalctl -u panel-controller | grep 'password:')"
|
||||
fi
|
||||
else
|
||||
ADMIN_PW_SHOWN="(unchanged from previous install)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 7. Pair token + local agent ------------------------------------------
|
||||
PAIR_TOKEN=""
|
||||
if [ -n "$PANEL_ADMIN_PASSWORD" ]; then
|
||||
COOKIES="$(mktemp)"
|
||||
if curl -fsS -c "$COOKIES" -H 'Content-Type: application/json' \
|
||||
-d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$PANEL_ADMIN_PASSWORD\"}" \
|
||||
"http://127.0.0.1:$PANEL_HTTP_PORT/api/login" >/dev/null 2>&1; then
|
||||
PAIR_TOKEN="$(curl -fsS -b "$COOKIES" -H 'Content-Type: application/json' \
|
||||
-d '{"description":"install.sh local agent","expires_minutes":60}' \
|
||||
"http://127.0.0.1:$PANEL_HTTP_PORT/api/admin/pair-tokens" 2>/dev/null \
|
||||
| sed -n 's/.*"token":"\([^"]*\)".*/\1/p' || true)"
|
||||
fi
|
||||
rm -f "$COOKIES"
|
||||
fi
|
||||
|
||||
PAIR_CMD="$PANEL_DIR/bin/agent --pair-url http://<controller-host>:$PANEL_HTTP_PORT --pair-token <token-from-dashboard> --cert-dir ./data/certs"
|
||||
if [ "$PANEL_SKIP_AGENT" != 1 ]; then
|
||||
if [ -f "$PANEL_DATA_DIR/certs/agent.crt" ]; then
|
||||
log "local agent already paired — restarting"
|
||||
systemctl enable --now panel-agent >/dev/null
|
||||
systemctl restart panel-agent
|
||||
elif [ -n "$PAIR_TOKEN" ]; then
|
||||
log "pairing local agent"
|
||||
(cd "$PANEL_DIR" && sudo -u "$PANEL_USER" \
|
||||
"$PANEL_DIR/bin/agent" --pair-url "http://127.0.0.1:$PANEL_HTTP_PORT" \
|
||||
--pair-token "$PAIR_TOKEN" --cert-dir ./data/certs)
|
||||
systemctl enable --now panel-agent >/dev/null
|
||||
else
|
||||
warn "could not auto-create a pair token (admin password unknown to this run)."
|
||||
warn "Create one in the dashboard (Admin → Pair tokens) and run:"
|
||||
warn " cd $PANEL_DIR && sudo -u $PANEL_USER $PAIR_CMD"
|
||||
warn "then: systemctl enable --now panel-agent"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 8. Summary ------------------------------------------------------------
|
||||
HOST_IP="$(hostname -I 2>/dev/null | awk '{print $1}' || echo 127.0.0.1)"
|
||||
echo
|
||||
log "──────────────────────────────────────────────────────────"
|
||||
log " Panel is installed."
|
||||
log " Dashboard : http://${HOST_IP:-127.0.0.1}:$PANEL_HTTP_PORT"
|
||||
log " Login : $ADMIN_EMAIL"
|
||||
log " Password : $ADMIN_PW_SHOWN"
|
||||
log " (log in once and change the password in the UI)"
|
||||
log ""
|
||||
log " To pair an agent on ANOTHER machine, create a pair token in the"
|
||||
log " dashboard, copy the panel binaries + modules/ there, then run:"
|
||||
log " $PAIR_CMD"
|
||||
log " and start it: ./bin/agent --controller ${HOST_IP:-<controller-host>}:$PANEL_GRPC_PORT --cert-dir ./data/certs"
|
||||
log "──────────────────────────────────────────────────────────"
|
||||
Reference in New Issue
Block a user