349 lines
15 KiB
Bash
Executable file
349 lines
15 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_URL="https://git.lohmar.co.uk/lexton-it/NextWks.git"
|
|
BUILD_DIR="/tmp/nextwks-build"
|
|
TARGET_DIR="/opt/nextworkspace"
|
|
BACKUP_DIR="/opt/backup"
|
|
NETWORK_NAME="nextwks-net"
|
|
HEALTH_CHECK_RETRIES=15
|
|
HEALTH_CHECK_INTERVAL=3
|
|
|
|
usage() {
|
|
echo "Usage: $0 [--install|--update|--destroy]"
|
|
echo " --install First-time setup on a bare VM (prompts for config)"
|
|
echo " --update Smart update: pull, build, copy, bounce containers"
|
|
echo " --destroy Full greenfield redeploy (uses saved secrets)"
|
|
exit 1
|
|
}
|
|
|
|
[ $# -eq 0 ] && usage
|
|
MODE="${1#--}"
|
|
case "$MODE" in install|update|destroy) ;; *) usage ;; esac
|
|
|
|
# MUST NOT run as root — podman must be rootless
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
echo "ERROR: Do NOT run this script with sudo or as root."
|
|
echo " Run it as your normal user: ./nextwks.sh --$MODE"
|
|
echo " The script will prompt for sudo only where needed (apt, /opt/, iptables)."
|
|
exit 1
|
|
fi
|
|
|
|
# Helper: run with sudo for operations that need root
|
|
maybe_sudo() {
|
|
sudo "$@"
|
|
}
|
|
|
|
# --- Load existing env (if any), skip if unreadable ---
|
|
# Temporarily disable -u because .env may contain $ signs (bcrypt hashes)
|
|
set +u
|
|
if [ -r "$BACKUP_DIR/.env" ]; then
|
|
set -a; source "$BACKUP_DIR/.env"; set +a
|
|
elif [ -r "$TARGET_DIR/.env" ]; then
|
|
set -a; source "$TARGET_DIR/.env"; set +a
|
|
fi
|
|
set -u
|
|
DOMAIN="${DOMAIN:-nextwks.eu}"
|
|
|
|
echo "=== NextWorkspace ${MODE} ==="
|
|
|
|
# ============================================================
|
|
# 1. INSTALL MODE — first-time setup (only on bare VM)
|
|
# ============================================================
|
|
if [ "$MODE" = "install" ]; then
|
|
if [ -f "$TARGET_DIR/nextworkspace" ]; then
|
|
echo "================================================================="
|
|
echo " NextWorkspace is already installed at $TARGET_DIR"
|
|
echo ""
|
|
echo " Use --update to rebuild and restart:"
|
|
echo " ./nextwks.sh --update"
|
|
echo ""
|
|
echo " Use --destroy for a full greenfield redeploy:"
|
|
echo " ./nextwks.sh --destroy"
|
|
echo "================================================================="
|
|
exit 0
|
|
fi
|
|
echo "[*] Installing system dependencies..."
|
|
maybe_sudo apt-get update -qq
|
|
maybe_sudo apt-get install -y -qq git build-essential curl podman podman-compose iptables-persistent
|
|
|
|
if ! command -v go &>/dev/null; then
|
|
echo "[*] Installing Go..."
|
|
GO_VERSION=$(curl -sL https://go.dev/VERSION?m=text)
|
|
GO_URL="https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz"
|
|
curl -sL "$GO_URL" -o /tmp/go.tar.gz
|
|
maybe_sudo rm -rf /usr/local/go
|
|
maybe_sudo tar -C /usr/local -xzf /tmp/go.tar.gz
|
|
rm /tmp/go.tar.gz
|
|
maybe_sudo sh -c 'echo "export PATH=\$PATH:/usr/local/go/bin" > /etc/profile.d/go.sh'
|
|
maybe_sudo chmod +x /etc/profile.d/go.sh
|
|
export PATH=$PATH:/usr/local/go/bin
|
|
fi
|
|
|
|
# Enable user lingering — containers stay alive after logout
|
|
maybe_sudo loginctl enable-linger "$USER" 2>/dev/null || true
|
|
|
|
# Clean up any old rootful containers from a previous deploy
|
|
echo "[*] Cleaning up old rootful containers (if any)..."
|
|
maybe_sudo podman stop caddy authelia launcher 2>/dev/null || true
|
|
maybe_sudo podman rm caddy authelia launcher 2>/dev/null || true
|
|
maybe_sudo podman network rm "$NETWORK_NAME" 2>/dev/null || true
|
|
|
|
echo ""
|
|
echo "--- NextWorkspace Configuration ---"
|
|
read -p "Domain [nextwks.eu]: " input; DOMAIN="${input:-$DOMAIN}"
|
|
read -p "TLS email (Let's Encrypt): " TLS_EMAIL
|
|
while [ -z "$TLS_EMAIL" ]; do read -p "TLS email (required): " TLS_EMAIL; done
|
|
while echo "$TLS_EMAIL" | grep -qv '@'; do read -p "Invalid email: " TLS_EMAIL; done
|
|
read -p "Admin username: " ADMIN_USERNAME
|
|
while [ -z "$ADMIN_USERNAME" ]; do read -p "Admin username (required): " ADMIN_USERNAME; done
|
|
# 24 chars, mixed case + numbers, no special chars (safe for .env)
|
|
ADMIN_PASSWORD=$(openssl rand -base64 30 | tr -dc 'A-Za-z0-9')
|
|
ADMIN_PASSWORD="${ADMIN_PASSWORD:0:24}"
|
|
|
|
echo ""
|
|
echo "========================================"
|
|
echo " Domain: $DOMAIN"
|
|
echo " TLS email: $TLS_EMAIL"
|
|
echo " Admin username: $ADMIN_USERNAME"
|
|
echo " Admin password: $ADMIN_PASSWORD"
|
|
echo " Save this password — it won't be shown again!"
|
|
echo "========================================"
|
|
echo ""
|
|
|
|
read -p "SMTP host [smtp.openxchange.eu]: " SMTP_HOST; SMTP_HOST="${SMTP_HOST:-smtp.openxchange.eu}"
|
|
read -p "SMTP port [587]: " SMTP_PORT; SMTP_PORT="${SMTP_PORT:-587}"
|
|
read -p "SMTP user [post@nextwks.eu]: " SMTP_USER; SMTP_USER="${SMTP_USER:-post@nextwks.eu}"
|
|
read -sp "SMTP password: " SMTP_PASS; echo ""
|
|
[ -z "$SMTP_PASS" ] && echo "ERROR: SMTP password required" && exit 1
|
|
|
|
read -p "IMAP host [imap.openxchange.eu]: " IMAP_HOST; IMAP_HOST="${IMAP_HOST:-imap.openxchange.eu}"
|
|
read -p "IMAP port [993]: " IMAP_PORT; IMAP_PORT="${IMAP_PORT:-993}"
|
|
|
|
# Persist config to backup vault (single-quote values to protect $ signs)
|
|
maybe_sudo mkdir -p "$BACKUP_DIR"
|
|
maybe_sudo sh -c "cat > '$BACKUP_DIR/.env' <<'ENVEOF'
|
|
# NextWorkspace Configuration — auto-generated by nextwks.sh --install
|
|
DOMAIN='$DOMAIN'
|
|
TLS_EMAIL='$TLS_EMAIL'
|
|
ADMIN_USERNAME='$ADMIN_USERNAME'
|
|
ADMIN_PASSWORD='$ADMIN_PASSWORD'
|
|
SMTP_HOST='$SMTP_HOST'
|
|
SMTP_PORT='$SMTP_PORT'
|
|
SMTP_USER='$SMTP_USER'
|
|
SMTP_PASS='$SMTP_PASS'
|
|
IMAP_HOST='$IMAP_HOST'
|
|
IMAP_PORT='$IMAP_PORT'
|
|
ENVEOF"
|
|
maybe_sudo chmod 600 "$BACKUP_DIR/.env"
|
|
fi
|
|
|
|
# ============================================================
|
|
# 2. CLONE fresh (every mode — ensures latest code)
|
|
# ============================================================
|
|
echo "[*] Cloning repository..."
|
|
rm -rf "$BUILD_DIR"
|
|
git clone --depth 1 "$REPO_URL" "$BUILD_DIR"
|
|
cd "$BUILD_DIR"
|
|
|
|
# Save script to user's home for easy future access (--install only)
|
|
if [ "$MODE" = "install" ]; then
|
|
cp "$BUILD_DIR/tools/nextwks.sh" "$HOME/nextwks.sh"
|
|
chmod +x "$HOME/nextwks.sh"
|
|
echo "[*] Saved to $HOME/nextwks.sh — use it for future updates"
|
|
fi
|
|
|
|
# ============================================================
|
|
# 3. FIREWALL SETUP (all modes)
|
|
# ============================================================
|
|
if [ "$MODE" = "install" ]; then
|
|
echo "[*] Applying firewall and port redirects (80→8080, 443→8443)..."
|
|
maybe_sudo bash "$BUILD_DIR/tools/firewall-routing.sh"
|
|
elif [ "$MODE" = "update" ] || [ "$MODE" = "destroy" ]; then
|
|
# Lightweight: ensure redirects exist without flushing existing rules
|
|
echo "[*] Ensuring port redirects (80→8080, 443→8443)..."
|
|
maybe_sudo iptables -t nat -C PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 2>/dev/null || \
|
|
maybe_sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080
|
|
maybe_sudo iptables -t nat -C PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443 2>/dev/null || \
|
|
maybe_sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8443
|
|
maybe_sudo iptables -t nat -C OUTPUT -o lo -p tcp --dport 80 -j REDIRECT --to-ports 8080 2>/dev/null || \
|
|
maybe_sudo iptables -t nat -A OUTPUT -o lo -p tcp --dport 80 -j REDIRECT --to-ports 8080
|
|
maybe_sudo iptables -t nat -C OUTPUT -o lo -p tcp --dport 443 -j REDIRECT --to-ports 8443 2>/dev/null || \
|
|
maybe_sudo iptables -t nat -A OUTPUT -o lo -p tcp --dport 443 -j REDIRECT --to-ports 8443
|
|
fi
|
|
# Persist across reboots (always)
|
|
if command -v netfilter-persistent &>/dev/null; then
|
|
maybe_sudo netfilter-persistent save 2>/dev/null || true
|
|
else
|
|
maybe_sudo mkdir -p /etc/iptables
|
|
maybe_sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
|
|
fi
|
|
|
|
# ============================================================
|
|
# 4. BUILD static binary
|
|
# ============================================================
|
|
echo "[*] Building static binary..."
|
|
export PATH=$PATH:/usr/local/go/bin
|
|
CGO_ENABLED=0 go build -o nextworkspace .
|
|
|
|
# ============================================================
|
|
# 5. STOP containers (all modes — binary is mounted, must stop before copy)
|
|
# ============================================================
|
|
echo "[*] Stopping containers..."
|
|
podman stop caddy authelia launcher 2>/dev/null || true
|
|
sleep 1
|
|
|
|
# ============================================================
|
|
# 6. CREATE target & backup directories (as root)
|
|
# ============================================================
|
|
maybe_sudo mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
|
|
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
|
|
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
|
|
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs" \
|
|
"$BACKUP_DIR"
|
|
|
|
# ============================================================
|
|
# 7. TEARDOWN (destroy mode only — wipes target dir)
|
|
# ============================================================
|
|
if [ "$MODE" = "destroy" ]; then
|
|
echo "[*] Full teardown..."
|
|
# Stop rootless containers
|
|
podman stop caddy authelia launcher 2>/dev/null || true
|
|
podman rm caddy authelia launcher 2>/dev/null || true
|
|
podman network rm "$NETWORK_NAME" 2>/dev/null || true
|
|
# Wipe target
|
|
maybe_sudo rm -rf "$TARGET_DIR"
|
|
maybe_sudo mkdir -p "$TARGET_DIR/config/caddy" "$TARGET_DIR/config/authelia" \
|
|
"$TARGET_DIR/data/caddy" "$TARGET_DIR/data/authelia" \
|
|
"$TARGET_DIR/compose" "$TARGET_DIR/www" \
|
|
"$TARGET_DIR/config/nextworkspace" "$TARGET_DIR/logs"
|
|
fi
|
|
|
|
# ============================================================
|
|
# 8. COPY artifacts to target (as root)
|
|
# ============================================================
|
|
echo "[*] Copying artifacts..."
|
|
maybe_sudo cp nextworkspace "$TARGET_DIR/nextworkspace"
|
|
maybe_sudo cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION"
|
|
if [ -d "$BUILD_DIR/config/www" ]; then
|
|
maybe_sudo cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/"
|
|
fi
|
|
if [ -d "$BUILD_DIR/lng" ]; then
|
|
maybe_sudo rm -rf "$TARGET_DIR/lng"
|
|
maybe_sudo cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng"
|
|
fi
|
|
if [ -d "$BUILD_DIR/config/nextworkspace" ]; then
|
|
maybe_sudo cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/"
|
|
fi
|
|
# Restore .env from backup
|
|
if [ -f "$BACKUP_DIR/.env" ]; then
|
|
maybe_sudo cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env"
|
|
maybe_sudo chmod 644 "$TARGET_DIR/.env"
|
|
fi
|
|
|
|
# ============================================================
|
|
# 9. GENERATE config files with placeholder substitution
|
|
# Write to /tmp first, then sudo cp to target
|
|
# ============================================================
|
|
GEN_DIR=$(mktemp -d)
|
|
trap "rm -rf '$GEN_DIR'" EXIT
|
|
|
|
if [ "$MODE" != "update" ]; then
|
|
echo "[*] Generating config files..."
|
|
|
|
# Caddyfile
|
|
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
|
|
"$BUILD_DIR/config/caddy/Caddyfile" > "$GEN_DIR/Caddyfile"
|
|
|
|
# Authelia config — preserve existing secrets if present
|
|
JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}"
|
|
SESSION_SECRET="${SESSION_SECRET:-$(openssl rand -hex 32)}"
|
|
STORAGE_ENCRYPTION_KEY="${STORAGE_ENCRYPTION_KEY:-$(openssl rand -hex 32)}"
|
|
sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{JWT_SECRET}|$JWT_SECRET|g" \
|
|
-e "s|{SESSION_SECRET}|$SESSION_SECRET|g" \
|
|
-e "s|{STORAGE_ENCRYPTION_KEY}|$STORAGE_ENCRYPTION_KEY|g" \
|
|
-e "s|{SMTP_HOST}|${SMTP_HOST:-smtp.openxchange.eu}|g" \
|
|
-e "s|{SMTP_PORT}|${SMTP_PORT:-587}|g" \
|
|
-e "s|{SMTP_USER}|${SMTP_USER:-post@nextwks.eu}|g" \
|
|
-e "s|{SMTP_PASS}|$SMTP_PASS|g" \
|
|
"$BUILD_DIR/config/authelia/configuration.yml" > "$GEN_DIR/configuration.yml"
|
|
|
|
# Users database
|
|
ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}"
|
|
if [ -z "$ADMIN_PASSWORD_HASH" ] && [ -n "${ADMIN_PASSWORD:-}" ]; then
|
|
ADMIN_PASSWORD_HASH=$(cd "$BUILD_DIR" && go run ./tools/hash-password/ "$ADMIN_PASSWORD" 2>/dev/null || echo "")
|
|
fi
|
|
sed -e "s|{ADMIN_PASSWORD_HASH}|$ADMIN_PASSWORD_HASH|g" \
|
|
-e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \
|
|
"$BUILD_DIR/config/authelia/users_database.yml" > "$GEN_DIR/users_database.yml"
|
|
|
|
# Copy generated configs to target
|
|
maybe_sudo cp "$GEN_DIR/Caddyfile" "$TARGET_DIR/config/caddy/Caddyfile"
|
|
maybe_sudo cp "$GEN_DIR/configuration.yml" "$TARGET_DIR/config/authelia/configuration.yml"
|
|
maybe_sudo cp "$GEN_DIR/users_database.yml" "$TARGET_DIR/config/authelia/users_database.yml"
|
|
|
|
# Persist generated secrets so --destroy is idempotent
|
|
if [ -f "$BACKUP_DIR/.env" ]; then
|
|
maybe_sudo sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^STORAGE_ENCRYPTION_KEY=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true
|
|
fi
|
|
# Quote values to protect $ signs (bcrypt hashes, etc.) when sourcing
|
|
maybe_sudo bash -c "echo \"JWT_SECRET='$JWT_SECRET'\" >> '$BACKUP_DIR/.env'"
|
|
maybe_sudo bash -c "echo \"SESSION_SECRET='$SESSION_SECRET'\" >> '$BACKUP_DIR/.env'"
|
|
maybe_sudo bash -c "echo \"STORAGE_ENCRYPTION_KEY='$STORAGE_ENCRYPTION_KEY'\" >> '$BACKUP_DIR/.env'"
|
|
[ -n "$ADMIN_PASSWORD_HASH" ] && maybe_sudo bash -c "echo \"ADMIN_PASSWORD_HASH='$ADMIN_PASSWORD_HASH'\" >> '$BACKUP_DIR/.env'"
|
|
maybe_sudo chmod 600 "$BACKUP_DIR/.env"
|
|
fi
|
|
|
|
# ============================================================
|
|
# 10. FIX OWNERSHIP — all files in TARGET_DIR/BACKUP_DIR to user
|
|
# ============================================================
|
|
RUN_USER="${SUDO_USER:-${USER}}"
|
|
echo "[*] Setting file ownership to $RUN_USER..."
|
|
maybe_sudo chown -R "$RUN_USER:" "$TARGET_DIR" 2>/dev/null || true
|
|
maybe_sudo chown -R "$RUN_USER:" "$BACKUP_DIR" 2>/dev/null || true
|
|
|
|
# ============================================================
|
|
# 11. DEPLOY stack (rootless podman — no sudo!)
|
|
# ============================================================
|
|
echo "[*] Deploying containers on $NETWORK_NAME..."
|
|
|
|
podman network create "$NETWORK_NAME" 2>/dev/null || true
|
|
|
|
# AUTHELIA_SECRET is SESSION_SECRET (Authelia session.secret)
|
|
AUTHELIA_SECRET="${SESSION_SECRET:-}"
|
|
if [ -z "$AUTHELIA_SECRET" ]; then
|
|
AUTHELIA_SECRET=$(sed -n '/^session:/,/^[a-z]/p' "$TARGET_DIR/config/authelia/configuration.yml" \
|
|
| grep 'secret:' | awk '{print $2}' 2>/dev/null || echo "")
|
|
fi
|
|
|
|
# Generate compose file with substituted secret
|
|
sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \
|
|
"$BUILD_DIR/compose/stack.yaml" > "$GEN_DIR/stack.yaml"
|
|
cp "$GEN_DIR/stack.yaml" "$TARGET_DIR/compose/stack.yaml"
|
|
|
|
podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true
|
|
sleep 1
|
|
podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues"
|
|
|
|
# ============================================================
|
|
# 12. HEALTH CHECK
|
|
# ============================================================
|
|
echo "[*] Running health check..."
|
|
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
|
|
HEALTH=$(podman exec launcher curl -sf http://127.0.0.1:9000/health 2>/dev/null || echo "")
|
|
if [ "$HEALTH" = "OK" ]; then
|
|
echo "[OK] NextWorkspace launcher is healthy"
|
|
echo "[OK] https://$DOMAIN/"
|
|
exit 0
|
|
fi
|
|
sleep $HEALTH_CHECK_INTERVAL
|
|
done
|
|
|
|
echo "[FAIL] Health check failed — launcher did not respond"
|
|
echo ""
|
|
echo "--- Container status ---"
|
|
podman ps -a --filter "name=caddy|authelia|launcher" 2>/dev/null || true
|
|
echo ""
|
|
echo "--- Launcher logs (last 20 lines) ---"
|
|
podman logs launcher --tail 20 2>/dev/null || echo " (no logs)"
|
|
exit 1
|