diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/get-cameleer.ps1 b/get-cameleer.ps1 new file mode 100644 index 0000000..384c8fd --- /dev/null +++ b/get-cameleer.ps1 @@ -0,0 +1,57 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Bootstrap script — downloads the Cameleer installer and runs it. +.EXAMPLE + irm https://registry.cameleer.io/cameleer/cameleer-saas-installer/raw/branch/main/get-cameleer.ps1 | iex + .\get-cameleer.ps1 -Version v1.2.0 +#> +param( + [string]$Version, + [string]$Ref, + [switch]$Run +) + +$ErrorActionPreference = 'Stop' + +$Repo = 'https://registry.cameleer.io/cameleer/cameleer-saas-installer/raw' +if ($Version) { $RefPath = "tag/$Version" } +elseif ($Ref) { $RefPath = "branch/$Ref" } +else { $RefPath = 'branch/main' } + +$Base = "$Repo/$RefPath" +$Dir = '.\installer' + +$Files = @( + 'install.sh' + 'templates/docker-compose.yml' + 'templates/docker-compose.saas.yml' + 'templates/docker-compose.server.yml' + 'templates/docker-compose.tls.yml' + 'templates/docker-compose.monitoring.yml' + 'templates/traefik-dynamic.yml' + 'templates/.env.example' +) + +Write-Host 'Downloading Cameleer installer...' + +New-Item -ItemType Directory -Path "$Dir\templates" -Force | Out-Null + +foreach ($file in $Files) { + $localPath = Join-Path $Dir $file + $localDir = Split-Path $localPath -Parent + if (-not (Test-Path $localDir)) { New-Item -ItemType Directory -Path $localDir -Force | Out-Null } + + Write-Host " $file" + Invoke-WebRequest -Uri "$Base/$file" -OutFile $localPath -UseBasicParsing +} + +Write-Host '' +Write-Host "Installer ready in $Dir\" +Write-Host 'Run: cd installer; .\install.sh' +Write-Host '' + +if ($Run) { + Set-Location $Dir + & .\install.sh @args +} diff --git a/get-cameleer.sh b/get-cameleer.sh new file mode 100644 index 0000000..9240cd7 --- /dev/null +++ b/get-cameleer.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bootstrap script — downloads the Cameleer installer and runs it. +# Usage: +# curl -fsSL https://get.cameleer.io/install | bash +# curl -fsSL https://get.cameleer.io/install | bash -s -- --version v1.2.0 + +REPO="https://registry.cameleer.io/cameleer/cameleer-saas-installer/raw" +REF="branch/main" +DIR="./installer" + +# Parse --version / --ref +for arg in "$@"; do + case "$arg" in + --version=*) REF="tag/${arg#*=}"; shift ;; + --ref=*) REF="branch/${arg#*=}"; shift ;; + esac +done + +BASE="$REPO/$REF" + +FILES=( + "install.sh" + "templates/docker-compose.yml" + "templates/docker-compose.saas.yml" + "templates/docker-compose.server.yml" + "templates/docker-compose.tls.yml" + "templates/docker-compose.monitoring.yml" + "templates/traefik-dynamic.yml" + "templates/.env.example" +) + +echo "Downloading Cameleer installer..." + +mkdir -p "$DIR/templates" + +for file in "${FILES[@]}"; do + echo " $file" + curl -fsSL "$BASE/$file" -o "$DIR/$file" +done + +chmod +x "$DIR/install.sh" + +echo "" +echo "Installer ready in $DIR/" +echo "Run: cd $DIR && ./install.sh" +echo "" + +# Auto-run if not piped with extra args that look like they want manual control +if [ "${1:-}" = "--run" ]; then + shift + cd "$DIR" + exec ./install.sh "$@" +fi diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..8586d31 --- /dev/null +++ b/install.sh @@ -0,0 +1,1525 @@ +#!/usr/bin/env bash +set -euo pipefail + +CAMELEER_INSTALLER_VERSION="1.0.0" +CAMELEER_DEFAULT_VERSION="latest" +DEFAULT_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_AUTH_HOST="${AUTH_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:-}" +_ENV_SMTP_HOST="${SMTP_HOST:-}" +_ENV_SMTP_PORT="${SMTP_PORT:-}" +_ENV_SMTP_USER="${SMTP_USER:-}" +_ENV_SMTP_PASS="${SMTP_PASS:-}" +_ENV_SMTP_FROM_EMAIL="${SMTP_FROM_EMAIL:-}" +_ENV_REGISTRY="${REGISTRY:-}" +_ENV_REGISTRY_USER="${REGISTRY_USER:-}" +_ENV_REGISTRY_TOKEN="${REGISTRY_TOKEN:-}" + +INSTALL_DIR="" +PUBLIC_HOST="" +AUTH_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="" +SMTP_HOST="" +SMTP_PORT="" +SMTP_USER="" +SMTP_PASS="" +SMTP_FROM_EMAIL="" +REGISTRY="" +REGISTRY_USER="" +REGISTRY_TOKEN="" + +# --- 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 ;; + --auth-host) AUTH_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 ;; + --smtp-host) SMTP_HOST="$2"; shift ;; + --smtp-port) SMTP_PORT="$2"; shift ;; + --smtp-user) SMTP_USER="$2"; shift ;; + --smtp-pass) SMTP_PASS="$2"; shift ;; + --smtp-from-email) SMTP_FROM_EMAIL="$2"; shift ;; + --registry) REGISTRY="$2"; shift ;; + --registry-user) REGISTRY_USER="$2"; shift ;; + --registry-token) REGISTRY_TOKEN="$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 " --auth-host HOST Auth domain for Logto (default: same as public-host)" + 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 "Registry options:" + echo " --registry REGISTRY Image registry (default: gitea.siegeln.net/cameleer)" + echo " --registry-user USER Registry username for docker login" + echo " --registry-token TOKEN Registry token/password for docker login" + 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" ;; + auth_host) [ -z "$AUTH_HOST" ] && AUTH_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" ;; + smtp_host) [ -z "$SMTP_HOST" ] && SMTP_HOST="$value" ;; + smtp_port) [ -z "$SMTP_PORT" ] && SMTP_PORT="$value" ;; + smtp_user) [ -z "$SMTP_USER" ] && SMTP_USER="$value" ;; + smtp_pass) [ -z "$SMTP_PASS" ] && SMTP_PASS="$value" ;; + smtp_from_email) [ -z "$SMTP_FROM_EMAIL" ] && SMTP_FROM_EMAIL="$value" ;; + registry) [ -z "$REGISTRY" ] && REGISTRY="$value" ;; + registry_user) [ -z "$REGISTRY_USER" ] && REGISTRY_USER="$value" ;; + registry_token) [ -z "$REGISTRY_TOKEN" ] && REGISTRY_TOKEN="$value" ;; + esac + done < "$file" +} + +load_env_overrides() { + [ -z "$INSTALL_DIR" ] && INSTALL_DIR="${CAMELEER_INSTALL_DIR:-}" + [ -z "$PUBLIC_HOST" ] && PUBLIC_HOST="$_ENV_PUBLIC_HOST" + [ -z "$AUTH_HOST" ] && AUTH_HOST="$_ENV_AUTH_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" + [ -z "$SMTP_HOST" ] && SMTP_HOST="$_ENV_SMTP_HOST" + [ -z "$SMTP_PORT" ] && SMTP_PORT="$_ENV_SMTP_PORT" + [ -z "$SMTP_USER" ] && SMTP_USER="$_ENV_SMTP_USER" + [ -z "$SMTP_PASS" ] && SMTP_PASS="$_ENV_SMTP_PASS" + [ -z "$SMTP_FROM_EMAIL" ] && SMTP_FROM_EMAIL="$_ENV_SMTP_FROM_EMAIL" + [ -z "$REGISTRY" ] && REGISTRY="$_ENV_REGISTRY" + [ -z "$REGISTRY_USER" ] && REGISTRY_USER="$_ENV_REGISTRY_USER" + [ -z "$REGISTRY_TOKEN" ] && REGISTRY_TOKEN="$_ENV_REGISTRY_TOKEN" +} + +# --- 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 "" + if prompt_yesno "Pull images from a private registry?"; then + prompt REGISTRY "Registry" "${REGISTRY:-$DEFAULT_REGISTRY}" + prompt REGISTRY_USER "Registry username" "${REGISTRY_USER:-}" + prompt_password REGISTRY_TOKEN "Registry token/password" "${REGISTRY_TOKEN:-}" + fi + + 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 + + # SMTP for email verification (SaaS mode only) + if [ "$DEPLOYMENT_MODE" = "saas" ]; then + echo "" + if prompt_yesno "Configure SMTP for email verification? (required for self-service sign-up)"; then + prompt SMTP_HOST "SMTP host" "${SMTP_HOST:-}" + prompt SMTP_PORT "SMTP port" "${SMTP_PORT:-587}" + prompt SMTP_USER "SMTP username" "${SMTP_USER:-}" + prompt_password SMTP_PASS "SMTP password" "${SMTP_PASS:-}" + prompt SMTP_FROM_EMAIL "From email address" "${SMTP_FROM_EMAIL:-noreply@${PUBLIC_HOST}}" + fi + fi +} + +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}" + prompt AUTH_HOST "Auth domain (Logto) — same as hostname for single-domain" "${AUTH_HOST:-$PUBLIC_HOST}" + 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}" + : "${REGISTRY:=$DEFAULT_REGISTRY}" + + if [ "$DEPLOYMENT_MODE" = "standalone" ]; then + : "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT_STANDALONE}" + else + : "${COMPOSE_PROJECT:=$DEFAULT_COMPOSE_PROJECT}" + fi + + # Default AUTH_HOST to PUBLIC_HOST (single-domain setup) + : "${AUTH_HOST:=$PUBLIC_HOST}" + + # Force lowercase hostnames — Logto normalizes internally, case mismatch breaks JWT validation + PUBLIC_HOST=$(echo "$PUBLIC_HOST" | tr '[:upper:]' '[:lower:]') + AUTH_HOST=$(echo "$AUTH_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 + +# Registry +TRAEFIK_IMAGE=${REGISTRY}/cameleer-traefik +CLICKHOUSE_IMAGE=${REGISTRY}/cameleer-clickhouse +SERVER_IMAGE=${REGISTRY}/cameleer-server +SERVER_UI_IMAGE=${REGISTRY}/cameleer-server-ui + +# 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} +AUTH_HOST=${AUTH_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") + +# Registry +TRAEFIK_IMAGE=${REGISTRY}/cameleer-traefik +POSTGRES_IMAGE=${REGISTRY}/cameleer-postgres +CLICKHOUSE_IMAGE=${REGISTRY}/cameleer-clickhouse +LOGTO_IMAGE=${REGISTRY}/cameleer-logto +CAMELEER_IMAGE=${REGISTRY}/cameleer-saas + +# 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) + +# SMTP (for email verification during registration) +SMTP_HOST=${SMTP_HOST} +SMTP_PORT=${SMTP_PORT:-587} +SMTP_USER=${SMTP_USER} +SMTP_PASS=${SMTP_PASS} +SMTP_FROM_EMAIL=${SMTP_FROM_EMAIL:-noreply@${PUBLIC_HOST}} + +# 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_registry_login() { + if [ -n "$REGISTRY_USER" ] && [ -n "$REGISTRY_TOKEN" ]; then + local registry_host + registry_host=$(echo "$REGISTRY" | cut -d/ -f1) + log_info "Logging in to registry ${registry_host}..." + echo "$REGISTRY_TOKEN" | docker login "$registry_host" -u "$REGISTRY_USER" --password-stdin + log_success "Registry login successful." + fi +} + +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}" resolve="${4:-}" + local start_time=$(date +%s) + local extra_flags="" + [ -n "$resolve" ] && extra_flags="--resolve $resolve" + + 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 $extra_flags -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://${PUBLIC_HOST}:${HTTPS_PORT}/" 30 "${PUBLIC_HOST}:${HTTPS_PORT}:127.0.0.1" || 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} +auth_host=${AUTH_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} +smtp_host=${SMTP_HOST} +smtp_port=${SMTP_PORT} +smtp_user=${SMTP_USER} +smtp_pass=${SMTP_PASS} +smtp_from_email=${SMTP_FROM_EMAIL} +registry=${REGISTRY} +registry_user=${REGISTRY_USER} +registry_token=${REGISTRY_TOKEN} +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}://${AUTH_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}://${AUTH_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}://${AUTH_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..." + # Config already loaded by detect_existing_install + load_env_overrides in main. + # Calling load_config_file again with set -e causes silent exit because + # [ -z "$VAR" ] && VAR="$value" returns 1 for every already-set variable. + merge_config + copy_templates + docker_registry_login + 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_registry_login + 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 "$@" diff --git a/templates/.env.example b/templates/.env.example new file mode 100644 index 0000000..f5618a3 --- /dev/null +++ b/templates/.env.example @@ -0,0 +1,102 @@ +# Cameleer Configuration +# Copy this file to .env and fill in the values. +# The installer generates .env automatically — this file is for reference. + +# ============================================================ +# Compose file assembly (set by installer) +# ============================================================ +# SaaS: docker-compose.yml:docker-compose.saas.yml +# Standalone: docker-compose.yml:docker-compose.server.yml +# Add :docker-compose.tls.yml for custom TLS certificates +# Add :docker-compose.monitoring.yml for external monitoring network +COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml + +# ============================================================ +# Image version +# ============================================================ +VERSION=latest + +# ============================================================ +# Public access +# ============================================================ +PUBLIC_HOST=localhost +PUBLIC_PROTOCOL=https +# Auth domain (Logto). Defaults to PUBLIC_HOST for single-domain setups. +# Set to a separate subdomain (e.g. auth.cameleer.io) to split auth from the app. +# AUTH_HOST=localhost + +# ============================================================ +# Ports +# ============================================================ +HTTP_PORT=80 +HTTPS_PORT=443 +# Set to 0.0.0.0 to expose Logto admin console externally (default: localhost only) +# LOGTO_CONSOLE_BIND=0.0.0.0 +LOGTO_CONSOLE_PORT=3002 + +# ============================================================ +# PostgreSQL +# ============================================================ +POSTGRES_USER=cameleer +POSTGRES_PASSWORD=CHANGE_ME +# SaaS: cameleer_saas, Standalone: cameleer +POSTGRES_DB=cameleer_saas + +# ============================================================ +# ClickHouse +# ============================================================ +CLICKHOUSE_PASSWORD=CHANGE_ME + +# ============================================================ +# Admin credentials (SaaS mode) +# ============================================================ +SAAS_ADMIN_USER=admin +SAAS_ADMIN_PASS=CHANGE_ME + +# ============================================================ +# Admin credentials (standalone mode) +# ============================================================ +# SERVER_ADMIN_USER=admin +# SERVER_ADMIN_PASS=CHANGE_ME +# BOOTSTRAP_TOKEN=CHANGE_ME + +# ============================================================ +# SMTP (for email verification during registration) +# ============================================================ +# Required for self-service sign-up. Without SMTP, only admin-created users can sign in. +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= +SMTP_FROM_EMAIL=noreply@cameleer.io + +# ============================================================ +# TLS +# ============================================================ +# Set to 1 to reject unauthorized TLS certificates (production) +NODE_TLS_REJECT=0 +# Custom TLS certificate paths (inside container, set by installer) +# CERT_FILE=/user-certs/cert.pem +# KEY_FILE=/user-certs/key.pem +# CA_FILE=/user-certs/ca.pem + +# ============================================================ +# Docker +# ============================================================ +DOCKER_SOCKET=/var/run/docker.sock +# GID of the docker socket — detected by installer, used for container group_add +DOCKER_GID=0 + +# ============================================================ +# Provisioning images (SaaS mode only) +# ============================================================ +# CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest +# CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest +# CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=gitea.siegeln.net/cameleer/cameleer-runtime-base:latest + +# ============================================================ +# Monitoring (optional) +# ============================================================ +# External Docker network name for Prometheus scraping. +# Only needed when docker-compose.monitoring.yml is in COMPOSE_FILE. +# MONITORING_NETWORK=prometheus diff --git a/templates/docker-compose.monitoring.yml b/templates/docker-compose.monitoring.yml new file mode 100644 index 0000000..ddbb723 --- /dev/null +++ b/templates/docker-compose.monitoring.yml @@ -0,0 +1,7 @@ +# External monitoring network overlay +# Overrides the noop monitoring bridge with a real external network + +networks: + monitoring: + external: true + name: ${MONITORING_NETWORK:?MONITORING_NETWORK must be set in .env} diff --git a/templates/docker-compose.saas.yml b/templates/docker-compose.saas.yml new file mode 100644 index 0000000..5b95e2b --- /dev/null +++ b/templates/docker-compose.saas.yml @@ -0,0 +1,131 @@ +# Cameleer SaaS — Logto + management plane +# Loaded in SaaS deployment mode + +services: + cameleer-logto: + image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest} + restart: unless-stopped + depends_on: + cameleer-postgres: + condition: service_healthy + environment: + DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@cameleer-postgres:5432/logto + ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost} + ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002} + TRUST_PROXY_HEADER: 1 + NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}" + LOGTO_ENDPOINT: http://cameleer-logto:3001 + LOGTO_ADMIN_ENDPOINT: http://cameleer-logto:3002 + LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost} + PUBLIC_HOST: ${PUBLIC_HOST:-localhost} + AUTH_HOST: ${AUTH_HOST:-localhost} + PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https} + PG_HOST: cameleer-postgres + PG_USER: ${POSTGRES_USER:-cameleer} + PG_PASSWORD: ${POSTGRES_PASSWORD} + PG_DB_SAAS: cameleer_saas + SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin} + SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env} + # SMTP (for email verification during registration) + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASS: ${SMTP_PASS:-} + SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-noreply@cameleer.io} + extra_hosts: + # Logto validates M2M tokens by fetching its own JWKS from ENDPOINT. + # Route the public hostname back to the Docker host (Traefik on :443) + # so the container can reach itself without going through the tunnel. + - "${AUTH_HOST:-localhost}:host-gateway" + healthcheck: + test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 30s + labels: + - traefik.enable=true + - "traefik.http.routers.cameleer-logto.rule=Host(`${AUTH_HOST:-localhost}`)" + - traefik.http.routers.cameleer-logto.priority=1 + - traefik.http.routers.cameleer-logto.entrypoints=websecure + - traefik.http.routers.cameleer-logto.tls=true + - traefik.http.routers.cameleer-logto.service=cameleer-logto + - traefik.http.routers.cameleer-logto.middlewares=cameleer-logto-cors + - "traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}" + - traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS + - traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type + - traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowCredentials=true + - traefik.http.services.cameleer-logto.loadbalancer.server.port=3001 + - traefik.http.routers.cameleer-logto-console.rule=PathPrefix(`/`) + - traefik.http.routers.cameleer-logto-console.entrypoints=admin-console + - traefik.http.routers.cameleer-logto-console.tls=true + - traefik.http.routers.cameleer-logto-console.service=cameleer-logto-console + - traefik.http.services.cameleer-logto-console.loadbalancer.server.port=3002 + volumes: + - cameleer-bootstrapdata:/data + networks: + - cameleer + - monitoring + + cameleer-saas: + image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest} + restart: unless-stopped + depends_on: + cameleer-logto: + condition: service_healthy + environment: + # SaaS database + SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/cameleer_saas + SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer} + SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} + # Identity (Logto) + CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://cameleer-logto:3001 + CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost} + CAMELEER_SAAS_IDENTITY_AUTHHOST: ${AUTH_HOST:-localhost} + # Provisioning — passed to per-tenant server containers + CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost} + CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https} + CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer + CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik + CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer} + CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD} + CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD} + CAMELEER_SERVER_SECURITY_JWTSECRET: ${CAMELEER_SERVER_SECURITY_JWTSECRET:?CAMELEER_SERVER_SECURITY_JWTSECRET must be set in .env} + CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer-server:latest} + CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui:latest} + CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE: ${CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE:-gitea.siegeln.net/cameleer/cameleer-runtime-base:latest} + labels: + - traefik.enable=true + - traefik.http.routers.saas.rule=PathPrefix(`/platform`) + - traefik.http.routers.saas.entrypoints=websecure + - traefik.http.routers.saas.tls=true + - traefik.http.services.saas.loadbalancer.server.port=8080 + # Root redirect: / → /platform/ (scoped to app host so it doesn't catch auth domain) + - "traefik.http.routers.saas-root.rule=Host(`${PUBLIC_HOST:-localhost}`) && Path(`/`)" + - traefik.http.routers.saas-root.priority=100 + - traefik.http.routers.saas-root.entrypoints=websecure + - traefik.http.routers.saas-root.tls=true + - traefik.http.routers.saas-root.middlewares=root-to-platform + - traefik.http.routers.saas-root.service=saas + - "traefik.http.middlewares.root-to-platform.redirectRegex.regex=^(https?://[^/]+)/?$$" + - "traefik.http.middlewares.root-to-platform.redirectRegex.replacement=$${1}/platform/" + - traefik.http.middlewares.root-to-platform.redirectRegex.permanent=false + - "prometheus.io/scrape=true" + - "prometheus.io/port=8080" + - "prometheus.io/path=/platform/actuator/prometheus" + volumes: + - cameleer-bootstrapdata:/data/bootstrap:ro + - cameleer-certs:/certs + - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock + group_add: + - "${DOCKER_GID:-0}" + networks: + - cameleer + - monitoring + +volumes: + cameleer-bootstrapdata: + +networks: + monitoring: + name: cameleer-monitoring-noop diff --git a/templates/docker-compose.server.yml b/templates/docker-compose.server.yml new file mode 100644 index 0000000..ff8f78b --- /dev/null +++ b/templates/docker-compose.server.yml @@ -0,0 +1,99 @@ +# Cameleer Server (standalone) +# Loaded in standalone deployment mode + +services: + cameleer-traefik: + volumes: + - ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro + + cameleer-postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-cameleer} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer}"] + + cameleer-server: + image: ${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:${VERSION:-latest} + container_name: cameleer-server + restart: unless-stopped + depends_on: + cameleer-postgres: + condition: service_healthy + environment: + CAMELEER_SERVER_TENANT_ID: default + SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/${POSTGRES_DB:-cameleer}?currentSchema=tenant_default + SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer} + SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} + CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://cameleer-clickhouse:8123/cameleer + CAMELEER_SERVER_CLICKHOUSE_USERNAME: default + CAMELEER_SERVER_CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} + CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: ${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN must be set in .env} + CAMELEER_SERVER_SECURITY_JWTSECRET: ${CAMELEER_SERVER_SECURITY_JWTSECRET:?CAMELEER_SERVER_SECURITY_JWTSECRET must be set in .env} + CAMELEER_SERVER_SECURITY_UIUSER: ${SERVER_ADMIN_USER:-admin} + CAMELEER_SERVER_SECURITY_UIPASSWORD: ${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env} + CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost} + CAMELEER_SERVER_RUNTIME_ENABLED: "true" + CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081 + CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: ${PUBLIC_HOST:-localhost} + CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path + CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars + CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps + CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars + CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest} + labels: + - traefik.enable=true + - traefik.http.routers.server-api.rule=PathPrefix(`/api`) + - traefik.http.routers.server-api.entrypoints=websecure + - traefik.http.routers.server-api.tls=true + - traefik.http.services.server-api.loadbalancer.server.port=8081 + - traefik.docker.network=cameleer-traefik + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 30s + volumes: + - jars:/data/jars + - cameleer-certs:/certs:ro + - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock + group_add: + - "${DOCKER_GID:-0}" + networks: + - cameleer + - cameleer-traefik + - cameleer-apps + - monitoring + + cameleer-server-ui: + image: ${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui}:${VERSION:-latest} + restart: unless-stopped + depends_on: + cameleer-server: + condition: service_healthy + environment: + CAMELEER_API_URL: http://cameleer-server:8081 + BASE_PATH: "" + labels: + - traefik.enable=true + - traefik.http.routers.ui.rule=PathPrefix(`/`) + - traefik.http.routers.ui.priority=1 + - traefik.http.routers.ui.entrypoints=websecure + - traefik.http.routers.ui.tls=true + - traefik.http.services.ui.loadbalancer.server.port=80 + - traefik.docker.network=cameleer-traefik + networks: + - cameleer-traefik + - monitoring + +volumes: + jars: + name: cameleer-jars + +networks: + cameleer-apps: + name: cameleer-apps + driver: bridge + monitoring: + name: cameleer-monitoring-noop diff --git a/templates/docker-compose.tls.yml b/templates/docker-compose.tls.yml new file mode 100644 index 0000000..489d08c --- /dev/null +++ b/templates/docker-compose.tls.yml @@ -0,0 +1,7 @@ +# Custom TLS certificates overlay +# Adds user-supplied certificate volume to traefik + +services: + cameleer-traefik: + volumes: + - ./certs:/user-certs:ro diff --git a/templates/docker-compose.yml b/templates/docker-compose.yml new file mode 100644 index 0000000..dd60d37 --- /dev/null +++ b/templates/docker-compose.yml @@ -0,0 +1,80 @@ +# Cameleer Infrastructure +# Shared base — always loaded. Mode-specific services in separate compose files. + +services: + cameleer-traefik: + image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest} + restart: unless-stopped + ports: + - "${HTTP_PORT:-80}:80" + - "${HTTPS_PORT:-443}:443" + - "${LOGTO_CONSOLE_BIND:-127.0.0.1}:${LOGTO_CONSOLE_PORT:-3002}:3002" + environment: + PUBLIC_HOST: ${PUBLIC_HOST:-localhost} + AUTH_HOST: ${AUTH_HOST:-localhost} + CERT_FILE: ${CERT_FILE:-} + KEY_FILE: ${KEY_FILE:-} + CA_FILE: ${CA_FILE:-} + volumes: + - cameleer-certs:/certs + - ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro + labels: + - "prometheus.io/scrape=true" + - "prometheus.io/port=8082" + - "prometheus.io/path=/metrics" + networks: + - cameleer + - cameleer-traefik + - monitoring + + cameleer-postgres: + image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest} + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas} + POSTGRES_USER: ${POSTGRES_USER:-cameleer} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env} + volumes: + - cameleer-pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer_saas}"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - cameleer + - monitoring + + cameleer-clickhouse: + image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest} + restart: unless-stopped + environment: + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD must be set in .env} + volumes: + - cameleer-chdata:/var/lib/clickhouse + healthcheck: + test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"] + interval: 10s + timeout: 5s + retries: 3 + labels: + - "prometheus.io/scrape=true" + - "prometheus.io/port=9363" + - "prometheus.io/path=/metrics" + networks: + - cameleer + - monitoring + +volumes: + cameleer-pgdata: + cameleer-chdata: + cameleer-certs: + +networks: + cameleer: + driver: bridge + cameleer-traefik: + name: cameleer-traefik + driver: bridge + monitoring: + name: cameleer-monitoring-noop diff --git a/templates/traefik-dynamic.yml b/templates/traefik-dynamic.yml new file mode 100644 index 0000000..b2a8787 --- /dev/null +++ b/templates/traefik-dynamic.yml @@ -0,0 +1,6 @@ +tls: + stores: + default: + defaultCertificate: + certFile: /certs/cert.pem + keyFile: /certs/key.pem