Files
proxmox/scripts/deployment/deploy-currencicombo-8604.sh

337 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
source "$PROJECT_ROOT/config/ip-addresses.conf" 2>/dev/null || true
PROXMOX_HOST="${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}"
PROXMOX_USER="${PROXMOX_USER:-root}"
VMID="${VMID:-8604}"
CT_HOSTNAME="${CT_HOSTNAME:-currencicombo-phoenix-1}"
CT_IP="${CT_IP:-10.160.0.14}"
CT_PREFIX="${CT_PREFIX:-22}"
CT_GW="${CT_GW:-10.160.0.1}"
CT_VLAN_TAG="${CT_VLAN_TAG:-160}"
CT_STORAGE="${CT_STORAGE:-thin1}"
CT_TEMPLATE="${CT_TEMPLATE:-ubuntu-22.04-standard_22.04-1_amd64.tar.zst}"
CT_MEMORY_MB="${CT_MEMORY_MB:-6144}"
CT_CORES="${CT_CORES:-4}"
CT_ROOTFS_GB="${CT_ROOTFS_GB:-40}"
CT_SWAP_MB="${CT_SWAP_MB:-1024}"
CT_TIMEZONE="${CT_TIMEZONE:-America/Los_Angeles}"
CT_NODE_ENV="${CT_NODE_ENV:-production}"
DEPLOY_ROOT="${DEPLOY_ROOT:-/opt/currencicombo}"
REPO_URL="${REPO_URL:-https://gitea.d-bis.org/d-bis/CurrenciCombo.git}"
REPO_BRANCH="${REPO_BRANCH:-main}"
REPO_REF="${REPO_REF:-}"
LOCAL_SRC="${CURRENCICOMBO_SRC:-}"
POSTGRES_DB="${POSTGRES_DB:-comboflow}"
POSTGRES_USER="${POSTGRES_USER:-comboflow}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-comboflow-prod-please-rotate}"
REDIS_URL="${REDIS_URL:-redis://127.0.0.1:6379}"
ORCH_PORT="${ORCH_PORT:-8080}"
WEB_PORT="${WEB_PORT:-3000}"
NEXTAUTH_URL="${NEXTAUTH_URL:-http://${CT_IP}:${WEB_PORT}}"
NEXT_PUBLIC_ORCH_URL="${NEXT_PUBLIC_ORCH_URL:-http://${CT_IP}:${ORCH_PORT}}"
SESSION_SECRET="${SESSION_SECRET:-currencicombo-session-secret-change-me-32chars}"
JWT_SECRET="${JWT_SECRET:-currencicombo-jwt-secret-change-me-32chars}"
API_KEYS="${API_KEYS:-currencicombo-phoenix-dev-key}"
SSH_OPTS=(-o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new)
DRY_RUN=0
APPLY=0
SKIP_CREATE=0
SKIP_SYSTEM_PACKAGES=0
TMP_ARCHIVE="$(mktemp /tmp/currencicombo-8604-XXXXXX.tgz)"
REMOTE_ARCHIVE="/tmp/$(basename "$TMP_ARCHIVE")"
PUSH_ARCHIVE="/root/$(basename "$TMP_ARCHIVE")"
usage() {
cat <<USAGE
Usage: $(basename "$0") [--dry-run] [--apply] [--skip-create] [--skip-system-packages]
Deploy CurrenciCombo to Phoenix CT ${VMID} on ${PROXMOX_HOST}.
Options:
--dry-run Print planned actions without changing anything.
--apply Execute the deployment.
--skip-create Assume the CT already exists; do not create it.
--skip-system-packages Skip apt/node/postgres/redis installation inside the CT.
USAGE
}
log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"; }
fail() { echo "ERROR: $*" >&2; exit 1; }
run_local() {
if [[ "$DRY_RUN" == "1" ]]; then
printf '[dry-run] '
printf '%q ' "$@"
printf '\n'
else
"$@"
fi
}
run_remote() {
local cmd="$1"
if [[ "$DRY_RUN" == "1" ]]; then
echo "[dry-run] ssh ${PROXMOX_USER}@${PROXMOX_HOST} $cmd"
else
ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "$cmd"
fi
}
cleanup() {
rm -f "$TMP_ARCHIVE"
}
trap cleanup EXIT
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--apply) APPLY=1 ;;
--skip-create) SKIP_CREATE=1 ;;
--skip-system-packages) SKIP_SYSTEM_PACKAGES=1 ;;
-h|--help) usage; exit 0 ;;
*) fail "Unknown argument: $1" ;;
esac
shift
done
if [[ "$DRY_RUN" == "0" && "$APPLY" == "0" ]]; then
fail "Refusing to make changes without --apply. Use --dry-run to preview."
fi
build_archive_from_local() {
local src="$1"
[[ -d "$src" ]] || fail "Local source repo not found: $src"
log "Packing local source from $src"
tar -C "$src" \
--exclude='.git' \
--exclude='node_modules' \
--exclude='webapp/node_modules' \
--exclude='orchestrator/node_modules' \
--exclude='contracts/node_modules' \
--exclude='webapp/.next' \
--exclude='orchestrator/dist' \
--exclude='.env' \
--exclude='.env.local' \
--exclude='webapp/.env.local' \
-czf "$TMP_ARCHIVE" .
}
ensure_archive() {
if [[ -n "$LOCAL_SRC" ]]; then
build_archive_from_local "$LOCAL_SRC"
return
fi
local scratch
scratch="$(mktemp -d /tmp/currencicombo-src-XXXXXX)"
trap 'rm -rf "$scratch"; cleanup' EXIT
log "Cloning $REPO_URL#$REPO_BRANCH to build deploy archive"
git clone --depth=1 --branch "$REPO_BRANCH" "$REPO_URL" "$scratch/repo" >/dev/null 2>&1
if [[ -n "$REPO_REF" ]]; then
git -C "$scratch/repo" fetch --depth=1 origin "$REPO_REF" >/dev/null 2>&1 || true
git -C "$scratch/repo" checkout "$REPO_REF" >/dev/null 2>&1
fi
build_archive_from_local "$scratch/repo"
}
ensure_ct() {
local exists_cmd="pct status ${VMID} >/dev/null 2>&1"
if ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "$exists_cmd" >/dev/null 2>&1; then
log "CT ${VMID} already exists on ${PROXMOX_HOST}"
return
fi
[[ "$SKIP_CREATE" == "0" ]] || fail "CT ${VMID} does not exist and --skip-create was set"
local create_cmd="pct create ${VMID} local:vztmpl/${CT_TEMPLATE} --storage ${CT_STORAGE} --hostname ${CT_HOSTNAME} --memory ${CT_MEMORY_MB} --cores ${CT_CORES} --rootfs ${CT_STORAGE}:${CT_ROOTFS_GB} --net0 name=eth0,bridge=vmbr0,tag=${CT_VLAN_TAG},ip=${CT_IP}/${CT_PREFIX},gw=${CT_GW},type=veth --unprivileged 1 --swap ${CT_SWAP_MB} --onboot 1 --timezone ${CT_TIMEZONE} --features nesting=1,keyctl=1"
log "Creating CT ${VMID} (${CT_HOSTNAME})"
run_remote "$create_cmd"
}
ensure_ct_running() {
local status
status="$(ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "pct status ${VMID} 2>/dev/null | awk '{print \$2}'" || true)"
if [[ "$status" != "running" ]]; then
log "Starting CT ${VMID}"
run_remote "pct start ${VMID}"
if [[ "$DRY_RUN" == "0" ]]; then
sleep 10
fi
else
log "CT ${VMID} already running"
fi
}
push_archive() {
log "Uploading deploy archive to ${PROXMOX_HOST}"
run_local scp "${SSH_OPTS[@]}" "$TMP_ARCHIVE" "${PROXMOX_USER}@${PROXMOX_HOST}:${REMOTE_ARCHIVE}"
run_remote "pct push ${VMID} ${REMOTE_ARCHIVE} ${PUSH_ARCHIVE}"
run_remote "rm -f ${REMOTE_ARCHIVE}"
}
run_ct_script() {
local body
body="$(cat <<'INNER'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
DEPLOY_ROOT='__DEPLOY_ROOT__'
POSTGRES_DB='__POSTGRES_DB__'
POSTGRES_USER='__POSTGRES_USER__'
POSTGRES_PASSWORD='__POSTGRES_PASSWORD__'
REDIS_URL='__REDIS_URL__'
ORCH_PORT='__ORCH_PORT__'
WEB_PORT='__WEB_PORT__'
NEXTAUTH_URL='__NEXTAUTH_URL__'
NEXT_PUBLIC_ORCH_URL='__NEXT_PUBLIC_ORCH_URL__'
SESSION_SECRET='__SESSION_SECRET__'
JWT_SECRET='__JWT_SECRET__'
API_KEYS='__API_KEYS__'
CT_NODE_ENV='__CT_NODE_ENV__'
PUSH_ARCHIVE='__PUSH_ARCHIVE__'
SKIP_SYSTEM_PACKAGES='__SKIP_SYSTEM_PACKAGES__'
if [[ "$SKIP_SYSTEM_PACKAGES" != "1" ]]; then
apt-get update -qq
apt-get install -y -qq ca-certificates curl gnupg git rsync build-essential postgresql redis-server
if ! command -v node >/dev/null 2>&1 || [[ "$(node -v 2>/dev/null || true)" != v20* ]]; then
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y -qq nodejs
fi
fi
systemctl enable postgresql redis-server >/dev/null 2>&1 || true
systemctl restart postgresql redis-server
install -d -m 0755 "$DEPLOY_ROOT"
rm -rf /tmp/currencicombo-incoming
mkdir -p /tmp/currencicombo-incoming
tar -xzf "$PUSH_ARCHIVE" -C /tmp/currencicombo-incoming
rsync -a --delete /tmp/currencicombo-incoming/ "$DEPLOY_ROOT/"
rm -rf /tmp/currencicombo-incoming "$PUSH_ARCHIVE"
cat > "$DEPLOY_ROOT/orchestrator/.env" <<ENV
PORT=$ORCH_PORT
NODE_ENV=$CT_NODE_ENV
LOG_LEVEL=info
API_KEYS=$API_KEYS
SESSION_SECRET=$SESSION_SECRET
JWT_SECRET=$JWT_SECRET
ALLOWED_IPS=127.0.0.1,::1
RUN_MIGRATIONS=false
DATABASE_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:5432/$POSTGRES_DB
REDIS_URL=$REDIS_URL
ENV
cat > "$DEPLOY_ROOT/webapp/.env.local" <<ENV
NEXTAUTH_URL=$NEXTAUTH_URL
NEXTAUTH_SECRET=$SESSION_SECRET
NEXT_PUBLIC_ORCH_URL=$NEXT_PUBLIC_ORCH_URL
ENV
runuser -u postgres -- psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${POSTGRES_USER}'" | grep -q 1 || \
runuser -u postgres -- psql -c "CREATE USER ${POSTGRES_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';"
runuser -u postgres -- psql -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" | grep -q 1 || \
runuser -u postgres -- psql -c "CREATE DATABASE ${POSTGRES_DB} OWNER ${POSTGRES_USER};"
runuser -u postgres -- psql -d "$POSTGRES_DB" -c "GRANT ALL ON SCHEMA public TO ${POSTGRES_USER};" >/dev/null
cd "$DEPLOY_ROOT/orchestrator"
npm ci
RUN_MIGRATIONS=true npm run migrate
npm run build
cd "$DEPLOY_ROOT/webapp"
npm ci
npm run build
cat > /etc/systemd/system/currencicombo-orchestrator.service <<UNIT
[Unit]
Description=CurrenciCombo Orchestrator
After=network-online.target postgresql.service redis-server.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=$DEPLOY_ROOT/orchestrator
Environment=NODE_ENV=$CT_NODE_ENV
Environment=PORT=$ORCH_PORT
ExecStart=/usr/bin/npm start
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
UNIT
cat > /etc/systemd/system/currencicombo-webapp.service <<UNIT
[Unit]
Description=CurrenciCombo Webapp
After=network-online.target currencicombo-orchestrator.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=$DEPLOY_ROOT/webapp
Environment=NODE_ENV=$CT_NODE_ENV
Environment=PORT=$WEB_PORT
ExecStart=/usr/bin/npm start
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable currencicombo-orchestrator currencicombo-webapp >/dev/null
systemctl restart currencicombo-orchestrator currencicombo-webapp
sleep 8
curl -fsS "http://127.0.0.1:${ORCH_PORT}/health" >/dev/null
curl -fsS "http://127.0.0.1:${WEB_PORT}/" >/dev/null
INNER
)"
body="${body//__DEPLOY_ROOT__/$DEPLOY_ROOT}"
body="${body//__POSTGRES_DB__/$POSTGRES_DB}"
body="${body//__POSTGRES_USER__/$POSTGRES_USER}"
body="${body//__POSTGRES_PASSWORD__/$POSTGRES_PASSWORD}"
body="${body//__REDIS_URL__/$REDIS_URL}"
body="${body//__ORCH_PORT__/$ORCH_PORT}"
body="${body//__WEB_PORT__/$WEB_PORT}"
body="${body//__NEXTAUTH_URL__/$NEXTAUTH_URL}"
body="${body//__NEXT_PUBLIC_ORCH_URL__/$NEXT_PUBLIC_ORCH_URL}"
body="${body//__SESSION_SECRET__/$SESSION_SECRET}"
body="${body//__JWT_SECRET__/$JWT_SECRET}"
body="${body//__API_KEYS__/$API_KEYS}"
body="${body//__CT_NODE_ENV__/$CT_NODE_ENV}"
body="${body//__PUSH_ARCHIVE__/$PUSH_ARCHIVE}"
body="${body//__SKIP_SYSTEM_PACKAGES__/$SKIP_SYSTEM_PACKAGES}"
if [[ "$DRY_RUN" == "1" ]]; then
echo "[dry-run] pct exec ${VMID} -- bash -lc '<currencicombo bootstrap script>'"
else
ssh "${SSH_OPTS[@]}" "${PROXMOX_USER}@${PROXMOX_HOST}" "pct exec ${VMID} -- bash -lc $(printf '%q' "$body")"
fi
}
verify_from_host() {
local cmd="pct exec ${VMID} -- bash -lc 'curl -fsS http://127.0.0.1:${ORCH_PORT}/health >/dev/null && curl -fsS http://127.0.0.1:${WEB_PORT}/ >/dev/null && systemctl is-active currencicombo-orchestrator currencicombo-webapp'"
run_remote "$cmd"
}
ensure_archive
if [[ "$DRY_RUN" == "0" ]]; then
[[ -s "$TMP_ARCHIVE" ]] || fail "Failed to create deploy archive"
fi
ensure_ct
ensure_ct_running
push_archive
run_ct_script
verify_from_host
log "CurrenciCombo deploy complete: CT ${VMID} on ${PROXMOX_HOST} (${CT_IP})"
log "Web: http://${CT_IP}:${WEB_PORT}/"
log "API: http://${CT_IP}:${ORCH_PORT}/health"