build(core): polish installer with smoke test, status check, and production-ready systemd service

This commit is contained in:
Claus Lohmar 2026-06-14 13:24:48 +00:00
parent 926c25018e
commit 5f9dec740c
2 changed files with 176 additions and 35 deletions

View file

@ -3,10 +3,13 @@
# Deploys compiled binaries to /opt/nextwks/ for production use # Deploys compiled binaries to /opt/nextwks/ for production use
# #
# Usage: # Usage:
# ./install.sh # Build and install # ./install.sh Build and install
# ./install.sh --skip-build # Install existing binaries only # ./install.sh --skip-build Install existing binaries only
# ./install.sh --config-only # Generate config only # ./install.sh --build-only Compile binary only (no install)
# ./install.sh --uninstall # Remove installation # ./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 set -euo pipefail
@ -28,23 +31,85 @@ INSTALL_DIR="/opt/nextwks"
BIN_DIR="${INSTALL_DIR}/bin" BIN_DIR="${INSTALL_DIR}/bin"
DATA_DIR="${INSTALL_DIR}/data" DATA_DIR="${INSTALL_DIR}/data"
MODULES_DIR="${INSTALL_DIR}/modules" MODULES_DIR="${INSTALL_DIR}/modules"
STATIC_DIR="${INSTALL_DIR}/static"
CONFIG_FILE="${INSTALL_DIR}/config.yaml" CONFIG_FILE="${INSTALL_DIR}/config.yaml"
SERVICE_FILE="/etc/systemd/system/nextwks.service" SERVICE_FILE="/etc/systemd/system/nextwks.service"
HEALTH_URL="http://localhost:8080/api/health"
# Parse arguments # Parse arguments
SKIP_BUILD=false SKIP_BUILD=false
BUILD_ONLY=false
CONFIG_ONLY=false CONFIG_ONLY=false
UNINSTALL=false UNINSTALL=false
CHECK_STATUS=false
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--skip-build) SKIP_BUILD=true ;; --skip-build) SKIP_BUILD=true ;;
--build-only) BUILD_ONLY=true ;;
--config-only) CONFIG_ONLY=true ;; --config-only) CONFIG_ONLY=true ;;
--uninstall) UNINSTALL=true ;; --uninstall) UNINSTALL=true ;;
*) error "Unknown argument: $arg"; exit 1 ;; --status) CHECK_STATUS=true ;;
--help)
head -11 "$0" | sed 's/^# //; 1s/.*/NextWks Installer/' | sed 's/^$/ /'
exit 0
;;
*) error "Unknown argument: $arg (use --help for options)"; exit 1 ;;
esac esac
done 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 --- # --- Uninstall ---
if [ "$UNINSTALL" = true ]; then if [ "$UNINSTALL" = true ]; then
info "Uninstalling Next Workspace..." info "Uninstalling Next Workspace..."
@ -68,28 +133,54 @@ if [ "$UNINSTALL" = true ]; then
exit 0 exit 0
fi fi
# --- Check Authelia --- # --- 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 if [ ! -f "/opt/authelia/authelia" ]; then
warn "Authelia is not installed at /opt/authelia/" warn "Authelia is not installed at /opt/authelia/"
warn "User management requires Authelia to function." warn "Admin user management requires Authelia to function."
warn "Install with: bash scripts/install-authelia.sh" warn "Install with: bash $REPO_DIR/scripts/install-authelia.sh"
echo "" echo ""
fi 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 --- # --- Build ---
if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ]; then if [ "$SKIP_BUILD" = false ] && [ "$CONFIG_ONLY" = false ]; then
info "Building Next Workspace core..." info "Building Next Workspace core..."
if ! command -v go &>/dev/null; then
error "Go is not installed. Install Go 1.22+ first."
exit 1
fi
cd "$REPO_DIR/src" cd "$REPO_DIR/src"
# Build the core binary # 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" . go build -o "$REPO_DIR/app/core" -ldflags="-s -w" .
success "Core binary built: app/core" success "Core binary built: app/core"
if [ "$BUILD_ONLY" = true ]; then
success "Build complete (--build-only, skipping install)"
exit 0
fi
fi fi
# --- Install --- # --- Install ---
@ -97,7 +188,7 @@ if [ "$CONFIG_ONLY" = false ]; then
info "Installing to $INSTALL_DIR..." info "Installing to $INSTALL_DIR..."
# Create directory structure # Create directory structure
mkdir -p "$BIN_DIR" "$DATA_DIR" "$MODULES_DIR" mkdir -p "$BIN_DIR" "$DATA_DIR" "$MODULES_DIR" "$STATIC_DIR"
# Copy binary # Copy binary
if [ -f "$REPO_DIR/app/core" ]; then if [ -f "$REPO_DIR/app/core" ]; then
@ -105,10 +196,15 @@ if [ "$CONFIG_ONLY" = false ]; then
chmod 755 "$BIN_DIR/core" chmod 755 "$BIN_DIR/core"
success "Installed binary to $BIN_DIR/core" success "Installed binary to $BIN_DIR/core"
else else
error "Binary not found. Run without --skip-build or build manually." error "Binary not found at app/core. Run without --skip-build or build manually."
exit 1 exit 1
fi 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 # Copy modules if any exist
if [ -d "$REPO_DIR/app/modules" ] && [ "$(ls -A "$REPO_DIR/app/modules" 2>/dev/null)" ]; then if [ -d "$REPO_DIR/app/modules" ] && [ "$(ls -A "$REPO_DIR/app/modules" 2>/dev/null)" ]; then
cp -r "$REPO_DIR/app/modules"/* "$MODULES_DIR/" cp -r "$REPO_DIR/app/modules"/* "$MODULES_DIR/"
@ -120,7 +216,6 @@ fi
info "Generating configuration..." info "Generating configuration..."
if [ ! -f "$CONFIG_FILE" ]; then if [ ! -f "$CONFIG_FILE" ]; then
# Generate unique secrets
ADMIN_SECRET=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | xxd -p -c 32) 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) SESSION_SECRET=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | xxd -p -c 32)
@ -164,8 +259,10 @@ CONFIGEOF
chmod 600 "$CONFIG_FILE" chmod 600 "$CONFIG_FILE"
success "Configuration generated: $CONFIG_FILE" success "Configuration generated: $CONFIG_FILE"
warn "Admin secret token: ${ADMIN_SECRET}" echo ""
warn "Save this token! You need it for admin API access." warn " Admin Secret Token: ${ADMIN_SECRET}"
warn " Store this securely! Required for admin API access."
echo ""
else else
info "Configuration already exists at $CONFIG_FILE (not overwritten)" info "Configuration already exists at $CONFIG_FILE (not overwritten)"
fi fi
@ -190,15 +287,44 @@ RestartSec=5
StandardOutput=journal StandardOutput=journal
StandardError=journal StandardError=journal
# Security hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=${DATA_DIR} ${MODULES_DIR}
ReadOnlyPaths=${INSTALL_DIR}/config.yaml ${INSTALL_DIR}/static
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
SERVICEEOF SERVICEEOF
systemctl daemon-reload systemctl daemon-reload
success "Systemd service created: $SERVICE_FILE" success "Systemd service created: $SERVICE_FILE"
info "Enable with: systemctl enable --now nextwks" fi
info "Start with: systemctl start nextwks"
info "Status with: systemctl status nextwks" # --- 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
kill $SMOKE_PID 2>/dev/null || true
wait $SMOKE_PID 2>/dev/null || true
fi
fi fi
# --- Summary --- # --- Summary ---
@ -210,11 +336,20 @@ echo ""
info " Binary: ${BIN_DIR}/core" info " Binary: ${BIN_DIR}/core"
info " Config: ${CONFIG_FILE}" info " Config: ${CONFIG_FILE}"
info " Data: ${DATA_DIR}/" info " Data: ${DATA_DIR}/"
info " Service: nextwks" info " Static assets: ${STATIC_DIR}/"
info " Service: systemctl start nextwks"
echo "" echo ""
info " Workspace: https://wks.lohmar.co.uk/"
info " Admin UI: http://localhost:8080/admin" info " Admin UI: http://localhost:8080/admin"
info " Health API: http://localhost:8080/api/health" info " Health API: http://localhost:8080/api/health"
info " Auth status: http://localhost:8080/auth/status"
echo "" echo ""
info " Start with: systemctl start nextwks" info " Start: systemctl enable --now nextwks"
info " Logs: journalctl -u nextwks -f" 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 "" echo ""

View file

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"syscall" "syscall"
"git.lohmar.co.uk/lexton-it/NextWks/core/admin" "git.lohmar.co.uk/lexton-it/NextWks/core/admin"
@ -74,7 +75,12 @@ func main() {
oidcHandler := auth.NewOIDCHandler(oidcCfg, sessionStore) oidcHandler := auth.NewOIDCHandler(oidcCfg, sessionStore)
// Initialize launcher UI handler // Initialize launcher UI handler
uiHandler := ui.NewHandler(".") // appDir is the directory containing config.yaml (and static/ subdir)
appDir := filepath.Dir(*configPath)
if appDir == "." {
appDir = "./"
}
uiHandler := ui.NewHandler(appDir)
// Setup HTTP router // Setup HTTP router
mux := http.NewServeMux() mux := http.NewServeMux()