NextWks/install.sh

434 lines
14 KiB
Bash
Executable file

#!/bin/bash
# Next Workspace (NextWks) - Bare-Metal Installer
# Deploys compiled binaries to /opt/nextwks/ for production use
#
# Usage:
# ./install.sh Build and install
# ./install.sh --skip-build Install existing binaries only
# ./install.sh --build-only Compile binary only (no install)
# ./install.sh --config-only Generate config only
# ./install.sh --status Check installation health
# ./install.sh --uninstall Remove installation
# ./install.sh --help Show this help
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
error() { echo -e "${RED}Error:${NC} $1" >&2; }
success() { echo -e "${GREEN}$1${NC}"; }
info() { echo -e "${BLUE}$1${NC}"; }
warn() { echo -e "${YELLOW}Warning:${NC} $1"; }
# Configuration
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_DIR="/opt/nextwks"
BIN_DIR="${INSTALL_DIR}/bin"
DATA_DIR="${INSTALL_DIR}/data"
MODULES_DIR="${INSTALL_DIR}/modules"
STATIC_DIR="${INSTALL_DIR}/static"
CONFIG_FILE="${INSTALL_DIR}/config.yaml"
SERVICE_FILE="/etc/systemd/system/nextwks.service"
HEALTH_URL="http://localhost:8080/api/health"
# Parse arguments
SKIP_BUILD=false
BUILD_ONLY=false
CONFIG_ONLY=false
UNINSTALL=false
CHECK_STATUS=false
ADMIN_USER=""
for arg in "$@"; do
case "$arg" in
--skip-build) SKIP_BUILD=true ;;
--build-only) BUILD_ONLY=true ;;
--config-only) CONFIG_ONLY=true ;;
--uninstall) UNINSTALL=true ;;
--status) CHECK_STATUS=true ;;
--admin=*) ADMIN_USER="${arg#*=}" ;;
--admin)
error "Use --admin=username,email (e.g., --admin=cclohmar,claus@lohmar.co.uk)"
exit 1
;;
--help)
cat << 'HELPEOF'
NextWks Installer — Bare-metal deployment tool
./install.sh Build and install
./install.sh --skip-build Install existing binary only
./install.sh --build-only Compile only (no install)
./install.sh --config-only Generate config only
./install.sh --admin=user,email Create initial admin user
./install.sh --status Check installation health
./install.sh --uninstall Remove installation
Examples:
./install.sh Full build + install
./install.sh --admin=cclohmar,cl@sechpoint.app Create admin during install
./install.sh --status Check what's running
HELPEOF
exit 0
;;
*) error "Unknown argument: $arg (use --help for options)"; exit 1 ;;
esac
done
# --- Status Check ---
if [ "$CHECK_STATUS" = true ]; then
echo ""
info "Next Workspace Installation Status"
echo "-----------------------------------"
if [ -f "${INSTALL_DIR}/bin/core" ]; then
VERSION=$("${INSTALL_DIR}/bin/core" 2>&1 | head -1 || echo "unknown")
success "✓ Binary installed: ${INSTALL_DIR}/bin/core"
else
warn "✗ Binary not found: ${INSTALL_DIR}/bin/core (not installed)"
fi
if [ -f "$CONFIG_FILE" ]; then
success "✓ Configuration: $CONFIG_FILE"
else
warn "✗ Configuration: not found"
fi
if systemctl is-active --quiet nextwks 2>/dev/null; then
success "✓ Service running: nextwks"
elif systemctl is-enabled --quiet nextwks 2>/dev/null; then
warn "⚠ Service nextwks: enabled but not running"
else
info " Service nextwks: not active"
fi
if [ -f "/opt/authelia/authelia" ]; then
success "✓ Authelia found: /opt/authelia/"
if systemctl is-active --quiet authelia 2>/dev/null; then
success " Authelia service: running (port 9091)"
fi
else
warn "✗ Authelia: not installed"
fi
# Try health check if server appears to be running
if command -v curl &>/dev/null; then
HEALTH=$(curl -s --max-time 2 "$HEALTH_URL" 2>/dev/null || echo "")
if [ "$HEALTH" = '{"status":"ok"}' ]; then
success "✓ Health endpoint: OK (http://localhost:8080)"
elif [ -n "$HEALTH" ]; then
warn "⚠ Health endpoint: unexpected response: $HEALTH"
else
info " Health endpoint: not responding"
fi
fi
echo ""
exit 0
fi
# --- Uninstall ---
if [ "$UNINSTALL" = true ]; then
info "Uninstalling Next Workspace..."
if [ -f "$SERVICE_FILE" ]; then
systemctl stop nextwks 2>/dev/null || true
systemctl disable nextwks 2>/dev/null || true
rm -f "$SERVICE_FILE"
systemctl daemon-reload
info "Removed systemd service"
fi
if [ -d "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR"
success "Removed $INSTALL_DIR"
else
info "No installation found at $INSTALL_DIR"
fi
success "Uninstall complete"
exit 0
fi
# --- Prerequisites ---
info "Checking prerequisites..."
if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ] && [ "$BUILD_ONLY" = false ]; then
if ! command -v go &>/dev/null; then
error "Go is not installed. Install Go 1.22+ first."
exit 1
fi
fi
# Check Authelia
if [ ! -f "/opt/authelia/authelia" ]; then
warn "Authelia is not installed at /opt/authelia/"
warn "Admin user management requires Authelia to function."
warn "Install with: bash $REPO_DIR/scripts/install-authelia.sh"
echo ""
fi
# --- Static Assets ---
if [ "$CONFIG_ONLY" = false ] && [ "$BUILD_ONLY" = false ]; then
info "Installing static assets..."
if [ -d "$REPO_DIR/app/static" ]; then
mkdir -p "$STATIC_DIR"
cp -r "$REPO_DIR/app/static"/* "$STATIC_DIR/"
success "Static assets installed to $STATIC_DIR"
fi
fi
# --- Build ---
if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ]; then
info "Building Next Workspace core..."
cd "$REPO_DIR/src"
# Run tests first
info "Running tests..."
if ! go test -count=1 ./... 2>&1 | tail -1; then
warn "Some tests failed — continuing build anyway"
fi
# Build with stripped symbols for smaller binary
go build -o "$REPO_DIR/app/core" -ldflags="-s -w" .
success "Core binary built: app/core"
if [ "$BUILD_ONLY" = true ]; then
success "Build complete (--build-only, skipping install)"
exit 0
fi
fi
# --- Install ---
if [ "$CONFIG_ONLY" = false ]; then
info "Installing to $INSTALL_DIR..."
# Create directory structure
mkdir -p "$BIN_DIR" "$DATA_DIR" "$MODULES_DIR" "$STATIC_DIR"
# Copy binary
if [ -f "$REPO_DIR/app/core" ]; then
cp "$REPO_DIR/app/core" "$BIN_DIR/core"
chmod 755 "$BIN_DIR/core"
success "Installed binary to $BIN_DIR/core"
else
error "Binary not found at app/core. Run without --skip-build or build manually."
exit 1
fi
# Copy static assets if not already done
if [ -d "$REPO_DIR/app/static" ] && [ ! -f "$STATIC_DIR/manifest.json" ]; then
cp -r "$REPO_DIR/app/static"/* "$STATIC_DIR/"
fi
# Copy modules if any exist
if [ -d "$REPO_DIR/app/modules" ] && [ "$(ls -A "$REPO_DIR/app/modules" 2>/dev/null)" ]; then
cp -r "$REPO_DIR/app/modules"/* "$MODULES_DIR/"
success "Installed modules"
fi
fi
# --- Configuration ---
info "Generating configuration..."
if [ ! -f "$CONFIG_FILE" ]; then
ADMIN_SECRET=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | xxd -p -c 32)
SESSION_SECRET=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | xxd -p -c 32)
cat > "$CONFIG_FILE" << CONFIGEOF
# Next Workspace (NextWks) - Production Configuration
# Auto-generated by install.sh on $(date)
server:
host: "0.0.0.0"
port: 8080
admin:
secret_token: "${ADMIN_SECRET}"
database:
type: "sqlite"
path: "${DATA_DIR}/nextwks.db"
authelia:
host: "http://127.0.0.1:9091"
config_path: "/opt/authelia/configuration.yml"
users_db_path: "/opt/authelia/users_database.yml"
oidc:
issuer_url: "https://auth.sechpoint.app"
client_id: "nextwks"
client_secret: ""
redirect_url: "https://wks.lohmar.co.uk/auth/callback"
domain: "wks.lohmar.co.uk"
smtp:
host: ""
port: 587
username: ""
password: ""
from: "noreply@nextwks.local"
session:
secret: "${SESSION_SECRET}"
expiry_minutes: 60
CONFIGEOF
chmod 600 "$CONFIG_FILE"
success "Configuration generated: $CONFIG_FILE"
echo ""
warn " Admin Secret Token: ${ADMIN_SECRET}"
warn " Store this securely! Required for admin API access."
echo ""
else
info "Configuration already exists at $CONFIG_FILE (not overwritten)"
fi
# --- Systemd Service ---
if [ "$CONFIG_ONLY" = false ]; then
info "Setting up systemd service..."
cat > "$SERVICE_FILE" << SERVICEEOF
[Unit]
Description=Next Workspace (NextWks) Core
After=network.target authelia.service
Wants=authelia.service
[Service]
Type=simple
User=root
WorkingDirectory=${INSTALL_DIR}
ExecStart=${BIN_DIR}/core -config ${CONFIG_FILE}
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Security hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=${DATA_DIR} ${MODULES_DIR} /opt/authelia/users_database.yml
ReadOnlyPaths=${INSTALL_DIR}/config.yaml ${INSTALL_DIR}/static
[Install]
WantedBy=multi-user.target
SERVICEEOF
systemctl daemon-reload
success "Systemd service created: $SERVICE_FILE"
fi
# --- Smoke Test ---
if [ "$CONFIG_ONLY" = false ]; then
info "Starting smoke test..."
# Start the server temporarily to verify it works
if [ -f "$BIN_DIR/core" ] && [ -f "$CONFIG_FILE" ]; then
"$BIN_DIR/core" -config "$CONFIG_FILE" &
SMOKE_PID=$!
sleep 2
if command -v curl &>/dev/null; then
RESPONSE=$(curl -s --max-time 3 "$HEALTH_URL" 2>/dev/null || echo "")
if [ "$RESPONSE" = '{"status":"ok"}' ]; then
success "Smoke test passed — server responds OK"
else
warn "Smoke test: server started but health check returned '$RESPONSE'"
fi
fi
# --- Admin User Creation ---
if [ "$CONFIG_ONLY" = false ]; then
# Read admin token from config
ADMIN_TOKEN=$(grep secret_token "$CONFIG_FILE" | head -1 | sed 's/.*: *"*//;s/"*$//' | xargs)
ADMIN_API="http://localhost:8080/admin/api/users"
# Check if interactive or --admin flag was provided
if [ -t 0 ] && [ -z "$ADMIN_USER" ] && [ ! -f "$CONFIG_FILE.initialized" ]; then
echo ""
info "No --admin flag provided. Create an initial admin user?"
read -p "Enter username:email (or press Enter to skip): " ADMIN_INPUT
if [ -n "$ADMIN_INPUT" ]; then
ADMIN_USER="$ADMIN_INPUT"
fi
fi
if [ -n "$ADMIN_USER" ]; then
# Parse username,email
ADMIN_UNAME="${ADMIN_USER%%,*}"
ADMIN_EMAIL="${ADMIN_USER#*,}"
if [ "$ADMIN_UNAME" = "$ADMIN_EMAIL" ]; then
ADMIN_EMAIL=""
fi
info "Creating admin user: $ADMIN_UNAME..."
RESULT=$(curl -s -X POST "$ADMIN_API" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"users\":[{\"username\":\"$ADMIN_UNAME\",\"display_name\":\"$ADMIN_UNAME\",\"email\":\"$ADMIN_EMAIL\",\"role\":\"admin\",\"groups\":\"admins\"}]}")
PASSWORD=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['results'][0].get('generated_password',''))" 2>/dev/null || echo "")
ERROR=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['results'][0].get('error',''))" 2>/dev/null || echo "")
if [ -n "$PASSWORD" ]; then
success "Admin user created!"
echo ""
warn " ┌─────────────────────────────────────────┐"
warn " │ Username: $ADMIN_UNAME"
warn " │ Email: ${ADMIN_EMAIL:-<not set>}"
warn " │ Password: $PASSWORD"
warn " │ Groups: admins"
warn " └─────────────────────────────────────────┘"
echo ""
warn " Save this password! It cannot be recovered."
warn " User will be synced to Authelia automatically."
echo ""
elif [ -n "$ERROR" ]; then
warn "Admin creation failed: $ERROR"
else
warn "Could not parse response from admin API"
fi
fi
fi
# Mark as initialized to skip interactive prompt next time
touch "$CONFIG_FILE.initialized" 2>/dev/null || true
kill $SMOKE_PID 2>/dev/null || true
wait $SMOKE_PID 2>/dev/null || true
fi
fi
# --- Summary ---
echo ""
success "═══════════════════════════════════════════"
success " Next Workspace (NextWks) installed!"
success "═══════════════════════════════════════════"
echo ""
info " Binary: ${BIN_DIR}/core"
info " Config: ${CONFIG_FILE}"
info " Data: ${DATA_DIR}/"
info " Static assets: ${STATIC_DIR}/"
info " Service: systemctl start nextwks"
echo ""
info " Workspace: https://wks.lohmar.co.uk/"
info " Admin UI: http://localhost:8080/admin"
info " Health API: http://localhost:8080/api/health"
info " Auth status: http://localhost:8080/auth/status"
echo ""
info " Start: systemctl enable --now nextwks"
info " Logs: journalctl -u nextwks -f"
info " Status: ./install.sh --status"
echo ""
warn " Next step: Register NextWks as OIDC client in Authelia:"
warn " • Edit /opt/authelia/configuration.yml"
warn " • Add client_id: nextwks with redirect_uri: https://wks.lohmar.co.uk/auth/callback"
warn " • Restart: systemctl restart authelia"
echo ""