From 5f9dec740c7315b9368852f5c9f6938aefeaaafe Mon Sep 17 00:00:00 2001 From: cclohmar Date: Sun, 14 Jun 2026 13:24:48 +0000 Subject: [PATCH] build(core): polish installer with smoke test, status check, and production-ready systemd service --- install.sh | 203 +++++++++++++++++++++++++++++++++++++++++++--------- src/main.go | 8 ++- 2 files changed, 176 insertions(+), 35 deletions(-) diff --git a/install.sh b/install.sh index 48c0223..2214a44 100755 --- a/install.sh +++ b/install.sh @@ -3,10 +3,13 @@ # 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 --config-only # Generate config only -# ./install.sh --uninstall # Remove installation +# ./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 @@ -17,10 +20,10 @@ YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' -error() { echo -e "${RED}Error:${NC} $1" >&2; } +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"; } +info() { echo -e "${BLUE}$1${NC}"; } +warn() { echo -e "${YELLOW}Warning:${NC} $1"; } # Configuration REPO_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -28,23 +31,85 @@ 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 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 ;; - *) error "Unknown argument: $arg"; exit 1 ;; + --uninstall) UNINSTALL=true ;; + --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 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..." @@ -68,28 +133,54 @@ if [ "$UNINSTALL" = true ]; then exit 0 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 warn "Authelia is not installed at /opt/authelia/" - warn "User management requires Authelia to function." - warn "Install with: bash scripts/install-authelia.sh" + 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..." - if ! command -v go &>/dev/null; then - error "Go is not installed. Install Go 1.22+ first." - exit 1 - fi - 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" . success "Core binary built: app/core" + + if [ "$BUILD_ONLY" = true ]; then + success "Build complete (--build-only, skipping install)" + exit 0 + fi fi # --- Install --- @@ -97,7 +188,7 @@ if [ "$CONFIG_ONLY" = false ]; then info "Installing to $INSTALL_DIR..." # Create directory structure - mkdir -p "$BIN_DIR" "$DATA_DIR" "$MODULES_DIR" + mkdir -p "$BIN_DIR" "$DATA_DIR" "$MODULES_DIR" "$STATIC_DIR" # Copy binary if [ -f "$REPO_DIR/app/core" ]; then @@ -105,10 +196,15 @@ if [ "$CONFIG_ONLY" = false ]; then chmod 755 "$BIN_DIR/core" success "Installed binary to $BIN_DIR/core" 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 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/" @@ -120,7 +216,6 @@ fi info "Generating configuration..." 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) 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" success "Configuration generated: $CONFIG_FILE" - warn "Admin secret token: ${ADMIN_SECRET}" - warn "Save this token! You need it for admin API access." + 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 @@ -190,15 +287,44 @@ RestartSec=5 StandardOutput=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] WantedBy=multi-user.target SERVICEEOF systemctl daemon-reload success "Systemd service created: $SERVICE_FILE" - info "Enable with: systemctl enable --now nextwks" - info "Start with: systemctl start nextwks" - info "Status with: systemctl status nextwks" +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 + + kill $SMOKE_PID 2>/dev/null || true + wait $SMOKE_PID 2>/dev/null || true + fi fi # --- Summary --- @@ -207,14 +333,23 @@ success "═══════════════════════ success " Next Workspace (NextWks) installed!" success "═══════════════════════════════════════════" echo "" -info " Binary: ${BIN_DIR}/core" -info " Config: ${CONFIG_FILE}" -info " Data: ${DATA_DIR}/" -info " Service: nextwks" +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 " Admin UI: http://localhost:8080/admin" -info " Health API: http://localhost:8080/api/health" +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 with: systemctl start nextwks" -info " Logs: journalctl -u nextwks -f" +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 "" diff --git a/src/main.go b/src/main.go index aace6af..5035209 100644 --- a/src/main.go +++ b/src/main.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "syscall" "git.lohmar.co.uk/lexton-it/NextWks/core/admin" @@ -74,7 +75,12 @@ func main() { oidcHandler := auth.NewOIDCHandler(oidcCfg, sessionStore) // 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 mux := http.NewServeMux()