2026-04-13 16:39:24 +02:00
|
|
|
#Requires -Version 5.1
|
|
|
|
|
<#
|
|
|
|
|
.SYNOPSIS
|
2026-04-14 19:17:41 +02:00
|
|
|
Cameleer SaaS Platform Installer
|
2026-04-13 16:39:24 +02:00
|
|
|
.DESCRIPTION
|
2026-04-14 19:17:41 +02:00
|
|
|
Installs the Cameleer SaaS platform using Docker Compose.
|
2026-04-13 16:39:24 +02:00
|
|
|
.EXAMPLE
|
|
|
|
|
.\install.ps1
|
2026-04-14 19:17:41 +02:00
|
|
|
.\install.ps1 -Expert
|
|
|
|
|
.\install.ps1 -Silent -PublicHost myhost.example.com
|
2026-04-13 16:39:24 +02:00
|
|
|
#>
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
[CmdletBinding()]
|
|
|
|
|
param(
|
|
|
|
|
[switch]$Silent,
|
|
|
|
|
[switch]$Expert,
|
|
|
|
|
[string]$Config,
|
|
|
|
|
[string]$InstallDir,
|
|
|
|
|
[string]$PublicHost,
|
|
|
|
|
[string]$PublicProtocol,
|
|
|
|
|
[string]$AdminUser,
|
|
|
|
|
[string]$AdminPassword,
|
|
|
|
|
[string]$TlsMode,
|
|
|
|
|
[string]$CertFile,
|
|
|
|
|
[string]$KeyFile,
|
|
|
|
|
[string]$CaFile,
|
|
|
|
|
[string]$PostgresPassword,
|
|
|
|
|
[string]$ClickhousePassword,
|
2026-04-14 19:17:41 +02:00
|
|
|
[string]$HttpPort,
|
|
|
|
|
[string]$HttpsPort,
|
|
|
|
|
[string]$LogtoConsolePort,
|
2026-04-13 16:39:24 +02:00
|
|
|
[string]$LogtoConsoleExposed,
|
|
|
|
|
[string]$MonitoringNetwork,
|
|
|
|
|
[string]$Version,
|
|
|
|
|
[string]$ComposeProject,
|
|
|
|
|
[string]$DockerSocket,
|
|
|
|
|
[string]$NodeTlsReject,
|
2026-04-14 19:17:41 +02:00
|
|
|
[string]$DeploymentMode,
|
2026-04-13 16:39:24 +02:00
|
|
|
[switch]$Reconfigure,
|
|
|
|
|
[switch]$Reinstall,
|
|
|
|
|
[switch]$ConfirmDestroy,
|
|
|
|
|
[switch]$Help
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
Set-StrictMode -Version Latest
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
# --- Constants ---
|
|
|
|
|
|
|
|
|
|
$CAMELEER_INSTALLER_VERSION = '1.0.0'
|
|
|
|
|
$CAMELEER_DEFAULT_VERSION = 'latest'
|
|
|
|
|
$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_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
|
|
|
|
|
|
|
|
|
|
# --- Mutable config state ---
|
|
|
|
|
|
|
|
|
|
$script:cfg = @{
|
|
|
|
|
InstallDir = $InstallDir
|
|
|
|
|
PublicHost = $PublicHost
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($Silent) { $script:Mode = 'silent' }
|
|
|
|
|
elseif ($Expert) { $script:Mode = 'expert' }
|
|
|
|
|
else { $script:Mode = '' }
|
2026-04-13 16:39:24 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($Reconfigure) { $script:RerunAction = 'reconfigure' }
|
|
|
|
|
elseif ($Reinstall) { $script:RerunAction = 'reinstall' }
|
|
|
|
|
else { $script:RerunAction = '' }
|
2026-04-13 16:39:24 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
$script:IsRerun = $false
|
|
|
|
|
$script:ConfirmDestroy = $ConfirmDestroy.IsPresent
|
2026-04-13 16:39:24 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
# --- Logging ---
|
2026-04-13 16:39:24 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
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 }
|
2026-04-13 16:39:24 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
function Print-Banner {
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host ' ____ _ ' -ForegroundColor Cyan
|
|
|
|
|
Write-Host ' / ___|__ _ _______ ___ | | ___ ___ _ _ ' -ForegroundColor Cyan
|
|
|
|
|
Write-Host '| | / _` | _ _ _ \ / _ \| |/ _ \/ _ \ `_|' -ForegroundColor Cyan
|
|
|
|
|
Write-Host '| |__| (_| | | | | | | __/| | __/ __/ | ' -ForegroundColor Cyan
|
|
|
|
|
Write-Host ' \____\__,_|_| |_| |_|\___||_|\___|\___|_| ' -ForegroundColor Cyan
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host " SaaS Platform Installer v$CAMELEER_INSTALLER_VERSION" -ForegroundColor Cyan
|
2026-04-13 16:39:24 +02:00
|
|
|
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 ' -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 '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)'
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host ' -Reinstall -ConfirmDestroy Fresh install (destroys data)'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Helpers ---
|
|
|
|
|
|
|
|
|
|
function Coalesce {
|
|
|
|
|
param($a, $b)
|
|
|
|
|
if ($a) { return $a } else { return $b }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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"
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Prompt-YesNo {
|
|
|
|
|
param([string]$Text, [string]$Default = 'n')
|
2026-04-13 16:39:24 +02:00
|
|
|
if ($Default -eq 'y') {
|
2026-04-14 19:17:41 +02:00
|
|
|
$inp = Read-Host " $Text [Y/n]"
|
|
|
|
|
if ($inp -match '^[nN]') { return $false } else { return $true }
|
2026-04-13 16:39:24 +02:00
|
|
|
} else {
|
2026-04-14 19:17:41 +02:00
|
|
|
$inp = Read-Host " $Text [y/N]"
|
|
|
|
|
return ($inp -match '^[yY]')
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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 ---
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
function Load-ConfigFile {
|
|
|
|
|
param([string]$FilePath)
|
2026-04-14 19:17:41 +02:00
|
|
|
if (-not (Test-Path $FilePath)) { return }
|
|
|
|
|
foreach ($line in Get-Content $FilePath) {
|
2026-04-13 16:39:24 +02:00
|
|
|
if ($line -match '^\s*#' -or $line -match '^\s*$') { continue }
|
2026-04-14 19:17:41 +02:00
|
|
|
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 } }
|
|
|
|
|
'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 } }
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
function Load-EnvOverrides {
|
2026-04-14 19:17:41 +02:00
|
|
|
$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.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 }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Prerequisites ---
|
|
|
|
|
|
|
|
|
|
function Check-PortAvailable {
|
2026-04-13 16:39:24 +02:00
|
|
|
param([string]$Port, [string]$Name)
|
|
|
|
|
try {
|
2026-04-14 19:17:41 +02:00
|
|
|
$hits = netstat -an 2>$null | Select-String ":${Port} "
|
|
|
|
|
if ($hits) { Log-Warn "Port $Port ($Name) appears to be in use." }
|
|
|
|
|
} catch {}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Check-Prerequisites {
|
|
|
|
|
Log-Info 'Checking prerequisites...'
|
2026-04-13 16:39:24 +02:00
|
|
|
$errors = 0
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
|
|
|
|
Log-Error 'Docker is not installed.'
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ' Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/'
|
|
|
|
|
$errors++
|
2026-04-14 19:17:41 +02:00
|
|
|
} else {
|
|
|
|
|
$v = docker version --format '{{.Server.Version}}' 2>$null
|
|
|
|
|
Log-Info "Docker version: $v"
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
docker compose version 2>$null | Out-Null
|
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
|
|
|
Log-Error "Docker Compose v2 not available. 'docker compose' subcommand required."
|
2026-04-13 16:39:24 +02:00
|
|
|
$errors++
|
2026-04-14 19:17:41 +02:00
|
|
|
} 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?'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
} elseif (-not (Test-Path $socket)) {
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Warn "Docker socket not found at $socket"
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
if ($errors -gt 0) {
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Error "$errors prerequisite(s) not met. Please install missing dependencies and retry."
|
2026-04-13 16:39:24 +02:00
|
|
|
exit 1
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Success 'All prerequisites met.'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Auto-detection ---
|
|
|
|
|
|
|
|
|
|
function Auto-Detect {
|
|
|
|
|
if (-not $script:cfg.PublicHost) {
|
2026-04-14 22:46:05 +02:00
|
|
|
$detectedHost = $null
|
|
|
|
|
# Try reverse DNS on each host IP — picks up FQDN from DNS server
|
2026-04-13 16:39:24 +02:00
|
|
|
try {
|
2026-04-14 22:46:05 +02:00
|
|
|
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()
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 22:46:05 +02:00
|
|
|
$script:cfg.PublicHost = $detectedHost.ToLower()
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
if (-not $script:cfg.DockerSocket) {
|
2026-04-14 22:46:05 +02:00
|
|
|
# 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
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Detect-ExistingInstall {
|
|
|
|
|
$dir = Coalesce $script:cfg.InstallDir $DEFAULT_INSTALL_DIR
|
2026-04-13 16:39:24 +02:00
|
|
|
$confPath = Join-Path $dir 'cameleer.conf'
|
2026-04-14 19:17:41 +02:00
|
|
|
if (Test-Path $confPath) {
|
|
|
|
|
$script:IsRerun = $true
|
|
|
|
|
$script:cfg.InstallDir = $dir
|
2026-04-13 16:39:24 +02:00
|
|
|
Load-ConfigFile $confPath
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Interactive prompts ---
|
|
|
|
|
|
|
|
|
|
function Select-Mode {
|
|
|
|
|
if ($script:Mode) { return }
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
|
|
|
|
Write-Host ' Installation mode:'
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host ' [1] Simple -- 6 questions, sensible defaults (recommended)'
|
|
|
|
|
Write-Host ' [2] Expert -- configure everything'
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
|
|
|
|
$choice = Read-Host ' Select mode [1]'
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($choice -eq '2') { $script:Mode = 'expert' } else { $script:Mode = 'simple' }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Run-SimplePrompts {
|
|
|
|
|
$c = $script:cfg
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host '--- Simple Installation ---' -ForegroundColor Cyan
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
$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 = ''
|
2026-04-13 16:39:24 +02:00
|
|
|
} else {
|
2026-04-14 19:17:41 +02:00
|
|
|
$c.AdminPass = Prompt-Password 'Admin password'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
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)'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-04-14 19:17:41 +02:00
|
|
|
$c.TlsMode = 'self-signed'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
$c.MonitoringNetwork = Prompt-Value 'Monitoring network name (empty = skip)' ''
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
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'
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
$deployChoice = Read-Host ' Select mode [1]'
|
|
|
|
|
if ($deployChoice -eq '2') { $c.DeploymentMode = 'standalone' } else { $c.DeploymentMode = 'saas' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Run-ExpertPrompts {
|
|
|
|
|
$c = $script:cfg
|
|
|
|
|
Run-SimplePrompts
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host ' Credentials:' -ForegroundColor Cyan
|
|
|
|
|
if (Prompt-YesNo 'Auto-generate database passwords?' 'y') {
|
|
|
|
|
$c.PostgresPassword = ''
|
|
|
|
|
$c.ClickhousePassword = ''
|
2026-04-13 16:39:24 +02:00
|
|
|
} else {
|
2026-04-14 19:17:41 +02:00
|
|
|
$c.PostgresPassword = Prompt-Password 'PostgreSQL password'
|
|
|
|
|
$c.ClickhousePassword = Prompt-Password 'ClickHouse password'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
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
|
|
|
|
|
if (Prompt-YesNo 'Expose Logto admin console externally?' 'y') {
|
|
|
|
|
$c.LogtoConsoleExposed = 'true'
|
|
|
|
|
} else {
|
|
|
|
|
$c.LogtoConsoleExposed = 'false'
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Config merge & validation ---
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
function Merge-Config {
|
2026-04-14 19:17:41 +02:00
|
|
|
$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.ComposeProject) {
|
|
|
|
|
if ($c.DeploymentMode -eq 'standalone') {
|
|
|
|
|
$c.ComposeProject = $DEFAULT_COMPOSE_PROJECT_STANDALONE
|
2026-04-13 16:39:24 +02:00
|
|
|
} else {
|
2026-04-14 19:17:41 +02:00
|
|
|
$c.ComposeProject = $DEFAULT_COMPOSE_PROJECT
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# Force lowercase -- Logto normalises internally; case mismatch breaks JWT validation
|
|
|
|
|
$c.PublicHost = $c.PublicHost.ToLower()
|
|
|
|
|
|
|
|
|
|
if ($c.DeploymentMode -ne 'standalone' -and (-not $c.NodeTlsReject)) {
|
|
|
|
|
if ($c.TlsMode -eq 'custom') { $c.NodeTlsReject = '1' } else { $c.NodeTlsReject = '0' }
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Validate-Config {
|
|
|
|
|
$c = $script:cfg
|
2026-04-13 16:39:24 +02:00
|
|
|
$errors = 0
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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++
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
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++
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
if ($errors -gt 0) { Log-Error 'Configuration validation failed.'; exit 1 }
|
|
|
|
|
Log-Success 'Configuration validated.'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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.' }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- 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'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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"
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- .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
|
|
|
|
|
|
|
|
|
|
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)
|
2026-04-15 15:28:44 +02:00
|
|
|
POSTGRES_DB=cameleer
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# ClickHouse
|
|
|
|
|
CLICKHOUSE_PASSWORD=$($c.ClickhousePassword)
|
|
|
|
|
|
|
|
|
|
# Server admin
|
|
|
|
|
SERVER_ADMIN_USER=$($c.AdminUser)
|
|
|
|
|
SERVER_ADMIN_PASS=$($c.AdminPass)
|
|
|
|
|
|
|
|
|
|
# Bootstrap token
|
|
|
|
|
BOOTSTRAP_TOKEN=$bt
|
|
|
|
|
|
|
|
|
|
# Docker
|
|
|
|
|
DOCKER_SOCKET=$($c.DockerSocket)
|
|
|
|
|
DOCKER_GID=$gid
|
2026-04-15 21:08:34 +02:00
|
|
|
|
|
|
|
|
POSTGRES_IMAGE=postgres:16-alpine
|
2026-04-14 19:17:41 +02:00
|
|
|
"@
|
|
|
|
|
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" }
|
|
|
|
|
}
|
2026-04-15 22:11:34 +02:00
|
|
|
$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' }
|
2026-04-15 21:08:34 +02:00
|
|
|
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
|
|
|
|
|
if ($c.MonitoringNetwork) {
|
|
|
|
|
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
} else {
|
2026-04-15 21:08:34 +02:00
|
|
|
$consoleBind = if ($c.LogtoConsoleExposed -eq 'true') { '0.0.0.0' } else { '127.0.0.1' }
|
2026-04-14 19:17:41 +02:00
|
|
|
$content = @"
|
2026-04-13 16:39:24 +02:00
|
|
|
# Cameleer SaaS Configuration
|
2026-04-14 19:17:41 +02:00
|
|
|
# Generated by installer v${CAMELEER_INSTALLER_VERSION} on $ts
|
2026-04-15 21:08:34 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
VERSION=$($c.Version)
|
2026-04-15 21:08:34 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
PUBLIC_HOST=$($c.PublicHost)
|
|
|
|
|
PUBLIC_PROTOCOL=$($c.PublicProtocol)
|
2026-04-15 21:08:34 +02:00
|
|
|
|
2026-04-14 19:17:41 +02:00
|
|
|
HTTP_PORT=$($c.HttpPort)
|
|
|
|
|
HTTPS_PORT=$($c.HttpsPort)
|
|
|
|
|
LOGTO_CONSOLE_PORT=$($c.LogtoConsolePort)
|
2026-04-15 21:08:34 +02:00
|
|
|
LOGTO_CONSOLE_BIND=$consoleBind
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
# PostgreSQL
|
|
|
|
|
POSTGRES_USER=cameleer
|
2026-04-14 19:17:41 +02:00
|
|
|
POSTGRES_PASSWORD=$($c.PostgresPassword)
|
2026-04-13 16:39:24 +02:00
|
|
|
POSTGRES_DB=cameleer_saas
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
# ClickHouse
|
2026-04-14 19:17:41 +02:00
|
|
|
CLICKHOUSE_PASSWORD=$($c.ClickhousePassword)
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
# Admin user
|
2026-04-14 19:17:41 +02:00
|
|
|
SAAS_ADMIN_USER=$($c.AdminUser)
|
|
|
|
|
SAAS_ADMIN_PASS=$($c.AdminPass)
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
# TLS
|
2026-04-14 19:17:41 +02:00
|
|
|
NODE_TLS_REJECT=$($c.NodeTlsReject)
|
2026-04-13 16:39:24 +02:00
|
|
|
"@
|
2026-04-14 19:17:41 +02:00
|
|
|
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" }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
$provisioningBlock = @"
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
# Docker
|
2026-04-14 19:17:41 +02:00
|
|
|
DOCKER_SOCKET=$($c.DockerSocket)
|
|
|
|
|
DOCKER_GID=$gid
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
# Provisioning images
|
2026-04-15 15:28:44 +02:00
|
|
|
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer-server:$($c.Version)
|
|
|
|
|
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:$($c.Version)
|
2026-04-15 23:20:46 +02:00
|
|
|
CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE=${REGISTRY}/cameleer-runtime-base:$($c.Version)
|
2026-04-13 16:39:24 +02:00
|
|
|
"@
|
2026-04-14 19:17:41 +02:00
|
|
|
$content += $provisioningBlock
|
2026-04-15 22:11:34 +02:00
|
|
|
$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' }
|
2026-04-15 21:08:34 +02:00
|
|
|
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
|
|
|
|
|
if ($c.MonitoringNetwork) {
|
|
|
|
|
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Utf8File $f $content
|
|
|
|
|
Copy-Item $f (Join-Path $c.InstallDir '.env.bak') -Force
|
|
|
|
|
Log-Info 'Generated .env'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-15 21:08:34 +02:00
|
|
|
# --- Copy docker-compose templates ---
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-15 21:08:34 +02:00
|
|
|
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
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-15 21:08:34 +02:00
|
|
|
|
|
|
|
|
# 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
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($c.MonitoringNetwork) {
|
2026-04-15 21:08:34 +02:00
|
|
|
Copy-Item (Join-Path $src 'docker-compose.monitoring.yml') (Join-Path $c.InstallDir 'docker-compose.monitoring.yml') -Force
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-15 21:08:34 +02:00
|
|
|
|
|
|
|
|
Log-Info "Copied docker-compose templates to $($c.InstallDir)"
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Docker operations ---
|
|
|
|
|
|
|
|
|
|
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.'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Invoke-ComposeUp {
|
|
|
|
|
$c = $script:cfg
|
|
|
|
|
Log-Info 'Starting Cameleer platform...'
|
|
|
|
|
Push-Location $c.InstallDir
|
2026-04-15 08:50:40 +02:00
|
|
|
try { docker compose -p $c.ComposeProject up -d --pull always --force-recreate }
|
2026-04-14 19:17:41 +02:00
|
|
|
finally { Pop-Location }
|
|
|
|
|
Log-Info 'Containers started -- verifying health next.'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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.
|
2026-04-13 16:39:24 +02:00
|
|
|
try {
|
2026-04-14 19:17:41 +02:00
|
|
|
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
|
2026-04-13 16:39:24 +02:00
|
|
|
Pop-Location
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
$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
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Test-Endpoint {
|
|
|
|
|
param([string]$Name, [string]$Url, [int]$TimeoutSecs = 120)
|
|
|
|
|
$start = Get-Date
|
|
|
|
|
$lastDot = -1
|
2026-04-13 16:39:24 +02:00
|
|
|
while ($true) {
|
2026-04-14 19:17:41 +02:00
|
|
|
$elapsed = [int]((Get-Date) - $start).TotalSeconds
|
2026-04-13 16:39:24 +02:00
|
|
|
if ($elapsed -ge $TimeoutSecs) {
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host (" [FAIL] {0,-20} not reachable after {1}s" -f $Name, $TimeoutSecs) -ForegroundColor Red
|
2026-04-13 16:39:24 +02:00
|
|
|
return $false
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
try {
|
|
|
|
|
# -SkipCertificateCheck is PS6+ only; SSL trust is handled by Enable-TrustAllCerts above
|
|
|
|
|
$resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5 -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
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Verify-Health {
|
|
|
|
|
$c = $script:cfg
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Info 'Verifying installation...'
|
|
|
|
|
Enable-TrustAllCerts # allow self-signed certs for PS5.1 Invoke-WebRequest
|
2026-04-13 16:39:24 +02:00
|
|
|
$failed = $false
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
if (-not (Wait-DockerHealthy 'PostgreSQL' 'cameleer-postgres' 120)) { $failed = $true }
|
2026-04-13 16:39:24 +02:00
|
|
|
if (-not $failed) {
|
2026-04-14 19:17:41 +02:00
|
|
|
if (-not (Wait-DockerHealthy 'ClickHouse' 'cameleer-clickhouse' 120)) { $failed = $true }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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)) { $failed = $true }
|
|
|
|
|
}
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($failed) { Log-Error 'Installation verification failed. Stack is running -- check logs.'; exit 1 }
|
|
|
|
|
Log-Success 'All services healthy.'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- 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)
|
|
|
|
|
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)
|
|
|
|
|
"@
|
|
|
|
|
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 = @"
|
2026-04-13 16:39:24 +02:00
|
|
|
===========================================
|
2026-04-14 19:17:41 +02:00
|
|
|
CAMELEER SERVER CREDENTIALS
|
|
|
|
|
Generated: $ts
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
SECURE THIS FILE AND DELETE AFTER NOTING
|
|
|
|
|
THESE CREDENTIALS CANNOT BE RECOVERED
|
|
|
|
|
===========================================
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
Server Dashboard: $($c.PublicProtocol)://$($c.PublicHost)/
|
|
|
|
|
Admin User: $($c.AdminUser)
|
|
|
|
|
Admin Password: $($c.AdminPass)
|
|
|
|
|
|
|
|
|
|
PostgreSQL: cameleer / $($c.PostgresPassword)
|
|
|
|
|
ClickHouse: default / $($c.ClickhousePassword)
|
2026-04-13 16:39:24 +02:00
|
|
|
"@
|
|
|
|
|
} else {
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($c.LogtoConsoleExposed -eq 'true') {
|
|
|
|
|
$logtoLine = "Logto Console: $($c.PublicProtocol)://$($c.PublicHost):$($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
|
|
|
|
|
"@
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
Write-Utf8File $f $txt
|
|
|
|
|
|
|
|
|
|
# Restrict permissions on Windows (best effort)
|
2026-04-13 16:39:24 +02:00
|
|
|
try {
|
2026-04-14 19:17:41 +02:00
|
|
|
$acl = Get-Acl $f
|
2026-04-13 16:39:24 +02:00
|
|
|
$acl.SetAccessRuleProtection($true, $false)
|
2026-04-14 19:17:41 +02:00
|
|
|
$rule = New-Object Security.AccessControl.FileSystemAccessRule(
|
|
|
|
|
[Security.Principal.WindowsIdentity]::GetCurrent().Name,
|
2026-04-13 16:39:24 +02:00
|
|
|
'FullControl', 'Allow')
|
|
|
|
|
$acl.SetAccessRule($rule)
|
|
|
|
|
Set-Acl $f $acl
|
2026-04-14 19:17:41 +02:00
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
Log-Info 'Saved credentials to credentials.txt'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
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.PublicHost):$($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
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Installation Summary
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
| | |
|
|
|
|
|
|---|---|
|
2026-04-14 19:17:41 +02:00
|
|
|
| **Version** | $($c.Version) |
|
|
|
|
|
| **Date** | $ts |
|
2026-04-13 16:39:24 +02:00
|
|
|
| **Installer** | v${CAMELEER_INSTALLER_VERSION} |
|
2026-04-14 19:17:41 +02:00
|
|
|
| **Install Directory** | $($c.InstallDir) |
|
|
|
|
|
| **Hostname** | $($c.PublicHost) |
|
|
|
|
|
| **TLS** | $tlsDesc |
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Service URLs
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
- **Platform UI:** $($c.PublicProtocol)://$($c.PublicHost)/platform/
|
|
|
|
|
- **API Endpoint:** $($c.PublicProtocol)://$($c.PublicHost)/platform/api/
|
|
|
|
|
$logtoConsoleRow
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## First Steps
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
1. Open the Platform UI in your browser
|
2026-04-14 19:17:41 +02:00
|
|
|
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
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Architecture
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
| Container | Purpose |
|
|
|
|
|
|---|---|
|
2026-04-14 19:17:41 +02:00
|
|
|
| ``traefik`` | Reverse proxy, TLS termination, routing |
|
|
|
|
|
| ``postgres`` | PostgreSQL database (SaaS + Logto + tenant schemas) |
|
|
|
|
|
| ``clickhouse`` | Time-series storage (traces, metrics, logs) |
|
|
|
|
|
| ``logto`` | OIDC identity provider + bootstrap |
|
|
|
|
|
| ``cameleer-saas`` | SaaS platform (Spring Boot + React) |
|
|
|
|
|
|
2026-04-15 15:28:44 +02:00
|
|
|
Per-tenant ``cameleer-server`` and ``cameleer-server-ui`` containers are provisioned dynamically.
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Networking
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
| 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.
|
2026-04-13 16:39:24 +02:00
|
|
|
'@
|
2026-04-14 19:17:41 +02:00
|
|
|
} else {
|
|
|
|
|
$tlsSection = ''
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
$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 |
|
|
|
|
|
|---|---|
|
|
|
|
|
| ``traefik`` | Reverse proxy, TLS termination, routing |
|
|
|
|
|
| ``postgres`` | PostgreSQL database (server data) |
|
|
|
|
|
| ``clickhouse`` | Time-series storage (traces, metrics, logs) |
|
|
|
|
|
| ``server`` | Cameleer Server (Spring Boot backend) |
|
|
|
|
|
| ``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
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Data & Backups
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
| Docker Volume | Contains |
|
|
|
|
|
|---|---|
|
2026-04-14 19:17:41 +02:00
|
|
|
| ``cameleer-pgdata`` | PostgreSQL data (server config, routes, deployments) |
|
|
|
|
|
| ``cameleer-chdata`` | ClickHouse data (traces, metrics, logs) |
|
|
|
|
|
| ``cameleer-certs`` | TLS certificates |
|
|
|
|
|
| ``jars`` | Uploaded application JARs |
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
### Backup Commands
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
``````bash
|
2026-04-15 15:28:44 +02:00
|
|
|
docker compose -p $($c.ComposeProject) exec cameleer-postgres pg_dump -U cameleer cameleer > backup.sql
|
2026-04-14 19:17:41 +02:00
|
|
|
docker compose -p $($c.ComposeProject) exec cameleer-clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native
|
|
|
|
|
``````
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Upgrading
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
``````powershell
|
|
|
|
|
.\install.ps1 -InstallDir $($c.InstallDir) -Version NEW_VERSION
|
|
|
|
|
``````
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Troubleshooting
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
| Issue | Command |
|
|
|
|
|
|---|---|
|
2026-04-14 19:17:41 +02:00
|
|
|
| Service not starting | ``docker compose -p $($c.ComposeProject) logs SERVICE_NAME`` |
|
|
|
|
|
| Server issues | ``docker compose -p $($c.ComposeProject) logs server`` |
|
|
|
|
|
| Routing issues | ``docker compose -p $($c.ComposeProject) logs cameleer-traefik`` |
|
2026-04-15 15:28:44 +02:00
|
|
|
| Database issues | ``docker compose -p $($c.ComposeProject) exec cameleer-postgres psql -U cameleer -d cameleer`` |
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
## Uninstalling
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
``````powershell
|
|
|
|
|
Set-Location $($c.InstallDir)
|
|
|
|
|
docker compose -p $($c.ComposeProject) down
|
|
|
|
|
docker compose -p $($c.ComposeProject) down -v
|
|
|
|
|
Remove-Item -Recurse -Force $($c.InstallDir)
|
|
|
|
|
``````
|
2026-04-13 16:39:24 +02:00
|
|
|
"@
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Utf8File $f $txt
|
|
|
|
|
Log-Info 'Generated INSTALL.md'
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Console output ---
|
|
|
|
|
|
|
|
|
|
function Print-Credentials {
|
|
|
|
|
$c = $script:cfg
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
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
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
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)"
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host " PostgreSQL: cameleer / $($c.PostgresPassword)"
|
|
|
|
|
Write-Host " ClickHouse: default / $($c.ClickhousePassword)"
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($c.DeploymentMode -eq 'saas' -and $c.LogtoConsoleExposed -eq 'true') {
|
|
|
|
|
Write-Host ' Logto Console: ' -NoNewline
|
|
|
|
|
Write-Host "$($c.PublicProtocol)://$($c.PublicHost):$($c.LogtoConsolePort)" -ForegroundColor Blue
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host " Credentials saved to: $($c.InstallDir)\credentials.txt"
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ' Secure this file and delete after noting credentials.' -ForegroundColor Yellow
|
|
|
|
|
Write-Host ''
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Print-Summary {
|
|
|
|
|
$c = $script:cfg
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host '==========================================' -ForegroundColor Green
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host ' Installation complete!' -ForegroundColor Green
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host '==========================================' -ForegroundColor Green
|
|
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host " Install directory: $($c.InstallDir)"
|
|
|
|
|
Write-Host " Documentation: $($c.InstallDir)\INSTALL.md"
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
|
|
|
|
Write-Host ' To manage the stack:'
|
2026-04-14 19:17:41 +02:00
|
|
|
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"
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Re-run / upgrade ---
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
function Show-RerunMenu {
|
2026-04-14 19:17:41 +02:00
|
|
|
$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] }
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-Host "Existing Cameleer installation detected (v$currentVersion)" -ForegroundColor Cyan
|
|
|
|
|
Write-Host " Install directory: $($c.InstallDir)"
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host " Public host: $currentHost"
|
|
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
if ($script:Mode -eq 'silent') {
|
|
|
|
|
if (-not $script:RerunAction) { $script:RerunAction = 'upgrade' }
|
2026-04-13 16:39:24 +02:00
|
|
|
return
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
if ($script:RerunAction) { return }
|
|
|
|
|
|
|
|
|
|
$newVersion = Coalesce $c.Version $CAMELEER_DEFAULT_VERSION
|
|
|
|
|
Write-Host " [1] Upgrade to v$newVersion (pull new images, update compose)"
|
2026-04-13 16:39:24 +02:00
|
|
|
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) {
|
2026-04-14 19:17:41 +02:00
|
|
|
'2' { $script:RerunAction = 'reconfigure' }
|
|
|
|
|
'3' { $script:RerunAction = 'reinstall' }
|
|
|
|
|
'4' { Write-Host 'Cancelled.'; exit 0 }
|
|
|
|
|
default { $script:RerunAction = 'upgrade' }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
function Handle-Rerun {
|
|
|
|
|
$c = $script:cfg
|
|
|
|
|
switch ($script:RerunAction) {
|
2026-04-13 16:39:24 +02:00
|
|
|
'upgrade' {
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Info 'Upgrading installation...'
|
|
|
|
|
Load-ConfigFile (Join-Path $c.InstallDir 'cameleer.conf')
|
2026-04-13 16:39:24 +02:00
|
|
|
Load-EnvOverrides
|
|
|
|
|
Merge-Config
|
2026-04-14 19:17:41 +02:00
|
|
|
$script:cfg.InstallDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
|
|
|
|
|
$script:cfg.InstallDir)
|
2026-04-15 21:08:34 +02:00
|
|
|
Copy-Templates
|
2026-04-14 19:17:41 +02:00
|
|
|
Invoke-ComposePull
|
|
|
|
|
Invoke-ComposeDown
|
|
|
|
|
Invoke-ComposeUp
|
|
|
|
|
Verify-Health
|
|
|
|
|
Generate-InstallDoc
|
|
|
|
|
Print-Summary
|
2026-04-13 16:39:24 +02:00
|
|
|
exit 0
|
|
|
|
|
}
|
|
|
|
|
'reconfigure' {
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Info 'Reconfiguring installation...'
|
2026-04-13 16:39:24 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
'reinstall' {
|
2026-04-14 19:17:41 +02:00
|
|
|
if (-not $script:ConfirmDestroy) {
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host ''
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Warn 'This will destroy ALL data (databases, certificates, bootstrap).'
|
|
|
|
|
if (-not (Prompt-YesNo 'Are you sure? This cannot be undone.')) {
|
2026-04-13 16:39:24 +02:00
|
|
|
Write-Host 'Cancelled.'
|
|
|
|
|
exit 0
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
Log-Info 'Reinstalling...'
|
|
|
|
|
try { Invoke-ComposeDown } catch {}
|
2026-04-14 22:46:05 +02:00
|
|
|
Push-Location $c.InstallDir
|
2026-04-13 16:39:24 +02:00
|
|
|
try {
|
2026-04-14 19:17:41 +02:00
|
|
|
$proj = Coalesce $c.ComposeProject 'cameleer-saas'
|
|
|
|
|
docker compose -p $proj down -v 2>$null
|
|
|
|
|
} catch {}
|
2026-04-14 22:46:05 +02:00
|
|
|
finally { Pop-Location }
|
2026-04-15 21:08:34 +02:00
|
|
|
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')) {
|
2026-04-14 19:17:41 +02:00
|
|
|
$fp = Join-Path $c.InstallDir $fname
|
2026-04-13 16:39:24 +02:00
|
|
|
if (Test-Path $fp) { Remove-Item $fp -Force }
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
$certsDir = Join-Path $c.InstallDir 'certs'
|
|
|
|
|
if (Test-Path $certsDir) { Remove-Item $certsDir -Recurse -Force }
|
|
|
|
|
$script:IsRerun = $false
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
# --- Entry point ---
|
|
|
|
|
|
|
|
|
|
function Main {
|
2026-04-13 16:39:24 +02:00
|
|
|
if ($Help) { Show-Help; exit 0 }
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
Print-Banner
|
|
|
|
|
|
|
|
|
|
if ($Config) { Load-ConfigFile $Config }
|
2026-04-13 16:39:24 +02:00
|
|
|
Load-EnvOverrides
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
Detect-ExistingInstall
|
|
|
|
|
if ($script:IsRerun) {
|
2026-04-13 16:39:24 +02:00
|
|
|
Show-RerunMenu
|
2026-04-14 19:17:41 +02:00
|
|
|
Handle-Rerun
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
Check-Prerequisites
|
|
|
|
|
Auto-Detect
|
|
|
|
|
|
|
|
|
|
if ($script:Mode -ne 'silent') {
|
|
|
|
|
Select-Mode
|
|
|
|
|
if ($script:Mode -eq 'expert') { Run-ExpertPrompts } else { Run-SimplePrompts }
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
2026-04-13 16:39:24 +02:00
|
|
|
Merge-Config
|
2026-04-14 19:17:41 +02:00
|
|
|
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
|
2026-04-15 21:08:34 +02:00
|
|
|
Copy-Templates
|
2026-04-14 19:17:41 +02:00
|
|
|
Write-ConfigFile
|
|
|
|
|
|
|
|
|
|
Invoke-ComposePull
|
|
|
|
|
Invoke-ComposeUp
|
|
|
|
|
Verify-Health
|
|
|
|
|
|
|
|
|
|
Generate-CredentialsFile
|
|
|
|
|
Generate-InstallDoc
|
|
|
|
|
|
|
|
|
|
Print-Credentials
|
|
|
|
|
Print-Summary
|
2026-04-13 16:39:24 +02:00
|
|
|
}
|
2026-04-14 19:17:41 +02:00
|
|
|
|
|
|
|
|
Main
|