658bda1d24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
262 lines
12 KiB
Bash
262 lines
12 KiB
Bash
#!/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)
|
|
# 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 panel-postgres container (default: generated)
|
|
# PANEL_SKIP_AGENT=1 install controller only (add agents on other boxes later)
|
|
#
|
|
# 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}"
|
|
PANEL_PREBUILT_URL="${PANEL_PREBUILT_URL:-}"
|
|
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_PASSWORD="${PANEL_ADMIN_PASSWORD:-}"
|
|
PANEL_DB_URL="${PANEL_DB_URL:-}"
|
|
PANEL_DB_PASSWORD="${PANEL_DB_PASSWORD:-}"
|
|
PANEL_SKIP_AGENT="${PANEL_SKIP_AGENT:-0}"
|
|
PANEL_USER=panel
|
|
PG_CONTAINER=panel-postgres
|
|
|
|
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"
|
|
|
|
# ---- 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'"
|
|
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:5432:5432 \
|
|
-v panel_pgdata:/var/lib/postgresql/data \
|
|
postgres:17-alpine >/dev/null
|
|
fi
|
|
PANEL_DB_URL="postgres://panel:${PANEL_DB_PASSWORD}@127.0.0.1:5432/panel?sslmode=disable"
|
|
log "waiting for postgres to accept connections"
|
|
for _ in $(seq 1 30); 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"
|
|
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"
|
|
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
|
|
command -v go >/dev/null || die "Go toolchain not found. Install Go >= 1.26 (https://go.dev/dl/) or set PANEL_PREBUILT_URL."
|
|
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_PASSWORD" ] && echo "PANEL_ADMIN_PASSWORD=$PANEL_ADMIN_PASSWORD"
|
|
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"
|
|
|
|
if [ -n "$SRC" ] && [ -d "$SRC/deploy/systemd" ]; then
|
|
install -m 0644 "$SRC/deploy/systemd/panel-controller.service" \
|
|
"$SRC/deploy/systemd/panel-agent.service" /etc/systemd/system/
|
|
else
|
|
# Prebuilt path: units are expected inside the tarball at deploy/systemd.
|
|
[ -f "$PANEL_DIR/deploy/systemd/panel-controller.service" ] || die "no systemd units found (deploy/systemd missing)"
|
|
install -m 0644 "$PANEL_DIR/deploy/systemd/panel-controller.service" \
|
|
"$PANEL_DIR/deploy/systemd/panel-agent.service" /etc/systemd/system/
|
|
fi
|
|
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="admin@panel.local"
|
|
ADMIN_PW_SHOWN="(set via PANEL_ADMIN_PASSWORD)"
|
|
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 "──────────────────────────────────────────────────────────"
|