337 lines
11 KiB
Bash
Executable File
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"
|