#!/usr/bin/env bash set -euo pipefail CAMELEER_INSTALLER_VERSION="1.0.0" CAMELEER_DEFAULT_VERSION="latest" REGISTRY="gitea.siegeln.net/cameleer" # --- Colors --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' # --- Defaults --- DEFAULT_INSTALL_DIR="./cameleer" DEFAULT_PUBLIC_PROTOCOL="https" DEFAULT_ADMIN_USER="admin" DEFAULT_TLS_MODE="self-signed" DEFAULT_HTTP_PORT="80" DEFAULT_HTTPS_PORT="443" DEFAULT_LOGTO_CONSOLE_PORT="3002" DEFAULT_LOGTO_CONSOLE_EXPOSED="true" DEFAULT_COMPOSE_PROJECT="cameleer-saas" DEFAULT_COMPOSE_PROJECT_STANDALONE="cameleer" DEFAULT_DOCKER_SOCKET="/var/run/docker.sock" # --- Config values (set by args/env/config/prompts) --- # Save environment values before initialization (CLI args override these) _ENV_PUBLIC_HOST="${PUBLIC_HOST:-}" _ENV_PUBLIC_PROTOCOL="${PUBLIC_PROTOCOL:-}" _ENV_POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}" _ENV_CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-}" _ENV_TLS_MODE="${TLS_MODE:-}" _ENV_CERT_FILE="${CERT_FILE:-}" _ENV_KEY_FILE="${KEY_FILE:-}" _ENV_CA_FILE="${CA_FILE:-}" _ENV_HTTP_PORT="${HTTP_PORT:-}" _ENV_HTTPS_PORT="${HTTPS_PORT:-}" _ENV_LOGTO_CONSOLE_PORT="${LOGTO_CONSOLE_PORT:-}" _ENV_LOGTO_CONSOLE_EXPOSED="${LOGTO_CONSOLE_EXPOSED:-}" _ENV_MONITORING_NETWORK="${MONITORING_NETWORK:-}" _ENV_COMPOSE_PROJECT="${COMPOSE_PROJECT:-}" _ENV_DOCKER_SOCKET="${DOCKER_SOCKET:-}" _ENV_NODE_TLS_REJECT="${NODE_TLS_REJECT:-}" _ENV_DEPLOYMENT_MODE="${DEPLOYMENT_MODE:-}" INSTALL_DIR="" PUBLIC_HOST="" PUBLIC_PROTOCOL="" ADMIN_USER="" ADMIN_PASS="" TLS_MODE="" CERT_FILE="" KEY_FILE="" CA_FILE="" POSTGRES_PASSWORD="" CLICKHOUSE_PASSWORD="" HTTP_PORT="" HTTPS_PORT="" LOGTO_CONSOLE_PORT="" LOGTO_CONSOLE_EXPOSED="" MONITORING_NETWORK="" VERSION="" COMPOSE_PROJECT="" DOCKER_SOCKET="" NODE_TLS_REJECT="" DEPLOYMENT_MODE="" # --- State --- MODE="" # simple, expert, silent IS_RERUN=false RERUN_ACTION="" # upgrade, reconfigure, reinstall CONFIRM_DESTROY=false CONFIG_FILE_PATH="" # --- Utility functions --- log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_success() { echo -e "${GREEN}[OK]${NC} $1"; } print_banner() { echo -e "${BOLD}" echo " ____ _ " echo " / ___|__ _ _ __ ___ ___ | | ___ ___ _ __ " echo "| | / _\` | '_ \` _ \\ / _ \\| |/ _ \\/ _ \\ '__|" echo "| |__| (_| | | | | | | __/| | __/ __/ | " echo " \\____\\__,_|_| |_| |_|\\___||_|\\___|\\___||_| " echo "" echo " SaaS Platform Installer v${CAMELEER_INSTALLER_VERSION}" echo -e "${NC}" } prompt() { local var_name="$1" prompt_text="$2" default="${3:-}" local input if [ -n "$default" ]; then read -rp " $prompt_text [$default]: " input eval "$var_name=\"\${input:-$default}\"" else read -rp " $prompt_text: " input eval "$var_name=\"\$input\"" fi } prompt_password() { local var_name="$1" prompt_text="$2" default="${3:-}" local input if [ -n "$default" ]; then read -rsp " $prompt_text [${default:+********}]: " input echo eval "$var_name=\"\${input:-$default}\"" else read -rsp " $prompt_text: " input echo eval "$var_name=\"\$input\"" fi } prompt_yesno() { local prompt_text="$1" default="${2:-n}" local input if [ "$default" = "y" ]; then read -rp " $prompt_text [Y/n]: " input case "${input:-y}" in [nN]|[nN][oO]) return 1 ;; *) return 0 ;; esac else read -rp " $prompt_text [y/N]: " input case "${input:-n}" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac fi } generate_password() { tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32 } # --- Argument parsing --- parse_args() { while [ $# -gt 0 ]; do case "$1" in --silent) MODE="silent" ;; --expert) MODE="expert" ;; --config) CONFIG_FILE_PATH="$2"; shift ;; --install-dir) INSTALL_DIR="$2"; shift ;; --public-host) PUBLIC_HOST="$2"; shift ;; --public-protocol) PUBLIC_PROTOCOL="$2"; shift ;; --admin-user) ADMIN_USER="$2"; shift ;; --admin-password) ADMIN_PASS="$2"; shift ;; --tls-mode) TLS_MODE="$2"; shift ;; --cert-file) CERT_FILE="$2"; shift ;; --key-file) KEY_FILE="$2"; shift ;; --ca-file) CA_FILE="$2"; shift ;; --postgres-password) POSTGRES_PASSWORD="$2"; shift ;; --clickhouse-password) CLICKHOUSE_PASSWORD="$2"; shift ;; --http-port) HTTP_PORT="$2"; shift ;; --https-port) HTTPS_PORT="$2"; shift ;; --logto-console-port) LOGTO_CONSOLE_PORT="$2"; shift ;; --logto-console-exposed) LOGTO_CONSOLE_EXPOSED="$2"; shift ;; --monitoring-network) MONITORING_NETWORK="$2"; shift ;; --version) VERSION="$2"; shift ;; --compose-project) COMPOSE_PROJECT="$2"; shift ;; --docker-socket) DOCKER_SOCKET="$2"; shift ;; --node-tls-reject) NODE_TLS_REJECT="$2"; shift ;; --deployment-mode) DEPLOYMENT_MODE="$2"; shift ;; --server-admin-user) ADMIN_USER="$2"; shift ;; --server-admin-password) ADMIN_PASS="$2"; shift ;; --reconfigure) RERUN_ACTION="reconfigure" ;; --reinstall) RERUN_ACTION="reinstall" ;; --confirm-destroy) CONFIRM_DESTROY=true ;; --help|-h) show_help; exit 0 ;; *) log_error "Unknown option: $1" echo " Run with --help for usage." exit 1 ;; esac shift done } show_help() { echo "Usage: install.sh [OPTIONS]" echo "" echo "Modes:" echo " (default) Interactive simple mode (6 questions)" echo " --expert Interactive expert mode (all options)" echo " --silent Non-interactive, use defaults + overrides" echo "" echo "Options:" echo " --install-dir DIR Install directory (default: ./cameleer)" echo " --public-host HOST Public hostname (default: auto-detect)" echo " --admin-user USER Admin username (default: admin)" echo " --admin-password PASS Admin password (default: generated)" echo " --tls-mode MODE self-signed or custom (default: self-signed)" echo " --cert-file PATH TLS certificate file" echo " --key-file PATH TLS key file" echo " --ca-file PATH CA bundle file" echo " --monitoring-network NAME Docker network for Prometheus scraping" echo " --version TAG Image version tag (default: latest)" echo " --config FILE Load config from file" echo " --help Show this help" echo "" echo "Expert options:" echo " --postgres-password, --clickhouse-password, --http-port," echo " --https-port, --logto-console-port, --logto-console-exposed," echo " --compose-project, --docker-socket, --node-tls-reject" echo "" echo "Re-run options:" echo " --reconfigure Re-run interactive setup (preserve data)" echo " --reinstall --confirm-destroy Fresh install (destroys data)" echo "" echo "Config precedence: CLI flags > env vars > config file > defaults" } # --- Config file handling --- load_config_file() { local file="$1" [ ! -f "$file" ] && return while IFS='=' read -r key value; do case "$key" in \#*|"") continue ;; esac key=$(echo "$key" | tr -d ' ') value=$(echo "$value" | sed 's/^[ ]*//;s/[ ]*$//') case "$key" in install_dir) [ -z "$INSTALL_DIR" ] && INSTALL_DIR="$value" ;; public_host) [ -z "$PUBLIC_HOST" ] && PUBLIC_HOST="$value" ;; public_protocol) [ -z "$PUBLIC_PROTOCOL" ] && PUBLIC_PROTOCOL="$value" ;; admin_user) [ -z "$ADMIN_USER" ] && ADMIN_USER="$value" ;; admin_password) [ -z "$ADMIN_PASS" ] && ADMIN_PASS="$value" ;; tls_mode) [ -z "$TLS_MODE" ] && TLS_MODE="$value" ;; cert_file) [ -z "$CERT_FILE" ] && CERT_FILE="$value" ;; key_file) [ -z "$KEY_FILE" ] && KEY_FILE="$value" ;; ca_file) [ -z "$CA_FILE" ] && CA_FILE="$value" ;; postgres_password) [ -z "$POSTGRES_PASSWORD" ] && POSTGRES_PASSWORD="$value" ;; clickhouse_password) [ -z "$CLICKHOUSE_PASSWORD" ] && CLICKHOUSE_PASSWORD="$value" ;; http_port) [ -z "$HTTP_PORT" ] && HTTP_PORT="$value" ;; https_port) [ -z "$HTTPS_PORT" ] && HTTPS_PORT="$value" ;; logto_console_port) [ -z "$LOGTO_CONSOLE_PORT" ] && LOGTO_CONSOLE_PORT="$value" ;; logto_console_exposed) [ -z "$LOGTO_CONSOLE_EXPOSED" ] && LOGTO_CONSOLE_EXPOSED="$value" ;; monitoring_network) [ -z "$MONITORING_NETWORK" ] && MONITORING_NETWORK="$value" ;; version) [ -z "$VERSION" ] && VERSION="$value" ;; compose_project) [ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$value" ;; docker_socket) [ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$value" ;; node_tls_reject) [ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$value" ;; deployment_mode) [ -z "$DEPLOYMENT_MODE" ] && DEPLOYMENT_MODE="$value" ;; esac done < "$file" } load_env_overrides() { [ -z "$INSTALL_DIR" ] && INSTALL_DIR="${CAMELEER_INSTALL_DIR:-}" [ -z "$PUBLIC_HOST" ] && PUBLIC_HOST="$_ENV_PUBLIC_HOST" [ -z "$PUBLIC_PROTOCOL" ] && PUBLIC_PROTOCOL="$_ENV_PUBLIC_PROTOCOL" [ -z "$ADMIN_USER" ] && ADMIN_USER="${SAAS_ADMIN_USER:-}" [ -z "$ADMIN_PASS" ] && ADMIN_PASS="${SAAS_ADMIN_PASS:-}" [ -z "$TLS_MODE" ] && TLS_MODE="$_ENV_TLS_MODE" [ -z "$CERT_FILE" ] && CERT_FILE="$_ENV_CERT_FILE" [ -z "$KEY_FILE" ] && KEY_FILE="$_ENV_KEY_FILE" [ -z "$CA_FILE" ] && CA_FILE="$_ENV_CA_FILE" [ -z "$POSTGRES_PASSWORD" ] && POSTGRES_PASSWORD="$_ENV_POSTGRES_PASSWORD" [ -z "$CLICKHOUSE_PASSWORD" ] && CLICKHOUSE_PASSWORD="$_ENV_CLICKHOUSE_PASSWORD" [ -z "$HTTP_PORT" ] && HTTP_PORT="$_ENV_HTTP_PORT" [ -z "$HTTPS_PORT" ] && HTTPS_PORT="$_ENV_HTTPS_PORT" [ -z "$LOGTO_CONSOLE_PORT" ] && LOGTO_CONSOLE_PORT="$_ENV_LOGTO_CONSOLE_PORT" [ -z "$LOGTO_CONSOLE_EXPOSED" ] && LOGTO_CONSOLE_EXPOSED="$_ENV_LOGTO_CONSOLE_EXPOSED" [ -z "$MONITORING_NETWORK" ] && MONITORING_NETWORK="$_ENV_MONITORING_NETWORK" [ -z "$VERSION" ] && VERSION="${CAMELEER_VERSION:-}" [ -z "$COMPOSE_PROJECT" ] && COMPOSE_PROJECT="$_ENV_COMPOSE_PROJECT" [ -z "$DOCKER_SOCKET" ] && DOCKER_SOCKET="$_ENV_DOCKER_SOCKET" [ -z "$NODE_TLS_REJECT" ] && NODE_TLS_REJECT="$_ENV_NODE_TLS_REJECT" [ -z "$DEPLOYMENT_MODE" ] && DEPLOYMENT_MODE="$_ENV_DEPLOYMENT_MODE" } # --- Prerequisites --- check_prerequisites() { log_info "Checking prerequisites..." local errors=0 if ! command -v docker >/dev/null 2>&1; then log_error "Docker is not installed." echo " Install Docker Engine: https://docs.docker.com/engine/install/" errors=$((errors + 1)) else local docker_version docker_version=$(docker version --format '{{.Server.Version}}' 2>/dev/null || echo "unknown") log_info "Docker version: $docker_version" fi if ! docker compose version >/dev/null 2>&1; then log_error "Docker Compose v2 is not available." echo " 'docker compose' subcommand required (not standalone docker-compose)." errors=$((errors + 1)) else local compose_version compose_version=$(docker compose version --short 2>/dev/null || echo "unknown") log_info "Docker Compose version: $compose_version" fi local socket="${DOCKER_SOCKET:-$DEFAULT_DOCKER_SOCKET}" if [ ! -S "$socket" ]; then log_warn "Docker socket not found at $socket" fi check_port_available "${HTTP_PORT:-$DEFAULT_HTTP_PORT}" "HTTP" check_port_available "${HTTPS_PORT:-$DEFAULT_HTTPS_PORT}" "HTTPS" if [ "$DEPLOYMENT_MODE" != "standalone" ]; then check_port_available "${LOGTO_CONSOLE_PORT:-$DEFAULT_LOGTO_CONSOLE_PORT}" "Logto Console" fi if [ $errors -gt 0 ]; then log_error "$errors prerequisite(s) not met. Please install missing dependencies and retry." exit 1 fi log_success "All prerequisites met." } check_port_available() { local port="$1" name="$2" if command -v ss >/dev/null 2>&1; then if ss -tlnp 2>/dev/null | grep -q ":${port} "; then log_warn "Port $port ($name) is already in use." fi elif command -v netstat >/dev/null 2>&1; then if netstat -tlnp 2>/dev/null | grep -q ":${port} "; then log_warn "Port $port ($name) is already in use." fi fi } # --- Auto-detection --- auto_detect() { if [ -z "$PUBLIC_HOST" ]; then PUBLIC_HOST=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "localhost") # Normalize to lowercase (Windows hostnames are uppercase) PUBLIC_HOST=$(echo "$PUBLIC_HOST" | tr '[:upper:]' '[:lower:]') local primary_ip primary_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || true) if [ -n "$primary_ip" ]; then local rdns rdns=$(dig +short -x "$primary_ip" 2>/dev/null | sed 's/\.$//' || true) [ -n "$rdns" ] && PUBLIC_HOST="$rdns" fi fi if [ -z "$DOCKER_SOCKET" ]; then DOCKER_SOCKET="$DEFAULT_DOCKER_SOCKET" fi } detect_existing_install() { local dir="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" if [ -f "$dir/cameleer.conf" ]; then IS_RERUN=true INSTALL_DIR="$dir" load_config_file "$dir/cameleer.conf" fi } # --- Interactive prompts --- select_mode() { if [ -n "$MODE" ]; then return; fi echo "" echo " Installation mode:" echo " [1] Simple — 6 questions, sensible defaults (recommended)" echo " [2] Expert — configure everything" echo "" local choice read -rp " Select mode [1]: " choice case "${choice:-1}" in 2) MODE="expert" ;; *) MODE="simple" ;; esac } run_simple_prompts() { echo "" echo -e "${BOLD}--- Simple Installation ---${NC}" echo "" prompt INSTALL_DIR "Install directory" "${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" prompt PUBLIC_HOST "Public hostname" "${PUBLIC_HOST:-localhost}" prompt ADMIN_USER "Admin username" "${ADMIN_USER:-$DEFAULT_ADMIN_USER}" if prompt_yesno "Auto-generate admin password?" "y"; then ADMIN_PASS="" else prompt_password ADMIN_PASS "Admin password" "" fi echo "" if prompt_yesno "Use custom TLS certificates? (no = self-signed)"; then TLS_MODE="custom" prompt CERT_FILE "Path to certificate file (PEM)" "" prompt KEY_FILE "Path to private key file (PEM)" "" if prompt_yesno "Include CA bundle?"; then prompt CA_FILE "Path to CA bundle (PEM)" "" fi else TLS_MODE="self-signed" fi echo "" prompt MONITORING_NETWORK "Monitoring network name (empty = skip)" "" echo "" echo " Deployment mode:" echo " [1] Multi-tenant SaaS — manage platform, provision tenants on demand" echo " [2] Single-tenant — one server instance, local auth, no identity provider" echo "" local deploy_choice read -rp " Select mode [1]: " deploy_choice case "${deploy_choice:-1}" in 2) DEPLOYMENT_MODE="standalone" ;; *) DEPLOYMENT_MODE="saas" ;; esac } run_expert_prompts() { echo "" echo -e "${BOLD}--- Expert Installation ---${NC}" run_simple_prompts echo "" echo -e "${BOLD} Credentials:${NC}" if prompt_yesno "Auto-generate database passwords?" "y"; then POSTGRES_PASSWORD="" CLICKHOUSE_PASSWORD="" else prompt_password POSTGRES_PASSWORD "PostgreSQL password" "" prompt_password CLICKHOUSE_PASSWORD "ClickHouse password" "" fi echo "" echo -e "${BOLD} Networking:${NC}" prompt HTTP_PORT "HTTP port" "${HTTP_PORT:-$DEFAULT_HTTP_PORT}" prompt HTTPS_PORT "HTTPS port" "${HTTPS_PORT:-$DEFAULT_HTTPS_PORT}" if [ "$DEPLOYMENT_MODE" = "saas" ]; then prompt LOGTO_CONSOLE_PORT "Logto admin console port" "${LOGTO_CONSOLE_PORT:-$DEFAULT_LOGTO_CONSOLE_PORT}" fi echo "" echo -e "${BOLD} Docker:${NC}" prompt VERSION "Image version/tag" "${VERSION:-$CAMELEER_DEFAULT_VERSION}" prompt COMPOSE_PROJECT "Compose project name" "${COMPOSE_PROJECT:-$DEFAULT_COMPOSE_PROJECT}" prompt DOCKER_SOCKET "Docker socket path" "${DOCKER_SOCKET:-$DEFAULT_DOCKER_SOCKET}" if [ "$DEPLOYMENT_MODE" = "saas" ]; then echo "" echo -e "${BOLD} Logto:${NC}" if prompt_yesno "Expose Logto admin console externally?" "y"; then LOGTO_CONSOLE_EXPOSED="true" else LOGTO_CONSOLE_EXPOSED="false" fi fi } # --- Config merge and validation --- merge_config() { : "${DEPLOYMENT_MODE:=saas}" : "${INSTALL_DIR:=$DEFAULT_INSTALL_DIR}" : "${PUBLIC_HOST:=localhost}" : "${PUBLIC_PROTOCOL:=$DEFAULT_PUBLIC_PROTOCOL}" : "${ADMIN_USER:=$DEFAULT_ADMIN_USER}" : "${TLS_MODE:=$DEFAULT_TLS_MODE}" : "${HTTP_PORT:=$DEFAULT_HTTP_PORT}" : "${HTTPS_PORT:=$DEFAULT_HTTPS_PORT}" : "${LOGTO_CONSOLE_PORT:=$DEFAULT_LOGTO_CONSOLE_PORT}" : "${LOGTO_CONSOLE_EXPOSED:=$DEFAULT_LOGTO_CONSOLE_EXPOSED}" : "${VERSION:=$CAMELEER_DEFAULT_VERSION}" : "${DOCKER_SOCKET:=$DEFAULT_DOCKER_SOCKET}" if [ "$DEPLOYMENT_MODE" = "standalone" ]; then : "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT_STANDALONE}" else : "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT}" fi # Force lowercase hostname — Logto normalizes internally, case mismatch breaks JWT validation PUBLIC_HOST=$(echo "$PUBLIC_HOST" | tr '[:upper:]' '[:lower:]') if [ "$DEPLOYMENT_MODE" != "standalone" ]; then if [ -z "$NODE_TLS_REJECT" ]; then if [ "$TLS_MODE" = "custom" ]; then NODE_TLS_REJECT="1" else NODE_TLS_REJECT="0" fi fi fi } validate_config() { local errors=0 if [ "$TLS_MODE" = "custom" ]; then if [ ! -f "$CERT_FILE" ]; then log_error "Certificate file not found: $CERT_FILE" errors=$((errors + 1)) fi if [ ! -f "$KEY_FILE" ]; then log_error "Key file not found: $KEY_FILE" errors=$((errors + 1)) fi if [ -n "$CA_FILE" ] && [ ! -f "$CA_FILE" ]; then log_error "CA bundle not found: $CA_FILE" errors=$((errors + 1)) fi fi local port_vars="HTTP_PORT HTTPS_PORT" [ "$DEPLOYMENT_MODE" != "standalone" ] && port_vars="HTTP_PORT HTTPS_PORT LOGTO_CONSOLE_PORT" for port_var in $port_vars; do local port_val eval "port_val=\$$port_var" if ! echo "$port_val" | grep -qE '^[0-9]+$' || [ "$port_val" -lt 1 ] || [ "$port_val" -gt 65535 ]; then log_error "Invalid port for $port_var: $port_val" errors=$((errors + 1)) fi done if [ $errors -gt 0 ]; then log_error "Configuration validation failed." exit 1 fi log_success "Configuration validated." } generate_passwords() { if [ -z "$ADMIN_PASS" ]; then ADMIN_PASS=$(generate_password) log_info "Generated admin password." fi if [ -z "$POSTGRES_PASSWORD" ]; then POSTGRES_PASSWORD=$(generate_password) log_info "Generated PostgreSQL password." fi if [ -z "$CLICKHOUSE_PASSWORD" ]; then CLICKHOUSE_PASSWORD=$(generate_password) log_info "Generated ClickHouse password." fi } # --- File generation --- copy_certs() { local certs_dir="$INSTALL_DIR/certs" mkdir -p "$certs_dir" cp "$CERT_FILE" "$certs_dir/cert.pem" cp "$KEY_FILE" "$certs_dir/key.pem" if [ -n "$CA_FILE" ]; then cp "$CA_FILE" "$certs_dir/ca.pem" fi log_info "Copied TLS certificates to $certs_dir/" } generate_env_file() { local f="$INSTALL_DIR/.env" if [ "$DEPLOYMENT_MODE" = "standalone" ]; then cat > "$f" << EOF # Cameleer Server Configuration (standalone) # Generated by installer v${CAMELEER_INSTALLER_VERSION} on $(date -u '+%Y-%m-%d %H:%M:%S UTC') VERSION=${VERSION} PUBLIC_HOST=${PUBLIC_HOST} PUBLIC_PROTOCOL=${PUBLIC_PROTOCOL} HTTP_PORT=${HTTP_PORT} HTTPS_PORT=${HTTPS_PORT} # PostgreSQL POSTGRES_USER=cameleer POSTGRES_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_DB=cameleer # ClickHouse CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} # Server admin SERVER_ADMIN_USER=${ADMIN_USER} SERVER_ADMIN_PASS=${ADMIN_PASS} # Bootstrap token (required by server, not used externally in standalone mode) BOOTSTRAP_TOKEN=$(generate_password) # JWT signing secret (required by server, must be non-empty) CAMELEER_SERVER_SECURITY_JWTSECRET=$(generate_password) # Docker DOCKER_SOCKET=${DOCKER_SOCKET} DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0") POSTGRES_IMAGE=postgres:16-alpine # Compose file assembly COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml") EOF if [ "$TLS_MODE" = "custom" ]; then echo "CERT_FILE=/user-certs/cert.pem" >> "$f" echo "KEY_FILE=/user-certs/key.pem" >> "$f" [ -n "$CA_FILE" ] && echo "CA_FILE=/user-certs/ca.pem" >> "$f" fi if [ -n "$MONITORING_NETWORK" ]; then echo "" >> "$f" echo "# Monitoring" >> "$f" echo "MONITORING_NETWORK=${MONITORING_NETWORK}" >> "$f" fi log_info "Generated .env" cp "$f" "$INSTALL_DIR/.env.bak" return fi cat > "$f" << EOF # Cameleer SaaS Configuration # Generated by installer v${CAMELEER_INSTALLER_VERSION} on $(date -u '+%Y-%m-%d %H:%M:%S UTC') # Image version VERSION=${VERSION} # Public access PUBLIC_HOST=${PUBLIC_HOST} PUBLIC_PROTOCOL=${PUBLIC_PROTOCOL} # Ports HTTP_PORT=${HTTP_PORT} HTTPS_PORT=${HTTPS_PORT} LOGTO_CONSOLE_PORT=${LOGTO_CONSOLE_PORT} LOGTO_CONSOLE_BIND=$([ "$LOGTO_CONSOLE_EXPOSED" = "true" ] && echo "0.0.0.0" || echo "127.0.0.1") # PostgreSQL POSTGRES_USER=cameleer POSTGRES_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_DB=cameleer_saas # ClickHouse CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} # Admin user SAAS_ADMIN_USER=${ADMIN_USER} SAAS_ADMIN_PASS=${ADMIN_PASS} # TLS NODE_TLS_REJECT=${NODE_TLS_REJECT} EOF if [ "$TLS_MODE" = "custom" ]; then cat >> "$f" << 'EOF' CERT_FILE=/user-certs/cert.pem KEY_FILE=/user-certs/key.pem EOF if [ -n "$CA_FILE" ]; then echo "CA_FILE=/user-certs/ca.pem" >> "$f" fi fi cat >> "$f" << EOF # Docker DOCKER_SOCKET=${DOCKER_SOCKET} DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0") # Provisioning images CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer-server:${VERSION} CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:${VERSION} CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=${REGISTRY}/cameleer-runtime-base:${VERSION} # JWT signing secret (forwarded to provisioned tenant servers, must be non-empty) CAMELEER_SERVER_SECURITY_JWTSECRET=$(generate_password) # Compose file assembly COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml") EOF if [ -n "$MONITORING_NETWORK" ]; then echo "" >> "$f" echo "# Monitoring" >> "$f" echo "MONITORING_NETWORK=${MONITORING_NETWORK}" >> "$f" fi log_info "Generated .env" cp "$f" "$INSTALL_DIR/.env.bak" } copy_templates() { local src src="$(cd "$(dirname "$0")" && pwd)/templates" # Base infra — always copied cp "$src/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml" cp "$src/.env.example" "$INSTALL_DIR/.env.example" # Mode-specific if [ "$DEPLOYMENT_MODE" = "standalone" ]; then cp "$src/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.server.yml" cp "$src/traefik-dynamic.yml" "$INSTALL_DIR/traefik-dynamic.yml" else cp "$src/docker-compose.saas.yml" "$INSTALL_DIR/docker-compose.saas.yml" fi # Optional overlays if [ "$TLS_MODE" = "custom" ]; then cp "$src/docker-compose.tls.yml" "$INSTALL_DIR/docker-compose.tls.yml" fi if [ -n "$MONITORING_NETWORK" ]; then cp "$src/docker-compose.monitoring.yml" "$INSTALL_DIR/docker-compose.monitoring.yml" fi log_info "Copied docker-compose templates to $INSTALL_DIR" } # --- Docker operations --- docker_compose_pull() { log_info "Pulling Docker images..." (cd "$INSTALL_DIR" && docker compose -p "$COMPOSE_PROJECT" pull) log_success "All images pulled." } docker_compose_up() { log_info "Starting Cameleer SaaS platform..." (cd "$INSTALL_DIR" && docker compose -p "$COMPOSE_PROJECT" up -d --pull always --force-recreate) || true log_info "Containers started — verifying health next." } docker_compose_down() { log_info "Stopping Cameleer SaaS platform..." (cd "$INSTALL_DIR" && docker compose -p "$COMPOSE_PROJECT" down) } # --- Health verification --- wait_for_docker_healthy() { local name="$1" service="$2" timeout_secs="${3:-300}" local start_time=$(date +%s) while true; do local elapsed=$(( $(date +%s) - start_time )) if [ $elapsed -ge $timeout_secs ]; then printf " ${RED}[FAIL]${NC} %-20s not healthy after %ds\n" "$name" "$timeout_secs" echo " Check: docker compose -p $COMPOSE_PROJECT logs $service" return 1 fi local health health=$(cd "$INSTALL_DIR" && docker compose -p "$COMPOSE_PROJECT" ps "$service" --format '{{.Health}}' 2>/dev/null || echo "unknown") case "$health" in healthy) local duration=$(( $(date +%s) - start_time )) printf " ${GREEN}[ok]${NC} %-20s ready (%ds)\n" "$name" "$duration" return 0 ;; unhealthy) printf " ${RED}[FAIL]${NC} %-20s unhealthy\n" "$name" echo " Check: docker compose -p $COMPOSE_PROJECT logs $service" return 1 ;; *) sleep 3 ;; esac done } check_endpoint() { local name="$1" url="$2" timeout_secs="${3:-120}" local start_time=$(date +%s) while true; do local elapsed=$(( $(date +%s) - start_time )) if [ $elapsed -ge $timeout_secs ]; then printf " ${RED}[FAIL]${NC} %-20s not reachable after %ds\n" "$name" "$timeout_secs" return 1 fi if curl -sfk -o /dev/null "$url" 2>/dev/null; then local duration=$(( $(date +%s) - start_time )) printf " ${GREEN}[ok]${NC} %-20s ready (%ds)\n" "$name" "$duration" return 0 fi sleep 3 done } verify_health() { echo "" log_info "Verifying installation..." local failed=0 wait_for_docker_healthy "PostgreSQL" "cameleer-postgres" 120 || failed=1 [ $failed -eq 0 ] && \ wait_for_docker_healthy "ClickHouse" "cameleer-clickhouse" 120 || failed=1 if [ "$DEPLOYMENT_MODE" = "standalone" ]; then [ $failed -eq 0 ] && \ wait_for_docker_healthy "Cameleer Server" "cameleer-server" 300 || failed=1 [ $failed -eq 0 ] && \ check_endpoint "Server UI" "https://localhost:${HTTPS_PORT}/" 60 || failed=1 else [ $failed -eq 0 ] && \ wait_for_docker_healthy "Logto + Bootstrap" "cameleer-logto" 300 || failed=1 [ $failed -eq 0 ] && \ check_endpoint "Cameleer SaaS" "https://localhost:${HTTPS_PORT}/platform/api/config" 120 || failed=1 [ $failed -eq 0 ] && \ check_endpoint "Traefik routing" "https://localhost:${HTTPS_PORT}/" 30 || failed=1 fi echo "" if [ $failed -ne 0 ]; then log_error "Installation verification failed. Stack is running — check logs." exit 1 fi log_success "All services healthy." } # --- Output file generation --- write_config_file() { local f="$INSTALL_DIR/cameleer.conf" cat > "$f" << EOF # Cameleer installation config # Generated by installer v${CAMELEER_INSTALLER_VERSION} on $(date -u '+%Y-%m-%d %H:%M:%S UTC') install_dir=${INSTALL_DIR} public_host=${PUBLIC_HOST} public_protocol=${PUBLIC_PROTOCOL} admin_user=${ADMIN_USER} tls_mode=${TLS_MODE} http_port=${HTTP_PORT} https_port=${HTTPS_PORT} logto_console_port=${LOGTO_CONSOLE_PORT} logto_console_exposed=${LOGTO_CONSOLE_EXPOSED} monitoring_network=${MONITORING_NETWORK} version=${VERSION} compose_project=${COMPOSE_PROJECT} docker_socket=${DOCKER_SOCKET} node_tls_reject=${NODE_TLS_REJECT} deployment_mode=${DEPLOYMENT_MODE} EOF log_info "Saved installer config to cameleer.conf" } generate_credentials_file() { local f="$INSTALL_DIR/credentials.txt" if [ "$DEPLOYMENT_MODE" = "standalone" ]; then cat > "$f" << EOF =========================================== CAMELEER SERVER CREDENTIALS Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC') SECURE THIS FILE AND DELETE AFTER NOTING THESE CREDENTIALS CANNOT BE RECOVERED =========================================== Server Dashboard: ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/ Admin User: ${ADMIN_USER} Admin Password: ${ADMIN_PASS} PostgreSQL: cameleer / ${POSTGRES_PASSWORD} ClickHouse: default / ${CLICKHOUSE_PASSWORD} EOF chmod 600 "$f" log_info "Saved credentials to credentials.txt" return fi cat > "$f" << EOF =========================================== CAMELEER PLATFORM CREDENTIALS Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC') SECURE THIS FILE AND DELETE AFTER NOTING THESE CREDENTIALS CANNOT BE RECOVERED =========================================== Admin Console: ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/platform/ Admin User: ${ADMIN_USER} Admin Password: ${ADMIN_PASS} PostgreSQL: cameleer / ${POSTGRES_PASSWORD} ClickHouse: default / ${CLICKHOUSE_PASSWORD} EOF if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then echo "Logto Console: ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}:${LOGTO_CONSOLE_PORT}" >> "$f" else echo "Logto Console: (not exposed)" >> "$f" fi chmod 600 "$f" log_info "Saved credentials to credentials.txt" } generate_install_doc() { if [ "$DEPLOYMENT_MODE" = "standalone" ]; then generate_install_doc_standalone return fi local f="$INSTALL_DIR/INSTALL.md" local tls_desc="Self-signed (auto-generated)" [ "$TLS_MODE" = "custom" ] && tls_desc="Custom certificate" cat > "$f" << EOF # Cameleer SaaS � Installation Documentation ## Installation Summary | | | |---|---| | **Version** | ${VERSION} | | **Date** | $(date -u '+%Y-%m-%d %H:%M:%S UTC') | | **Installer** | v${CAMELEER_INSTALLER_VERSION} | | **Install Directory** | ${INSTALL_DIR} | | **Hostname** | ${PUBLIC_HOST} | | **TLS** | ${tls_desc} | ## Service URLs - **Platform UI:** ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/platform/ - **API Endpoint:** ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/platform/api/ EOF if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then echo "- **Logto Admin Console:** ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}:${LOGTO_CONSOLE_PORT}" >> "$f" fi cat >> "$f" << 'EOF' ## First Steps 1. Open the Platform UI in your browser 2. Log in as admin with the credentials from `credentials.txt` 3. Create tenants from the admin console 4. The platform will provision a dedicated server instance for each tenant ## Architecture | Container | Purpose | |---|---| | `cameleer-traefik` | Reverse proxy, TLS termination, routing | | `cameleer-postgres` | PostgreSQL database (SaaS + Logto + tenant schemas) | | `cameleer-clickhouse` | Time-series storage (traces, metrics, logs) | | `cameleer-logto` | OIDC identity provider + bootstrap | | `cameleer-saas` | SaaS platform (Spring Boot + React) | Per-tenant `cameleer-server` and `cameleer-server-ui` containers are provisioned dynamically when tenants are created. ## Networking EOF cat >> "$f" << EOF | Port | Service | |---|---| | ${HTTP_PORT} | HTTP (redirects to HTTPS) | | ${HTTPS_PORT} | HTTPS (main entry point) | EOF if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then echo "| ${LOGTO_CONSOLE_PORT} | Logto Admin Console |" >> "$f" fi if [ -n "$MONITORING_NETWORK" ]; then cat >> "$f" << EOF ### Monitoring Services are connected to the \`${MONITORING_NETWORK}\` Docker network with Prometheus labels for auto-discovery. EOF fi cat >> "$f" << EOF ## TLS **Mode:** ${tls_desc} EOF if [ "$TLS_MODE" = "self-signed" ]; then cat >> "$f" << 'EOF' The platform generated a self-signed certificate on first boot. To replace it: 1. Log in as admin and navigate to **Certificates** in the admin console 2. Upload your certificate and key via the UI 3. Activate the new certificate (zero-downtime swap) EOF fi cat >> "$f" << EOF ## Data & Backups | Docker Volume | Contains | |---|---| | \`cameleer-pgdata\` | PostgreSQL data (tenants, licenses, audit) | | \`cameleer-chdata\` | ClickHouse data (traces, metrics, logs) | | \`cameleer-certs\` | TLS certificates | | \`cameleer-bootstrapdata\` | Logto bootstrap results | ### Backup Commands \`\`\`bash # PostgreSQL docker compose -p ${COMPOSE_PROJECT} exec cameleer-postgres pg_dump -U cameleer cameleer_saas > backup.sql # ClickHouse docker compose -p ${COMPOSE_PROJECT} exec cameleer-clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native \`\`\` ## Upgrading Re-run the installer with a new version: \`\`\`bash curl -sfL https://install.cameleer.io | bash -s -- --install-dir ${INSTALL_DIR} --version NEW_VERSION \`\`\` The installer preserves your \`.env\`, credentials, and data volumes. Only the compose file and images are updated. ## Troubleshooting | Issue | Command | |---|---| | Service not starting | \`docker compose -p ${COMPOSE_PROJECT} logs SERVICE_NAME\` | | Bootstrap failed | \`docker compose -p ${COMPOSE_PROJECT} logs cameleer-logto\` | | Routing issues | \`docker compose -p ${COMPOSE_PROJECT} logs cameleer-traefik\` | | Database issues | \`docker compose -p ${COMPOSE_PROJECT} exec cameleer-postgres psql -U cameleer -d cameleer_saas\` | ## Uninstalling \`\`\`bash # Stop and remove containers cd ${INSTALL_DIR} && docker compose -p ${COMPOSE_PROJECT} down # Remove data volumes (DESTRUCTIVE) cd ${INSTALL_DIR} && docker compose -p ${COMPOSE_PROJECT} down -v # Remove install directory rm -rf ${INSTALL_DIR} \`\`\` EOF log_info "Generated INSTALL.md" } generate_install_doc_standalone() { local f="$INSTALL_DIR/INSTALL.md" local tls_desc="Self-signed (auto-generated)" [ "$TLS_MODE" = "custom" ] && tls_desc="Custom certificate" cat > "$f" << EOF # Cameleer Server — Installation Documentation ## Installation Summary | | | |---|---| | **Version** | ${VERSION} | | **Date** | $(date -u '+%Y-%m-%d %H:%M:%S UTC') | | **Installer** | v${CAMELEER_INSTALLER_VERSION} | | **Mode** | Standalone (single-tenant) | | **Install Directory** | ${INSTALL_DIR} | | **Hostname** | ${PUBLIC_HOST} | | **TLS** | ${tls_desc} | ## Service URLs - **Server Dashboard:** ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/ - **API Endpoint:** ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/api/ ## First Steps 1. Open the Server Dashboard in your browser 2. Log in with the admin credentials from \`credentials.txt\` 3. Upload a Camel application JAR to deploy your first route 4. Monitor traces, metrics, and logs in the dashboard ## Architecture | Container | Purpose | |---|---| | \`cameleer-traefik\` | Reverse proxy, TLS termination, routing | | \`cameleer-postgres\` | PostgreSQL database (server data) | | \`cameleer-clickhouse\` | Time-series storage (traces, metrics, logs) | | \`cameleer-server\` | Cameleer Server (Spring Boot backend) | | \`cameleer-server-ui\` | Cameleer Dashboard (React frontend) | ## Networking | Port | Service | |---|---| | ${HTTP_PORT} | HTTP (redirects to HTTPS) | | ${HTTPS_PORT} | HTTPS (main entry point) | EOF if [ -n "$MONITORING_NETWORK" ]; then cat >> "$f" << EOF ### Monitoring Services are connected to the \`${MONITORING_NETWORK}\` Docker network for Prometheus auto-discovery. EOF fi cat >> "$f" << EOF ## TLS **Mode:** ${tls_desc} EOF if [ "$TLS_MODE" = "self-signed" ]; then cat >> "$f" << 'EOF' The platform generated a self-signed certificate on first boot. Replace it by placing your certificate and key files in the `certs/` directory and restarting. EOF fi cat >> "$f" << EOF ## Data & Backups | Docker Volume | Contains | |---|---| | \`cameleer-pgdata\` | PostgreSQL data (server config, routes, deployments) | | \`cameleer-chdata\` | ClickHouse data (traces, metrics, logs) | | \`cameleer-certs\` | TLS certificates | | \`jars\` | Uploaded application JARs | ### Backup Commands \`\`\`bash # PostgreSQL docker compose -p ${COMPOSE_PROJECT} exec cameleer-postgres pg_dump -U cameleer cameleer > backup.sql # ClickHouse docker compose -p ${COMPOSE_PROJECT} exec cameleer-clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native \`\`\` ## Upgrading Re-run the installer with a new version: \`\`\`bash curl -sfL https://install.cameleer.io | bash -s -- --install-dir ${INSTALL_DIR} --version NEW_VERSION \`\`\` The installer preserves your \`.env\`, credentials, and data volumes. Only the compose file and images are updated. ## Troubleshooting | Issue | Command | |---|---| | Service not starting | \`docker compose -p ${COMPOSE_PROJECT} logs SERVICE_NAME\` | | Server issues | \`docker compose -p ${COMPOSE_PROJECT} logs cameleer-server\` | | Routing issues | \`docker compose -p ${COMPOSE_PROJECT} logs cameleer-traefik\` | | Database issues | \`docker compose -p ${COMPOSE_PROJECT} exec cameleer-postgres psql -U cameleer -d cameleer\` | ## Uninstalling \`\`\`bash # Stop and remove containers cd ${INSTALL_DIR} && docker compose -p ${COMPOSE_PROJECT} down # Remove data volumes (DESTRUCTIVE) cd ${INSTALL_DIR} && docker compose -p ${COMPOSE_PROJECT} down -v # Remove install directory rm -rf ${INSTALL_DIR} \`\`\` EOF log_info "Generated INSTALL.md" } print_credentials() { echo "" echo -e "${BOLD}==========================================${NC}" if [ "$DEPLOYMENT_MODE" = "standalone" ]; then echo -e "${BOLD} CAMELEER SERVER CREDENTIALS${NC}" else echo -e "${BOLD} CAMELEER PLATFORM CREDENTIALS${NC}" fi echo -e "${BOLD}==========================================${NC}" echo "" if [ "$DEPLOYMENT_MODE" = "standalone" ]; then echo -e " Dashboard: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/${NC}" else echo -e " Admin Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/platform/${NC}" fi echo -e " Admin User: ${BOLD}${ADMIN_USER}${NC}" echo -e " Admin Password: ${BOLD}${ADMIN_PASS}${NC}" echo "" echo -e " PostgreSQL: cameleer / ${POSTGRES_PASSWORD}" echo -e " ClickHouse: default / ${CLICKHOUSE_PASSWORD}" echo "" if [ "$DEPLOYMENT_MODE" = "saas" ]; then if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then echo -e " Logto Console: ${BLUE}${PUBLIC_PROTOCOL}://${PUBLIC_HOST}:${LOGTO_CONSOLE_PORT}${NC}" echo "" fi fi echo -e " Credentials saved to: ${INSTALL_DIR}/credentials.txt" echo -e " ${YELLOW}Secure this file and delete after noting credentials.${NC}" echo "" } print_summary() { echo -e "${GREEN}==========================================${NC}" echo -e "${GREEN} Installation complete!${NC}" echo -e "${GREEN}==========================================${NC}" echo "" echo " Install directory: $INSTALL_DIR" echo " Documentation: $INSTALL_DIR/INSTALL.md" echo "" echo " To manage the stack:" echo " cd $INSTALL_DIR" echo " docker compose -p $COMPOSE_PROJECT ps # status" echo " docker compose -p $COMPOSE_PROJECT logs -f # logs" echo " docker compose -p $COMPOSE_PROJECT down # stop" echo "" } # --- Re-run and upgrade --- show_rerun_menu() { local current_version current_version=$(grep '^version=' "$INSTALL_DIR/cameleer.conf" 2>/dev/null | cut -d= -f2 || echo "unknown") local current_host current_host=$(grep '^public_host=' "$INSTALL_DIR/cameleer.conf" 2>/dev/null | cut -d= -f2 || echo "unknown") echo "" echo -e "${BOLD}Existing Cameleer installation detected (v${current_version})${NC}" echo " Install directory: $INSTALL_DIR" echo " Public host: $current_host" echo "" if [ "$MODE" = "silent" ]; then RERUN_ACTION="${RERUN_ACTION:-upgrade}" return fi if [ -n "$RERUN_ACTION" ]; then return; fi local new_version="${VERSION:-$CAMELEER_DEFAULT_VERSION}" echo " [1] Upgrade to v${new_version} (pull new images, update compose)" echo " [2] Reconfigure (re-run interactive setup, preserve data)" echo " [3] Reinstall (fresh install, WARNING: destroys data volumes)" echo " [4] Cancel" echo "" local choice read -rp " Select [1]: " choice case "${choice:-1}" in 1) RERUN_ACTION="upgrade" ;; 2) RERUN_ACTION="reconfigure" ;; 3) RERUN_ACTION="reinstall" ;; 4) echo "Cancelled."; exit 0 ;; *) echo "Invalid choice."; exit 1 ;; esac } handle_rerun() { case "$RERUN_ACTION" in upgrade) log_info "Upgrading installation..." load_config_file "$INSTALL_DIR/cameleer.conf" load_env_overrides merge_config copy_templates docker_compose_pull docker_compose_down docker_compose_up verify_health generate_install_doc print_summary exit 0 ;; reconfigure) log_info "Reconfiguring installation..." return ;; reinstall) if [ "${CONFIRM_DESTROY}" != "true" ]; then echo "" log_warn "This will destroy ALL data (databases, certificates, bootstrap)." if ! prompt_yesno "Are you sure? This cannot be undone."; then echo "Cancelled." exit 0 fi fi log_info "Reinstalling..." docker_compose_down 2>/dev/null || true (cd "$INSTALL_DIR" && docker compose -p "${COMPOSE_PROJECT:-cameleer-saas}" down -v 2>/dev/null || true) rm -f "$INSTALL_DIR/.env" "$INSTALL_DIR/.env.bak" "$INSTALL_DIR/.env.example" \ "$INSTALL_DIR/docker-compose.yml" "$INSTALL_DIR/docker-compose.saas.yml" \ "$INSTALL_DIR/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.tls.yml" \ "$INSTALL_DIR/docker-compose.monitoring.yml" "$INSTALL_DIR/traefik-dynamic.yml" \ "$INSTALL_DIR/cameleer.conf" "$INSTALL_DIR/credentials.txt" \ "$INSTALL_DIR/INSTALL.md" rm -rf "$INSTALL_DIR/certs" IS_RERUN=false return ;; esac } # --- Main --- main() { parse_args "$@" print_banner # Load config sources (CLI already loaded via parse_args) if [ -n "$CONFIG_FILE_PATH" ]; then load_config_file "$CONFIG_FILE_PATH" fi load_env_overrides # Check for existing installation detect_existing_install if [ "$IS_RERUN" = true ]; then show_rerun_menu handle_rerun fi # Prerequisites check_prerequisites # Auto-detect defaults auto_detect # Interactive prompts (unless silent) if [ "$MODE" != "silent" ]; then select_mode if [ "$MODE" = "expert" ]; then run_expert_prompts else run_simple_prompts fi fi # Merge remaining defaults and validate merge_config validate_config # Generate passwords for any empty values generate_passwords # Create install directory mkdir -p "$INSTALL_DIR" # Copy custom certs if provided if [ "$TLS_MODE" = "custom" ]; then copy_certs fi # Generate configuration files generate_env_file copy_templates write_config_file # Pull and start docker_compose_pull docker_compose_up # Verify health verify_health # Generate output files generate_credentials_file generate_install_doc # Print results print_credentials print_summary } main "$@"