From 81e5505f2a0ec37b9dde3450fc80a7bdd5cec4d4 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Sat, 11 Jul 2026 00:02:55 +0100 Subject: [PATCH] Unified install/update/destroy script with ephemeral build dir - Replace deploy.sh and install.sh with tools/nextwks.sh - Build in /tmp/nextwks-build (fresh clone every time), no more /opt/NextWks - Script saves itself to ~/nextwks.sh on --install for easy future access - Add AGENT.md with workflow rules for the new approach - Secrets persisted in /opt/backup/.env (JWT, SESSION, password hash) - Update README and CHANGELOG --- AGENT.md | 100 +++++++++++++++++++ CHANGELOG.md | 16 ++++ README.md | 51 ++++++---- VERSION | 2 +- deploy.sh | 141 --------------------------- install.sh | 143 --------------------------- tools/nextwks.sh | 244 +++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 396 insertions(+), 301 deletions(-) create mode 100644 AGENT.md delete mode 100755 deploy.sh delete mode 100755 install.sh create mode 100755 tools/nextwks.sh diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..0c411a1 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,100 @@ +# NextWorkspace — Agent Workflow Instructions + +## Versioning + +- Format: `MILESTONE.FEATURE.PATCH.BUILD` (e.g., `0.1.0.0032`) +- Bump VERSION file on every change before commit. +- Every commit must be tagged with the version: `git tag v$(cat VERSION)` + +## Deployment Model (Unified Script) + +The project uses a single unified script at `tools/nextwks.sh` for all operations. +The old `deploy.sh` and `install.sh` are deprecated. + +**Key principles:** +- Source of truth is **git remote only**. No local `/opt/NextWks` repo. +- Building happens in `/tmp/nextwks-build/` via `git clone --depth 1` (fresh every time). +- Runtime goes to `/opt/nextworkspace/` (configs, compose, binary, lng). +- Secrets persist in `/opt/backup/.env` and survive `--destroy`. +- Stack runs on a shared `nextwks-net` podman bridge network. + +### Script Flags + +| Flag | What it does | +|------|-------------| +| `--install` | First-time setup on a bare VM: installs deps, prompts for config, builds binary, generates configs, deploys stack | +| `--update` | Smart update: clones fresh, rebuilds binary, copies to target, bounces containers | +| `--destroy` | Full greenfield redeploy: tears down containers, wipes `/opt/nextworkspace/`, rebuilds from scratch using saved secrets from `/opt/backup/.env` | + +### Workflow for Making Changes + +1. Edit code in the development clone. +2. Test locally (e.g., `go build && go run .`). +3. Bump `VERSION` (increment BUILD). +4. Update `CHANGELOG.md`. +5. Commit: `git add -A && git commit -m "description"` +6. Tag: `git tag v$(cat VERSION)` +7. Push: `git push origin main --tags` +8. Deploy: `sudo bash ~/nextwks.sh --update` + +> **Note:** The deploy script is downloaded by users via `curl` from the repo. For production deployment, users run: +> ```bash +> curl -sL https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh | sudo bash -s -- --install +> ``` + +## Build Process + +- Static Go binary: `CGO_ENABLED=0 go build -o nextworkspace .` +- Must run on Alpine in the container (no glibc dependency). +- Binary runs as PID 1 in the `launcher` container. + +## Architecture + +``` +Caddy (:80/:443, host ports) + ├── auth.{DOMAIN} → Authelia :9091 (internal) + ├── app.{DOMAIN} → Launcher :9000 (internal) with forward auth + └── www.{DOMAIN} → static files + +All three containers on nextwks-net (podman bridge). +Only Caddy exposes ports to host. +``` + +## Config Template System + +Config files use `{PLACEHOLDER}` syntax. The script substitutes values at deploy time: + +| File | Placeholders | +|------|-------------| +| `config/caddy/Caddyfile` | `{DOMAIN}`, `{TLS_EMAIL}` | +| `config/authelia/configuration.yml` | `{DOMAIN}`, `{JWT_SECRET}`, `{SESSION_SECRET}`, `{SMTP_HOST}`, `{SMTP_PORT}`, `{SMTP_USER}`, `{SMTP_PASS}` | +| `config/authelia/users_database.yml` | `{ADMIN_PASSWORD_HASH}`, `{TLS_EMAIL}` | +| `compose/stack.yaml` | `{AUTHELIA_SECRET}` (same as `SESSION_SECRET`) | + +## Env Vault (`/opt/backup/.env`) + +Persisted secrets across destroys: +``` +DOMAIN=nextwks.eu +TLS_EMAIL=admin@nextwks.eu +ADMIN_USERNAME=master +ADMIN_PASSWORD= +SMTP_HOST=smtp.openxchange.eu +SMTP_PORT=587 +SMTP_USER=post@nextwks.eu +SMTP_PASS= +JWT_SECRET= +SESSION_SECRET= +ADMIN_PASSWORD_HASH= +``` + +## Health Check + +After deploy, the script polls `podman exec launcher wget -qO- http://127.0.0.1:9000/health` +up to 10 times (2s interval). Expected response: `OK`. + +## Commit Message Style + +- Imperative mood ("Add", "Fix", "Update", "Bump") +- Reference the component if relevant ("launcher: add health endpoint") +- Keep under 72 chars for the first line diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a0488..4d85d09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.1.0.0032 — 2026-07-11 + +### Added +- `tools/nextwks.sh` — unified install/update/destroy script +- `AGENT.md` — workflow instructions for agents + +### Changed +- Replaced `deploy.sh` and `install.sh` with single `tools/nextwks.sh` +- Build moved from `/opt/NextWks` (persistent git repo) to `/tmp/nextwks-build` (ephemeral clone) +- README.md updated for unified script workflow +- `SESSION_SECRET` persisted in `/opt/backup/.env` for idempotent `--destroy` + +### Removed +- `deploy.sh` (replaced by `tools/nextwks.sh --update / --destroy`) +- `install.sh` (replaced by `tools/nextwks.sh --install`) + ## 0.1.0.0007 — 2026-07-08 ### Added diff --git a/README.md b/README.md index 2d22641..1184028 100644 --- a/README.md +++ b/README.md @@ -24,44 +24,63 @@ app.nextwks.eu :443 auth.nextwks.eu :443 - **Authelia**: OIDC provider, 2FA, identity store - **Binary**: Go launcher + path-based reverse proxy to upstream apps -## Quick Start +## Quick Start (Bare VM) ```bash -sudo ./install.sh +curl -sL https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh \ + | sudo bash -s -- --install ``` -Prompts for domain, TLS email, and admin credentials. Installs dependencies, generates configs, deploys Caddy + Authelia + launcher. +Prompts for domain, TLS email, and admin credentials. Installs deps (Go, Podman, git), +clones repo to `/tmp/nextwks-build/`, builds binary, generates configs, deploys stack. ## Directory Layout ``` -/opt/nextworkspace/ +/opt/nextworkspace/ # Runtime (freshly populated on every deploy) ├── config/ │ ├── caddy/Caddyfile │ ├── authelia/configuration.yml │ ├── authelia/users_database.yml │ └── nextworkspace/{config,apps}.yaml ├── data/ -│ ├── caddy/ (certs + runtime data) +│ ├── caddy/ (certs + runtime) │ └── authelia/ (database) -├── compose/ -│ ├── caddy.yaml -│ └── authelia.yaml +├── compose/stack.yaml ├── www/ (landing page) -└── nextworkspace (binary) +├── lng/ (translations) +└── nextworkspace (static Go binary) -/opt/backup/ -├── .env (secrets vault) -└── certficates/ (LE cert backup) +/opt/backup/ # Secrets vault (survives --destroy) +├── .env +└── certificates/ + +/tmp/nextwks-build/ # Ephemeral build dir (git clone --depth 1) ``` -## Deployment +## Operations ```bash -sudo ./deploy.sh # Smart update (swap binary, restart) -sudo ./deploy.sh --destroy # Full rebuild (certs backed up) +# First-time install (download + run — saves itself to ~/nextwks.sh) +curl -sL https://git.lohmar.co.uk/lexton-it/NextWks/raw/branch/main/tools/nextwks.sh \ + | sudo bash -s -- --install + +# Smart update (pull, build, copy, restart) +sudo bash ~/nextwks.sh --update + +# Full redeploy (tear down, rebuild from scratch with saved secrets) +sudo bash ~/nextwks.sh --destroy ``` +## Workflow (Development) + +1. Edit code in your clone. +2. Bump `VERSION`, update `CHANGELOG.md`. +3. `git commit -m "message" && git tag v$(cat VERSION) && git push origin main --tags` +4. On the server: `sudo bash ~/nextwks.sh --update` + +The script clones fresh from git every time — no stale repos, no permissions issues. + ## Version -Current: 0.1.0.0007 — see [CHANGELOG.md](CHANGELOG.md) +Current: 0.1.0.0032 — see [CHANGELOG.md](CHANGELOG.md) diff --git a/VERSION b/VERSION index d13e5a3..eeb7893 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0.0031 +0.1.0.0032 diff --git a/deploy.sh b/deploy.sh deleted file mode 100755 index 22c4b91..0000000 --- a/deploy.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_DIR="/opt/NextWks" -TARGET_DIR="/opt/nextworkspace" -BACKUP_DIR="/opt/backup" -NETWORK_NAME="nextwks-net" -HEALTH_CHECK_RETRIES=10 -HEALTH_CHECK_INTERVAL=2 - -# --- Load .env --- -if [ -f "$BACKUP_DIR/.env" ]; then - set -a; source "$BACKUP_DIR/.env"; set +a -elif [ -f "$TARGET_DIR/.env" ]; then - set -a; source "$TARGET_DIR/.env"; set +a -fi -DOMAIN="${DOMAIN:-nextwks.eu}" - -# --- Mode --- -GREENFIELD=false -if [ "${1:-}" = "--destroy" ]; then GREENFIELD=true; echo "[MODE] Greenfield deploy (--destroy)" -elif [ ! -d "$TARGET_DIR" ]; then GREENFIELD=true; echo "[MODE] Greenfield deploy (target missing)" -else echo "[MODE] Smart update (target exists)"; fi - -# --- Pull + build --- -cd "$REPO_DIR" -echo "[1/5] Pulling latest code..." -git config --global --add safe.directory /opt/NextWks 2>/dev/null || true -git pull 2>&1 || echo "[WARN] Git pull failed — using existing code" - -echo "[2/5] Building binary (static)..." -export PATH=$PATH:/usr/local/go/bin -CGO_ENABLED=0 go build -o nextworkspace . - -# --- Greenfield --- -if [ "$GREENFIELD" = true ]; then - echo "[3/5] Full teardown..." - systemctl stop nextworkspace 2>/dev/null || true - systemctl disable nextworkspace 2>/dev/null || true - rm -f /etc/systemd/system/nextworkspace.service - systemctl daemon-reload - # Kill any lingering Caddy processes on port 80/443 - pkill -f "caddy" 2>/dev/null || true - # Force clean ALL old pods, containers, and networks - for p in $(podman pod ls -q 2>/dev/null); do podman pod rm -f "$p" 2>/dev/null || true; done - podman rm -f caddy authelia launcher 2>/dev/null || true - podman network rm "$NETWORK_NAME" 2>&1 || true - - if [ -d "$TARGET_DIR" ]; then - chattr -R -i "$TARGET_DIR" 2>/dev/null || true - rm -rf "$TARGET_DIR" - fi - - echo "[4/5] Building directories..." - mkdir -p "$TARGET_DIR/config/caddy" - mkdir -p "$TARGET_DIR/config/authelia" - mkdir -p "$TARGET_DIR/data/caddy" - mkdir -p "$TARGET_DIR/data/authelia" - mkdir -p "$TARGET_DIR/compose" - mkdir -p "$TARGET_DIR/www" - mkdir -p "$TARGET_DIR/config/nextworkspace" - mkdir -p "$TARGET_DIR/logs" - chown -R master:master "$TARGET_DIR/data/caddy" 2>/dev/null || true - chown -R master:master "$TARGET_DIR/logs" 2>/dev/null || true - - if [ -f "$BACKUP_DIR/.env" ]; then - cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env" - chmod 644 "$TARGET_DIR/.env" - fi - - # Generate Caddyfile - sed -e "s|{DOMAIN}|$DOMAIN|g" -e "s|{TLS_EMAIL}|${TLS_EMAIL:-admin@$DOMAIN}|g" \ - "$SCRIPT_DIR/config/caddy/Caddyfile" > "$TARGET_DIR/config/caddy/Caddyfile" - - # Generate Authelia config - JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}" - SESSION_SECRET="${SESSION_SECRET:-$(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|{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" \ - "$SCRIPT_DIR/config/authelia/configuration.yml" > "$TARGET_DIR/config/authelia/configuration.yml" - - ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-}" - if [ -z "$ADMIN_PASSWORD_HASH" ] && [ -n "${ADMIN_PASSWORD:-}" ] && [ -f "$SCRIPT_DIR/tools/hash-password/main.go" ]; then - ADMIN_PASSWORD_HASH=$(cd "$SCRIPT_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" \ - "$SCRIPT_DIR/config/authelia/users_database.yml" > "$TARGET_DIR/config/authelia/users_database.yml" - - cp nextworkspace "$TARGET_DIR/nextworkspace" - if [ -f "$REPO_DIR/VERSION" ]; then cp "$REPO_DIR/VERSION" "$TARGET_DIR/VERSION"; fi - if [ -d "$SCRIPT_DIR/config/www" ]; then cp -r "$SCRIPT_DIR/config/www"/* "$TARGET_DIR/www/"; fi - cp -r "$SCRIPT_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/" 2>/dev/null || true - - # Copy language files - if [ -d "$SCRIPT_DIR/lng" ]; then - cp -r "$SCRIPT_DIR/lng" "$TARGET_DIR/" - fi - - # Create podman network and deploy Caddy + Authelia - echo "[5/5] Deploying containers on $NETWORK_NAME..." - podman network create "$NETWORK_NAME" 2>&1 || true - sleep 3 - - # Extract AUTHELIA_SECRET from config - AUTHELIA_SECRET=$(grep -oP 'session_secret: \K.*' "$TARGET_DIR/config/authelia/configuration.yml" 2>/dev/null || echo "") - if [ -n "$AUTHELIA_SECRET" ] && ! grep -q "AUTHELIA_SECRET" "$BACKUP_DIR/.env" 2>/dev/null; then - echo "AUTHELIA_SECRET=$AUTHELIA_SECRET" >> "$BACKUP_DIR/.env" - fi - - # Generate compose file with secret, then deploy - sed -e "s|{AUTHELIA_SECRET}|$AUTHELIA_SECRET|g" \ - "$SCRIPT_DIR/compose/stack.yaml" > "$TARGET_DIR/compose/stack.yaml" - podman-compose -f "$TARGET_DIR/compose/stack.yaml" down 2>/dev/null || true - podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues" - -# --- Smart update --- -else - echo "[3/5] Redeploying containers..." - podman stop caddy authelia launcher 2>/dev/null || true - podman rm caddy authelia launcher 2>/dev/null || true - podman-compose -f "$TARGET_DIR/compose/stack.yaml" up -d 2>&1 || echo "[WARN] Stack deploy had issues" -fi - -# --- Health check --- -echo "[*] Running health check..." -for i in $(seq 1 $HEALTH_CHECK_RETRIES); do - HEALTH=$(podman exec launcher wget -qO- http://127.0.0.1:9000/health 2>/dev/null || echo "") - if [ "$HEALTH" = "OK" ]; then - echo "[OK] NextWorkspace launcher is healthy" - exit 0 - fi - echo " Attempt $i/$HEALTH_CHECK_RETRIES — not ready yet..." - sleep $HEALTH_CHECK_INTERVAL -done - -echo "[FAIL] Health check failed — launcher did not respond" -exit 1 diff --git a/install.sh b/install.sh deleted file mode 100755 index 835ecf2..0000000 --- a/install.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# NextWorkspace Installer — bootstraps a bare Linux VM -# Idempotent: safe to run multiple times. - -TARGET_DIR="/opt/nextworkspace" -BACKUP_DIR="/opt/backup" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - -# --- Create backup vault and runtime directories --- -echo "=== NextWorkspace Setup ===" -mkdir -p "$BACKUP_DIR/certificates" -mkdir -p "$TARGET_DIR/config/nextworkspace" -mkdir -p "$TARGET_DIR/config/caddy" -mkdir -p "$TARGET_DIR/config/authelia" -mkdir -p "$TARGET_DIR/data/caddy" -mkdir -p "$TARGET_DIR/data/authelia" -mkdir -p "$TARGET_DIR/compose" -mkdir -p "$TARGET_DIR/www" -mkdir -p "$TARGET_DIR/logs/caddy" -# Ensure data dirs are owned by the runtime user -chown -R master:master "$TARGET_DIR/data/caddy" 2>/dev/null || true -chown -R master:master "$TARGET_DIR/logs/caddy" 2>/dev/null || true - -# --- Interactive prompts (no defaults — user enters everything) --- -read -p "Domain: " DOMAIN -while [ -z "$DOMAIN" ]; do - read -p "Domain (required): " DOMAIN -done - -read -p "TLS email (Let's Encrypt): " TLS_EMAIL -while [ -z "$TLS_EMAIL" ]; do - read -p "TLS email (required): " TLS_EMAIL -done -# Basic email validation (must contain @) -while echo "$TLS_EMAIL" | grep -qv '@'; do - read -p "Invalid email — must contain @: " TLS_EMAIL -done - -read -p "Admin username: " ADMIN_USERNAME -while [ -z "$ADMIN_USERNAME" ]; do - read -p "Admin username (required): " ADMIN_USERNAME -done - -# Generate 12-char alphanumeric password (easy to type) -ADMIN_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 12 2>/dev/null || date +%s | head -c 12) - -# --- SMTP prompts --- -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 "" -if [ -z "$SMTP_PASS" ]; then - echo "[ERROR] SMTP password is required." - exit 1 -fi - -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}" - -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 "" - -# Write .env file in backup vault (deploy.sh copies it to production) -ENV_FILE="$BACKUP_DIR/.env" -cat > "$ENV_FILE" </dev/null; then - echo "[SKIP] Go already installed: $(go version)" -else - echo "[INSTALL] Installing Go..." - GO_URL="https://go.dev/dl/$(curl -sL https://go.dev/VERSION?m=text | head -1).linux-amd64.tar.gz" - curl -sL "$GO_URL" -o /tmp/go.tar.gz - rm -rf /usr/local/go - tar -C /usr/local -xzf /tmp/go.tar.gz - rm /tmp/go.tar.gz - echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh - chmod +x /etc/profile.d/go.sh - export PATH=$PATH:/usr/local/go/bin - echo "[OK] Go installed: $(go version)" -fi - -# ---- System deps ---- -echo "[INSTALL] git, build-essential, podman, podman-compose..." -apt-get update -qq -apt-get install -y -qq git build-essential curl podman podman-compose - -# ---- Deploy: use current scripts + tools --- -REPO_DIR="/opt/NextWks" -mkdir -p "$REPO_DIR" - -# Clone or update repo, then overlay our current code -echo "[SETUP] Preparing /opt/NextWks..." -if [ -d "$REPO_DIR/.git" ]; then - cd "$REPO_DIR" && git pull -elif command -v git &>/dev/null; then - # Try to clone the remote first, so git history is intact - git clone "https://git.lohmar.co.uk/lexton-it/NextWks.git" "$REPO_DIR.tmp" 2>/dev/null && \ - mv "$REPO_DIR.tmp" "$REPO_DIR" || true -fi - -# Copy current code on top (ensures latest changes) -mkdir -p "$REPO_DIR" -cp -r "$SCRIPT_DIR"/* "$REPO_DIR/" -cp "$SCRIPT_DIR"/.gitignore "$REPO_DIR/" 2>/dev/null || true -chmod +x "$REPO_DIR/deploy.sh" "$REPO_DIR/install.sh" 2>/dev/null || true - -echo "[DONE] Bootstrapping complete. Running first deploy..." -"$REPO_DIR/deploy.sh" --destroy diff --git a/tools/nextwks.sh b/tools/nextwks.sh new file mode 100755 index 0000000..86473d0 --- /dev/null +++ b/tools/nextwks.sh @@ -0,0 +1,244 @@ +#!/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=10 +HEALTH_CHECK_INTERVAL=2 + +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 + +# --- Load existing env (if any) --- +if [ -f "$BACKUP_DIR/.env" ]; then + set -a; source "$BACKUP_DIR/.env"; set +a +elif [ -f "$TARGET_DIR/.env" ]; then + set -a; source "$TARGET_DIR/.env"; set +a +fi +DOMAIN="${DOMAIN:-nextwks.eu}" + +echo "=== NextWorkspace ${MODE} ===" + +# ============================================================ +# 1. INSTALL MODE — first-time setup +# ============================================================ +if [ "$MODE" = "install" ]; then + echo "[*] Installing system dependencies..." + apt-get update -qq && apt-get install -y -qq git build-essential curl podman podman-compose + + if ! command -v go &>/dev/null; then + echo "[*] Installing Go..." + GO_URL="https://go.dev/dl/$(curl -sL https://go.dev/VERSION?m=text | head -1).linux-amd64.tar.gz" + curl -sL "$GO_URL" -o /tmp/go.tar.gz + rm -rf /usr/local/go + tar -C /usr/local -xzf /tmp/go.tar.gz + rm /tmp/go.tar.gz + echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh + chmod +x /etc/profile.d/go.sh + export PATH=$PATH:/usr/local/go/bin + fi + + 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 + ADMIN_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 12) + + 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 + + # Persist config to backup vault + mkdir -p "$BACKUP_DIR" + cat > "$BACKUP_DIR/.env" </dev/null || echo "$HOME") + cp "$BUILD_DIR/tools/nextwks.sh" "$USER_HOME/nextwks.sh" + chmod +x "$USER_HOME/nextwks.sh" + echo "[*] Saved to $USER_HOME/nextwks.sh — use it for future updates" +fi + +# ============================================================ +# 3. BUILD static binary +# ============================================================ +echo "[*] Building static binary..." +export PATH=$PATH:/usr/local/go/bin +CGO_ENABLED=0 go build -o nextworkspace . + +# ============================================================ +# 4. CREATE target directory structure +# ============================================================ +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" + +# ============================================================ +# 5. TEARDOWN (destroy mode only) +# ============================================================ +if [ "$MODE" = "destroy" ]; then + echo "[*] Full teardown..." + podman stop caddy authelia launcher 2>/dev/null || true + podman rm caddy authelia launcher 2>/dev/null || true + rm -rf "$TARGET_DIR" + 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 + +# ============================================================ +# 6. COPY artifacts to target +# ============================================================ +echo "[*] Copying artifacts..." +cp nextworkspace "$TARGET_DIR/nextworkspace" +cp "$BUILD_DIR/VERSION" "$TARGET_DIR/VERSION" +if [ -d "$BUILD_DIR/config/www" ]; then + cp -r "$BUILD_DIR/config/www"/* "$TARGET_DIR/www/" +fi +if [ -d "$BUILD_DIR/lng" ]; then + rm -rf "$TARGET_DIR/lng" + cp -r "$BUILD_DIR/lng" "$TARGET_DIR/lng" +fi +if [ -d "$BUILD_DIR/config/nextworkspace" ]; then + cp -r "$BUILD_DIR/config/nextworkspace"/* "$TARGET_DIR/config/nextworkspace/" +fi + +# Restore .env from backup +if [ -f "$BACKUP_DIR/.env" ]; then + cp "$BACKUP_DIR/.env" "$TARGET_DIR/.env" + chmod 644 "$TARGET_DIR/.env" +fi + +# ============================================================ +# 7. GENERATE config files with placeholder substitution +# (install + destroy modes; update skips to keep secrets stable) +# ============================================================ +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" > "$TARGET_DIR/config/caddy/Caddyfile" + + # Authelia config — preserve existing secrets if present + JWT_SECRET="${JWT_SECRET:-$(openssl rand -hex 32)}" + SESSION_SECRET="${SESSION_SECRET:-$(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|{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" > "$TARGET_DIR/config/authelia/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" > "$TARGET_DIR/config/authelia/users_database.yml" + + # Persist generated secrets so --destroy is idempotent + if [ -f "$BACKUP_DIR/.env" ]; then + # Update existing .env with any new secrets + sed -i "/^JWT_SECRET=/d; /^SESSION_SECRET=/d; /^ADMIN_PASSWORD_HASH=/d" "$BACKUP_DIR/.env" 2>/dev/null || true + fi + echo "JWT_SECRET=$JWT_SECRET" >> "$BACKUP_DIR/.env" + echo "SESSION_SECRET=$SESSION_SECRET" >> "$BACKUP_DIR/.env" + [ -n "$ADMIN_PASSWORD_HASH" ] && echo "ADMIN_PASSWORD_HASH=$ADMIN_PASSWORD_HASH" >> "$BACKUP_DIR/.env" + chmod 600 "$BACKUP_DIR/.env" +fi + +# ============================================================ +# 8. DEPLOY stack +# ============================================================ +echo "[*] Deploying containers on $NETWORK_NAME..." + +# Ensure network exists +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 + # Update mode — extract from existing config + 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" > "$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" + +# ============================================================ +# 9. HEALTH CHECK +# ============================================================ +echo "[*] Running health check..." +for i in $(seq 1 $HEALTH_CHECK_RETRIES); do + HEALTH=$(podman exec launcher wget -qO- 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" +exit 1