build(core): polish installer with smoke test, status check, and production-ready systemd service
This commit is contained in:
parent
926c25018e
commit
5f9dec740c
2 changed files with 176 additions and 35 deletions
203
install.sh
203
install.sh
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -17,10 +20,10 @@ YELLOW='\033[1;33m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m'
|
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}"; }
|
success() { echo -e "${GREEN}$1${NC}"; }
|
||||||
info() { echo -e "${BLUE}$1${NC}"; }
|
info() { echo -e "${BLUE}$1${NC}"; }
|
||||||
warn() { echo -e "${YELLOW}Warning:${NC} $1"; }
|
warn() { echo -e "${YELLOW}Warning:${NC} $1"; }
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
@ -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 ---
|
||||||
|
|
@ -207,14 +333,23 @@ success "═══════════════════════
|
||||||
success " Next Workspace (NextWks) installed!"
|
success " Next Workspace (NextWks) installed!"
|
||||||
success "═══════════════════════════════════════════"
|
success "═══════════════════════════════════════════"
|
||||||
echo ""
|
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 " Admin UI: http://localhost:8080/admin"
|
info " Workspace: https://wks.lohmar.co.uk/"
|
||||||
info " Health API: http://localhost:8080/api/health"
|
info " Admin UI: http://localhost:8080/admin"
|
||||||
|
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 ""
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue