All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Scripts: - common.sh: Fix select_option to use /dev/tty for interactive prompts - common.sh: Improve check_docker with detailed error messages - setup.sh: Add Traefik configuration options - setup.sh: Add argument validation for --mode, --external-authentik, etc. - setup.sh: Add fun taglines QA Reports: - Remove stale remediation reports - Keep current pending reports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2155 lines
63 KiB
Bash
Executable File
2155 lines
63 KiB
Bash
Executable File
#!/bin/bash
|
|
# Mosaic Stack Setup Wizard
|
|
# Interactive installer for Mosaic Stack personal assistant platform
|
|
# Supports Docker and native deployments
|
|
|
|
set -e
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
# Set up logging
|
|
LOG_DIR="$PROJECT_ROOT/logs"
|
|
mkdir -p "$LOG_DIR"
|
|
LOG_FILE="$LOG_DIR/setup-$(date +%Y%m%d_%H%M%S).log"
|
|
|
|
# Redirect stdout/stderr to both console and log file
|
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
|
|
|
# Send trace output (set -x) ONLY to log file on fd 3
|
|
exec 3>>"$LOG_FILE"
|
|
export BASH_XTRACEFD=3
|
|
|
|
# Enable verbose command tracing (only goes to log file now)
|
|
set -x
|
|
|
|
echo "==================================================================="
|
|
echo "Mosaic Stack Setup Wizard"
|
|
echo "Started: $(date)"
|
|
echo "Full log: $LOG_FILE"
|
|
echo "==================================================================="
|
|
echo ""
|
|
|
|
# Source common functions
|
|
# shellcheck source=lib/common.sh
|
|
source "$SCRIPT_DIR/lib/common.sh"
|
|
|
|
# ============================================================================
|
|
# Global Variables
|
|
# ============================================================================
|
|
|
|
NON_INTERACTIVE=false
|
|
DRY_RUN=false
|
|
MODE=""
|
|
ENABLE_SSO=false
|
|
USE_BUNDLED_AUTHENTIK=false
|
|
EXTERNAL_AUTHENTIK_URL=""
|
|
OLLAMA_MODE="disabled"
|
|
OLLAMA_URL=""
|
|
ENABLE_MOLTBOT=false
|
|
MOSAIC_BASE_URL=""
|
|
API_BASE_URL=""
|
|
AUTHENTIK_PUBLIC_URL=""
|
|
|
|
BASE_URL_MODE=""
|
|
BASE_URL_SCHEME="http"
|
|
BASE_URL_HOST=""
|
|
BASE_URL_PORT=""
|
|
|
|
TRAEFIK_MODE="none"
|
|
TRAEFIK_ENABLE=false
|
|
TRAEFIK_TLS_ENABLED=true
|
|
TRAEFIK_ENTRYPOINT="websecure"
|
|
TRAEFIK_NETWORK=""
|
|
TRAEFIK_DOCKER_NETWORK="mosaic-public"
|
|
TRAEFIK_ACME_EMAIL=""
|
|
TRAEFIK_CERTRESOLVER=""
|
|
TRAEFIK_DASHBOARD_ENABLED=true
|
|
TRAEFIK_DASHBOARD_PORT=""
|
|
|
|
MOSAIC_WEB_DOMAIN=""
|
|
MOSAIC_API_DOMAIN=""
|
|
MOSAIC_AUTH_DOMAIN=""
|
|
|
|
WEB_PORT=""
|
|
API_PORT=""
|
|
POSTGRES_PORT=""
|
|
VALKEY_PORT=""
|
|
AUTHENTIK_PORT_HTTP=""
|
|
AUTHENTIK_PORT_HTTPS=""
|
|
OLLAMA_PORT=""
|
|
TRAEFIK_HTTP_PORT=""
|
|
TRAEFIK_HTTPS_PORT=""
|
|
|
|
DETECTED_OS=""
|
|
DETECTED_PKG_MANAGER=""
|
|
PORT_OVERRIDES=()
|
|
|
|
# ============================================================================
|
|
# Taglines (OpenClaw-inspired flavor)
|
|
# ============================================================================
|
|
|
|
TAGLINES=(
|
|
"Claws out, configs in — let's ship a calm, clean stack."
|
|
"Less yak-shaving, more uptime."
|
|
"Turnkey today, productive tonight."
|
|
"Ports resolved. Secrets sealed. Stack ready."
|
|
"All signal, no ceremony."
|
|
"Your .env is safe with me."
|
|
)
|
|
|
|
pick_tagline() {
|
|
local idx=$((RANDOM % ${#TAGLINES[@]}))
|
|
echo "${TAGLINES[$idx]}"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Help and Usage
|
|
# ============================================================================
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Mosaic Stack Setup Wizard
|
|
|
|
USAGE:
|
|
$0 [OPTIONS]
|
|
|
|
OPTIONS:
|
|
-h, --help Show this help message
|
|
--non-interactive Run in non-interactive mode (requires all options)
|
|
--dry-run Show what would happen without executing
|
|
--mode MODE Deployment mode: docker or native
|
|
--enable-sso Enable Authentik SSO (Docker mode only)
|
|
--bundled-authentik Use bundled Authentik server (with --enable-sso)
|
|
--external-authentik URL Use external Authentik server URL (with --enable-sso)
|
|
--ollama-mode MODE Ollama mode: local, remote, disabled (default: disabled)
|
|
--ollama-url URL Ollama server URL (if --ollama-mode remote)
|
|
--enable-moltbot Enable MoltBot integration
|
|
--base-url URL Mosaic base URL (e.g., https://mosaic.example.com)
|
|
|
|
EXAMPLES:
|
|
# Interactive mode (recommended for first-time setup)
|
|
$0
|
|
|
|
# Non-interactive Docker deployment with bundled SSO
|
|
$0 --non-interactive --mode docker --enable-sso --bundled-authentik
|
|
|
|
# Non-interactive Docker with external Authentik and local Ollama
|
|
$0 --non-interactive --mode docker --enable-sso --external-authentik "https://auth.example.com" --ollama-mode local
|
|
|
|
# Dry run to see what would happen
|
|
$0 --dry-run --mode docker --enable-sso --bundled-authentik
|
|
|
|
EOF
|
|
}
|
|
|
|
# ============================================================================
|
|
# Argument Parsing
|
|
# ============================================================================
|
|
|
|
parse_arguments() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
--non-interactive)
|
|
NON_INTERACTIVE=true
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
;;
|
|
--mode)
|
|
if [[ -z "${2:-}" || "$2" == --* ]]; then
|
|
print_error "--mode requires a value (docker or native)"
|
|
exit 1
|
|
fi
|
|
MODE="$2"
|
|
shift
|
|
;;
|
|
--enable-sso)
|
|
ENABLE_SSO=true
|
|
;;
|
|
--bundled-authentik)
|
|
USE_BUNDLED_AUTHENTIK=true
|
|
;;
|
|
--external-authentik)
|
|
if [[ -z "${2:-}" || "$2" == --* ]]; then
|
|
print_error "--external-authentik requires a URL"
|
|
exit 1
|
|
fi
|
|
EXTERNAL_AUTHENTIK_URL="$2"
|
|
shift
|
|
;;
|
|
--ollama-mode)
|
|
if [[ -z "${2:-}" || "$2" == --* ]]; then
|
|
print_error "--ollama-mode requires a value (local, remote, or disabled)"
|
|
exit 1
|
|
fi
|
|
OLLAMA_MODE="$2"
|
|
shift
|
|
;;
|
|
--ollama-url)
|
|
if [[ -z "${2:-}" || "$2" == --* ]]; then
|
|
print_error "--ollama-url requires a URL"
|
|
exit 1
|
|
fi
|
|
OLLAMA_URL="$2"
|
|
shift
|
|
;;
|
|
--enable-moltbot)
|
|
ENABLE_MOLTBOT=true
|
|
;;
|
|
--base-url)
|
|
if [[ -z "${2:-}" || "$2" == --* ]]; then
|
|
print_error "--base-url requires a URL"
|
|
exit 1
|
|
fi
|
|
MOSAIC_BASE_URL="$2"
|
|
shift
|
|
;;
|
|
*)
|
|
print_error "Unknown option: $1"
|
|
echo "Use --help for usage information"
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Validate non-interactive mode
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
if [[ -z "$MODE" ]]; then
|
|
print_error "Non-interactive mode requires --mode"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$MODE" != "native" && "$MODE" != "docker" ]]; then
|
|
print_error "Invalid mode: $MODE (must be 'native' or 'docker')"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$OLLAMA_MODE" == "remote" && -z "$OLLAMA_URL" ]]; then
|
|
print_error "Remote Ollama mode requires --ollama-url"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" != true && -z "$EXTERNAL_AUTHENTIK_URL" ]]; then
|
|
print_error "SSO enabled in non-interactive mode requires --bundled-authentik or --external-authentik URL"
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Welcome Banner
|
|
# ============================================================================
|
|
|
|
show_banner() {
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
return
|
|
fi
|
|
|
|
cat << "EOF"
|
|
|
|
__ __ _ ____ _ _
|
|
| \/ | ___ ___ __ _(_) ___ / ___| |_ __ _ ___| | __
|
|
| |\/| |/ _ \/ __|/ _` | |/ __|\___ | __/ _` |/ __| |/ /
|
|
| | | | (_) \__ \ (_| | | (__ ___/ | || (_| | (__| <
|
|
|_| |_|\___/|___/\__,_|_|\___|____/ \__\__,_|\___|_|\_\
|
|
|
|
Multi-Tenant Personal Assistant Platform
|
|
|
|
EOF
|
|
local tagline
|
|
tagline=$(pick_tagline)
|
|
echo " $tagline"
|
|
echo ""
|
|
|
|
echo "Welcome to the Mosaic Stack Setup Wizard!"
|
|
echo ""
|
|
echo "This wizard will guide you through setting up Mosaic Stack on your system."
|
|
echo "You can choose between Docker (production) or native (development) deployment,"
|
|
echo "and configure optional features like Authentik SSO and Ollama LLM integration."
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Platform Detection
|
|
# ============================================================================
|
|
|
|
detect_platform() {
|
|
print_header "Detecting Platform"
|
|
|
|
DETECTED_OS=$(detect_os)
|
|
DETECTED_PKG_MANAGER=$(detect_package_manager "$DETECTED_OS")
|
|
|
|
local os_name
|
|
os_name=$(get_os_name "$DETECTED_OS")
|
|
|
|
if [[ "$DETECTED_OS" == "unknown" ]]; then
|
|
print_warning "Could not detect operating system"
|
|
print_info "Detected OS type: $OSTYPE"
|
|
echo ""
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
print_error "Cannot proceed in non-interactive mode on unknown OS"
|
|
exit 1
|
|
fi
|
|
if ! confirm "Continue anyway?"; then
|
|
exit 1
|
|
fi
|
|
else
|
|
print_success "Detected: $os_name ($DETECTED_PKG_MANAGER)"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Mode Selection
|
|
# ============================================================================
|
|
|
|
select_deployment_mode() {
|
|
if [[ -n "$MODE" ]]; then
|
|
print_header "Deployment Mode"
|
|
print_info "Using mode: $MODE"
|
|
return
|
|
fi
|
|
|
|
print_header "Deployment Mode"
|
|
echo ""
|
|
echo "How would you like to run Mosaic Stack?"
|
|
echo ""
|
|
echo " 1) Docker (Recommended)"
|
|
echo " - Best for production deployment"
|
|
echo " - Isolated environment with all dependencies"
|
|
echo " - Includes PostgreSQL, Valkey, all services"
|
|
echo " - Optional Authentik SSO integration"
|
|
echo " - Turnkey deployment"
|
|
echo ""
|
|
echo " 2) Native"
|
|
echo " - Best for development"
|
|
echo " - Runs directly on your system"
|
|
echo " - Requires manual dependency installation"
|
|
echo " - Easier to debug and modify"
|
|
echo ""
|
|
|
|
local selection
|
|
selection=$(select_option "Select deployment mode:" \
|
|
"Docker (production, recommended)" \
|
|
"Native (development)")
|
|
|
|
if [[ "$selection" == *"Docker"* ]]; then
|
|
MODE="docker"
|
|
else
|
|
MODE="native"
|
|
fi
|
|
|
|
print_success "Selected: $MODE mode"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Dependency Checking
|
|
# ============================================================================
|
|
|
|
check_and_install_dependencies() {
|
|
print_header "Checking Dependencies"
|
|
|
|
local missing_deps=()
|
|
local docker_status=0
|
|
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
check_docker
|
|
docker_status=$?
|
|
|
|
if [[ $docker_status -eq 1 ]]; then
|
|
# Docker not installed
|
|
missing_deps+=("Docker")
|
|
elif [[ $docker_status -ne 0 ]]; then
|
|
# Docker installed but not accessible (permission/daemon issue)
|
|
echo ""
|
|
print_error "Docker is installed but not accessible"
|
|
print_info "Please fix the issue above and run the setup script again"
|
|
exit 1
|
|
else
|
|
print_success "Docker: OK"
|
|
fi
|
|
|
|
if ! check_docker_compose; then
|
|
missing_deps+=("Docker Compose")
|
|
else
|
|
print_success "Docker Compose: OK"
|
|
fi
|
|
else
|
|
if ! check_node 18; then
|
|
missing_deps+=("Node.js 18+")
|
|
else
|
|
print_success "Node.js: OK"
|
|
fi
|
|
if ! check_pnpm; then
|
|
missing_deps+=("pnpm")
|
|
else
|
|
print_success "pnpm: OK"
|
|
fi
|
|
if ! check_postgres; then
|
|
missing_deps+=("PostgreSQL")
|
|
else
|
|
print_success "PostgreSQL: OK"
|
|
fi
|
|
fi
|
|
|
|
if [[ ${#missing_deps[@]} -eq 0 ]]; then
|
|
echo ""
|
|
print_success "All dependencies satisfied"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
print_warning "Missing dependencies:"
|
|
for dep in "${missing_deps[@]}"; do
|
|
echo " - $dep"
|
|
done
|
|
echo ""
|
|
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
print_error "Dependency check failed in non-interactive mode"
|
|
exit 1
|
|
fi
|
|
|
|
if confirm "Would you like to install missing dependencies?"; then
|
|
install_missing_dependencies
|
|
else
|
|
print_error "Cannot proceed without required dependencies"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
install_missing_dependencies() {
|
|
print_header "Installing Dependencies"
|
|
|
|
check_sudo
|
|
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
# Install Docker
|
|
if ! check_docker; then
|
|
local docker_pkg
|
|
docker_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker")
|
|
if [[ -n "$docker_pkg" ]]; then
|
|
install_package "$DETECTED_PKG_MANAGER" "$docker_pkg"
|
|
|
|
# Start Docker service
|
|
case "$DETECTED_PKG_MANAGER" in
|
|
pacman|dnf)
|
|
print_step "Starting Docker service..."
|
|
sudo systemctl enable --now docker
|
|
;;
|
|
esac
|
|
|
|
# Add user to docker group
|
|
print_step "Adding user to docker group..."
|
|
sudo usermod -aG docker "$USER"
|
|
print_warning "You may need to log out and back in for docker group membership to take effect"
|
|
print_info "Or run: newgrp docker"
|
|
fi
|
|
fi
|
|
|
|
# Install Docker Compose
|
|
if ! check_docker_compose; then
|
|
local compose_pkg
|
|
compose_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "docker-compose")
|
|
if [[ -n "$compose_pkg" ]]; then
|
|
install_package "$DETECTED_PKG_MANAGER" "$compose_pkg"
|
|
fi
|
|
fi
|
|
else
|
|
# Install Node.js
|
|
if ! check_node 18; then
|
|
local node_pkg
|
|
node_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "node")
|
|
if [[ -n "$node_pkg" ]]; then
|
|
install_package "$DETECTED_PKG_MANAGER" "$node_pkg"
|
|
fi
|
|
fi
|
|
|
|
# Install pnpm
|
|
if ! check_pnpm; then
|
|
print_step "Installing pnpm via npm..."
|
|
npm install -g pnpm
|
|
fi
|
|
|
|
# Install PostgreSQL
|
|
if ! check_postgres; then
|
|
local postgres_pkg
|
|
postgres_pkg=$(get_package_name "$DETECTED_PKG_MANAGER" "postgres")
|
|
if [[ -n "$postgres_pkg" ]]; then
|
|
install_package "$DETECTED_PKG_MANAGER" "$postgres_pkg"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Re-check dependencies
|
|
echo ""
|
|
local still_missing=false
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
if ! check_docker || ! check_docker_compose; then
|
|
still_missing=true
|
|
fi
|
|
else
|
|
if ! check_node 18 || ! check_pnpm; then
|
|
still_missing=true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$still_missing" == true ]]; then
|
|
print_error "Some dependencies are still missing after installation"
|
|
print_info "You may need to install them manually"
|
|
exit 1
|
|
else
|
|
print_success "All dependencies installed successfully"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Configuration Collection
|
|
# ============================================================================
|
|
|
|
load_existing_env() {
|
|
if [[ -f "$PROJECT_ROOT/.env" ]]; then
|
|
print_header "Detecting Existing Configuration"
|
|
parse_env_file "$PROJECT_ROOT/.env"
|
|
print_success "Found existing .env file"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# ============================================================================
|
|
# URL and Port Helpers
|
|
# ============================================================================
|
|
|
|
strip_trailing_slash() {
|
|
local value="$1"
|
|
echo "${value%/}"
|
|
}
|
|
|
|
format_url() {
|
|
local scheme="$1"
|
|
local host="$2"
|
|
local port="$3"
|
|
local always_port="${4:-false}"
|
|
|
|
if [[ -z "$host" ]]; then
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
if [[ -z "$port" ]]; then
|
|
echo "${scheme}://${host}"
|
|
return
|
|
fi
|
|
|
|
if [[ "$always_port" == true ]]; then
|
|
echo "${scheme}://${host}:${port}"
|
|
return
|
|
fi
|
|
|
|
if [[ "$scheme" == "http" && "$port" == "80" ]] || [[ "$scheme" == "https" && "$port" == "443" ]]; then
|
|
echo "${scheme}://${host}"
|
|
else
|
|
echo "${scheme}://${host}:${port}"
|
|
fi
|
|
}
|
|
|
|
bool_str() {
|
|
if [[ "$1" == true ]]; then
|
|
echo "true"
|
|
else
|
|
echo "false"
|
|
fi
|
|
}
|
|
|
|
resolve_env_value() {
|
|
local key="$1"
|
|
local fallback="$2"
|
|
local value
|
|
value=$(get_env_value "$key")
|
|
if [[ -n "$value" ]] && ! is_placeholder "$value"; then
|
|
echo "$value"
|
|
else
|
|
echo "$fallback"
|
|
fi
|
|
}
|
|
|
|
resolve_secret_value() {
|
|
local key="$1"
|
|
local generator="$2"
|
|
local length="$3"
|
|
local value
|
|
value=$(get_env_value "$key")
|
|
if [[ -n "$value" ]] && ! is_placeholder "$value"; then
|
|
echo "$value"
|
|
else
|
|
"$generator" "$length"
|
|
fi
|
|
}
|
|
|
|
derive_cookie_domain() {
|
|
local domain="$1"
|
|
if [[ "$domain" == *.* ]]; then
|
|
echo ".${domain#*.}"
|
|
else
|
|
echo ".${domain}"
|
|
fi
|
|
}
|
|
|
|
resolve_port_value() {
|
|
local key="$1"
|
|
local fallback="$2"
|
|
local value
|
|
value=$(get_env_value "$key")
|
|
if validate_port "$value"; then
|
|
echo "$value"
|
|
else
|
|
echo "$fallback"
|
|
fi
|
|
}
|
|
|
|
initialize_ports() {
|
|
WEB_PORT=$(resolve_port_value "WEB_PORT" "3000")
|
|
API_PORT=$(resolve_port_value "API_PORT" "3001")
|
|
POSTGRES_PORT=$(resolve_port_value "POSTGRES_PORT" "5432")
|
|
VALKEY_PORT=$(resolve_port_value "VALKEY_PORT" "6379")
|
|
AUTHENTIK_PORT_HTTP=$(resolve_port_value "AUTHENTIK_PORT_HTTP" "9000")
|
|
AUTHENTIK_PORT_HTTPS=$(resolve_port_value "AUTHENTIK_PORT_HTTPS" "9443")
|
|
OLLAMA_PORT=$(resolve_port_value "OLLAMA_PORT" "11434")
|
|
TRAEFIK_HTTP_PORT=$(resolve_port_value "TRAEFIK_HTTP_PORT" "80")
|
|
TRAEFIK_HTTPS_PORT=$(resolve_port_value "TRAEFIK_HTTPS_PORT" "443")
|
|
TRAEFIK_DASHBOARD_PORT=$(resolve_port_value "TRAEFIK_DASHBOARD_PORT" "8080")
|
|
}
|
|
|
|
parse_base_url() {
|
|
local url="$1"
|
|
|
|
if [[ "$url" =~ ^(https?)://([^/:]+)(:([0-9]+))?(/.*)?$ ]]; then
|
|
BASE_URL_SCHEME="${BASH_REMATCH[1]}"
|
|
BASE_URL_HOST="${BASH_REMATCH[2]}"
|
|
BASE_URL_PORT="${BASH_REMATCH[4]}"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
recalculate_urls() {
|
|
if [[ "$BASE_URL_MODE" == "traefik" ]]; then
|
|
local traefik_port=""
|
|
if [[ "$TRAEFIK_MODE" == "bundled" ]]; then
|
|
if [[ "$BASE_URL_SCHEME" == "https" ]]; then
|
|
traefik_port="$TRAEFIK_HTTPS_PORT"
|
|
else
|
|
traefik_port="$TRAEFIK_HTTP_PORT"
|
|
fi
|
|
fi
|
|
|
|
MOSAIC_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$MOSAIC_WEB_DOMAIN" "$traefik_port")
|
|
API_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$MOSAIC_API_DOMAIN" "$traefik_port")
|
|
else
|
|
local always_port=false
|
|
if [[ "$BASE_URL_MODE" == "localhost" || "$BASE_URL_MODE" == "ip" ]]; then
|
|
always_port=true
|
|
fi
|
|
MOSAIC_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$BASE_URL_HOST" "$WEB_PORT" "$always_port")
|
|
API_BASE_URL=$(format_url "$BASE_URL_SCHEME" "$BASE_URL_HOST" "$API_PORT" true)
|
|
fi
|
|
|
|
if [[ "$ENABLE_SSO" == true ]]; then
|
|
if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
if [[ "$BASE_URL_MODE" == "traefik" && -n "$MOSAIC_AUTH_DOMAIN" ]]; then
|
|
local auth_port=""
|
|
if [[ "$TRAEFIK_MODE" == "bundled" ]]; then
|
|
if [[ "$BASE_URL_SCHEME" == "https" ]]; then
|
|
auth_port="$TRAEFIK_HTTPS_PORT"
|
|
else
|
|
auth_port="$TRAEFIK_HTTP_PORT"
|
|
fi
|
|
fi
|
|
AUTHENTIK_PUBLIC_URL=$(format_url "$BASE_URL_SCHEME" "$MOSAIC_AUTH_DOMAIN" "$auth_port")
|
|
else
|
|
AUTHENTIK_PUBLIC_URL="http://localhost:${AUTHENTIK_PORT_HTTP}"
|
|
fi
|
|
else
|
|
AUTHENTIK_PUBLIC_URL="$EXTERNAL_AUTHENTIK_URL"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
port_label() {
|
|
local key="$1"
|
|
case "$key" in
|
|
WEB_PORT) echo "Web UI" ;;
|
|
API_PORT) echo "API" ;;
|
|
POSTGRES_PORT) echo "PostgreSQL" ;;
|
|
VALKEY_PORT) echo "Valkey" ;;
|
|
AUTHENTIK_PORT_HTTP) echo "Authentik HTTP" ;;
|
|
AUTHENTIK_PORT_HTTPS) echo "Authentik HTTPS" ;;
|
|
OLLAMA_PORT) echo "Ollama" ;;
|
|
TRAEFIK_HTTP_PORT) echo "Traefik HTTP" ;;
|
|
TRAEFIK_HTTPS_PORT) echo "Traefik HTTPS" ;;
|
|
TRAEFIK_DASHBOARD_PORT) echo "Traefik Dashboard" ;;
|
|
*) echo "$key" ;;
|
|
esac
|
|
}
|
|
|
|
suggest_port_for_key() {
|
|
local key="$1"
|
|
local port="$2"
|
|
|
|
case "$key" in
|
|
TRAEFIK_HTTP_PORT)
|
|
if ! check_port_in_use 8080; then
|
|
echo "8080"
|
|
return 0
|
|
fi
|
|
;;
|
|
TRAEFIK_HTTPS_PORT)
|
|
if ! check_port_in_use 8443; then
|
|
echo "8443"
|
|
return 0
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
suggest_alternative_port "$port"
|
|
}
|
|
|
|
apply_port_override() {
|
|
local key="$1"
|
|
local value="$2"
|
|
|
|
case "$key" in
|
|
WEB_PORT) WEB_PORT="$value" ;;
|
|
API_PORT) API_PORT="$value" ;;
|
|
POSTGRES_PORT) POSTGRES_PORT="$value" ;;
|
|
VALKEY_PORT) VALKEY_PORT="$value" ;;
|
|
AUTHENTIK_PORT_HTTP) AUTHENTIK_PORT_HTTP="$value" ;;
|
|
AUTHENTIK_PORT_HTTPS) AUTHENTIK_PORT_HTTPS="$value" ;;
|
|
OLLAMA_PORT) OLLAMA_PORT="$value" ;;
|
|
TRAEFIK_HTTP_PORT) TRAEFIK_HTTP_PORT="$value" ;;
|
|
TRAEFIK_HTTPS_PORT) TRAEFIK_HTTPS_PORT="$value" ;;
|
|
TRAEFIK_DASHBOARD_PORT) TRAEFIK_DASHBOARD_PORT="$value" ;;
|
|
esac
|
|
|
|
set_env_value "$key" "$value"
|
|
}
|
|
|
|
apply_port_overrides() {
|
|
if [[ ${#PORT_OVERRIDES[@]} -eq 0 ]]; then
|
|
return
|
|
fi
|
|
|
|
for override in "${PORT_OVERRIDES[@]}"; do
|
|
local key="${override%%=*}"
|
|
local value="${override#*=}"
|
|
apply_port_override "$key" "$value"
|
|
done
|
|
|
|
recalculate_urls
|
|
}
|
|
|
|
resolve_port_conflicts() {
|
|
if [[ "$MODE" != "docker" ]]; then
|
|
return
|
|
fi
|
|
|
|
print_header "Port Check"
|
|
|
|
local port_entries=()
|
|
port_entries+=("WEB_PORT=$WEB_PORT")
|
|
port_entries+=("API_PORT=$API_PORT")
|
|
port_entries+=("POSTGRES_PORT=$POSTGRES_PORT")
|
|
port_entries+=("VALKEY_PORT=$VALKEY_PORT")
|
|
|
|
if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
port_entries+=("AUTHENTIK_PORT_HTTP=$AUTHENTIK_PORT_HTTP")
|
|
port_entries+=("AUTHENTIK_PORT_HTTPS=$AUTHENTIK_PORT_HTTPS")
|
|
fi
|
|
|
|
if [[ "$OLLAMA_MODE" == "local" ]]; then
|
|
port_entries+=("OLLAMA_PORT=$OLLAMA_PORT")
|
|
fi
|
|
|
|
if [[ "$TRAEFIK_MODE" == "bundled" ]]; then
|
|
port_entries+=("TRAEFIK_HTTP_PORT=$TRAEFIK_HTTP_PORT")
|
|
port_entries+=("TRAEFIK_HTTPS_PORT=$TRAEFIK_HTTPS_PORT")
|
|
if [[ "$TRAEFIK_DASHBOARD_ENABLED" == true ]]; then
|
|
port_entries+=("TRAEFIK_DASHBOARD_PORT=$TRAEFIK_DASHBOARD_PORT")
|
|
fi
|
|
fi
|
|
|
|
local conflicts=()
|
|
local suggestions=()
|
|
|
|
for entry in "${port_entries[@]}"; do
|
|
local key="${entry%%=*}"
|
|
local port="${entry#*=}"
|
|
if check_port_in_use "$port"; then
|
|
local suggested
|
|
suggested=$(suggest_port_for_key "$key" "$port")
|
|
conflicts+=("${key}=${port}")
|
|
suggestions+=("${key}=${suggested}")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#conflicts[@]} -eq 0 ]]; then
|
|
print_success "No port conflicts detected"
|
|
return
|
|
fi
|
|
|
|
echo ""
|
|
print_warning "Port conflicts detected:"
|
|
for conflict in "${conflicts[@]}"; do
|
|
local key="${conflict%%=*}"
|
|
local port="${conflict#*=}"
|
|
echo " - $(port_label "$key"): $port"
|
|
done
|
|
echo ""
|
|
print_info "Suggested alternatives:"
|
|
for suggestion in "${suggestions[@]}"; do
|
|
local key="${suggestion%%=*}"
|
|
local port="${suggestion#*=}"
|
|
echo " - $(port_label "$key"): $port"
|
|
done
|
|
echo ""
|
|
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
PORT_OVERRIDES=("${suggestions[@]}")
|
|
print_success "Applying alternative ports automatically (non-interactive)"
|
|
apply_port_overrides
|
|
return
|
|
fi
|
|
|
|
if confirm "Apply suggested ports automatically?" "y"; then
|
|
PORT_OVERRIDES=("${suggestions[@]}")
|
|
apply_port_overrides
|
|
print_success "Alternative ports applied"
|
|
else
|
|
print_error "Port conflicts must be resolved to continue"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
collect_configuration() {
|
|
print_header "Configuration"
|
|
|
|
# Try to load existing .env
|
|
local has_existing_env=false
|
|
if load_existing_env; then
|
|
has_existing_env=true
|
|
echo ""
|
|
print_info "Found existing configuration. I'll ask about each setting."
|
|
echo ""
|
|
fi
|
|
|
|
initialize_ports
|
|
|
|
# Base URL Configuration
|
|
configure_base_url
|
|
|
|
# SSO Configuration (Docker mode only)
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
configure_sso
|
|
fi
|
|
|
|
# Ollama Configuration
|
|
configure_ollama
|
|
|
|
# MoltBot Configuration
|
|
configure_moltbot
|
|
|
|
# Generate secrets if needed
|
|
configure_secrets
|
|
}
|
|
|
|
configure_base_url() {
|
|
echo ""
|
|
print_step "Base URL Configuration"
|
|
echo ""
|
|
|
|
local current_url
|
|
current_url=$(get_env_value "MOSAIC_BASE_URL")
|
|
|
|
if [[ -n "$current_url" ]] && ! is_placeholder "$current_url"; then
|
|
echo "Current base URL: $current_url"
|
|
if [[ "$NON_INTERACTIVE" == true ]] || ! confirm "Change base URL?" "n"; then
|
|
if parse_base_url "$current_url"; then
|
|
BASE_URL_MODE="custom"
|
|
if [[ -n "$BASE_URL_PORT" ]]; then
|
|
WEB_PORT="$BASE_URL_PORT"
|
|
else
|
|
if [[ "$BASE_URL_SCHEME" == "https" ]]; then
|
|
WEB_PORT="443"
|
|
else
|
|
WEB_PORT="80"
|
|
fi
|
|
fi
|
|
recalculate_urls
|
|
return
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$MOSAIC_BASE_URL" ]]; then
|
|
# Already set via CLI argument
|
|
print_info "Using base URL from command line: $MOSAIC_BASE_URL"
|
|
if ! parse_base_url "$MOSAIC_BASE_URL"; then
|
|
print_error "Invalid base URL format. Expected: http(s)://host[:port]"
|
|
exit 1
|
|
fi
|
|
BASE_URL_MODE="custom"
|
|
if [[ -n "$BASE_URL_PORT" ]]; then
|
|
WEB_PORT="$BASE_URL_PORT"
|
|
else
|
|
if [[ "$BASE_URL_SCHEME" == "https" ]]; then
|
|
WEB_PORT="443"
|
|
else
|
|
WEB_PORT="80"
|
|
fi
|
|
fi
|
|
recalculate_urls
|
|
return
|
|
fi
|
|
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
BASE_URL_MODE="localhost"
|
|
BASE_URL_SCHEME="http"
|
|
BASE_URL_HOST="localhost"
|
|
BASE_URL_PORT="$WEB_PORT"
|
|
recalculate_urls
|
|
print_info "Non-interactive mode: using $MOSAIC_BASE_URL"
|
|
return
|
|
fi
|
|
|
|
echo "How will Mosaic Stack be accessed?"
|
|
echo ""
|
|
|
|
local options=(
|
|
"Localhost (recommended for local install)"
|
|
"Local network IP"
|
|
"Custom domain (direct ports)"
|
|
)
|
|
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
options+=("Traefik reverse proxy (bundled or existing)")
|
|
fi
|
|
|
|
local selection
|
|
selection=$(select_option "Select access method:" "${options[@]}")
|
|
|
|
case "$selection" in
|
|
*"Localhost"*)
|
|
configure_localhost_url
|
|
;;
|
|
*"Local network IP"*)
|
|
configure_ip_url
|
|
;;
|
|
*"Custom domain"*)
|
|
configure_dns_url
|
|
;;
|
|
*"Traefik"*)
|
|
configure_traefik_url
|
|
;;
|
|
esac
|
|
}
|
|
|
|
configure_dns_url() {
|
|
echo ""
|
|
BASE_URL_MODE="domain"
|
|
|
|
local domain
|
|
while true; do
|
|
read -r -p "Enter your domain (e.g., mosaic.example.com): " domain
|
|
if validate_domain "$domain"; then
|
|
break
|
|
else
|
|
print_error "Invalid domain format"
|
|
fi
|
|
done
|
|
|
|
# Ask about SSL
|
|
local use_https=true
|
|
if ! confirm "Use HTTPS?" "y"; then
|
|
use_https=false
|
|
fi
|
|
|
|
if [[ "$use_https" == true ]]; then
|
|
BASE_URL_SCHEME="https"
|
|
print_warning "Consider using a reverse proxy for TLS certificates"
|
|
else
|
|
BASE_URL_SCHEME="http"
|
|
fi
|
|
|
|
BASE_URL_HOST="$domain"
|
|
|
|
local default_port="$WEB_PORT"
|
|
local port
|
|
while true; do
|
|
read -r -p "Enter web port [$default_port]: " port
|
|
port=${port:-$default_port}
|
|
if validate_port "$port"; then
|
|
break
|
|
else
|
|
print_error "Invalid port number"
|
|
fi
|
|
done
|
|
|
|
WEB_PORT="$port"
|
|
BASE_URL_PORT="$WEB_PORT"
|
|
recalculate_urls
|
|
print_success "Base URL set to: $MOSAIC_BASE_URL"
|
|
}
|
|
|
|
configure_localhost_url() {
|
|
echo ""
|
|
BASE_URL_MODE="localhost"
|
|
BASE_URL_SCHEME="http"
|
|
BASE_URL_HOST="localhost"
|
|
|
|
local default_port="$WEB_PORT"
|
|
|
|
local port
|
|
while true; do
|
|
read -r -p "Enter web port [$default_port]: " port
|
|
port=${port:-$default_port}
|
|
if validate_port "$port"; then
|
|
break
|
|
else
|
|
print_error "Invalid port number"
|
|
fi
|
|
done
|
|
|
|
WEB_PORT="$port"
|
|
BASE_URL_PORT="$WEB_PORT"
|
|
recalculate_urls
|
|
print_success "Base URL set to: $MOSAIC_BASE_URL"
|
|
}
|
|
|
|
configure_ip_url() {
|
|
echo ""
|
|
BASE_URL_MODE="ip"
|
|
BASE_URL_SCHEME="http"
|
|
|
|
# Try to detect local IP
|
|
local detected_ip
|
|
if command -v ip >/dev/null 2>&1; then
|
|
detected_ip=$(ip route get 1 2>/dev/null | awk '{print $7; exit}')
|
|
elif command -v ifconfig >/dev/null 2>&1; then
|
|
detected_ip=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' | head -n1)
|
|
fi
|
|
|
|
if [[ -n "$detected_ip" ]]; then
|
|
print_info "Detected IP: $detected_ip"
|
|
fi
|
|
|
|
local ip_addr
|
|
while true; do
|
|
read -r -p "Enter IP address${detected_ip:+ [$detected_ip]}: " ip_addr
|
|
ip_addr=${ip_addr:-$detected_ip}
|
|
if validate_ipv4 "$ip_addr"; then
|
|
break
|
|
else
|
|
print_error "Invalid IP address (must be 0.0.0.0 - 255.255.255.255)"
|
|
fi
|
|
done
|
|
|
|
BASE_URL_HOST="$ip_addr"
|
|
|
|
local default_port="$WEB_PORT"
|
|
local port
|
|
while true; do
|
|
read -r -p "Enter web port [$default_port]: " port
|
|
port=${port:-$default_port}
|
|
if validate_port "$port"; then
|
|
break
|
|
else
|
|
print_error "Invalid port number"
|
|
fi
|
|
done
|
|
|
|
WEB_PORT="$port"
|
|
BASE_URL_PORT="$WEB_PORT"
|
|
recalculate_urls
|
|
print_success "Base URL set to: $MOSAIC_BASE_URL"
|
|
}
|
|
|
|
configure_traefik_url() {
|
|
echo ""
|
|
print_info "Configuring Traefik integration"
|
|
echo ""
|
|
|
|
local mode_selection
|
|
mode_selection=$(select_option "Traefik mode:" \
|
|
"Bundled Traefik (run inside this stack)" \
|
|
"Upstream Traefik (existing instance)")
|
|
|
|
if [[ "$mode_selection" == *"Bundled"* ]]; then
|
|
TRAEFIK_MODE="bundled"
|
|
TRAEFIK_ENABLE=true
|
|
else
|
|
TRAEFIK_MODE="upstream"
|
|
TRAEFIK_ENABLE=true
|
|
fi
|
|
|
|
if confirm "Enable TLS/HTTPS for Traefik?" "y"; then
|
|
TRAEFIK_TLS_ENABLED=true
|
|
TRAEFIK_ENTRYPOINT="websecure"
|
|
BASE_URL_SCHEME="https"
|
|
else
|
|
TRAEFIK_TLS_ENABLED=false
|
|
TRAEFIK_ENTRYPOINT="web"
|
|
BASE_URL_SCHEME="http"
|
|
fi
|
|
|
|
BASE_URL_MODE="traefik"
|
|
|
|
local default_web_domain="mosaic.local"
|
|
while true; do
|
|
read -r -p "Web domain [$default_web_domain]: " MOSAIC_WEB_DOMAIN
|
|
MOSAIC_WEB_DOMAIN=${MOSAIC_WEB_DOMAIN:-$default_web_domain}
|
|
if validate_domain "$MOSAIC_WEB_DOMAIN"; then
|
|
break
|
|
fi
|
|
print_error "Invalid domain format"
|
|
done
|
|
|
|
local default_api_domain="api.${MOSAIC_WEB_DOMAIN}"
|
|
while true; do
|
|
read -r -p "API domain [$default_api_domain]: " MOSAIC_API_DOMAIN
|
|
MOSAIC_API_DOMAIN=${MOSAIC_API_DOMAIN:-$default_api_domain}
|
|
if validate_domain "$MOSAIC_API_DOMAIN"; then
|
|
break
|
|
fi
|
|
print_error "Invalid domain format"
|
|
done
|
|
|
|
if [[ "$ENABLE_SSO" == true ]]; then
|
|
local default_auth_domain="auth.${MOSAIC_WEB_DOMAIN}"
|
|
while true; do
|
|
read -r -p "Auth domain [$default_auth_domain]: " MOSAIC_AUTH_DOMAIN
|
|
MOSAIC_AUTH_DOMAIN=${MOSAIC_AUTH_DOMAIN:-$default_auth_domain}
|
|
if validate_domain "$MOSAIC_AUTH_DOMAIN"; then
|
|
break
|
|
fi
|
|
print_error "Invalid domain format"
|
|
done
|
|
fi
|
|
|
|
if [[ "$TRAEFIK_MODE" == "upstream" ]]; then
|
|
local default_network="traefik-public"
|
|
read -r -p "External Traefik network name [$default_network]: " TRAEFIK_NETWORK
|
|
TRAEFIK_NETWORK=${TRAEFIK_NETWORK:-$default_network}
|
|
TRAEFIK_DOCKER_NETWORK="$TRAEFIK_NETWORK"
|
|
fi
|
|
|
|
if [[ "$TRAEFIK_MODE" == "bundled" ]]; then
|
|
if confirm "Enable Traefik dashboard?" "y"; then
|
|
TRAEFIK_DASHBOARD_ENABLED=true
|
|
local default_dash_port="$TRAEFIK_DASHBOARD_PORT"
|
|
local dash_port
|
|
while true; do
|
|
read -r -p "Traefik dashboard port [$default_dash_port]: " dash_port
|
|
dash_port=${dash_port:-$default_dash_port}
|
|
if validate_port "$dash_port"; then
|
|
break
|
|
else
|
|
print_error "Invalid port number"
|
|
fi
|
|
done
|
|
TRAEFIK_DASHBOARD_PORT="$dash_port"
|
|
else
|
|
TRAEFIK_DASHBOARD_ENABLED=false
|
|
fi
|
|
fi
|
|
|
|
recalculate_urls
|
|
print_success "Base URL set to: $MOSAIC_BASE_URL"
|
|
print_info "Traefik domains configured for web and API"
|
|
}
|
|
|
|
configure_sso() {
|
|
echo ""
|
|
print_step "SSO Configuration (Authentik)"
|
|
echo ""
|
|
|
|
# Check if SSO already configured
|
|
local current_sso_enabled
|
|
current_sso_enabled=$(get_env_value "ENABLE_SSO")
|
|
|
|
if [[ -n "$ENABLE_SSO" ]]; then
|
|
# Already set via CLI
|
|
print_info "SSO enabled: $ENABLE_SSO"
|
|
elif [[ "$current_sso_enabled" == "true" ]]; then
|
|
echo "SSO is currently enabled"
|
|
if [[ "$NON_INTERACTIVE" == false ]] && ! confirm "Keep SSO enabled?" "y"; then
|
|
ENABLE_SSO=false
|
|
return
|
|
fi
|
|
ENABLE_SSO=true
|
|
else
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
ENABLE_SSO=false
|
|
return
|
|
fi
|
|
if ! confirm "Enable Authentik SSO?"; then
|
|
ENABLE_SSO=false
|
|
return
|
|
fi
|
|
ENABLE_SSO=true
|
|
fi
|
|
|
|
if [[ "$ENABLE_SSO" != true ]]; then
|
|
return
|
|
fi
|
|
|
|
# Bundled vs External Authentik
|
|
local current_bundled
|
|
current_bundled=$(get_env_value "USE_BUNDLED_AUTHENTIK")
|
|
|
|
if [[ -n "$USE_BUNDLED_AUTHENTIK" ]]; then
|
|
# Already set via CLI
|
|
print_info "Using bundled Authentik: $USE_BUNDLED_AUTHENTIK"
|
|
elif [[ "$current_bundled" == "true" ]]; then
|
|
echo "Currently using bundled Authentik"
|
|
if [[ "$NON_INTERACTIVE" == false ]] && ! confirm "Keep using bundled Authentik?" "y"; then
|
|
USE_BUNDLED_AUTHENTIK=false
|
|
else
|
|
USE_BUNDLED_AUTHENTIK=true
|
|
fi
|
|
else
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
USE_BUNDLED_AUTHENTIK=false
|
|
elif confirm "Use bundled Authentik server?" "y"; then
|
|
USE_BUNDLED_AUTHENTIK=true
|
|
else
|
|
USE_BUNDLED_AUTHENTIK=false
|
|
fi
|
|
fi
|
|
|
|
# If external, get URL
|
|
if [[ "$USE_BUNDLED_AUTHENTIK" != true ]]; then
|
|
local current_auth_url
|
|
current_auth_url=$(get_env_value "AUTHENTIK_BASE_URL")
|
|
|
|
if [[ -n "$EXTERNAL_AUTHENTIK_URL" ]]; then
|
|
EXTERNAL_AUTHENTIK_URL=$(strip_trailing_slash "$EXTERNAL_AUTHENTIK_URL")
|
|
elif [[ -n "$current_auth_url" ]] && ! is_placeholder "$current_auth_url"; then
|
|
echo "Current Authentik URL: $current_auth_url"
|
|
if [[ "$NON_INTERACTIVE" == true ]] || ! confirm "Change Authentik URL?" "n"; then
|
|
EXTERNAL_AUTHENTIK_URL=$(strip_trailing_slash "$current_auth_url")
|
|
else
|
|
read_authentik_url
|
|
fi
|
|
else
|
|
read_authentik_url
|
|
fi
|
|
|
|
if [[ -z "$EXTERNAL_AUTHENTIK_URL" ]]; then
|
|
print_error "External Authentik URL is required when not using bundled Authentik"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
if [[ "$BASE_URL_MODE" == "traefik" && -z "$MOSAIC_AUTH_DOMAIN" ]]; then
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
MOSAIC_AUTH_DOMAIN="auth.${MOSAIC_WEB_DOMAIN:-mosaic.local}"
|
|
else
|
|
local default_auth_domain="auth.${MOSAIC_WEB_DOMAIN:-mosaic.local}"
|
|
read -r -p "Auth domain [$default_auth_domain]: " MOSAIC_AUTH_DOMAIN
|
|
MOSAIC_AUTH_DOMAIN=${MOSAIC_AUTH_DOMAIN:-$default_auth_domain}
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
recalculate_urls
|
|
}
|
|
|
|
read_authentik_url() {
|
|
while true; do
|
|
read -r -p "Enter external Authentik URL: " EXTERNAL_AUTHENTIK_URL
|
|
if validate_url "$EXTERNAL_AUTHENTIK_URL"; then
|
|
EXTERNAL_AUTHENTIK_URL=$(strip_trailing_slash "$EXTERNAL_AUTHENTIK_URL")
|
|
break
|
|
else
|
|
print_error "Invalid URL format"
|
|
fi
|
|
done
|
|
}
|
|
|
|
configure_ollama() {
|
|
echo ""
|
|
print_step "Ollama Configuration"
|
|
echo ""
|
|
|
|
local current_mode
|
|
current_mode=$(get_env_value "OLLAMA_MODE")
|
|
|
|
if [[ -n "$OLLAMA_MODE" ]] && [[ "$OLLAMA_MODE" != "disabled" ]]; then
|
|
# Already set via CLI
|
|
print_info "Ollama mode: $OLLAMA_MODE"
|
|
elif [[ -n "$current_mode" ]] && [[ "$current_mode" != "disabled" ]]; then
|
|
echo "Current Ollama mode: $current_mode"
|
|
if [[ "$NON_INTERACTIVE" == false ]] && confirm "Change Ollama configuration?" "n"; then
|
|
select_ollama_mode
|
|
else
|
|
OLLAMA_MODE="$current_mode"
|
|
fi
|
|
else
|
|
select_ollama_mode
|
|
fi
|
|
|
|
# Get Ollama URL if remote mode
|
|
if [[ "$OLLAMA_MODE" == "remote" ]]; then
|
|
local current_url
|
|
current_url=$(get_env_value "OLLAMA_ENDPOINT")
|
|
|
|
if [[ -n "$OLLAMA_URL" ]]; then
|
|
# Already set via CLI
|
|
print_info "Ollama URL: $OLLAMA_URL"
|
|
elif [[ -n "$current_url" ]] && ! is_placeholder "$current_url"; then
|
|
echo "Current Ollama URL: $current_url"
|
|
if [[ "$NON_INTERACTIVE" == false ]] && confirm "Change Ollama URL?" "n"; then
|
|
read_ollama_url
|
|
else
|
|
OLLAMA_URL="$current_url"
|
|
fi
|
|
else
|
|
read_ollama_url
|
|
fi
|
|
fi
|
|
}
|
|
|
|
select_ollama_mode() {
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
OLLAMA_MODE="disabled"
|
|
return
|
|
fi
|
|
|
|
local selection
|
|
selection=$(select_option "Select Ollama mode:" \
|
|
"Disabled (no local LLM)" \
|
|
"Local (run Ollama in Docker)" \
|
|
"Remote (connect to existing Ollama server)")
|
|
|
|
case "$selection" in
|
|
*"Disabled"*)
|
|
OLLAMA_MODE="disabled"
|
|
;;
|
|
*"Local"*)
|
|
OLLAMA_MODE="local"
|
|
;;
|
|
*"Remote"*)
|
|
OLLAMA_MODE="remote"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
read_ollama_url() {
|
|
while true; do
|
|
read -r -p "Enter Ollama server URL: " OLLAMA_URL
|
|
if validate_url "$OLLAMA_URL"; then
|
|
break
|
|
else
|
|
print_error "Invalid URL format"
|
|
fi
|
|
done
|
|
}
|
|
|
|
configure_moltbot() {
|
|
echo ""
|
|
print_step "MoltBot Configuration"
|
|
echo ""
|
|
|
|
local current_enabled
|
|
current_enabled=$(get_env_value "ENABLE_MOLTBOT")
|
|
|
|
if [[ -n "$ENABLE_MOLTBOT" ]]; then
|
|
# Already set via CLI
|
|
print_info "MoltBot enabled: $ENABLE_MOLTBOT"
|
|
elif [[ "$current_enabled" == "true" ]]; then
|
|
echo "MoltBot is currently enabled"
|
|
if [[ "$NON_INTERACTIVE" == false ]] && ! confirm "Keep MoltBot enabled?" "y"; then
|
|
ENABLE_MOLTBOT=false
|
|
else
|
|
ENABLE_MOLTBOT=true
|
|
fi
|
|
else
|
|
if [[ "$NON_INTERACTIVE" == true ]]; then
|
|
ENABLE_MOLTBOT=false
|
|
elif confirm "Enable MoltBot integration?"; then
|
|
ENABLE_MOLTBOT=true
|
|
else
|
|
ENABLE_MOLTBOT=false
|
|
fi
|
|
fi
|
|
}
|
|
|
|
configure_secrets() {
|
|
echo ""
|
|
print_step "Secret Generation"
|
|
echo ""
|
|
|
|
# Check if secrets exist and are not placeholders
|
|
local db_password
|
|
local jwt_secret
|
|
local auth_db_password
|
|
local auth_secret_key
|
|
local auth_bootstrap_password
|
|
|
|
db_password=$(get_env_value "POSTGRES_PASSWORD")
|
|
jwt_secret=$(get_env_value "JWT_SECRET")
|
|
auth_db_password=$(get_env_value "AUTHENTIK_POSTGRES_PASSWORD")
|
|
auth_secret_key=$(get_env_value "AUTHENTIK_SECRET_KEY")
|
|
auth_bootstrap_password=$(get_env_value "AUTHENTIK_BOOTSTRAP_PASSWORD")
|
|
|
|
local needs_generation=false
|
|
|
|
if is_placeholder "$db_password"; then
|
|
print_info "Will generate database password"
|
|
needs_generation=true
|
|
fi
|
|
|
|
if is_placeholder "$jwt_secret"; then
|
|
print_info "Will generate JWT secret"
|
|
needs_generation=true
|
|
fi
|
|
|
|
if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
if is_placeholder "$auth_db_password"; then
|
|
print_info "Will generate Authentik database password"
|
|
needs_generation=true
|
|
fi
|
|
|
|
if is_placeholder "$auth_secret_key"; then
|
|
print_info "Will generate Authentik secret key"
|
|
needs_generation=true
|
|
fi
|
|
|
|
if is_placeholder "$auth_bootstrap_password"; then
|
|
print_info "Will generate Authentik bootstrap password"
|
|
needs_generation=true
|
|
fi
|
|
fi
|
|
|
|
if [[ "$needs_generation" == true ]]; then
|
|
print_success "Secrets will be generated during .env creation"
|
|
else
|
|
print_success "Existing secrets will be preserved"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# .env File Generation
|
|
# ============================================================================
|
|
|
|
declare -gA ENV_OVERRIDES
|
|
|
|
env_override_set() {
|
|
local key="$1"
|
|
local value="$2"
|
|
ENV_OVERRIDES["$key"]="$value"
|
|
}
|
|
|
|
write_env_with_overrides() {
|
|
local template="$1"
|
|
local output="$2"
|
|
|
|
local overrides_file
|
|
overrides_file=$(mktemp)
|
|
|
|
for key in "${!ENV_OVERRIDES[@]}"; do
|
|
printf '%s=%s\n' "$key" "${ENV_OVERRIDES[$key]}" >> "$overrides_file"
|
|
done
|
|
|
|
awk -v overrides_file="$overrides_file" '
|
|
BEGIN {
|
|
while ((getline < overrides_file) > 0) {
|
|
line = $0
|
|
if (line ~ /^[A-Za-z0-9_]+=*/) {
|
|
key = substr(line, 1, index(line, "=") - 1)
|
|
value = substr(line, index(line, "=") + 1)
|
|
map[key] = value
|
|
}
|
|
}
|
|
close(overrides_file)
|
|
}
|
|
{
|
|
if (match($0, /^([A-Za-z0-9_]+)=/, m)) {
|
|
key = m[1]
|
|
if (key in map) {
|
|
print key "=" map[key]
|
|
used[key] = 1
|
|
next
|
|
}
|
|
}
|
|
print
|
|
}
|
|
END {
|
|
for (key in map) {
|
|
if (!(key in used)) {
|
|
print key "=" map[key]
|
|
}
|
|
}
|
|
}
|
|
' "$template" > "$output"
|
|
|
|
rm -f "$overrides_file"
|
|
}
|
|
|
|
generate_env_file() {
|
|
print_header ".env File Generation"
|
|
|
|
# Backup existing .env if it exists
|
|
if [[ -f "$PROJECT_ROOT/.env" ]]; then
|
|
backup_file "$PROJECT_ROOT/.env"
|
|
fi
|
|
|
|
echo ""
|
|
print_step "Generating .env file with your configuration..."
|
|
echo ""
|
|
|
|
# Start with .env.example template
|
|
local template="$PROJECT_ROOT/.env.example"
|
|
local output="$PROJECT_ROOT/.env"
|
|
|
|
if [[ ! -f "$template" ]]; then
|
|
print_error ".env.example template not found"
|
|
exit 1
|
|
fi
|
|
|
|
ENV_OVERRIDES=()
|
|
|
|
for key in "${!ENV_VALUES[@]}"; do
|
|
local value="${ENV_VALUES[$key]}"
|
|
if [[ -n "$value" ]] && ! is_placeholder "$value"; then
|
|
env_override_set "$key" "$value"
|
|
fi
|
|
done
|
|
|
|
local xtrace_was_on=false
|
|
if [[ "$-" == *x* ]]; then
|
|
xtrace_was_on=true
|
|
set +x
|
|
fi
|
|
|
|
local postgres_user
|
|
local postgres_db
|
|
local db_password
|
|
local jwt_secret
|
|
local jwt_expiration
|
|
local api_host
|
|
local valkey_url
|
|
local database_url
|
|
|
|
postgres_user=$(resolve_env_value "POSTGRES_USER" "mosaic")
|
|
postgres_db=$(resolve_env_value "POSTGRES_DB" "mosaic")
|
|
api_host=$(resolve_env_value "API_HOST" "0.0.0.0")
|
|
jwt_expiration=$(resolve_env_value "JWT_EXPIRATION" "24h")
|
|
|
|
local existing_db_password
|
|
existing_db_password=$(get_env_value "POSTGRES_PASSWORD")
|
|
db_password=$(resolve_secret_value "POSTGRES_PASSWORD" generate_password 32)
|
|
if [[ -n "$existing_db_password" ]] && ! is_placeholder "$existing_db_password"; then
|
|
print_info "Preserving existing database password"
|
|
else
|
|
print_info "Generated database password"
|
|
fi
|
|
|
|
local existing_jwt_secret
|
|
existing_jwt_secret=$(get_env_value "JWT_SECRET")
|
|
jwt_secret=$(resolve_secret_value "JWT_SECRET" generate_secret 50)
|
|
if [[ -n "$existing_jwt_secret" ]] && ! is_placeholder "$existing_jwt_secret"; then
|
|
print_info "Preserving existing JWT secret"
|
|
else
|
|
print_info "Generated JWT secret"
|
|
fi
|
|
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
database_url="postgresql://${postgres_user}:${db_password}@postgres:5432/${postgres_db}"
|
|
valkey_url="redis://valkey:6379"
|
|
else
|
|
database_url="postgresql://${postgres_user}:${db_password}@localhost:${POSTGRES_PORT}/${postgres_db}"
|
|
valkey_url="redis://localhost:${VALKEY_PORT}"
|
|
fi
|
|
|
|
local authentik_db_password=""
|
|
local authentik_secret_key=""
|
|
local authentik_bootstrap_password=""
|
|
local authentik_postgres_user=""
|
|
local authentik_postgres_db=""
|
|
local authentik_bootstrap_email=""
|
|
local authentik_cookie_domain=""
|
|
|
|
if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
authentik_postgres_user=$(resolve_env_value "AUTHENTIK_POSTGRES_USER" "authentik")
|
|
authentik_postgres_db=$(resolve_env_value "AUTHENTIK_POSTGRES_DB" "authentik")
|
|
authentik_bootstrap_email=$(resolve_env_value "AUTHENTIK_BOOTSTRAP_EMAIL" "admin@localhost")
|
|
|
|
local existing_auth_db_password
|
|
existing_auth_db_password=$(get_env_value "AUTHENTIK_POSTGRES_PASSWORD")
|
|
authentik_db_password=$(resolve_secret_value "AUTHENTIK_POSTGRES_PASSWORD" generate_password 32)
|
|
if [[ -n "$existing_auth_db_password" ]] && ! is_placeholder "$existing_auth_db_password"; then
|
|
print_info "Preserving existing Authentik database password"
|
|
else
|
|
print_info "Generated Authentik database password"
|
|
fi
|
|
|
|
local existing_auth_secret
|
|
existing_auth_secret=$(get_env_value "AUTHENTIK_SECRET_KEY")
|
|
authentik_secret_key=$(resolve_secret_value "AUTHENTIK_SECRET_KEY" generate_secret 64)
|
|
if [[ -n "$existing_auth_secret" ]] && ! is_placeholder "$existing_auth_secret"; then
|
|
print_info "Preserving existing Authentik secret key"
|
|
else
|
|
print_info "Generated Authentik secret key"
|
|
fi
|
|
|
|
local existing_auth_bootstrap
|
|
existing_auth_bootstrap=$(get_env_value "AUTHENTIK_BOOTSTRAP_PASSWORD")
|
|
authentik_bootstrap_password=$(resolve_secret_value "AUTHENTIK_BOOTSTRAP_PASSWORD" generate_password 24)
|
|
if [[ -n "$existing_auth_bootstrap" ]] && ! is_placeholder "$existing_auth_bootstrap"; then
|
|
print_info "Preserving existing Authentik bootstrap password"
|
|
else
|
|
print_info "Generated Authentik bootstrap password"
|
|
fi
|
|
|
|
authentik_cookie_domain=$(resolve_env_value "AUTHENTIK_COOKIE_DOMAIN" "")
|
|
if [[ -z "$authentik_cookie_domain" ]]; then
|
|
if [[ "$BASE_URL_MODE" == "traefik" && -n "$MOSAIC_AUTH_DOMAIN" ]]; then
|
|
authentik_cookie_domain=$(derive_cookie_domain "$MOSAIC_AUTH_DOMAIN")
|
|
else
|
|
authentik_cookie_domain=".localhost"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
local oidc_issuer=""
|
|
local oidc_client_id=""
|
|
local oidc_client_secret=""
|
|
local oidc_redirect_uri=""
|
|
|
|
if [[ "$ENABLE_SSO" == true ]]; then
|
|
local auth_url
|
|
if [[ "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
auth_url=$(strip_trailing_slash "$AUTHENTIK_PUBLIC_URL")
|
|
else
|
|
auth_url=$(strip_trailing_slash "$EXTERNAL_AUTHENTIK_URL")
|
|
fi
|
|
|
|
oidc_issuer="${auth_url}/application/o/mosaic-stack/"
|
|
oidc_client_id=$(resolve_env_value "OIDC_CLIENT_ID" "mosaic-stack")
|
|
oidc_client_secret=$(resolve_env_value "OIDC_CLIENT_SECRET" "change-after-authentik-setup")
|
|
oidc_redirect_uri="${API_BASE_URL}/auth/callback"
|
|
fi
|
|
|
|
local ollama_mode_env=""
|
|
local ollama_endpoint=""
|
|
if [[ "$OLLAMA_MODE" == "local" ]]; then
|
|
ollama_mode_env="local"
|
|
ollama_endpoint="http://ollama:11434"
|
|
elif [[ "$OLLAMA_MODE" == "remote" ]]; then
|
|
ollama_mode_env="remote"
|
|
ollama_endpoint="$OLLAMA_URL"
|
|
else
|
|
ollama_mode_env="remote"
|
|
ollama_endpoint=$(resolve_env_value "OLLAMA_ENDPOINT" "http://localhost:11434")
|
|
fi
|
|
|
|
local compose_profiles=()
|
|
if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
compose_profiles+=("authentik")
|
|
fi
|
|
if [[ "$OLLAMA_MODE" == "local" ]]; then
|
|
compose_profiles+=("ollama")
|
|
fi
|
|
if [[ "$TRAEFIK_MODE" == "bundled" ]]; then
|
|
compose_profiles+=("traefik-bundled")
|
|
fi
|
|
|
|
local profiles_string=""
|
|
if [[ ${#compose_profiles[@]} -gt 0 ]]; then
|
|
profiles_string=$(IFS=,; echo "${compose_profiles[*]}")
|
|
fi
|
|
|
|
env_override_set "API_PORT" "$API_PORT"
|
|
env_override_set "API_HOST" "$api_host"
|
|
env_override_set "WEB_PORT" "$WEB_PORT"
|
|
env_override_set "POSTGRES_USER" "$postgres_user"
|
|
env_override_set "POSTGRES_PASSWORD" "$db_password"
|
|
env_override_set "POSTGRES_DB" "$postgres_db"
|
|
env_override_set "POSTGRES_PORT" "$POSTGRES_PORT"
|
|
env_override_set "DATABASE_URL" "$database_url"
|
|
env_override_set "VALKEY_URL" "$valkey_url"
|
|
env_override_set "VALKEY_PORT" "$VALKEY_PORT"
|
|
env_override_set "NEXT_PUBLIC_API_URL" "$API_BASE_URL"
|
|
if [[ -n "$MOSAIC_BASE_URL" ]]; then
|
|
env_override_set "MOSAIC_BASE_URL" "$MOSAIC_BASE_URL"
|
|
fi
|
|
env_override_set "JWT_SECRET" "$jwt_secret"
|
|
env_override_set "JWT_EXPIRATION" "$jwt_expiration"
|
|
env_override_set "OLLAMA_MODE" "$ollama_mode_env"
|
|
env_override_set "OLLAMA_ENDPOINT" "$ollama_endpoint"
|
|
|
|
if [[ -n "$profiles_string" ]]; then
|
|
env_override_set "COMPOSE_PROFILES" "$profiles_string"
|
|
else
|
|
env_override_set "COMPOSE_PROFILES" ""
|
|
fi
|
|
|
|
env_override_set "TRAEFIK_MODE" "$TRAEFIK_MODE"
|
|
env_override_set "TRAEFIK_ENABLE" "$(bool_str "$TRAEFIK_ENABLE")"
|
|
env_override_set "TRAEFIK_TLS_ENABLED" "$(bool_str "$TRAEFIK_TLS_ENABLED")"
|
|
env_override_set "TRAEFIK_ENTRYPOINT" "$TRAEFIK_ENTRYPOINT"
|
|
|
|
if [[ -n "$TRAEFIK_NETWORK" ]]; then
|
|
env_override_set "TRAEFIK_NETWORK" "$TRAEFIK_NETWORK"
|
|
fi
|
|
if [[ -n "$TRAEFIK_DOCKER_NETWORK" ]]; then
|
|
env_override_set "TRAEFIK_DOCKER_NETWORK" "$TRAEFIK_DOCKER_NETWORK"
|
|
fi
|
|
if [[ -n "$MOSAIC_WEB_DOMAIN" ]]; then
|
|
env_override_set "MOSAIC_WEB_DOMAIN" "$MOSAIC_WEB_DOMAIN"
|
|
fi
|
|
if [[ -n "$MOSAIC_API_DOMAIN" ]]; then
|
|
env_override_set "MOSAIC_API_DOMAIN" "$MOSAIC_API_DOMAIN"
|
|
fi
|
|
if [[ -n "$MOSAIC_AUTH_DOMAIN" ]]; then
|
|
env_override_set "MOSAIC_AUTH_DOMAIN" "$MOSAIC_AUTH_DOMAIN"
|
|
fi
|
|
|
|
if [[ "$TRAEFIK_MODE" == "bundled" ]]; then
|
|
env_override_set "TRAEFIK_HTTP_PORT" "$TRAEFIK_HTTP_PORT"
|
|
env_override_set "TRAEFIK_HTTPS_PORT" "$TRAEFIK_HTTPS_PORT"
|
|
env_override_set "TRAEFIK_DASHBOARD_ENABLED" "$(bool_str "$TRAEFIK_DASHBOARD_ENABLED")"
|
|
env_override_set "TRAEFIK_DASHBOARD_PORT" "$TRAEFIK_DASHBOARD_PORT"
|
|
if [[ -n "$TRAEFIK_ACME_EMAIL" ]]; then
|
|
env_override_set "TRAEFIK_ACME_EMAIL" "$TRAEFIK_ACME_EMAIL"
|
|
fi
|
|
if [[ -n "$TRAEFIK_CERTRESOLVER" ]]; then
|
|
env_override_set "TRAEFIK_CERTRESOLVER" "$TRAEFIK_CERTRESOLVER"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$ENABLE_SSO" == true ]]; then
|
|
env_override_set "OIDC_ISSUER" "$oidc_issuer"
|
|
env_override_set "OIDC_CLIENT_ID" "$oidc_client_id"
|
|
env_override_set "OIDC_CLIENT_SECRET" "$oidc_client_secret"
|
|
env_override_set "OIDC_REDIRECT_URI" "$oidc_redirect_uri"
|
|
fi
|
|
|
|
if [[ "$ENABLE_SSO" == true && "$USE_BUNDLED_AUTHENTIK" == true ]]; then
|
|
env_override_set "AUTHENTIK_POSTGRES_USER" "$authentik_postgres_user"
|
|
env_override_set "AUTHENTIK_POSTGRES_PASSWORD" "$authentik_db_password"
|
|
env_override_set "AUTHENTIK_POSTGRES_DB" "$authentik_postgres_db"
|
|
env_override_set "AUTHENTIK_SECRET_KEY" "$authentik_secret_key"
|
|
env_override_set "AUTHENTIK_BOOTSTRAP_PASSWORD" "$authentik_bootstrap_password"
|
|
env_override_set "AUTHENTIK_BOOTSTRAP_EMAIL" "$authentik_bootstrap_email"
|
|
env_override_set "AUTHENTIK_COOKIE_DOMAIN" "$authentik_cookie_domain"
|
|
env_override_set "AUTHENTIK_PORT_HTTP" "$AUTHENTIK_PORT_HTTP"
|
|
env_override_set "AUTHENTIK_PORT_HTTPS" "$AUTHENTIK_PORT_HTTPS"
|
|
fi
|
|
|
|
if [[ "$OLLAMA_MODE" == "local" ]]; then
|
|
env_override_set "OLLAMA_PORT" "$OLLAMA_PORT"
|
|
fi
|
|
|
|
write_env_with_overrides "$template" "$output"
|
|
|
|
chmod 600 "$output"
|
|
print_success "Generated .env file"
|
|
|
|
# Write credentials file
|
|
write_credentials_file "$db_password" "$authentik_bootstrap_password"
|
|
|
|
if [[ "$xtrace_was_on" == true ]]; then
|
|
set -x
|
|
fi
|
|
}
|
|
|
|
write_credentials_file() {
|
|
local db_password="$1"
|
|
local authentik_password="$2"
|
|
|
|
local creds_file="$PROJECT_ROOT/.admin-credentials"
|
|
local db_user
|
|
db_user=$(resolve_env_value "POSTGRES_USER" "mosaic")
|
|
|
|
cat > "$creds_file" << EOF
|
|
# Mosaic Stack Admin Credentials
|
|
# Generated: $(date)
|
|
# KEEP THIS FILE SECURE AND DO NOT COMMIT TO VERSION CONTROL
|
|
|
|
Database (PostgreSQL):
|
|
Username: ${db_user}
|
|
Password: $(mask_value "$db_password")
|
|
Full password available in .env as POSTGRES_PASSWORD
|
|
|
|
JWT Secret:
|
|
Available in .env as JWT_SECRET
|
|
|
|
EOF
|
|
|
|
if [[ "$ENABLE_SSO" == true && -n "$authentik_password" ]]; then
|
|
cat >> "$creds_file" << EOF
|
|
Authentik Admin:
|
|
Email: admin@localhost
|
|
Password: $(mask_value "$authentik_password")
|
|
Full password available in .env as AUTHENTIK_BOOTSTRAP_PASSWORD
|
|
URL: ${AUTHENTIK_PUBLIC_URL:-http://localhost:9000}
|
|
|
|
IMPORTANT: Change the bootstrap password after first login!
|
|
|
|
EOF
|
|
fi
|
|
|
|
chmod 600 "$creds_file"
|
|
print_success "Saved credentials to .admin-credentials"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Deployment
|
|
# ============================================================================
|
|
|
|
run_deployment() {
|
|
print_header "Deployment"
|
|
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
deploy_docker
|
|
else
|
|
deploy_native
|
|
fi
|
|
}
|
|
|
|
prepare_traefik_upstream() {
|
|
if [[ "$TRAEFIK_MODE" != "upstream" ]]; then
|
|
return
|
|
fi
|
|
|
|
print_header "Traefik Upstream Setup"
|
|
|
|
local network="${TRAEFIK_NETWORK:-traefik-public}"
|
|
|
|
if ! docker network ls --format '{{.Name}}' | grep -q "^${network}$"; then
|
|
print_warning "Traefik network '${network}' not found"
|
|
if [[ "$NON_INTERACTIVE" == true ]] || confirm "Create network '${network}' now?" "y"; then
|
|
docker network create "$network"
|
|
print_success "Created network: ${network}"
|
|
else
|
|
print_warning "Continuing without creating network"
|
|
fi
|
|
fi
|
|
|
|
local override_file="$PROJECT_ROOT/docker-compose.override.yml"
|
|
if [[ -f "$override_file" ]]; then
|
|
print_info "docker-compose.override.yml already exists. Leaving it unchanged."
|
|
return
|
|
fi
|
|
|
|
cat > "$override_file" << 'EOF'
|
|
version: '3.9'
|
|
|
|
services:
|
|
api:
|
|
networks:
|
|
- traefik-public
|
|
|
|
web:
|
|
networks:
|
|
- traefik-public
|
|
|
|
authentik-server:
|
|
networks:
|
|
- traefik-public
|
|
|
|
networks:
|
|
traefik-public:
|
|
external: true
|
|
name: ${TRAEFIK_NETWORK:-traefik-public}
|
|
EOF
|
|
|
|
print_success "Created docker-compose.override.yml for upstream Traefik"
|
|
}
|
|
|
|
deploy_docker() {
|
|
echo ""
|
|
print_step "Starting Docker deployment..."
|
|
echo ""
|
|
|
|
prepare_traefik_upstream
|
|
|
|
# Check if docker-compose.yml exists
|
|
if [[ ! -f "$PROJECT_ROOT/docker-compose.yml" ]]; then
|
|
print_error "docker-compose.yml not found"
|
|
exit 1
|
|
fi
|
|
|
|
cd "$PROJECT_ROOT" || exit 1
|
|
|
|
local compose_files=("-f" "docker-compose.yml")
|
|
if [[ -f "docker-compose.override.yml" ]]; then
|
|
compose_files+=("-f" "docker-compose.override.yml")
|
|
fi
|
|
|
|
# Set up error trap for rollback
|
|
local deployment_started=false
|
|
rollback_deployment() {
|
|
if [[ "$deployment_started" == true ]]; then
|
|
print_warning "Deployment failed, rolling back..."
|
|
docker compose "${compose_files[@]}" down --remove-orphans 2>/dev/null || true
|
|
print_info "Rollback complete"
|
|
fi
|
|
}
|
|
trap rollback_deployment ERR
|
|
|
|
# Pull images
|
|
print_step "Pulling Docker images..."
|
|
if ! docker compose "${compose_files[@]}" pull; then
|
|
print_warning "Failed to pull some images (will build locally)"
|
|
fi
|
|
|
|
# Build services
|
|
print_step "Building services..."
|
|
if ! docker compose "${compose_files[@]}" build; then
|
|
print_error "Build failed"
|
|
trap - ERR
|
|
return 1
|
|
fi
|
|
|
|
# Start services
|
|
print_step "Starting services..."
|
|
deployment_started=true
|
|
if ! docker compose "${compose_files[@]}" up -d; then
|
|
print_error "Failed to start services"
|
|
trap - ERR
|
|
rollback_deployment
|
|
return 1
|
|
fi
|
|
|
|
# Clear trap on success
|
|
trap - ERR
|
|
|
|
# Wait for services to be healthy
|
|
echo ""
|
|
print_step "Waiting for services to start..."
|
|
sleep 5
|
|
|
|
# Check service health
|
|
local all_healthy=true
|
|
local services=("postgres" "valkey" "api" "web")
|
|
|
|
for service in "${services[@]}"; do
|
|
if docker compose "${compose_files[@]}" ps "$service" 2>/dev/null | grep -q "Up"; then
|
|
print_success "$service: Running"
|
|
else
|
|
print_warning "$service: Not running"
|
|
all_healthy=false
|
|
fi
|
|
done
|
|
|
|
if [[ "$all_healthy" == true ]]; then
|
|
echo ""
|
|
print_success "All services started successfully"
|
|
else
|
|
echo ""
|
|
print_warning "Some services may not be running correctly"
|
|
print_info "Check logs with: docker compose logs"
|
|
fi
|
|
}
|
|
|
|
deploy_native() {
|
|
echo ""
|
|
print_step "Native deployment setup..."
|
|
echo ""
|
|
|
|
cd "$PROJECT_ROOT" || exit 1
|
|
|
|
# Install dependencies
|
|
print_step "Installing Node.js dependencies..."
|
|
if ! pnpm install; then
|
|
print_error "Failed to install dependencies"
|
|
exit 1
|
|
fi
|
|
|
|
# Database setup
|
|
print_step "Setting up database..."
|
|
print_info "Make sure PostgreSQL is running and accessible"
|
|
|
|
# Run migrations
|
|
print_step "Running database migrations..."
|
|
if ! pnpm -F api run prisma:migrate; then
|
|
print_warning "Database migration failed - you may need to set this up manually"
|
|
fi
|
|
|
|
# Build
|
|
print_step "Building application..."
|
|
pnpm run build
|
|
|
|
print_success "Native setup complete"
|
|
echo ""
|
|
print_info "To start the application:"
|
|
print_info " pnpm dev # Development mode"
|
|
print_info " pnpm start # Production mode"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Post-Install Information
|
|
# ============================================================================
|
|
|
|
show_post_install_info() {
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
print_header "Dry Run Summary"
|
|
echo ""
|
|
print_info "No files were written and no containers were started."
|
|
echo ""
|
|
print_step "Planned URLs:"
|
|
echo ""
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
echo " Web Interface: $MOSAIC_BASE_URL"
|
|
echo " API: $API_BASE_URL"
|
|
if [[ "$ENABLE_SSO" == true ]]; then
|
|
echo " Authentik SSO: ${AUTHENTIK_PUBLIC_URL:-http://localhost:9000}"
|
|
fi
|
|
fi
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
print_header "Installation Complete"
|
|
|
|
echo ""
|
|
echo "🎉 Mosaic Stack has been set up successfully!"
|
|
echo ""
|
|
|
|
# Show URLs
|
|
print_step "Access URLs:"
|
|
echo ""
|
|
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
local web_url="$MOSAIC_BASE_URL"
|
|
local api_url="$API_BASE_URL"
|
|
|
|
echo " Web Interface: $web_url"
|
|
echo " API: $api_url"
|
|
|
|
if [[ "$ENABLE_SSO" == true ]]; then
|
|
local auth_url="${AUTHENTIK_PUBLIC_URL:-http://localhost:9000}"
|
|
echo " Authentik SSO: $auth_url"
|
|
fi
|
|
|
|
if [[ "$OLLAMA_MODE" == "local" ]]; then
|
|
echo " Ollama: http://localhost:${OLLAMA_PORT}"
|
|
fi
|
|
else
|
|
echo " Run 'pnpm dev' to start the development server"
|
|
fi
|
|
|
|
echo ""
|
|
print_step "Credentials:"
|
|
echo ""
|
|
echo " Saved to: .admin-credentials"
|
|
print_warning "Keep this file secure!"
|
|
|
|
echo ""
|
|
print_step "Next Steps:"
|
|
echo ""
|
|
|
|
if [[ "$ENABLE_SSO" == true ]]; then
|
|
echo " 1. Access Authentik at ${AUTHENTIK_PUBLIC_URL:-http://localhost:9000}"
|
|
echo " 2. Log in with credentials from .admin-credentials"
|
|
echo " 3. Complete SSO setup (create application, get client secret)"
|
|
echo " 4. Update OIDC_CLIENT_SECRET in .env"
|
|
echo " 5. Access Mosaic Stack at $MOSAIC_BASE_URL"
|
|
else
|
|
echo " 1. Access Mosaic Stack at $MOSAIC_BASE_URL"
|
|
echo " 2. Create your first user account"
|
|
fi
|
|
|
|
if [[ "$TRAEFIK_MODE" == "upstream" ]]; then
|
|
echo ""
|
|
print_info "Upstream Traefik: ensure your Traefik is attached to '${TRAEFIK_NETWORK:-traefik-public}'"
|
|
elif [[ "$TRAEFIK_MODE" == "bundled" && "$TRAEFIK_DASHBOARD_ENABLED" == true ]]; then
|
|
echo ""
|
|
print_info "Traefik dashboard: http://localhost:${TRAEFIK_DASHBOARD_PORT}/dashboard/"
|
|
fi
|
|
|
|
echo ""
|
|
print_step "Useful Commands:"
|
|
echo ""
|
|
|
|
if [[ "$MODE" == "docker" ]]; then
|
|
echo " View logs: docker compose logs -f"
|
|
echo " Stop services: docker compose down"
|
|
echo " Restart: docker compose restart"
|
|
echo " View status: docker compose ps"
|
|
else
|
|
echo " Development: pnpm dev"
|
|
echo " Production: pnpm start"
|
|
echo " Database: pnpm -F api run prisma:migrate"
|
|
fi
|
|
|
|
echo ""
|
|
print_step "Documentation:"
|
|
echo ""
|
|
echo " Setup docs: /home/jwoltje/src/mosaic-stack/scripts/README.md"
|
|
echo " Architecture: /home/jwoltje/src/mosaic-stack/docs/"
|
|
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main Execution
|
|
# ============================================================================
|
|
|
|
main() {
|
|
parse_arguments "$@"
|
|
|
|
show_banner
|
|
|
|
detect_platform
|
|
|
|
select_deployment_mode
|
|
|
|
check_and_install_dependencies
|
|
|
|
collect_configuration
|
|
resolve_port_conflicts
|
|
|
|
if [[ "$DRY_RUN" != true ]]; then
|
|
generate_env_file
|
|
run_deployment
|
|
else
|
|
echo ""
|
|
print_warning "Dry run mode - skipping .env generation and deployment"
|
|
echo ""
|
|
print_info "Configuration collected but not applied"
|
|
fi
|
|
|
|
show_post_install_info
|
|
|
|
echo ""
|
|
print_success "Setup complete!"
|
|
echo ""
|
|
echo "==================================================================="
|
|
echo "Setup completed: $(date)"
|
|
echo "Full log saved to: $LOG_FILE"
|
|
echo "==================================================================="
|
|
echo ""
|
|
|
|
# Reset terminal colors before exiting
|
|
printf "${NC}"
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|