#!/usr/bin/env bash # ┌─────────────────────────────────────────────────────────────────────────┐ # │ Hotel Manage — Add App LXC │ # │ Provisions a single app container and wires it into the stack. │ # │ │ # │ Usage: bash add-app.sh (or curl-bootstrap, see install/README.md) │ # └─────────────────────────────────────────────────────────────────────────┘ set -euo pipefail YW="\033[33m"; BL="\033[36m"; RD="\033[01;31m" GN="\033[1;92m"; DGN="\033[32m"; CL="\033[m" BFR="\\r\\033[K"; CM="${GN}✓${CL}"; CROSS="${RD}✗${CL}" msg_info() { printf " ◌ ${YW}%-55s${CL}" "$*"; } msg_ok() { printf "${BFR} ${CM} ${DGN}%s${CL}\n" "$*"; } msg_error() { printf "${BFR} ${CROSS} ${RD}%s${CL}\n" "$*"; exit 1; } msg_warn() { printf "\n ${CROSS} ${YW}%s${CL}\n" "$*"; } [[ $EUID -ne 0 ]] && msg_error "Must run as root on the Proxmox VE host" command -v pct &>/dev/null || msg_error "pct not found — run this on a Proxmox VE host" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" # Load secrets from credentials file if present CREDS_FILE=/root/hotel-manage-credentials.txt if [[ -f "$CREDS_FILE" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line//[[:space:]]/}" ]] && continue [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || continue export "${BASH_REMATCH[1]}"="${BASH_REMATCH[2]}" done < "$CREDS_FILE" fi # ── Collect config ──────────────────────────────────────────────────────────── APP_NAME=$(whiptail --title "Add App LXC" \ --inputbox "App slug (e.g. kitchen, cashup, housekeeping):" 8 52 "" 3>&1 1>&2 2>&3) || exit 0 APP_NAME=$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') LXC_ID=$(whiptail --title "Add App LXC" \ --inputbox "LXC ID to allocate (check Proxmox for free IDs):" 8 52 "" 3>&1 1>&2 2>&3) || exit 0 LAST_OCTET=$(whiptail --title "Add App LXC" \ --inputbox "Internal IP last octet (10.10.10.X):" 8 52 "$LXC_ID" 3>&1 1>&2 2>&3) || exit 0 APP_PORT=$(whiptail --title "Add App LXC" \ --inputbox "Port the app container listens on internally:" 8 52 "3000" 3>&1 1>&2 2>&3) || exit 0 APP_PATH=$(whiptail --title "Add App LXC" \ --inputbox "URL path prefix (e.g. /kitchen/):" 8 52 "/${APP_NAME}/" 3>&1 1>&2 2>&3) || exit 0 APP_DB=$(whiptail --title "Add App LXC" --yesno \ "Create a dedicated postgres database for this app?" 8 52 && echo "yes" || echo "no") if [[ "$APP_DB" == "yes" ]]; then APP_DB_PASS=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) APP_DB_USER="$APP_NAME" APP_DB_NAME="${APP_NAME}_db" fi if whiptail --title "Add App LXC" --yesno \ "Deploy from Forgejo git repo?" 8 52; then USE_FORGEJO=true FORGEJO_REPO=$(whiptail --title "Add App LXC" \ --inputbox "Full Forgejo repo URL (.git):" 8 70 \ "${FORGEJO_BASE:-https://git.pterois.co.uk/hotel-manage-stack}/${APP_NAME}.git" \ 3>&1 1>&2 2>&3) || exit 0 else USE_FORGEJO=false LOCAL_SRC=$(whiptail --title "Add App LXC" \ --inputbox "Local app directory path:" 8 70 \ "${REPO_ROOT}/apps/${APP_NAME}" 3>&1 1>&2 2>&3) || exit 0 fi ENV_EXTRA=$(whiptail --title "Add App LXC" \ --inputbox \ "Additional .env lines (KEY=VALUE, one per line). CENTRAL_AUTH_SECRET and DATABASE_URL are added automatically. Leave blank if none." \ 12 70 "" 3>&1 1>&2 2>&3) || exit 0 # Confirm whiptail --title "Add App LXC — Confirm" --yesno \ "Create LXC for app: ${APP_NAME} LXC ID: ${LXC_ID} IP: 10.10.10.${LAST_OCTET} Port: ${APP_PORT} Path: ${APP_PATH} Database: $( [[ "$APP_DB" == "yes" ]] && echo "${APP_DB_NAME}" || echo "none" ) Proceed?" 16 52 || exit 0 # ── Detect storage ──────────────────────────────────────────────────────────── STORAGE=$(pvesm status 2>/dev/null | awk '{print $1}' | grep -E "^local-lvm$|^local-zfs$" | head -1) STORAGE="${STORAGE:-local}" # ── Get template ────────────────────────────────────────────────────────────── TEMPLATE=$(pveam list local 2>/dev/null | awk '/ubuntu-22\.04/{print "local:vztmpl/"$1; exit}') if [[ -z "$TEMPLATE" ]]; then msg_info "Downloading Ubuntu 22.04 template" pveam update &>/dev/null && pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst &>/dev/null TEMPLATE=$(pveam list local 2>/dev/null | awk '/ubuntu-22\.04/{print "local:vztmpl/"$1; exit}') msg_ok "Template ready" fi # ── Get mgmt public key ─────────────────────────────────────────────────────── MGMT_PUBKEY="" [[ -f /root/.ssh/hotel-manage_deploy.pub ]] && MGMT_PUBKEY=$(cat /root/.ssh/hotel-manage_deploy.pub) # ── Resource pool (created by install-stack.sh; reuse if present) ────────────── POOL="hotel-manage" POOL_OPT="" pvesh get "/pools/${POOL}" &>/dev/null || pvesh create /pools --poolid "${POOL}" &>/dev/null || true pvesh get "/pools/${POOL}" &>/dev/null && POOL_OPT="--pool ${POOL}" # ── Create LXC ──────────────────────────────────────────────────────────────── msg_info "Creating LXC ${LXC_ID} (hotel-manage-${APP_NAME} at 10.10.10.${LAST_OCTET})" pct create "$LXC_ID" "$TEMPLATE" \ --hostname "hotel-manage-${APP_NAME}" \ --memory 1024 \ --cores 1 \ --rootfs "${STORAGE}:8" \ --net0 "name=eth0,bridge=vmbr1,ip=10.10.10.${LAST_OCTET}/24,gw=10.10.10.1" \ --features nesting=1 \ --unprivileged 0 \ --onboot 1 \ --tags "${POOL}" \ ${POOL_OPT} \ --start 1 &>/dev/null sleep 5 msg_ok "LXC ${LXC_ID} created" # ── Install Docker ──────────────────────────────────────────────────────────── msg_info "Installing Docker" pct exec "$LXC_ID" -- bash -s &>/dev/null <<'DOCKER' export DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y -qq ca-certificates curl gnupg git openssh-server install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -qq && apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin systemctl enable --now docker ssh DOCKER msg_ok "Docker installed" # ── Install management SSH key ──────────────────────────────────────────────── if [[ -n "$MGMT_PUBKEY" ]]; then pct exec "$LXC_ID" -- bash -c " mkdir -p /root/.ssh && chmod 700 /root/.ssh echo '${MGMT_PUBKEY}' >> /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys " &>/dev/null msg_ok "Management SSH key installed" fi # ── Create database ─────────────────────────────────────────────────────────── if [[ "$APP_DB" == "yes" ]]; then msg_info "Creating database ${APP_DB_NAME}" # Run SQL via postgres LXC pct exec 100 -- bash -c " docker exec hotel-manage-postgres psql -U postgres -c \ \"CREATE USER ${APP_DB_USER} WITH PASSWORD '${APP_DB_PASS}';\" 2>/dev/null || true docker exec hotel-manage-postgres psql -U postgres -c \ \"CREATE DATABASE ${APP_DB_NAME} OWNER ${APP_DB_USER};\" 2>/dev/null || true docker exec hotel-manage-postgres psql -U postgres -d ${APP_DB_NAME} -c \ \"GRANT ALL ON SCHEMA public TO ${APP_DB_USER};\" 2>/dev/null || true " &>/dev/null msg_ok "Database ${APP_DB_NAME} created" # Append DB creds to credentials file cat >> /root/hotel-manage-credentials.txt </dev/null else tmp=$(mktemp /tmp/hotel-manage-app-XXXX.tar.gz) tar czf "$tmp" -C "$(dirname "$LOCAL_SRC")" "$(basename "$LOCAL_SRC")" 2>/dev/null pct push "$LXC_ID" "$tmp" /tmp/hotel-manage-app.tar.gz 2>/dev/null pct exec "$LXC_ID" -- bash -c " mkdir -p /opt && tar xzf /tmp/hotel-manage-app.tar.gz -C /opt mv /opt/$(basename "$LOCAL_SRC") /opt/${APP_NAME} 2>/dev/null || true rm -f /tmp/hotel-manage-app.tar.gz " &>/dev/null rm -f "$tmp" fi msg_ok "App files deployed to /opt/${APP_NAME}" # ── Write .env ──────────────────────────────────────────────────────────────── msg_info "Writing .env" ENV_TMP=$(mktemp /tmp/hotel-manage-env-XXXX) { echo "NODE_ENV=production" echo "APP_SLUG=${APP_NAME}" echo "CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET:-REPLACE_ME}" echo "OFFICE_IP_CHECK=${OFFICE_IP_CHECK:-disabled}" if [[ "$APP_DB" == "yes" ]]; then echo "DATABASE_URL=postgresql://${APP_DB_USER}:${APP_DB_PASS}@10.10.10.100:5432/${APP_DB_NAME}" fi [[ -n "$ENV_EXTRA" ]] && echo "$ENV_EXTRA" } > "$ENV_TMP" pct push "$LXC_ID" "$ENV_TMP" "/opt/${APP_NAME}/.env" 2>/dev/null rm -f "$ENV_TMP" msg_ok ".env written" # ── Register app in auth service DB ────────────────────────────────────────── msg_info "Registering ${APP_NAME} in auth service" pct exec 100 -- docker exec hotel-manage-postgres psql -U postgres -d auth_db -c \ "INSERT INTO apps (slug, name, description, base_path, icon, theme_color, internal_host, internal_port) VALUES ('${APP_NAME}', '${APP_NAME^}', '', '${APP_PATH%/}', 'ClipboardList', '#1e3a5f', '10.10.10.${LAST_OCTET}', ${APP_PORT}) ON CONFLICT (slug) DO UPDATE SET internal_host = '10.10.10.${LAST_OCTET}', internal_port = ${APP_PORT};" &>/dev/null \ && msg_ok "${APP_NAME} registered in auth DB (visible in portal after next login)" \ || msg_warn "Auto-register failed — add ${APP_NAME} to the apps table in auth_db manually" # ── NPM route reminder ──────────────────────────────────────────────────────── NPM_LAN="${NPM_LAN_IP:-}" printf "\n${GN}── Summary ──────────────────────────────────────────────────${CL}\n" cat <