From 0b8cdf6dd982203d41c2e0eba085da5a429fefc4 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:59:44 +0200 Subject: [PATCH] refactor: move installer to dedicated repo, wire as git submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installer (install.sh, templates/, bootstrap scripts) now lives in cameleer/cameleer-saas-installer (public repo). Added as a git submodule at installer/ so compose templates remain the single source of truth. Dev compose is now a thin overlay (ports + volume mount + dev env vars). Production templates are chained via COMPOSE_FILE in .env: installer/templates/docker-compose.yml installer/templates/docker-compose.saas.yml docker-compose.yml (dev overrides) No code duplication — fixes to compose templates go to the installer repo and propagate to both production deployments and dev via submodule. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitmodules | 3 + docker-compose.dev.yml | 37 - docker-compose.yml | 182 +- get-cameleer.ps1 | 64 - get-cameleer.sh | 57 - installer | 1 + installer/CLAUDE.md | 49 - installer/install.ps1 | 1551 ----------------- installer/install.sh | 1525 ---------------- installer/templates/.env.example | 102 -- .../templates/docker-compose.monitoring.yml | 7 - installer/templates/docker-compose.saas.yml | 131 -- installer/templates/docker-compose.server.yml | 99 -- installer/templates/docker-compose.tls.yml | 7 - installer/templates/docker-compose.yml | 80 - installer/templates/traefik-dynamic.yml | 6 - 16 files changed, 17 insertions(+), 3884 deletions(-) create mode 100644 .gitmodules delete mode 100644 docker-compose.dev.yml delete mode 100644 get-cameleer.ps1 delete mode 100644 get-cameleer.sh create mode 160000 installer delete mode 100644 installer/CLAUDE.md delete mode 100644 installer/install.ps1 delete mode 100644 installer/install.sh delete mode 100644 installer/templates/.env.example delete mode 100644 installer/templates/docker-compose.monitoring.yml delete mode 100644 installer/templates/docker-compose.saas.yml delete mode 100644 installer/templates/docker-compose.server.yml delete mode 100644 installer/templates/docker-compose.tls.yml delete mode 100644 installer/templates/docker-compose.yml delete mode 100644 installer/templates/traefik-dynamic.yml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1cc067a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "installer"] + path = installer + url = https://gitea.siegeln.net/cameleer/cameleer-saas-installer.git diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index b5a8f55..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Development overrides: exposes ports for direct access -# Usage: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -services: - cameleer-postgres: - ports: - - "5432:5432" - - cameleer-logto: - ports: - - "3001:3001" - - logto-bootstrap: - environment: - VENDOR_SEED_ENABLED: "true" - - cameleer-saas: - ports: - - "8080:8080" - volumes: - - ./ui/dist:/app/static - - /var/run/docker.sock:/var/run/docker.sock - group_add: - - "0" - environment: - SPRING_PROFILES_ACTIVE: dev - SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/ - CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost} - CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https} - CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: gitea.siegeln.net/cameleer/cameleer-server:${VERSION:-latest} - CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: gitea.siegeln.net/cameleer/cameleer-server-ui:${VERSION:-latest} - CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest} - CAMELEER_SAAS_PROVISIONING_NETWORKNAME: cameleer-saas_cameleer - CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik - - cameleer-clickhouse: - ports: - - "8123:8123" diff --git a/docker-compose.yml b/docker-compose.yml index 3be0ba5..6035a82 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,179 +1,23 @@ +# Dev overrides — layered on top of installer/templates/ via COMPOSE_FILE in .env +# Usage: docker compose up (reads .env automatically) 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_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 - - /var/run/docker.sock:/var/run/docker.sock:ro - networks: - - cameleer - - cameleer-traefik - 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:-cameleer_dev} - 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 + ports: + - "5432:5432" cameleer-clickhouse: - image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest} - restart: unless-stopped - environment: - CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch} - volumes: - - cameleer-chdata:/var/lib/clickhouse - healthcheck: - test: ["CMD-SHELL", "clickhouse-client --password ${CLICKHOUSE_PASSWORD:-cameleer_ch} --query 'SELECT 1'"] - interval: 10s - timeout: 5s - retries: 3 - labels: - - prometheus.scrape=true - - prometheus.path=/metrics - - prometheus.port=9363 - networks: - - cameleer + ports: + - "8123:8123" 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_dev}@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:-cameleer_dev} - PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas} - SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin} - SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin} - # 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: - - "${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 + ports: + - "3001:3001" cameleer-saas: - image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest} - restart: unless-stopped - depends_on: - cameleer-logto: - condition: service_healthy + ports: + - "8080:8080" volumes: - - cameleer-bootstrapdata:/data/bootstrap:ro - - cameleer-certs:/certs - - /var/run/docker.sock:/var/run/docker.sock + - ./ui/dist:/app/static environment: - # SaaS database - SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/${POSTGRES_DB:-cameleer_saas} - SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer} - SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev} - # Identity (Logto) - CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: ${LOGTO_ENDPOINT:-http://cameleer-logto:3001} - CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${AUTH_HOST:-localhost} - CAMELEER_SAAS_IDENTITY_AUTHHOST: ${AUTH_HOST:-localhost} - CAMELEER_SAAS_IDENTITY_M2MCLIENTID: ${LOGTO_M2M_CLIENT_ID:-} - CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET: ${LOGTO_M2M_CLIENT_SECRET:-} - CAMELEER_SERVER_SECURITY_JWTSECRET: ${CAMELEER_SERVER_SECURITY_JWTSECRET:-cameleer-dev-jwt-secret} - # Provisioning — passed to per-tenant server containers - CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost} - CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https} - CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer} - CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev} - CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch} - 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 - group_add: - - "${DOCKER_GID:-0}" - networks: - - cameleer - -networks: - cameleer: - driver: bridge - cameleer-traefik: - name: cameleer-traefik - driver: bridge - -volumes: - cameleer-pgdata: - cameleer-chdata: - cameleer-certs: - cameleer-bootstrapdata: + SPRING_PROFILES_ACTIVE: dev + SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/ diff --git a/get-cameleer.ps1 b/get-cameleer.ps1 deleted file mode 100644 index 669aa61..0000000 --- a/get-cameleer.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -#Requires -Version 5.1 -<# -.SYNOPSIS - Bootstrap script — downloads the Cameleer installer and runs it. -.EXAMPLE - irm https://gitea.siegeln.net/cameleer/cameleer-saas/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://gitea.siegeln.net/cameleer/cameleer-saas/raw' -if ($Version) { $RefPath = "tag/$Version" } -elseif ($Ref) { $RefPath = "branch/$Ref" } -else { $RefPath = 'branch/main' } - -$Base = "$Repo/$RefPath" -$Dir = '.\installer' - -$Files = @( - 'installer/install.sh' - 'installer/install.ps1' - 'installer/templates/docker-compose.yml' - 'installer/templates/docker-compose.saas.yml' - 'installer/templates/docker-compose.server.yml' - 'installer/templates/docker-compose.tls.yml' - 'installer/templates/docker-compose.monitoring.yml' - 'installer/templates/traefik-dynamic.yml' - 'installer/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 -replace '^installer/', '') - $localDir = Split-Path $localPath -Parent - if (-not (Test-Path $localDir)) { New-Item -ItemType Directory -Path $localDir -Force | Out-Null } - - Write-Host " $($file -replace '^installer/', '')" - try { - Invoke-WebRequest -Uri "$Base/$file" -OutFile $localPath -UseBasicParsing - } catch { - # install.ps1 may not exist yet — skip gracefully - if ($file -match 'install\.ps1$') { continue } - throw - } -} - -Write-Host '' -Write-Host "Installer ready in $Dir\" -Write-Host 'Run: cd installer; .\install.ps1' -Write-Host '' - -if ($Run) { - Set-Location $Dir - & .\install.ps1 @args -} diff --git a/get-cameleer.sh b/get-cameleer.sh deleted file mode 100644 index 5ed6130..0000000 --- a/get-cameleer.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/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 -# wget -qO- https://gitea.siegeln.net/cameleer/cameleer-saas/raw/branch/main/get-cameleer.sh | bash - -REPO="https://gitea.siegeln.net/cameleer/cameleer-saas/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=( - "installer/install.sh" - "installer/templates/docker-compose.yml" - "installer/templates/docker-compose.saas.yml" - "installer/templates/docker-compose.server.yml" - "installer/templates/docker-compose.tls.yml" - "installer/templates/docker-compose.monitoring.yml" - "installer/templates/traefik-dynamic.yml" - "installer/templates/.env.example" -) - -echo "Downloading Cameleer installer..." - -mkdir -p "$DIR/templates" - -for file in "${FILES[@]}"; do - local_path="$DIR/${file#installer/}" - echo " ${file#installer/}" - curl -fsSL "$BASE/$file" -o "$local_path" -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/installer b/installer new file mode 160000 index 0000000..afbef27 --- /dev/null +++ b/installer @@ -0,0 +1 @@ +Subproject commit afbef2737a78ca1ba98a43dc602f2e7c78298ca6 diff --git a/installer/CLAUDE.md b/installer/CLAUDE.md deleted file mode 100644 index 3c9a2af..0000000 --- a/installer/CLAUDE.md +++ /dev/null @@ -1,49 +0,0 @@ -# Installer - -## Deployment Modes - -The installer (`installer/install.sh`) supports two deployment modes: - -| | Multi-tenant SaaS (`DEPLOYMENT_MODE=saas`) | Standalone (`DEPLOYMENT_MODE=standalone`) | -|---|---|---| -| **Containers** | traefik, postgres, clickhouse, logto, cameleer-saas | traefik, postgres, clickhouse, server, server-ui | -| **Auth** | Logto OIDC (SaaS admin + tenant users) | Local auth (built-in admin, no identity provider) | -| **Tenant management** | SaaS admin creates/manages tenants via UI | Single server instance, no fleet management | -| **PostgreSQL** | `cameleer-postgres` image (multi-DB init) | Stock `postgres:16-alpine` (server creates schema via Flyway) | -| **Use case** | Platform vendor managing multiple customers | Single customer running the product directly | - -Standalone mode generates a simpler compose with the server running directly. No Logto, no SaaS management plane, no bootstrap. The admin logs in with local credentials at `/`. - -## Compose templates - -The installer uses static docker-compose templates in `installer/templates/`. Templates are copied to the install directory and composed via `COMPOSE_FILE` in `.env`: -- `docker-compose.yml` — shared infrastructure (traefik, postgres, clickhouse) -- `docker-compose.saas.yml` — SaaS mode (logto, cameleer-saas) -- `docker-compose.server.yml` — standalone mode (server, server-ui) -- `docker-compose.tls.yml` — overlay: custom TLS cert volume -- `docker-compose.monitoring.yml` — overlay: external monitoring network - -## SMTP configuration - -Both installers (`install.sh` and `install.ps1`) prompt for SMTP settings in SaaS mode when the user opts in ("Configure SMTP for email verification?"). SMTP is required for self-service sign-up — without it, only admin-created users can sign in. - -Env vars: `SMTP_HOST`, `SMTP_PORT` (default 587), `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM_EMAIL` (default `noreply@`). Passed to the `cameleer-logto` container. The bootstrap script (Phase 8b) discovers the SMTP connector factory and creates the connector with Cameleer-branded email templates. - -CLI args: `--smtp-host`, `--smtp-port`, `--smtp-user`, `--smtp-pass`, `--smtp-from-email` (bash) / `-SmtpHost`, `-SmtpPort`, `-SmtpUser`, `-SmtpPass`, `-SmtpFromEmail` (PS1). Persisted in `cameleer.conf` for upgrades/reconfigure. - -## Registry configuration - -Both installers support pulling images from a custom Docker registry via `--registry` (bash) / `-Registry` (PS1). Default: `gitea.siegeln.net/cameleer`. - -When a registry is configured, the installer writes `*_IMAGE` env vars to `.env` (e.g. `TRAEFIK_IMAGE`, `POSTGRES_IMAGE`, `CAMELEER_IMAGE`) which override the defaults baked into the compose templates. In SaaS mode, provisioning image refs (`CAMELEER_SAAS_PROVISIONING_*IMAGE`) are also set from the registry. - -For private registries, pass `--registry-user` / `--registry-token` (bash) or `-RegistryUser` / `-RegistryToken` (PS1). The installer runs `docker login` before pulling images. Credentials are persisted in `cameleer.conf` for upgrades/reconfigure. - -## Env var naming convention - -- `CAMELEER_AGENT_*` — agent config (consumed by the Java agent) -- `CAMELEER_SERVER_*` — server config (consumed by cameleer-server) -- `CAMELEER_SAAS_*` — SaaS management plane config -- `CAMELEER_SAAS_PROVISIONING_*` — "SaaS forwards this to provisioned tenant servers" -- `SMTP_*` — email delivery config for Logto (consumed by bootstrap, SaaS mode only) -- No prefix (e.g. `POSTGRES_PASSWORD`, `PUBLIC_HOST`) — shared infrastructure, consumed by multiple components diff --git a/installer/install.ps1 b/installer/install.ps1 deleted file mode 100644 index 5af1524..0000000 --- a/installer/install.ps1 +++ /dev/null @@ -1,1551 +0,0 @@ -#Requires -Version 5.1 -<# -.SYNOPSIS - Cameleer SaaS Platform Installer -.DESCRIPTION - Installs the Cameleer SaaS platform using Docker Compose. -.EXAMPLE - .\install.ps1 - .\install.ps1 -Expert - .\install.ps1 -Silent -PublicHost myhost.example.com -#> - -[CmdletBinding()] -param( - [switch]$Silent, - [switch]$Expert, - [string]$Config, - [string]$InstallDir, - [string]$PublicHost, - [string]$AuthHost, - [string]$PublicProtocol, - [string]$AdminUser, - [string]$AdminPassword, - [string]$TlsMode, - [string]$CertFile, - [string]$KeyFile, - [string]$CaFile, - [string]$PostgresPassword, - [string]$ClickhousePassword, - [string]$HttpPort, - [string]$HttpsPort, - [string]$LogtoConsolePort, - [string]$LogtoConsoleExposed, - [string]$MonitoringNetwork, - [string]$Version, - [string]$ComposeProject, - [string]$DockerSocket, - [string]$NodeTlsReject, - [string]$DeploymentMode, - [string]$SmtpHost, - [string]$SmtpPort, - [string]$SmtpUser, - [string]$SmtpPass, - [string]$SmtpFromEmail, - [string]$Registry, - [string]$RegistryUser, - [string]$RegistryToken, - [switch]$Reconfigure, - [switch]$Reinstall, - [switch]$ConfirmDestroy, - [switch]$Help -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# --- Constants --- - -$CAMELEER_INSTALLER_VERSION = '1.0.0' -$CAMELEER_DEFAULT_VERSION = 'latest' -$DEFAULT_REGISTRY = 'gitea.siegeln.net/cameleer' - -$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' - -# --- Capture env vars before any overrides --- - -$_ENV_PUBLIC_HOST = $env:PUBLIC_HOST -$_ENV_AUTH_HOST = $env:AUTH_HOST -$_ENV_PUBLIC_PROTOCOL = $env:PUBLIC_PROTOCOL -$_ENV_POSTGRES_PASSWORD = $env:POSTGRES_PASSWORD -$_ENV_CLICKHOUSE_PASSWORD = $env:CLICKHOUSE_PASSWORD -$_ENV_TLS_MODE = $env:TLS_MODE -$_ENV_CERT_FILE = $env:CERT_FILE -$_ENV_KEY_FILE = $env:KEY_FILE -$_ENV_CA_FILE = $env:CA_FILE -$_ENV_HTTP_PORT = $env:HTTP_PORT -$_ENV_HTTPS_PORT = $env:HTTPS_PORT -$_ENV_LOGTO_CONSOLE_PORT = $env:LOGTO_CONSOLE_PORT -$_ENV_LOGTO_CONSOLE_EXPOSED = $env:LOGTO_CONSOLE_EXPOSED -$_ENV_MONITORING_NETWORK = $env:MONITORING_NETWORK -$_ENV_COMPOSE_PROJECT = $env:COMPOSE_PROJECT -$_ENV_DOCKER_SOCKET = $env:DOCKER_SOCKET -$_ENV_NODE_TLS_REJECT = $env:NODE_TLS_REJECT -$_ENV_DEPLOYMENT_MODE = $env:DEPLOYMENT_MODE -$_ENV_SMTP_HOST = $env:SMTP_HOST -$_ENV_SMTP_PORT = $env:SMTP_PORT -$_ENV_SMTP_USER = $env:SMTP_USER -$_ENV_SMTP_PASS = $env:SMTP_PASS -$_ENV_SMTP_FROM_EMAIL = $env:SMTP_FROM_EMAIL -$_ENV_REGISTRY = $env:REGISTRY -$_ENV_REGISTRY_USER = $env:REGISTRY_USER -$_ENV_REGISTRY_TOKEN = $env:REGISTRY_TOKEN - -# --- Mutable config state --- - -$script:cfg = @{ - InstallDir = $InstallDir - PublicHost = $PublicHost - AuthHost = $AuthHost - PublicProtocol = $PublicProtocol - AdminUser = $AdminUser - AdminPass = $AdminPassword - TlsMode = $TlsMode - CertFile = $CertFile - KeyFile = $KeyFile - CaFile = $CaFile - PostgresPassword = $PostgresPassword - ClickhousePassword = $ClickhousePassword - HttpPort = $HttpPort - HttpsPort = $HttpsPort - LogtoConsolePort = $LogtoConsolePort - LogtoConsoleExposed = $LogtoConsoleExposed - MonitoringNetwork = $MonitoringNetwork - Version = $Version - ComposeProject = $ComposeProject - DockerSocket = $DockerSocket - NodeTlsReject = $NodeTlsReject - DeploymentMode = $DeploymentMode - SmtpHost = $SmtpHost - SmtpPort = $SmtpPort - SmtpUser = $SmtpUser - SmtpPass = $SmtpPass - SmtpFromEmail = $SmtpFromEmail - Registry = $Registry - RegistryUser = $RegistryUser - RegistryToken = $RegistryToken -} - -if ($Silent) { $script:Mode = 'silent' } -elseif ($Expert) { $script:Mode = 'expert' } -else { $script:Mode = '' } - -if ($Reconfigure) { $script:RerunAction = 'reconfigure' } -elseif ($Reinstall) { $script:RerunAction = 'reinstall' } -else { $script:RerunAction = '' } - -$script:IsRerun = $false -$script:ConfirmDestroy = $ConfirmDestroy.IsPresent - -# --- Logging --- - -function Log-Info { param([string]$msg) Write-Host "[INFO] $msg" -ForegroundColor Green } -function Log-Warn { param([string]$msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow } -function Log-Error { param([string]$msg) Write-Host "[ERROR] $msg" -ForegroundColor Red } -function Log-Success { param([string]$msg) Write-Host "[OK] $msg" -ForegroundColor Green } - -function Print-Banner { - Write-Host '' - Write-Host ' ____ _ ' -ForegroundColor Cyan - Write-Host ' / ___|__ _ _______ ___ | | ___ ___ _ _ ' -ForegroundColor Cyan - Write-Host '| | / _` | _ _ _ \ / _ \| |/ _ \/ _ \ `_|' -ForegroundColor Cyan - Write-Host '| |__| (_| | | | | | | __/| | __/ __/ | ' -ForegroundColor Cyan - Write-Host ' \____\__,_|_| |_| |_|\___||_|\___|\___|_| ' -ForegroundColor Cyan - Write-Host '' - Write-Host " SaaS Platform Installer v$CAMELEER_INSTALLER_VERSION" -ForegroundColor Cyan - Write-Host '' -} - -function Show-Help { - Write-Host 'Usage: install.ps1 [OPTIONS]' - Write-Host '' - Write-Host 'Modes:' - Write-Host ' (default) Interactive simple mode (6 questions)' - Write-Host ' -Expert Interactive expert mode (all options)' - Write-Host ' -Silent Non-interactive, use defaults + overrides' - Write-Host '' - Write-Host 'Options:' - Write-Host ' -InstallDir DIR Install directory (default: ./cameleer)' - Write-Host ' -PublicHost HOST Public hostname (default: auto-detect)' - Write-Host ' -AuthHost HOST Auth domain for Logto (default: same as PublicHost)' - Write-Host ' -AdminUser USER Admin username (default: admin)' - Write-Host ' -AdminPassword PASS Admin password (default: generated)' - Write-Host ' -TlsMode MODE self-signed or custom (default: self-signed)' - Write-Host ' -CertFile PATH TLS certificate file' - Write-Host ' -KeyFile PATH TLS key file' - Write-Host ' -CaFile PATH CA bundle file' - Write-Host ' -MonitoringNetwork NAME Docker network for Prometheus scraping' - Write-Host ' -Version TAG Image version tag (default: latest)' - Write-Host ' -Config FILE Load config from file' - Write-Host ' -Help Show this help' - Write-Host '' - Write-Host 'Registry options:' - Write-Host ' -Registry REGISTRY Image registry (default: gitea.siegeln.net/cameleer)' - Write-Host ' -RegistryUser USER Registry username for docker login' - Write-Host ' -RegistryToken TOKEN Registry token/password for docker login' - Write-Host '' - Write-Host 'Expert options:' - Write-Host ' -PostgresPassword, -ClickhousePassword, -HttpPort,' - Write-Host ' -HttpsPort, -LogtoConsolePort, -LogtoConsoleExposed,' - Write-Host ' -ComposeProject, -DockerSocket, -NodeTlsReject' - Write-Host '' - Write-Host 'Re-run options:' - Write-Host ' -Reconfigure Re-run interactive setup (preserve data)' - Write-Host ' -Reinstall -ConfirmDestroy Fresh install (destroys data)' -} - -# --- Helpers --- - -function Coalesce { - param($a, $b) - if ($a) { return $a } else { return $b } -} - -function Prompt-Value { - param([string]$Text, [string]$Default = '') - if ($Default) { - $inp = Read-Host " $Text [$Default]" - if ([string]::IsNullOrWhiteSpace($inp)) { return $Default } - return $inp - } - return Read-Host " $Text" -} - -function Prompt-Password { - param([string]$Text, [string]$Default = '') - if ($Default) { $hint = '[********]' } else { $hint = '' } - $secure = Read-Host " $Text $hint" -AsSecureString - $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto( - [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)) - if ([string]::IsNullOrWhiteSpace($plain) -and $Default) { return $Default } - return $plain -} - -function Prompt-YesNo { - param([string]$Text, [string]$Default = 'n') - if ($Default -eq 'y') { - $inp = Read-Host " $Text [Y/n]" - if ($inp -match '^[nN]') { return $false } else { return $true } - } else { - $inp = Read-Host " $Text [y/N]" - return ($inp -match '^[yY]') - } -} - -function Generate-Password { - $bytes = New-Object byte[] 32 - [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) - $b64 = [Convert]::ToBase64String($bytes) -replace '[/+=]', '' - return $b64.Substring(0, [Math]::Min(32, $b64.Length)) -} - -# Writes UTF-8 without BOM (PS5 Set-Content -Encoding UTF8 adds BOM) -function Write-Utf8File { - param([string]$Path, [string]$Content) - $enc = New-Object System.Text.UTF8Encoding $false - [System.IO.File]::WriteAllText($Path, $Content, $enc) -} - -# --- Config file --- - -function Load-ConfigFile { - param([string]$FilePath) - if (-not (Test-Path $FilePath)) { return } - foreach ($line in Get-Content $FilePath) { - if ($line -match '^\s*#' -or $line -match '^\s*$') { continue } - if ($line -match '^\s*([^=]+?)\s*=\s*(.*?)\s*$') { - $key = $Matches[1].Trim() - $val = $Matches[2].Trim() - switch ($key) { - 'install_dir' { if (-not $script:cfg.InstallDir) { $script:cfg.InstallDir = $val } } - 'public_host' { if (-not $script:cfg.PublicHost) { $script:cfg.PublicHost = $val } } - 'auth_host' { if (-not $script:cfg.AuthHost) { $script:cfg.AuthHost = $val } } - 'public_protocol' { if (-not $script:cfg.PublicProtocol) { $script:cfg.PublicProtocol = $val } } - 'admin_user' { if (-not $script:cfg.AdminUser) { $script:cfg.AdminUser = $val } } - 'admin_password' { if (-not $script:cfg.AdminPass) { $script:cfg.AdminPass = $val } } - 'tls_mode' { if (-not $script:cfg.TlsMode) { $script:cfg.TlsMode = $val } } - 'cert_file' { if (-not $script:cfg.CertFile) { $script:cfg.CertFile = $val } } - 'key_file' { if (-not $script:cfg.KeyFile) { $script:cfg.KeyFile = $val } } - 'ca_file' { if (-not $script:cfg.CaFile) { $script:cfg.CaFile = $val } } - 'postgres_password' { if (-not $script:cfg.PostgresPassword) { $script:cfg.PostgresPassword = $val } } - 'clickhouse_password' { if (-not $script:cfg.ClickhousePassword) { $script:cfg.ClickhousePassword = $val } } - 'http_port' { if (-not $script:cfg.HttpPort) { $script:cfg.HttpPort = $val } } - 'https_port' { if (-not $script:cfg.HttpsPort) { $script:cfg.HttpsPort = $val } } - 'logto_console_port' { if (-not $script:cfg.LogtoConsolePort) { $script:cfg.LogtoConsolePort = $val } } - 'logto_console_exposed' { if (-not $script:cfg.LogtoConsoleExposed) { $script:cfg.LogtoConsoleExposed = $val } } - 'monitoring_network' { if (-not $script:cfg.MonitoringNetwork) { $script:cfg.MonitoringNetwork = $val } } - 'version' { if (-not $script:cfg.Version) { $script:cfg.Version = $val } } - 'compose_project' { if (-not $script:cfg.ComposeProject) { $script:cfg.ComposeProject = $val } } - 'docker_socket' { if (-not $script:cfg.DockerSocket) { $script:cfg.DockerSocket = $val } } - 'node_tls_reject' { if (-not $script:cfg.NodeTlsReject) { $script:cfg.NodeTlsReject = $val } } - 'deployment_mode' { if (-not $script:cfg.DeploymentMode) { $script:cfg.DeploymentMode = $val } } - 'smtp_host' { if (-not $script:cfg.SmtpHost) { $script:cfg.SmtpHost = $val } } - 'smtp_port' { if (-not $script:cfg.SmtpPort) { $script:cfg.SmtpPort = $val } } - 'smtp_user' { if (-not $script:cfg.SmtpUser) { $script:cfg.SmtpUser = $val } } - 'smtp_pass' { if (-not $script:cfg.SmtpPass) { $script:cfg.SmtpPass = $val } } - 'smtp_from_email' { if (-not $script:cfg.SmtpFromEmail) { $script:cfg.SmtpFromEmail = $val } } - 'registry' { if (-not $script:cfg.Registry) { $script:cfg.Registry = $val } } - 'registry_user' { if (-not $script:cfg.RegistryUser) { $script:cfg.RegistryUser = $val } } - 'registry_token' { if (-not $script:cfg.RegistryToken) { $script:cfg.RegistryToken = $val } } - } - } - } -} - -function Load-EnvOverrides { - $c = $script:cfg - if (-not $c.InstallDir) { $c.InstallDir = $env:CAMELEER_INSTALL_DIR } - if (-not $c.PublicHost) { $c.PublicHost = $_ENV_PUBLIC_HOST } - if (-not $c.AuthHost) { $c.AuthHost = $_ENV_AUTH_HOST } - if (-not $c.PublicProtocol) { $c.PublicProtocol = $_ENV_PUBLIC_PROTOCOL } - if (-not $c.AdminUser) { $c.AdminUser = $env:SAAS_ADMIN_USER } - if (-not $c.AdminPass) { $c.AdminPass = $env:SAAS_ADMIN_PASS } - if (-not $c.TlsMode) { $c.TlsMode = $_ENV_TLS_MODE } - if (-not $c.CertFile) { $c.CertFile = $_ENV_CERT_FILE } - if (-not $c.KeyFile) { $c.KeyFile = $_ENV_KEY_FILE } - if (-not $c.CaFile) { $c.CaFile = $_ENV_CA_FILE } - if (-not $c.PostgresPassword) { $c.PostgresPassword = $_ENV_POSTGRES_PASSWORD } - if (-not $c.ClickhousePassword) { $c.ClickhousePassword = $_ENV_CLICKHOUSE_PASSWORD } - if (-not $c.HttpPort) { $c.HttpPort = $_ENV_HTTP_PORT } - if (-not $c.HttpsPort) { $c.HttpsPort = $_ENV_HTTPS_PORT } - if (-not $c.LogtoConsolePort) { $c.LogtoConsolePort = $_ENV_LOGTO_CONSOLE_PORT } - if (-not $c.LogtoConsoleExposed) { $c.LogtoConsoleExposed = $_ENV_LOGTO_CONSOLE_EXPOSED } - if (-not $c.MonitoringNetwork) { $c.MonitoringNetwork = $_ENV_MONITORING_NETWORK } - if (-not $c.Version) { $c.Version = $env:CAMELEER_VERSION } - if (-not $c.ComposeProject) { $c.ComposeProject = $_ENV_COMPOSE_PROJECT } - if (-not $c.DockerSocket) { $c.DockerSocket = $_ENV_DOCKER_SOCKET } - if (-not $c.NodeTlsReject) { $c.NodeTlsReject = $_ENV_NODE_TLS_REJECT } - if (-not $c.DeploymentMode) { $c.DeploymentMode = $_ENV_DEPLOYMENT_MODE } - if (-not $c.SmtpHost) { $c.SmtpHost = $_ENV_SMTP_HOST } - if (-not $c.SmtpPort) { $c.SmtpPort = $_ENV_SMTP_PORT } - if (-not $c.SmtpUser) { $c.SmtpUser = $_ENV_SMTP_USER } - if (-not $c.SmtpPass) { $c.SmtpPass = $_ENV_SMTP_PASS } - if (-not $c.SmtpFromEmail) { $c.SmtpFromEmail = $_ENV_SMTP_FROM_EMAIL } - if (-not $c.Registry) { $c.Registry = $_ENV_REGISTRY } - if (-not $c.RegistryUser) { $c.RegistryUser = $_ENV_REGISTRY_USER } - if (-not $c.RegistryToken) { $c.RegistryToken = $_ENV_REGISTRY_TOKEN } -} - -# --- Prerequisites --- - -function Check-PortAvailable { - param([string]$Port, [string]$Name) - try { - $hits = netstat -an 2>$null | Select-String ":${Port} " - if ($hits) { Log-Warn "Port $Port ($Name) appears to be in use." } - } catch {} -} - -function Check-Prerequisites { - Log-Info 'Checking prerequisites...' - $errors = 0 - - if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { - Log-Error 'Docker is not installed.' - Write-Host ' Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/' - $errors++ - } else { - $v = docker version --format '{{.Server.Version}}' 2>$null - Log-Info "Docker version: $v" - } - - docker compose version 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { - Log-Error "Docker Compose v2 not available. 'docker compose' subcommand required." - $errors++ - } else { - $v = docker compose version --short 2>$null - Log-Info "Docker Compose version: $v" - } - - if (-not (Get-Command openssl -ErrorAction SilentlyContinue)) { - Log-Warn 'OpenSSL not found -- using .NET RNG for passwords (fine).' - } - - $socket = Coalesce $script:cfg.DockerSocket $DEFAULT_DOCKER_SOCKET - if ($env:OS -eq 'Windows_NT') { - if (-not (Test-Path '\\.\pipe\docker_engine' -ErrorAction SilentlyContinue)) { - Log-Warn 'Docker named pipe not found. Is Docker Desktop running?' - } - } elseif (-not (Test-Path $socket)) { - Log-Warn "Docker socket not found at $socket" - } - - Check-PortAvailable (Coalesce $script:cfg.HttpPort $DEFAULT_HTTP_PORT) 'HTTP' - Check-PortAvailable (Coalesce $script:cfg.HttpsPort $DEFAULT_HTTPS_PORT) 'HTTPS' - if ($script:cfg.DeploymentMode -ne 'standalone') { - Check-PortAvailable (Coalesce $script:cfg.LogtoConsolePort $DEFAULT_LOGTO_CONSOLE_PORT) 'Logto Console' - } - - if ($errors -gt 0) { - Log-Error "$errors prerequisite(s) not met. Please install missing dependencies and retry." - exit 1 - } - Log-Success 'All prerequisites met.' -} - -# --- Auto-detection --- - -function Auto-Detect { - if (-not $script:cfg.PublicHost) { - $detectedHost = $null - # Try reverse DNS on each host IP — picks up FQDN from DNS server - try { - foreach ($addr in [System.Net.Dns]::GetHostAddresses([System.Net.Dns]::GetHostName())) { - if ($addr.AddressFamily -ne 'InterNetwork') { continue } # IPv4 only - if ($addr.ToString().StartsWith('127.')) { continue } - try { - $rev = [System.Net.Dns]::GetHostEntry($addr).HostName - if ($rev -and $rev.Contains('.')) { - $detectedHost = $rev - break - } - } catch {} - } - } catch {} - if (-not $detectedHost) { - # Fallback: .NET forward lookup, then bare hostname - try { - $detectedHost = [System.Net.Dns]::GetHostEntry([System.Net.Dns]::GetHostName()).HostName - } catch { - $detectedHost = [System.Net.Dns]::GetHostName() - } - } - $script:cfg.PublicHost = $detectedHost.ToLower() - } - if (-not $script:cfg.DockerSocket) { - # Always use /var/run/docker.sock — containers are Linux and Docker Desktop - # maps the host socket into the VM automatically. The Windows named pipe - # (//./pipe/docker_engine) does NOT work as a volume mount for Linux containers. - $script:cfg.DockerSocket = $DEFAULT_DOCKER_SOCKET - } -} - -function Detect-ExistingInstall { - $dir = Coalesce $script:cfg.InstallDir $DEFAULT_INSTALL_DIR - $confPath = Join-Path $dir 'cameleer.conf' - if (Test-Path $confPath) { - $script:IsRerun = $true - $script:cfg.InstallDir = $dir - Load-ConfigFile $confPath - } -} - -# --- Interactive prompts --- - -function Select-Mode { - if ($script:Mode) { return } - Write-Host '' - Write-Host ' Installation mode:' - Write-Host ' [1] Simple -- 6 questions, sensible defaults (recommended)' - Write-Host ' [2] Expert -- configure everything' - Write-Host '' - $choice = Read-Host ' Select mode [1]' - if ($choice -eq '2') { $script:Mode = 'expert' } else { $script:Mode = 'simple' } -} - -function Run-SimplePrompts { - $c = $script:cfg - Write-Host '' - Write-Host '--- Simple Installation ---' -ForegroundColor Cyan - Write-Host '' - - $c.InstallDir = Prompt-Value 'Install directory' (Coalesce $c.InstallDir $DEFAULT_INSTALL_DIR) - $c.PublicHost = Prompt-Value 'Public hostname' (Coalesce $c.PublicHost 'localhost') - $c.AdminUser = Prompt-Value 'Admin username' (Coalesce $c.AdminUser $DEFAULT_ADMIN_USER) - - if (Prompt-YesNo 'Auto-generate admin password?' 'y') { - $c.AdminPass = '' - } else { - $c.AdminPass = Prompt-Password 'Admin password' - } - - Write-Host '' - if (Prompt-YesNo 'Use custom TLS certificates? (no = self-signed)') { - $c.TlsMode = 'custom' - $c.CertFile = Prompt-Value 'Path to certificate file (PEM)' - $c.KeyFile = Prompt-Value 'Path to private key file (PEM)' - if (Prompt-YesNo 'Include CA bundle?') { - $c.CaFile = Prompt-Value 'Path to CA bundle (PEM)' - } - } else { - $c.TlsMode = 'self-signed' - } - - Write-Host '' - $c.MonitoringNetwork = Prompt-Value 'Monitoring network name (empty = skip)' '' - - Write-Host '' - if (Prompt-YesNo 'Pull images from a private registry?') { - $c.Registry = Prompt-Value 'Registry' (Coalesce $c.Registry $DEFAULT_REGISTRY) - $c.RegistryUser = Prompt-Value 'Registry username' (Coalesce $c.RegistryUser '') - $c.RegistryToken = Prompt-Password 'Registry token/password' (Coalesce $c.RegistryToken '') - } - - Write-Host '' - Write-Host ' Deployment mode:' - Write-Host ' [1] Multi-tenant SaaS -- manage platform, provision tenants on demand' - Write-Host ' [2] Single-tenant -- one server instance, local auth, no identity provider' - Write-Host '' - $deployChoice = Read-Host ' Select mode [1]' - if ($deployChoice -eq '2') { $c.DeploymentMode = 'standalone' } else { $c.DeploymentMode = 'saas' } - - # SMTP for email verification (SaaS mode only) - if ($c.DeploymentMode -eq 'saas') { - Write-Host '' - if (Prompt-YesNo 'Configure SMTP for email verification? (required for self-service sign-up)') { - $c.SmtpHost = Prompt-Value 'SMTP host' (Coalesce $c.SmtpHost '') - $c.SmtpPort = Prompt-Value 'SMTP port' (Coalesce $c.SmtpPort '587') - $c.SmtpUser = Prompt-Value 'SMTP username' (Coalesce $c.SmtpUser '') - $c.SmtpPass = Prompt-Password 'SMTP password' (Coalesce $c.SmtpPass '') - $c.SmtpFromEmail = Prompt-Value 'From email address' (Coalesce $c.SmtpFromEmail "noreply@$($c.PublicHost)") - } - } -} - -function Run-ExpertPrompts { - $c = $script:cfg - Run-SimplePrompts - - Write-Host '' - Write-Host ' Credentials:' -ForegroundColor Cyan - if (Prompt-YesNo 'Auto-generate database passwords?' 'y') { - $c.PostgresPassword = '' - $c.ClickhousePassword = '' - } else { - $c.PostgresPassword = Prompt-Password 'PostgreSQL password' - $c.ClickhousePassword = Prompt-Password 'ClickHouse password' - } - - Write-Host '' - Write-Host ' Networking:' -ForegroundColor Cyan - $c.HttpPort = Prompt-Value 'HTTP port' (Coalesce $c.HttpPort $DEFAULT_HTTP_PORT) - $c.HttpsPort = Prompt-Value 'HTTPS port' (Coalesce $c.HttpsPort $DEFAULT_HTTPS_PORT) - if ($c.DeploymentMode -eq 'saas') { - $c.LogtoConsolePort = Prompt-Value 'Logto admin console port' (Coalesce $c.LogtoConsolePort $DEFAULT_LOGTO_CONSOLE_PORT) - } - - Write-Host '' - Write-Host ' Docker:' -ForegroundColor Cyan - $c.Version = Prompt-Value 'Image version/tag' (Coalesce $c.Version $CAMELEER_DEFAULT_VERSION) - $c.ComposeProject = Prompt-Value 'Compose project name' (Coalesce $c.ComposeProject $DEFAULT_COMPOSE_PROJECT) - $c.DockerSocket = Prompt-Value 'Docker socket path' (Coalesce $c.DockerSocket $DEFAULT_DOCKER_SOCKET) - - if ($c.DeploymentMode -eq 'saas') { - Write-Host '' - Write-Host ' Logto:' -ForegroundColor Cyan - $c.AuthHost = Prompt-Value 'Auth domain (Logto) -- same as hostname for single-domain' (Coalesce $c.AuthHost $c.PublicHost) - if (Prompt-YesNo 'Expose Logto admin console externally?' 'y') { - $c.LogtoConsoleExposed = 'true' - } else { - $c.LogtoConsoleExposed = 'false' - } - } -} - -# --- Config merge & validation --- - -function Merge-Config { - $c = $script:cfg - if (-not $c.DeploymentMode) { $c.DeploymentMode = 'saas' } - if (-not $c.InstallDir) { $c.InstallDir = $DEFAULT_INSTALL_DIR } - if (-not $c.PublicHost) { $c.PublicHost = 'localhost' } - if (-not $c.PublicProtocol) { $c.PublicProtocol = $DEFAULT_PUBLIC_PROTOCOL } - if (-not $c.AdminUser) { $c.AdminUser = $DEFAULT_ADMIN_USER } - if (-not $c.TlsMode) { $c.TlsMode = $DEFAULT_TLS_MODE } - if (-not $c.HttpPort) { $c.HttpPort = $DEFAULT_HTTP_PORT } - if (-not $c.HttpsPort) { $c.HttpsPort = $DEFAULT_HTTPS_PORT } - if (-not $c.LogtoConsolePort) { $c.LogtoConsolePort = $DEFAULT_LOGTO_CONSOLE_PORT } - if (-not $c.LogtoConsoleExposed) { $c.LogtoConsoleExposed = $DEFAULT_LOGTO_CONSOLE_EXPOSED } - if (-not $c.Version) { $c.Version = $CAMELEER_DEFAULT_VERSION } - if (-not $c.DockerSocket) { $c.DockerSocket = $DEFAULT_DOCKER_SOCKET } - if (-not $c.Registry) { $c.Registry = $DEFAULT_REGISTRY } - - if (-not $c.ComposeProject) { - if ($c.DeploymentMode -eq 'standalone') { - $c.ComposeProject = $DEFAULT_COMPOSE_PROJECT_STANDALONE - } else { - $c.ComposeProject = $DEFAULT_COMPOSE_PROJECT - } - } - - # Default AUTH_HOST to PUBLIC_HOST (single-domain setup) - if (-not $c.AuthHost) { $c.AuthHost = $c.PublicHost } - - # Force lowercase -- Logto normalises internally; case mismatch breaks JWT validation - $c.PublicHost = $c.PublicHost.ToLower() - $c.AuthHost = $c.AuthHost.ToLower() - - if ($c.DeploymentMode -ne 'standalone' -and (-not $c.NodeTlsReject)) { - if ($c.TlsMode -eq 'custom') { $c.NodeTlsReject = '1' } else { $c.NodeTlsReject = '0' } - } -} - -function Validate-Config { - $c = $script:cfg - $errors = 0 - - if ($c.TlsMode -eq 'custom') { - if (-not (Test-Path $c.CertFile)) { Log-Error "Certificate file not found: $($c.CertFile)"; $errors++ } - if (-not (Test-Path $c.KeyFile)) { Log-Error "Key file not found: $($c.KeyFile)"; $errors++ } - if ($c.CaFile -and -not (Test-Path $c.CaFile)) { Log-Error "CA bundle not found: $($c.CaFile)"; $errors++ } - } - - foreach ($portKey in @('HttpPort','HttpsPort')) { - $val = $c[$portKey]; $num = 0 - if (-not ([int]::TryParse($val, [ref]$num)) -or $num -lt 1 -or $num -gt 65535) { - Log-Error "Invalid port for ${portKey}: $val"; $errors++ - } - } - if ($c.DeploymentMode -ne 'standalone') { - $val = $c.LogtoConsolePort; $num = 0 - if (-not ([int]::TryParse($val, [ref]$num)) -or $num -lt 1 -or $num -gt 65535) { - Log-Error "Invalid port for LogtoConsolePort: $val"; $errors++ - } - } - - if ($errors -gt 0) { Log-Error 'Configuration validation failed.'; exit 1 } - Log-Success 'Configuration validated.' -} - -function Generate-Passwords { - $c = $script:cfg - if (-not $c.AdminPass) { $c.AdminPass = Generate-Password; Log-Info 'Generated admin password.' } - if (-not $c.PostgresPassword) { $c.PostgresPassword = Generate-Password; Log-Info 'Generated PostgreSQL password.' } - if (-not $c.ClickhousePassword) { $c.ClickhousePassword = Generate-Password; Log-Info 'Generated ClickHouse password.' } -} - -# --- File helpers --- - -function Get-DockerGid { - param([string]$Socket) - if ($env:OS -ne 'Windows_NT') { - try { $g = (& stat -c '%g' $Socket 2>$null); if ($g) { return $g } } catch {} - } - return '0' -} - -function Copy-Certs { - $c = $script:cfg - $d = Join-Path $c.InstallDir 'certs' - New-Item -ItemType Directory -Force -Path $d | Out-Null - Copy-Item $c.CertFile (Join-Path $d 'cert.pem') -Force - Copy-Item $c.KeyFile (Join-Path $d 'key.pem') -Force - if ($c.CaFile) { Copy-Item $c.CaFile (Join-Path $d 'ca.pem') -Force } - Log-Info "Copied TLS certificates to $d" -} - -# --- .env generation --- - -function Generate-EnvFile { - $c = $script:cfg - $f = Join-Path $c.InstallDir '.env' - $gid = Get-DockerGid $c.DockerSocket - $ts = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' UTC' - $bt = Generate-Password - - $jwtSecret = Generate-Password - - if ($c.DeploymentMode -eq 'standalone') { - $content = @" -# Cameleer Server Configuration (standalone) -# Generated by installer v${CAMELEER_INSTALLER_VERSION} on $ts - -VERSION=$($c.Version) -PUBLIC_HOST=$($c.PublicHost) -PUBLIC_PROTOCOL=$($c.PublicProtocol) -HTTP_PORT=$($c.HttpPort) -HTTPS_PORT=$($c.HttpsPort) - -# PostgreSQL -POSTGRES_USER=cameleer -POSTGRES_PASSWORD=$($c.PostgresPassword) -POSTGRES_DB=cameleer - -# ClickHouse -CLICKHOUSE_PASSWORD=$($c.ClickhousePassword) - -# Server admin -SERVER_ADMIN_USER=$($c.AdminUser) -SERVER_ADMIN_PASS=$($c.AdminPass) - -# Bootstrap token -BOOTSTRAP_TOKEN=$bt - -# JWT signing secret (required by server, must be non-empty) -CAMELEER_SERVER_SECURITY_JWTSECRET=$jwtSecret - -# Docker -DOCKER_SOCKET=$($c.DockerSocket) -DOCKER_GID=$gid - -POSTGRES_IMAGE=postgres:16-alpine - -# Registry -TRAEFIK_IMAGE=$($c.Registry)/cameleer-traefik -CLICKHOUSE_IMAGE=$($c.Registry)/cameleer-clickhouse -SERVER_IMAGE=$($c.Registry)/cameleer-server -SERVER_UI_IMAGE=$($c.Registry)/cameleer-server-ui -"@ - if ($c.TlsMode -eq 'custom') { - $content += "`nCERT_FILE=/user-certs/cert.pem" - $content += "`nKEY_FILE=/user-certs/key.pem" - if ($c.CaFile) { $content += "`nCA_FILE=/user-certs/ca.pem" } - } - $composeFile = 'docker-compose.yml;docker-compose.server.yml' - if ($c.TlsMode -eq 'custom') { $composeFile += ';docker-compose.tls.yml' } - if ($c.MonitoringNetwork) { $composeFile += ';docker-compose.monitoring.yml' } - $content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile" - if ($c.MonitoringNetwork) { - $content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)" - } - } else { - $consoleBind = if ($c.LogtoConsoleExposed -eq 'true') { '0.0.0.0' } else { '127.0.0.1' } - $content = @" -# Cameleer SaaS Configuration -# Generated by installer v${CAMELEER_INSTALLER_VERSION} on $ts - -VERSION=$($c.Version) - -PUBLIC_HOST=$($c.PublicHost) -AUTH_HOST=$($c.AuthHost) -PUBLIC_PROTOCOL=$($c.PublicProtocol) - -HTTP_PORT=$($c.HttpPort) -HTTPS_PORT=$($c.HttpsPort) -LOGTO_CONSOLE_PORT=$($c.LogtoConsolePort) -LOGTO_CONSOLE_BIND=$consoleBind - -# PostgreSQL -POSTGRES_USER=cameleer -POSTGRES_PASSWORD=$($c.PostgresPassword) -POSTGRES_DB=cameleer_saas - -# ClickHouse -CLICKHOUSE_PASSWORD=$($c.ClickhousePassword) - -# Admin user -SAAS_ADMIN_USER=$($c.AdminUser) -SAAS_ADMIN_PASS=$($c.AdminPass) - -# TLS -NODE_TLS_REJECT=$($c.NodeTlsReject) -"@ - if ($c.TlsMode -eq 'custom') { - $content += "`nCERT_FILE=/user-certs/cert.pem" - $content += "`nKEY_FILE=/user-certs/key.pem" - if ($c.CaFile) { $content += "`nCA_FILE=/user-certs/ca.pem" } - } - $reg = $c.Registry - $provisioningBlock = @" - -# Docker -DOCKER_SOCKET=$($c.DockerSocket) -DOCKER_GID=$gid - -# Registry -TRAEFIK_IMAGE=$reg/cameleer-traefik -POSTGRES_IMAGE=$reg/cameleer-postgres -CLICKHOUSE_IMAGE=$reg/cameleer-clickhouse -LOGTO_IMAGE=$reg/cameleer-logto -CAMELEER_IMAGE=$reg/cameleer-saas - -# Provisioning images -CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=$reg/cameleer-server:$($c.Version) -CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=$reg/cameleer-server-ui:$($c.Version) -CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=$reg/cameleer-runtime-base:$($c.Version) - -# JWT signing secret (forwarded to provisioned tenant servers, must be non-empty) -CAMELEER_SERVER_SECURITY_JWTSECRET=$jwtSecret - -# SMTP (for email verification during registration) -SMTP_HOST=$($c.SmtpHost) -SMTP_PORT=$(if ($c.SmtpPort) { $c.SmtpPort } else { '587' }) -SMTP_USER=$($c.SmtpUser) -SMTP_PASS=$($c.SmtpPass) -SMTP_FROM_EMAIL=$(if ($c.SmtpFromEmail) { $c.SmtpFromEmail } else { "noreply@$($c.PublicHost)" }) -"@ - $content += $provisioningBlock - $composeFile = 'docker-compose.yml;docker-compose.saas.yml' - if ($c.TlsMode -eq 'custom') { $composeFile += ';docker-compose.tls.yml' } - if ($c.MonitoringNetwork) { $composeFile += ';docker-compose.monitoring.yml' } - $content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile" - if ($c.MonitoringNetwork) { - $content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)" - } - } - - Write-Utf8File $f $content - Copy-Item $f (Join-Path $c.InstallDir '.env.bak') -Force - Log-Info 'Generated .env' -} - -# --- Copy docker-compose templates --- - -function Copy-Templates { - $c = $script:cfg - $src = Join-Path $PSScriptRoot 'templates' - - # Base infra -- always copied - Copy-Item (Join-Path $src 'docker-compose.yml') (Join-Path $c.InstallDir 'docker-compose.yml') -Force - Copy-Item (Join-Path $src '.env.example') (Join-Path $c.InstallDir '.env.example') -Force - - # Mode-specific - if ($c.DeploymentMode -eq 'standalone') { - Copy-Item (Join-Path $src 'docker-compose.server.yml') (Join-Path $c.InstallDir 'docker-compose.server.yml') -Force - Copy-Item (Join-Path $src 'traefik-dynamic.yml') (Join-Path $c.InstallDir 'traefik-dynamic.yml') -Force - } else { - Copy-Item (Join-Path $src 'docker-compose.saas.yml') (Join-Path $c.InstallDir 'docker-compose.saas.yml') -Force - } - - # Optional overlays - if ($c.TlsMode -eq 'custom') { - Copy-Item (Join-Path $src 'docker-compose.tls.yml') (Join-Path $c.InstallDir 'docker-compose.tls.yml') -Force - } - if ($c.MonitoringNetwork) { - Copy-Item (Join-Path $src 'docker-compose.monitoring.yml') (Join-Path $c.InstallDir 'docker-compose.monitoring.yml') -Force - } - - Log-Info "Copied docker-compose templates to $($c.InstallDir)" -} - -# --- Docker operations --- - -function Invoke-RegistryLogin { - $c = $script:cfg - if ($c.RegistryUser -and $c.RegistryToken) { - $registryHost = $c.Registry.Split('/')[0] - Log-Info "Logging in to registry ${registryHost}..." - $c.RegistryToken | docker login $registryHost -u $c.RegistryUser --password-stdin - if ($LASTEXITCODE -ne 0) { Log-Error 'Registry login failed.'; exit 1 } - Log-Success 'Registry login successful.' - } -} - -function Invoke-ComposePull { - $c = $script:cfg - Log-Info 'Pulling Docker images...' - Push-Location $c.InstallDir - try { docker compose -p $c.ComposeProject pull } - finally { Pop-Location } - Log-Success 'All images pulled.' -} - -function Invoke-ComposeUp { - $c = $script:cfg - Log-Info 'Starting Cameleer platform...' - Push-Location $c.InstallDir - try { docker compose -p $c.ComposeProject up -d --pull always --force-recreate } - finally { Pop-Location } - Log-Info 'Containers started -- verifying health next.' -} - -function Invoke-ComposeDown { - $c = $script:cfg - Log-Info 'Stopping Cameleer platform...' - Push-Location $c.InstallDir - try { docker compose -p $c.ComposeProject down } - finally { Pop-Location } -} - -# --- Health verification --- - -function Enable-TrustAllCerts { - # PS5.1 has no -SkipCertificateCheck on Invoke-WebRequest. - # Bypass SSL validation globally for this session via the ServicePointManager. - try { - Add-Type -TypeDefinition @' -using System.Net; -using System.Security.Cryptography.X509Certificates; -public class TrustAllCertsPolicy : ICertificatePolicy { - public bool CheckValidationResult(ServicePoint sp, X509Certificate cert, - WebRequest req, int problem) { return true; } -} -'@ - } catch {} # already loaded -- ignore - [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy - [System.Net.ServicePointManager]::SecurityProtocol = - [System.Net.SecurityProtocolType]::Tls12 -bor - [System.Net.SecurityProtocolType]::Tls11 -bor - [System.Net.SecurityProtocolType]::Tls -} - -function Wait-DockerHealthy { - param([string]$Name, [string]$Service, [int]$TimeoutSecs = 300) - $c = $script:cfg - $start = Get-Date - $lastStatus = '' - while ($true) { - $elapsed = [int]((Get-Date) - $start).TotalSeconds - if ($elapsed -ge $TimeoutSecs) { - Write-Host (" [FAIL] {0,-20} not healthy after {1}s" -f $Name, $TimeoutSecs) -ForegroundColor Red - Write-Host " Check: docker compose -p $($c.ComposeProject) logs $Service" - return $false - } - - # Resolve container ID first, then use docker inspect for reliable health status. - # 'docker compose ps --format {{.Health}}' is inconsistent across Docker Desktop versions. - Push-Location $c.InstallDir - $cid = (docker compose -p $c.ComposeProject ps -q $Service 2>$null) | Select-Object -First 1 - Pop-Location - - $health = '' - if ($cid) { - $health = (docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{end}}' $cid 2>$null).Trim() - } - - if ($health -eq 'healthy') { - $dur = [int]((Get-Date) - $start).TotalSeconds - Write-Host (" [ok] {0,-20} ready ({1}s)" -f $Name, $dur) -ForegroundColor Green - return $true - } elseif ($health -eq 'unhealthy') { - Write-Host (" [FAIL] {0,-20} unhealthy" -f $Name) -ForegroundColor Red - Write-Host " Check: docker compose -p $($c.ComposeProject) logs $Service" - return $false - } else { - # Print a progress dot every 15 seconds so the user knows we haven't hung - if ($elapsed -gt 0 -and ($elapsed % 15) -lt 3 -and $elapsed -ne $lastStatus) { - $lastStatus = $elapsed - Write-Host (" [wait] {0,-20} {1}s elapsed (status: {2})" -f $Name, $elapsed, $(if ($health) { $health } else { 'starting' })) - } - Start-Sleep 3 - } - } -} - -function Test-Endpoint { - param([string]$Name, [string]$Url, [int]$TimeoutSecs = 120, [string]$HostHeader = '') - $start = Get-Date - $lastDot = -1 - $headers = @{} - if ($HostHeader) { $headers['Host'] = $HostHeader } - while ($true) { - $elapsed = [int]((Get-Date) - $start).TotalSeconds - if ($elapsed -ge $TimeoutSecs) { - Write-Host (" [FAIL] {0,-20} not reachable after {1}s" -f $Name, $TimeoutSecs) -ForegroundColor Red - return $false - } - try { - # -SkipCertificateCheck is PS6+ only; SSL trust is handled by Enable-TrustAllCerts above - $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5 -Headers $headers -ErrorAction Stop - $dur = [int]((Get-Date) - $start).TotalSeconds - Write-Host (" [ok] {0,-20} ready ({1}s)" -f $Name, $dur) -ForegroundColor Green - return $true - } catch { - if ($elapsed -gt 0 -and ($elapsed % 15) -lt 3 -and $elapsed -ne $lastDot) { - $lastDot = $elapsed - Write-Host (" [wait] {0,-20} {1}s elapsed" -f $Name, $elapsed) - } - Start-Sleep 3 - } - } -} - -function Verify-Health { - $c = $script:cfg - Write-Host '' - Log-Info 'Verifying installation...' - Enable-TrustAllCerts # allow self-signed certs for PS5.1 Invoke-WebRequest - $failed = $false - - if (-not (Wait-DockerHealthy 'PostgreSQL' 'cameleer-postgres' 120)) { $failed = $true } - if (-not $failed) { - if (-not (Wait-DockerHealthy 'ClickHouse' 'cameleer-clickhouse' 120)) { $failed = $true } - } - - if ($c.DeploymentMode -eq 'standalone') { - if (-not $failed) { - if (-not (Wait-DockerHealthy 'Cameleer Server' 'cameleer-server' 300)) { $failed = $true } - } - if (-not $failed) { - if (-not (Test-Endpoint 'Server UI' "https://localhost:$($c.HttpsPort)/" 60)) { $failed = $true } - } - } else { - if (-not $failed) { - if (-not (Wait-DockerHealthy 'Logto + Bootstrap' 'cameleer-logto' 300)) { $failed = $true } - } - if (-not $failed) { - if (-not (Test-Endpoint 'Cameleer SaaS' "https://localhost:$($c.HttpsPort)/platform/api/config" 120)) { $failed = $true } - } - if (-not $failed) { - if (-not (Test-Endpoint 'Traefik routing' "https://localhost:$($c.HttpsPort)/" 30 $c.PublicHost)) { $failed = $true } - } - } - - Write-Host '' - if ($failed) { Log-Error 'Installation verification failed. Stack is running -- check logs.'; exit 1 } - Log-Success 'All services healthy.' -} - -# --- Output files --- - -function Write-ConfigFile { - $c = $script:cfg - $f = Join-Path $c.InstallDir 'cameleer.conf' - $ts = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' UTC' - - $txt = @" -# Cameleer installation config -# Generated by installer v${CAMELEER_INSTALLER_VERSION} on $ts - -install_dir=$($c.InstallDir) -public_host=$($c.PublicHost) -auth_host=$($c.AuthHost) -public_protocol=$($c.PublicProtocol) -admin_user=$($c.AdminUser) -tls_mode=$($c.TlsMode) -http_port=$($c.HttpPort) -https_port=$($c.HttpsPort) -logto_console_port=$($c.LogtoConsolePort) -logto_console_exposed=$($c.LogtoConsoleExposed) -monitoring_network=$($c.MonitoringNetwork) -version=$($c.Version) -compose_project=$($c.ComposeProject) -docker_socket=$($c.DockerSocket) -node_tls_reject=$($c.NodeTlsReject) -deployment_mode=$($c.DeploymentMode) -smtp_host=$($c.SmtpHost) -smtp_port=$($c.SmtpPort) -smtp_user=$($c.SmtpUser) -smtp_pass=$($c.SmtpPass) -smtp_from_email=$($c.SmtpFromEmail) -registry=$($c.Registry) -registry_user=$($c.RegistryUser) -registry_token=$($c.RegistryToken) -"@ - Write-Utf8File $f $txt - Log-Info 'Saved installer config to cameleer.conf' -} - -function Generate-CredentialsFile { - $c = $script:cfg - $f = Join-Path $c.InstallDir 'credentials.txt' - $ts = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' UTC' - - if ($c.DeploymentMode -eq 'standalone') { - $txt = @" -=========================================== - CAMELEER SERVER CREDENTIALS - Generated: $ts - - SECURE THIS FILE AND DELETE AFTER NOTING - THESE CREDENTIALS CANNOT BE RECOVERED -=========================================== - -Server Dashboard: $($c.PublicProtocol)://$($c.PublicHost)/ -Admin User: $($c.AdminUser) -Admin Password: $($c.AdminPass) - -PostgreSQL: cameleer / $($c.PostgresPassword) -ClickHouse: default / $($c.ClickhousePassword) -"@ - } else { - if ($c.LogtoConsoleExposed -eq 'true') { - $logtoLine = "Logto Console: $($c.PublicProtocol)://$($c.AuthHost):$($c.LogtoConsolePort)" - } else { - $logtoLine = 'Logto Console: (not exposed)' - } - $txt = @" -=========================================== - CAMELEER PLATFORM CREDENTIALS - Generated: $ts - - SECURE THIS FILE AND DELETE AFTER NOTING - THESE CREDENTIALS CANNOT BE RECOVERED -=========================================== - -Admin Console: $($c.PublicProtocol)://$($c.PublicHost)/platform/ -Admin User: $($c.AdminUser) -Admin Password: $($c.AdminPass) - -PostgreSQL: cameleer / $($c.PostgresPassword) -ClickHouse: default / $($c.ClickhousePassword) - -$logtoLine -"@ - } - - Write-Utf8File $f $txt - - # Restrict permissions on Windows (best effort) - try { - $acl = Get-Acl $f - $acl.SetAccessRuleProtection($true, $false) - $rule = New-Object Security.AccessControl.FileSystemAccessRule( - [Security.Principal.WindowsIdentity]::GetCurrent().Name, - 'FullControl', 'Allow') - $acl.SetAccessRule($rule) - Set-Acl $f $acl - } catch {} - - Log-Info 'Saved credentials to credentials.txt' -} - -function Generate-InstallDoc { - $c = $script:cfg - if ($c.DeploymentMode -eq 'standalone') { Generate-InstallDocStandalone; return } - - $f = Join-Path $c.InstallDir 'INSTALL.md' - $ts = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' UTC' - if ($c.TlsMode -eq 'custom') { $tlsDesc = 'Custom certificate' } else { $tlsDesc = 'Self-signed (auto-generated)' } - - if ($c.LogtoConsoleExposed -eq 'true') { - $logtoConsoleRow = "- **Logto Admin Console:** $($c.PublicProtocol)://$($c.AuthHost):$($c.LogtoConsolePort)" - $logtoPortRow = "| $($c.LogtoConsolePort) | Logto Admin Console |" - } else { - $logtoConsoleRow = '' - $logtoPortRow = '' - } - - if ($c.MonitoringNetwork) { - $monSection = @" - -### Monitoring - -Services are connected to the ``$($c.MonitoringNetwork)`` Docker network with Prometheus labels for auto-discovery. -"@ - } else { - $monSection = '' - } - - if ($c.TlsMode -eq 'self-signed') { - $tlsSection = @' - -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) -'@ - } else { - $tlsSection = '' - } - - $txt = @" -# Cameleer SaaS -- Installation Documentation - -## Installation Summary - -| | | -|---|---| -| **Version** | $($c.Version) | -| **Date** | $ts | -| **Installer** | v${CAMELEER_INSTALLER_VERSION} | -| **Install Directory** | $($c.InstallDir) | -| **Hostname** | $($c.PublicHost) | -| **TLS** | $tlsDesc | - -## Service URLs - -- **Platform UI:** $($c.PublicProtocol)://$($c.PublicHost)/platform/ -- **API Endpoint:** $($c.PublicProtocol)://$($c.PublicHost)/platform/api/ -$logtoConsoleRow - -## 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. - -## Networking - -| Port | Service | -|---|---| -| $($c.HttpPort) | HTTP (redirects to HTTPS) | -| $($c.HttpsPort) | HTTPS (main entry point) | -$logtoPortRow -$monSection - -## TLS - -**Mode:** $tlsDesc -$tlsSection - -## 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 -docker compose -p $($c.ComposeProject) exec cameleer-postgres pg_dump -U cameleer cameleer_saas > backup.sql -docker compose -p $($c.ComposeProject) exec cameleer-clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native -`````` - -## Upgrading - -``````powershell -.\install.ps1 -InstallDir $($c.InstallDir) -Version NEW_VERSION -`````` - -## Troubleshooting - -| Issue | Command | -|---|---| -| Service not starting | ``docker compose -p $($c.ComposeProject) logs SERVICE_NAME`` | -| Bootstrap failed | ``docker compose -p $($c.ComposeProject) logs cameleer-logto`` | -| Routing issues | ``docker compose -p $($c.ComposeProject) logs cameleer-traefik`` | -| Database issues | ``docker compose -p $($c.ComposeProject) exec cameleer-postgres psql -U cameleer -d cameleer_saas`` | - -## Uninstalling - -``````powershell -Set-Location $($c.InstallDir) -docker compose -p $($c.ComposeProject) down -docker compose -p $($c.ComposeProject) down -v -Remove-Item -Recurse -Force $($c.InstallDir) -`````` -"@ - Write-Utf8File $f $txt - Log-Info 'Generated INSTALL.md' -} - -function Generate-InstallDocStandalone { - $c = $script:cfg - $f = Join-Path $c.InstallDir 'INSTALL.md' - $ts = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + ' UTC' - if ($c.TlsMode -eq 'custom') { $tlsDesc = 'Custom certificate' } else { $tlsDesc = 'Self-signed (auto-generated)' } - - if ($c.MonitoringNetwork) { - $monSection = @" - -### Monitoring - -Services are connected to the ``$($c.MonitoringNetwork)`` Docker network for Prometheus auto-discovery. -"@ - } else { - $monSection = '' - } - - if ($c.TlsMode -eq 'self-signed') { - $tlsSection = @' - -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. -'@ - } else { - $tlsSection = '' - } - - $txt = @" -# Cameleer Server -- Installation Documentation - -## Installation Summary - -| | | -|---|---| -| **Version** | $($c.Version) | -| **Date** | $ts | -| **Installer** | v${CAMELEER_INSTALLER_VERSION} | -| **Mode** | Standalone (single-tenant) | -| **Install Directory** | $($c.InstallDir) | -| **Hostname** | $($c.PublicHost) | -| **TLS** | $tlsDesc | - -## Service URLs - -- **Server Dashboard:** $($c.PublicProtocol)://$($c.PublicHost)/ -- **API Endpoint:** $($c.PublicProtocol)://$($c.PublicHost)/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 | -|---|---| -| $($c.HttpPort) | HTTP (redirects to HTTPS) | -| $($c.HttpsPort) | HTTPS (main entry point) | -$monSection - -## TLS - -**Mode:** $tlsDesc -$tlsSection - -## 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 -docker compose -p $($c.ComposeProject) exec cameleer-postgres pg_dump -U cameleer cameleer > backup.sql -docker compose -p $($c.ComposeProject) exec cameleer-clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native -`````` - -## Upgrading - -``````powershell -.\install.ps1 -InstallDir $($c.InstallDir) -Version NEW_VERSION -`````` - -## Troubleshooting - -| Issue | Command | -|---|---| -| Service not starting | ``docker compose -p $($c.ComposeProject) logs SERVICE_NAME`` | -| Server issues | ``docker compose -p $($c.ComposeProject) logs cameleer-server`` | -| Routing issues | ``docker compose -p $($c.ComposeProject) logs cameleer-traefik`` | -| Database issues | ``docker compose -p $($c.ComposeProject) exec cameleer-postgres psql -U cameleer -d cameleer`` | - -## Uninstalling - -``````powershell -Set-Location $($c.InstallDir) -docker compose -p $($c.ComposeProject) down -docker compose -p $($c.ComposeProject) down -v -Remove-Item -Recurse -Force $($c.InstallDir) -`````` -"@ - Write-Utf8File $f $txt - Log-Info 'Generated INSTALL.md' -} - -# --- Console output --- - -function Print-Credentials { - $c = $script:cfg - Write-Host '' - Write-Host '==========================================' -ForegroundColor Cyan - if ($c.DeploymentMode -eq 'standalone') { - Write-Host ' CAMELEER SERVER CREDENTIALS' -ForegroundColor Cyan - } else { - Write-Host ' CAMELEER PLATFORM CREDENTIALS' -ForegroundColor Cyan - } - Write-Host '==========================================' -ForegroundColor Cyan - Write-Host '' - if ($c.DeploymentMode -eq 'standalone') { - Write-Host ' Dashboard: ' -NoNewline - Write-Host "$($c.PublicProtocol)://$($c.PublicHost)/" -ForegroundColor Blue - } else { - Write-Host ' Admin Console: ' -NoNewline - Write-Host "$($c.PublicProtocol)://$($c.PublicHost)/platform/" -ForegroundColor Blue - } - Write-Host " Admin User: $($c.AdminUser)" - Write-Host " Admin Password: $($c.AdminPass)" - Write-Host '' - Write-Host " PostgreSQL: cameleer / $($c.PostgresPassword)" - Write-Host " ClickHouse: default / $($c.ClickhousePassword)" - Write-Host '' - if ($c.DeploymentMode -eq 'saas' -and $c.LogtoConsoleExposed -eq 'true') { - Write-Host ' Logto Console: ' -NoNewline - Write-Host "$($c.PublicProtocol)://$($c.AuthHost):$($c.LogtoConsolePort)" -ForegroundColor Blue - Write-Host '' - } - Write-Host " Credentials saved to: $($c.InstallDir)\credentials.txt" - Write-Host ' Secure this file and delete after noting credentials.' -ForegroundColor Yellow - Write-Host '' -} - -function Print-Summary { - $c = $script:cfg - Write-Host '==========================================' -ForegroundColor Green - Write-Host ' Installation complete!' -ForegroundColor Green - Write-Host '==========================================' -ForegroundColor Green - Write-Host '' - Write-Host " Install directory: $($c.InstallDir)" - Write-Host " Documentation: $($c.InstallDir)\INSTALL.md" - Write-Host '' - Write-Host ' To manage the stack:' - Write-Host " Set-Location $($c.InstallDir)" - Write-Host " docker compose -p $($c.ComposeProject) ps # status" - Write-Host " docker compose -p $($c.ComposeProject) logs -f # logs" - Write-Host " docker compose -p $($c.ComposeProject) down # stop" - Write-Host '' -} - -# --- Re-run / upgrade --- - -function Show-RerunMenu { - $c = $script:cfg - $confFile = Join-Path $c.InstallDir 'cameleer.conf' - $currentVersion = '' - $currentHost = '' - foreach ($line in (Get-Content $confFile -ErrorAction SilentlyContinue)) { - if ($line -match '^version=(.+)$') { $currentVersion = $Matches[1] } - if ($line -match '^public_host=(.+)$') { $currentHost = $Matches[1] } - } - - Write-Host '' - Write-Host "Existing Cameleer installation detected (v$currentVersion)" -ForegroundColor Cyan - Write-Host " Install directory: $($c.InstallDir)" - Write-Host " Public host: $currentHost" - Write-Host '' - - if ($script:Mode -eq 'silent') { - if (-not $script:RerunAction) { $script:RerunAction = 'upgrade' } - return - } - if ($script:RerunAction) { return } - - $newVersion = Coalesce $c.Version $CAMELEER_DEFAULT_VERSION - Write-Host " [1] Upgrade to v$newVersion (pull new images, update compose)" - Write-Host ' [2] Reconfigure (re-run interactive setup, preserve data)' - Write-Host ' [3] Reinstall (fresh install, WARNING: destroys data volumes)' - Write-Host ' [4] Cancel' - Write-Host '' - $choice = Read-Host ' Select [1]' - switch ($choice) { - '2' { $script:RerunAction = 'reconfigure' } - '3' { $script:RerunAction = 'reinstall' } - '4' { Write-Host 'Cancelled.'; exit 0 } - default { $script:RerunAction = 'upgrade' } - } -} - -function Handle-Rerun { - $c = $script:cfg - switch ($script:RerunAction) { - 'upgrade' { - Log-Info 'Upgrading installation...' - Load-ConfigFile (Join-Path $c.InstallDir 'cameleer.conf') - Load-EnvOverrides - Merge-Config - $script:cfg.InstallDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( - $script:cfg.InstallDir) - Copy-Templates - Invoke-RegistryLogin - Invoke-ComposePull - Invoke-ComposeDown - Invoke-ComposeUp - Verify-Health - Generate-InstallDoc - Print-Summary - exit 0 - } - 'reconfigure' { - Log-Info 'Reconfiguring installation...' - return - } - 'reinstall' { - if (-not $script:ConfirmDestroy) { - Write-Host '' - Log-Warn 'This will destroy ALL data (databases, certificates, bootstrap).' - if (-not (Prompt-YesNo 'Are you sure? This cannot be undone.')) { - Write-Host 'Cancelled.' - exit 0 - } - } - Log-Info 'Reinstalling...' - try { Invoke-ComposeDown } catch {} - Push-Location $c.InstallDir - try { - $proj = Coalesce $c.ComposeProject 'cameleer-saas' - docker compose -p $proj down -v 2>$null - } catch {} - finally { Pop-Location } - foreach ($fname in @('.env','.env.bak','.env.example','docker-compose.yml','docker-compose.saas.yml','docker-compose.server.yml','docker-compose.tls.yml','docker-compose.monitoring.yml','traefik-dynamic.yml','cameleer.conf','credentials.txt','INSTALL.md')) { - $fp = Join-Path $c.InstallDir $fname - if (Test-Path $fp) { Remove-Item $fp -Force } - } - $certsDir = Join-Path $c.InstallDir 'certs' - if (Test-Path $certsDir) { Remove-Item $certsDir -Recurse -Force } - $script:IsRerun = $false - } - } -} - -# --- Entry point --- - -function Main { - if ($Help) { Show-Help; exit 0 } - - Print-Banner - - if ($Config) { Load-ConfigFile $Config } - Load-EnvOverrides - - Detect-ExistingInstall - if ($script:IsRerun) { - Show-RerunMenu - Handle-Rerun - } - - Check-Prerequisites - Auto-Detect - - if ($script:Mode -ne 'silent') { - Select-Mode - if ($script:Mode -eq 'expert') { Run-ExpertPrompts } else { Run-SimplePrompts } - } - - Merge-Config - Validate-Config - Generate-Passwords - - # Resolve to absolute path NOW. - # [System.IO.File]::WriteAllText uses .NET's Environment.CurrentDirectory, - # which differs from PowerShell's $PWD on Windows (e.g. resolves ./cameleer - # to C:\Users\Hendrik\cameleer instead of the script's working directory). - # GetUnresolvedProviderPathFromPSPath always uses PowerShell's current location. - $script:cfg.InstallDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( - $script:cfg.InstallDir) - - New-Item -ItemType Directory -Force -Path $script:cfg.InstallDir | Out-Null - - if ($script:cfg.TlsMode -eq 'custom') { Copy-Certs } - - Generate-EnvFile - Copy-Templates - Write-ConfigFile - - Invoke-RegistryLogin - Invoke-ComposePull - Invoke-ComposeUp - Verify-Health - - Generate-CredentialsFile - Generate-InstallDoc - - Print-Credentials - Print-Summary -} - -Main \ No newline at end of file diff --git a/installer/install.sh b/installer/install.sh deleted file mode 100644 index 8586d31..0000000 --- a/installer/install.sh +++ /dev/null @@ -1,1525 +0,0 @@ -#!/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/installer/templates/.env.example b/installer/templates/.env.example deleted file mode 100644 index f5618a3..0000000 --- a/installer/templates/.env.example +++ /dev/null @@ -1,102 +0,0 @@ -# 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/installer/templates/docker-compose.monitoring.yml b/installer/templates/docker-compose.monitoring.yml deleted file mode 100644 index ddbb723..0000000 --- a/installer/templates/docker-compose.monitoring.yml +++ /dev/null @@ -1,7 +0,0 @@ -# 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/installer/templates/docker-compose.saas.yml b/installer/templates/docker-compose.saas.yml deleted file mode 100644 index 5b95e2b..0000000 --- a/installer/templates/docker-compose.saas.yml +++ /dev/null @@ -1,131 +0,0 @@ -# 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/installer/templates/docker-compose.server.yml b/installer/templates/docker-compose.server.yml deleted file mode 100644 index ff8f78b..0000000 --- a/installer/templates/docker-compose.server.yml +++ /dev/null @@ -1,99 +0,0 @@ -# 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/installer/templates/docker-compose.tls.yml b/installer/templates/docker-compose.tls.yml deleted file mode 100644 index 489d08c..0000000 --- a/installer/templates/docker-compose.tls.yml +++ /dev/null @@ -1,7 +0,0 @@ -# Custom TLS certificates overlay -# Adds user-supplied certificate volume to traefik - -services: - cameleer-traefik: - volumes: - - ./certs:/user-certs:ro diff --git a/installer/templates/docker-compose.yml b/installer/templates/docker-compose.yml deleted file mode 100644 index dd60d37..0000000 --- a/installer/templates/docker-compose.yml +++ /dev/null @@ -1,80 +0,0 @@ -# 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/installer/templates/traefik-dynamic.yml b/installer/templates/traefik-dynamic.yml deleted file mode 100644 index b2a8787..0000000 --- a/installer/templates/traefik-dynamic.yml +++ /dev/null @@ -1,6 +0,0 @@ -tls: - stores: - default: - defaultCertificate: - certFile: /certs/cert.pem - keyFile: /certs/key.pem