1627 lines
64 KiB
PowerShell
1627 lines
64 KiB
PowerShell
|
|
#Requires -Version 5.1
|
||
|
|
<#
|
||
|
|
.SYNOPSIS
|
||
|
|
Cameleer SaaS Platform Installer for Windows
|
||
|
|
.DESCRIPTION
|
||
|
|
PowerShell port of install.sh. Produces identical output files
|
||
|
|
(.env, docker-compose.yml, credentials.txt, INSTALL.md, cameleer.conf).
|
||
|
|
Supports simple/expert/silent modes with the same config precedence:
|
||
|
|
CLI > env vars > config file > defaults.
|
||
|
|
.EXAMPLE
|
||
|
|
.\install.ps1
|
||
|
|
.EXAMPLE
|
||
|
|
.\install.ps1 -Silent -PublicHost myserver.example.com -InstallDir C:\cameleer
|
||
|
|
.EXAMPLE
|
||
|
|
.\install.ps1 -Expert -TlsMode custom -CertFile .\cert.pem -KeyFile .\key.pem
|
||
|
|
#>
|
||
|
|
[CmdletBinding()]
|
||
|
|
param(
|
||
|
|
[switch]$Silent,
|
||
|
|
[switch]$Expert,
|
||
|
|
[string]$Config,
|
||
|
|
[string]$InstallDir,
|
||
|
|
[string]$PublicHost,
|
||
|
|
[string]$PublicProtocol,
|
||
|
|
[string]$AdminUser,
|
||
|
|
[string]$AdminPassword,
|
||
|
|
[ValidateSet('self-signed', 'custom')]
|
||
|
|
[string]$TlsMode,
|
||
|
|
[string]$CertFile,
|
||
|
|
[string]$KeyFile,
|
||
|
|
[string]$CaFile,
|
||
|
|
[string]$PostgresPassword,
|
||
|
|
[string]$ClickhousePassword,
|
||
|
|
[int]$HttpPort,
|
||
|
|
[int]$HttpsPort,
|
||
|
|
[int]$LogtoConsolePort,
|
||
|
|
[string]$LogtoConsoleExposed,
|
||
|
|
[string]$VendorEnabled,
|
||
|
|
[string]$VendorUser,
|
||
|
|
[string]$VendorPassword,
|
||
|
|
[string]$MonitoringNetwork,
|
||
|
|
[string]$Version,
|
||
|
|
[string]$ComposeProject,
|
||
|
|
[string]$DockerSocket,
|
||
|
|
[string]$NodeTlsReject,
|
||
|
|
[switch]$Reconfigure,
|
||
|
|
[switch]$Reinstall,
|
||
|
|
[switch]$ConfirmDestroy,
|
||
|
|
[switch]$Help
|
||
|
|
)
|
||
|
|
|
||
|
|
Set-StrictMode -Version Latest
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Constants and defaults
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
$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_VENDOR_ENABLED = 'false'
|
||
|
|
$DEFAULT_VENDOR_USER = 'vendor'
|
||
|
|
$DEFAULT_COMPOSE_PROJECT = 'cameleer-saas'
|
||
|
|
$DEFAULT_DOCKER_SOCKET = '//./pipe/docker_engine'
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Save environment values BEFORE CLI params override (same pattern as bash)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
$_ENV_PUBLIC_HOST = if ($env:PUBLIC_HOST) { $env:PUBLIC_HOST } else { '' }
|
||
|
|
$_ENV_PUBLIC_PROTOCOL = if ($env:PUBLIC_PROTOCOL) { $env:PUBLIC_PROTOCOL } else { '' }
|
||
|
|
$_ENV_POSTGRES_PASSWORD = if ($env:POSTGRES_PASSWORD) { $env:POSTGRES_PASSWORD } else { '' }
|
||
|
|
$_ENV_CLICKHOUSE_PASSWORD = if ($env:CLICKHOUSE_PASSWORD) { $env:CLICKHOUSE_PASSWORD } else { '' }
|
||
|
|
$_ENV_TLS_MODE = if ($env:TLS_MODE) { $env:TLS_MODE } else { '' }
|
||
|
|
$_ENV_CERT_FILE = if ($env:CERT_FILE) { $env:CERT_FILE } else { '' }
|
||
|
|
$_ENV_KEY_FILE = if ($env:KEY_FILE) { $env:KEY_FILE } else { '' }
|
||
|
|
$_ENV_CA_FILE = if ($env:CA_FILE) { $env:CA_FILE } else { '' }
|
||
|
|
$_ENV_HTTP_PORT = if ($env:HTTP_PORT) { $env:HTTP_PORT } else { '' }
|
||
|
|
$_ENV_HTTPS_PORT = if ($env:HTTPS_PORT) { $env:HTTPS_PORT } else { '' }
|
||
|
|
$_ENV_LOGTO_CONSOLE_PORT = if ($env:LOGTO_CONSOLE_PORT) { $env:LOGTO_CONSOLE_PORT } else { '' }
|
||
|
|
$_ENV_LOGTO_CONSOLE_EXPOSED = if ($env:LOGTO_CONSOLE_EXPOSED) { $env:LOGTO_CONSOLE_EXPOSED } else { '' }
|
||
|
|
$_ENV_VENDOR_ENABLED = if ($env:VENDOR_ENABLED) { $env:VENDOR_ENABLED } else { '' }
|
||
|
|
$_ENV_VENDOR_USER = if ($env:VENDOR_USER) { $env:VENDOR_USER } else { '' }
|
||
|
|
$_ENV_VENDOR_PASS = if ($env:VENDOR_PASS) { $env:VENDOR_PASS } else { '' }
|
||
|
|
$_ENV_MONITORING_NETWORK = if ($env:MONITORING_NETWORK) { $env:MONITORING_NETWORK } else { '' }
|
||
|
|
$_ENV_COMPOSE_PROJECT = if ($env:COMPOSE_PROJECT) { $env:COMPOSE_PROJECT } else { '' }
|
||
|
|
$_ENV_DOCKER_SOCKET = if ($env:DOCKER_SOCKET) { $env:DOCKER_SOCKET } else { '' }
|
||
|
|
$_ENV_NODE_TLS_REJECT = if ($env:NODE_TLS_REJECT) { $env:NODE_TLS_REJECT } else { '' }
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Mutable config state (populated from CLI, env, config file, prompts)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
$script:CFG_INSTALL_DIR = if ($InstallDir) { $InstallDir } else { '' }
|
||
|
|
$script:CFG_PUBLIC_HOST = if ($PublicHost) { $PublicHost } else { '' }
|
||
|
|
$script:CFG_PUBLIC_PROTOCOL = if ($PublicProtocol) { $PublicProtocol } else { '' }
|
||
|
|
$script:CFG_ADMIN_USER = if ($AdminUser) { $AdminUser } else { '' }
|
||
|
|
$script:CFG_ADMIN_PASS = if ($AdminPassword) { $AdminPassword } else { '' }
|
||
|
|
$script:CFG_TLS_MODE = if ($TlsMode) { $TlsMode } else { '' }
|
||
|
|
$script:CFG_CERT_FILE = if ($CertFile) { $CertFile } else { '' }
|
||
|
|
$script:CFG_KEY_FILE = if ($KeyFile) { $KeyFile } else { '' }
|
||
|
|
$script:CFG_CA_FILE = if ($CaFile) { $CaFile } else { '' }
|
||
|
|
$script:CFG_POSTGRES_PASSWORD = if ($PostgresPassword) { $PostgresPassword } else { '' }
|
||
|
|
$script:CFG_CLICKHOUSE_PASSWORD = if ($ClickhousePassword) { $ClickhousePassword } else { '' }
|
||
|
|
$script:CFG_HTTP_PORT = if ($HttpPort -ne 0) { "$HttpPort" } else { '' }
|
||
|
|
$script:CFG_HTTPS_PORT = if ($HttpsPort -ne 0) { "$HttpsPort" } else { '' }
|
||
|
|
$script:CFG_LOGTO_CONSOLE_PORT = if ($LogtoConsolePort -ne 0) { "$LogtoConsolePort" } else { '' }
|
||
|
|
$script:CFG_LOGTO_CONSOLE_EXPOSED = if ($LogtoConsoleExposed) { $LogtoConsoleExposed } else { '' }
|
||
|
|
$script:CFG_VENDOR_ENABLED = if ($VendorEnabled) { $VendorEnabled } else { '' }
|
||
|
|
$script:CFG_VENDOR_USER = if ($VendorUser) { $VendorUser } else { '' }
|
||
|
|
$script:CFG_VENDOR_PASS = if ($VendorPassword) { $VendorPassword } else { '' }
|
||
|
|
$script:CFG_MONITORING_NETWORK = if ($MonitoringNetwork) { $MonitoringNetwork } else { '' }
|
||
|
|
$script:CFG_VERSION = if ($Version) { $Version } else { '' }
|
||
|
|
$script:CFG_COMPOSE_PROJECT = if ($ComposeProject) { $ComposeProject } else { '' }
|
||
|
|
$script:CFG_DOCKER_SOCKET = if ($DockerSocket) { $DockerSocket } else { '' }
|
||
|
|
$script:CFG_NODE_TLS_REJECT = if ($NodeTlsReject) { $NodeTlsReject } else { '' }
|
||
|
|
|
||
|
|
# State
|
||
|
|
$script:MODE = '' # simple, expert, silent
|
||
|
|
$script:IS_RERUN = $false
|
||
|
|
$script:RERUN_ACTION = '' # upgrade, reconfigure, reinstall
|
||
|
|
$script:CONFIRM_DESTROY = $false
|
||
|
|
|
||
|
|
# Translate switch params to state
|
||
|
|
if ($Silent) { $script:MODE = 'silent' }
|
||
|
|
if ($Expert) { $script:MODE = 'expert' }
|
||
|
|
if ($Reconfigure) { $script:RERUN_ACTION = 'reconfigure' }
|
||
|
|
if ($Reinstall) { $script:RERUN_ACTION = 'reinstall' }
|
||
|
|
if ($ConfirmDestroy){ $script:CONFIRM_DESTROY = $true }
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Utility functions
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Green }
|
||
|
|
function Write-Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow }
|
||
|
|
function Write-Err($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red }
|
||
|
|
function Write-Success($msg) { Write-Host "[OK] $msg" -ForegroundColor Green }
|
||
|
|
|
||
|
|
function Show-Banner {
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host ' ____ _ ' -ForegroundColor White
|
||
|
|
Write-Host ' / ___|__ _ _ __ ___ ___ | | ___ ___ _ __ ' -ForegroundColor White
|
||
|
|
Write-Host '| | / _` | `''_ ` _ \ / _ \| |/ _ \/ _ \ `''__|' -ForegroundColor White
|
||
|
|
Write-Host '| |__| (_| | | | | | | __/| | __/ __/ | ' -ForegroundColor White
|
||
|
|
Write-Host ' \____\__,_|_| |_| |_|\___||_|\___|\___||_| ' -ForegroundColor White
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host " SaaS Platform Installer v$CAMELEER_INSTALLER_VERSION" -ForegroundColor White
|
||
|
|
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 ' -VendorEnabled, -VendorUser, -VendorPassword,'
|
||
|
|
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)'
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host 'Config precedence: CLI flags > env vars > config file > defaults'
|
||
|
|
}
|
||
|
|
|
||
|
|
function New-SecurePassword {
|
||
|
|
<#
|
||
|
|
.SYNOPSIS
|
||
|
|
Generates a random password equivalent to: openssl rand -base64 24 | tr -d '/+=' | head -c 32
|
||
|
|
#>
|
||
|
|
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
||
|
|
$bytes = New-Object byte[] 24
|
||
|
|
$rng.GetBytes($bytes)
|
||
|
|
$rng.Dispose()
|
||
|
|
$b64 = [Convert]::ToBase64String($bytes)
|
||
|
|
# Strip /+= characters like bash tr -d '/+='
|
||
|
|
$clean = $b64 -replace '[/+=]', ''
|
||
|
|
# Take up to 32 characters like head -c 32
|
||
|
|
if ($clean.Length -gt 32) { $clean = $clean.Substring(0, 32) }
|
||
|
|
return $clean
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-Prompt {
|
||
|
|
param(
|
||
|
|
[string]$PromptText,
|
||
|
|
[string]$Default = ''
|
||
|
|
)
|
||
|
|
if ($Default -ne '') {
|
||
|
|
$input = Read-Host " $PromptText [$Default]"
|
||
|
|
if ([string]::IsNullOrEmpty($input)) { return $Default }
|
||
|
|
return $input
|
||
|
|
} else {
|
||
|
|
$input = Read-Host " $PromptText"
|
||
|
|
return $input
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-PasswordPrompt {
|
||
|
|
param(
|
||
|
|
[string]$PromptText,
|
||
|
|
[string]$Default = ''
|
||
|
|
)
|
||
|
|
if ($Default -ne '') {
|
||
|
|
$ss = Read-Host " $PromptText [********]" -AsSecureString
|
||
|
|
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
||
|
|
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss))
|
||
|
|
if ([string]::IsNullOrEmpty($plain)) { return $Default }
|
||
|
|
return $plain
|
||
|
|
} else {
|
||
|
|
$ss = Read-Host " $PromptText" -AsSecureString
|
||
|
|
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
||
|
|
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss))
|
||
|
|
return $plain
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-YesNoPrompt {
|
||
|
|
param(
|
||
|
|
[string]$PromptText,
|
||
|
|
[string]$Default = 'n'
|
||
|
|
)
|
||
|
|
if ($Default -eq 'y') {
|
||
|
|
$input = Read-Host " $PromptText [Y/n]"
|
||
|
|
if ([string]::IsNullOrEmpty($input)) { return $true }
|
||
|
|
return ($input -match '^[yY]')
|
||
|
|
} else {
|
||
|
|
$input = Read-Host " $PromptText [y/N]"
|
||
|
|
if ([string]::IsNullOrEmpty($input)) { return $false }
|
||
|
|
return ($input -match '^[yY]')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Config file loading
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Load-ConfigFile {
|
||
|
|
param([string]$FilePath)
|
||
|
|
|
||
|
|
if (-not (Test-Path $FilePath -PathType Leaf)) { return }
|
||
|
|
|
||
|
|
foreach ($line in [System.IO.File]::ReadAllLines($FilePath)) {
|
||
|
|
# Skip comments and empty lines
|
||
|
|
if ($line -match '^\s*#' -or $line -match '^\s*$') { continue }
|
||
|
|
if ($line -notmatch '=') { continue }
|
||
|
|
|
||
|
|
$idx = $line.IndexOf('=')
|
||
|
|
$key = $line.Substring(0, $idx).Trim()
|
||
|
|
$value = $line.Substring($idx + 1).Trim()
|
||
|
|
|
||
|
|
switch ($key) {
|
||
|
|
'install_dir' { if ($script:CFG_INSTALL_DIR -eq '') { $script:CFG_INSTALL_DIR = $value } }
|
||
|
|
'public_host' { if ($script:CFG_PUBLIC_HOST -eq '') { $script:CFG_PUBLIC_HOST = $value } }
|
||
|
|
'public_protocol' { if ($script:CFG_PUBLIC_PROTOCOL -eq '') { $script:CFG_PUBLIC_PROTOCOL = $value } }
|
||
|
|
'admin_user' { if ($script:CFG_ADMIN_USER -eq '') { $script:CFG_ADMIN_USER = $value } }
|
||
|
|
'admin_password' { if ($script:CFG_ADMIN_PASS -eq '') { $script:CFG_ADMIN_PASS = $value } }
|
||
|
|
'tls_mode' { if ($script:CFG_TLS_MODE -eq '') { $script:CFG_TLS_MODE = $value } }
|
||
|
|
'cert_file' { if ($script:CFG_CERT_FILE -eq '') { $script:CFG_CERT_FILE = $value } }
|
||
|
|
'key_file' { if ($script:CFG_KEY_FILE -eq '') { $script:CFG_KEY_FILE = $value } }
|
||
|
|
'ca_file' { if ($script:CFG_CA_FILE -eq '') { $script:CFG_CA_FILE = $value } }
|
||
|
|
'postgres_password' { if ($script:CFG_POSTGRES_PASSWORD -eq '') { $script:CFG_POSTGRES_PASSWORD = $value } }
|
||
|
|
'clickhouse_password' { if ($script:CFG_CLICKHOUSE_PASSWORD -eq '') { $script:CFG_CLICKHOUSE_PASSWORD = $value } }
|
||
|
|
'http_port' { if ($script:CFG_HTTP_PORT -eq '') { $script:CFG_HTTP_PORT = $value } }
|
||
|
|
'https_port' { if ($script:CFG_HTTPS_PORT -eq '') { $script:CFG_HTTPS_PORT = $value } }
|
||
|
|
'logto_console_port' { if ($script:CFG_LOGTO_CONSOLE_PORT -eq '') { $script:CFG_LOGTO_CONSOLE_PORT = $value } }
|
||
|
|
'logto_console_exposed' { if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq '') { $script:CFG_LOGTO_CONSOLE_EXPOSED = $value } }
|
||
|
|
'vendor_enabled' { if ($script:CFG_VENDOR_ENABLED -eq '') { $script:CFG_VENDOR_ENABLED = $value } }
|
||
|
|
'vendor_user' { if ($script:CFG_VENDOR_USER -eq '') { $script:CFG_VENDOR_USER = $value } }
|
||
|
|
'vendor_password' { if ($script:CFG_VENDOR_PASS -eq '') { $script:CFG_VENDOR_PASS = $value } }
|
||
|
|
'monitoring_network' { if ($script:CFG_MONITORING_NETWORK -eq '') { $script:CFG_MONITORING_NETWORK = $value } }
|
||
|
|
'version' { if ($script:CFG_VERSION -eq '') { $script:CFG_VERSION = $value } }
|
||
|
|
'compose_project' { if ($script:CFG_COMPOSE_PROJECT -eq '') { $script:CFG_COMPOSE_PROJECT = $value } }
|
||
|
|
'docker_socket' { if ($script:CFG_DOCKER_SOCKET -eq '') { $script:CFG_DOCKER_SOCKET = $value } }
|
||
|
|
'node_tls_reject' { if ($script:CFG_NODE_TLS_REJECT -eq '') { $script:CFG_NODE_TLS_REJECT = $value } }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Environment variable overrides (fills gaps left by CLI)
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Load-EnvOverrides {
|
||
|
|
if ($script:CFG_INSTALL_DIR -eq '') { $script:CFG_INSTALL_DIR = if ($env:CAMELEER_INSTALL_DIR) { $env:CAMELEER_INSTALL_DIR } else { '' } }
|
||
|
|
if ($script:CFG_PUBLIC_HOST -eq '') { $script:CFG_PUBLIC_HOST = $_ENV_PUBLIC_HOST }
|
||
|
|
if ($script:CFG_PUBLIC_PROTOCOL -eq '') { $script:CFG_PUBLIC_PROTOCOL = $_ENV_PUBLIC_PROTOCOL }
|
||
|
|
if ($script:CFG_ADMIN_USER -eq '') { $script:CFG_ADMIN_USER = if ($env:SAAS_ADMIN_USER) { $env:SAAS_ADMIN_USER } else { '' } }
|
||
|
|
if ($script:CFG_ADMIN_PASS -eq '') { $script:CFG_ADMIN_PASS = if ($env:SAAS_ADMIN_PASS) { $env:SAAS_ADMIN_PASS } else { '' } }
|
||
|
|
if ($script:CFG_TLS_MODE -eq '') { $script:CFG_TLS_MODE = $_ENV_TLS_MODE }
|
||
|
|
if ($script:CFG_CERT_FILE -eq '') { $script:CFG_CERT_FILE = $_ENV_CERT_FILE }
|
||
|
|
if ($script:CFG_KEY_FILE -eq '') { $script:CFG_KEY_FILE = $_ENV_KEY_FILE }
|
||
|
|
if ($script:CFG_CA_FILE -eq '') { $script:CFG_CA_FILE = $_ENV_CA_FILE }
|
||
|
|
if ($script:CFG_POSTGRES_PASSWORD -eq '') { $script:CFG_POSTGRES_PASSWORD = $_ENV_POSTGRES_PASSWORD }
|
||
|
|
if ($script:CFG_CLICKHOUSE_PASSWORD -eq '') { $script:CFG_CLICKHOUSE_PASSWORD = $_ENV_CLICKHOUSE_PASSWORD }
|
||
|
|
if ($script:CFG_HTTP_PORT -eq '') { $script:CFG_HTTP_PORT = $_ENV_HTTP_PORT }
|
||
|
|
if ($script:CFG_HTTPS_PORT -eq '') { $script:CFG_HTTPS_PORT = $_ENV_HTTPS_PORT }
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_PORT -eq '') { $script:CFG_LOGTO_CONSOLE_PORT = $_ENV_LOGTO_CONSOLE_PORT }
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq '') { $script:CFG_LOGTO_CONSOLE_EXPOSED = $_ENV_LOGTO_CONSOLE_EXPOSED }
|
||
|
|
if ($script:CFG_VENDOR_ENABLED -eq '') { $script:CFG_VENDOR_ENABLED = $_ENV_VENDOR_ENABLED }
|
||
|
|
if ($script:CFG_VENDOR_USER -eq '') { $script:CFG_VENDOR_USER = $_ENV_VENDOR_USER }
|
||
|
|
if ($script:CFG_VENDOR_PASS -eq '') { $script:CFG_VENDOR_PASS = $_ENV_VENDOR_PASS }
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -eq '') { $script:CFG_MONITORING_NETWORK = $_ENV_MONITORING_NETWORK }
|
||
|
|
if ($script:CFG_VERSION -eq '') { $script:CFG_VERSION = if ($env:CAMELEER_VERSION) { $env:CAMELEER_VERSION } else { '' } }
|
||
|
|
if ($script:CFG_COMPOSE_PROJECT -eq '') { $script:CFG_COMPOSE_PROJECT = $_ENV_COMPOSE_PROJECT }
|
||
|
|
if ($script:CFG_DOCKER_SOCKET -eq '') { $script:CFG_DOCKER_SOCKET = $_ENV_DOCKER_SOCKET }
|
||
|
|
if ($script:CFG_NODE_TLS_REJECT -eq '') { $script:CFG_NODE_TLS_REJECT = $_ENV_NODE_TLS_REJECT }
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Prerequisites
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Test-PortAvailable {
|
||
|
|
param([string]$Port, [string]$Name)
|
||
|
|
try {
|
||
|
|
$portNum = [int]$Port
|
||
|
|
$result = Test-NetConnection -ComputerName 127.0.0.1 -Port $portNum -WarningAction SilentlyContinue -ErrorAction SilentlyContinue
|
||
|
|
if ($result.TcpTestSucceeded) {
|
||
|
|
Write-Warn "Port $Port ($Name) is already in use."
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
# Ignore errors — port check is advisory only
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-Prerequisites {
|
||
|
|
Write-Info 'Checking prerequisites...'
|
||
|
|
$errors = 0
|
||
|
|
|
||
|
|
# Docker
|
||
|
|
try {
|
||
|
|
$dockerVer = & docker version --format '{{.Server.Version}}' 2>$null
|
||
|
|
if ($LASTEXITCODE -ne 0) { throw 'docker version failed' }
|
||
|
|
Write-Info "Docker version: $dockerVer"
|
||
|
|
} catch {
|
||
|
|
Write-Err 'Docker is not installed or not running.'
|
||
|
|
Write-Host ' Install Docker Desktop: https://docs.docker.com/desktop/install/windows-install/'
|
||
|
|
$errors++
|
||
|
|
}
|
||
|
|
|
||
|
|
# Docker Compose v2
|
||
|
|
try {
|
||
|
|
$null = & docker compose version 2>$null
|
||
|
|
if ($LASTEXITCODE -ne 0) { throw 'compose not available' }
|
||
|
|
$composeVer = & docker compose version --short 2>$null
|
||
|
|
Write-Info "Docker Compose version: $composeVer"
|
||
|
|
} catch {
|
||
|
|
Write-Err "Docker Compose v2 is not available. 'docker compose' subcommand required."
|
||
|
|
$errors++
|
||
|
|
}
|
||
|
|
|
||
|
|
# Docker socket / pipe
|
||
|
|
$socket = if ($script:CFG_DOCKER_SOCKET -ne '') { $script:CFG_DOCKER_SOCKET } else { $DEFAULT_DOCKER_SOCKET }
|
||
|
|
if ($socket -like '//./pipe/*') {
|
||
|
|
$pipeName = $socket -replace '^//\./pipe/', ''
|
||
|
|
if (-not (Test-Path "\\.\pipe\$pipeName")) {
|
||
|
|
Write-Warn "Docker named pipe not found at $socket"
|
||
|
|
}
|
||
|
|
} elseif (-not (Test-Path $socket)) {
|
||
|
|
Write-Warn "Docker socket not found at $socket"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Port checks
|
||
|
|
$httpPort = if ($script:CFG_HTTP_PORT -ne '') { $script:CFG_HTTP_PORT } else { $DEFAULT_HTTP_PORT }
|
||
|
|
$httpsPort = if ($script:CFG_HTTPS_PORT -ne '') { $script:CFG_HTTPS_PORT } else { $DEFAULT_HTTPS_PORT }
|
||
|
|
$logtoPort = if ($script:CFG_LOGTO_CONSOLE_PORT -ne '') { $script:CFG_LOGTO_CONSOLE_PORT } else { $DEFAULT_LOGTO_CONSOLE_PORT }
|
||
|
|
Test-PortAvailable $httpPort 'HTTP'
|
||
|
|
Test-PortAvailable $httpsPort 'HTTPS'
|
||
|
|
Test-PortAvailable $logtoPort 'Logto Console'
|
||
|
|
|
||
|
|
if ($errors -gt 0) {
|
||
|
|
Write-Err "$errors prerequisite(s) not met. Please install missing dependencies and retry."
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
Write-Success 'All prerequisites met.'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Auto-detection
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Get-AutoDetectedDefaults {
|
||
|
|
if ($script:CFG_PUBLIC_HOST -eq '') {
|
||
|
|
try {
|
||
|
|
$entry = [System.Net.Dns]::GetHostEntry([System.Net.Dns]::GetHostName())
|
||
|
|
$script:CFG_PUBLIC_HOST = $entry.HostName
|
||
|
|
} catch {
|
||
|
|
$script:CFG_PUBLIC_HOST = [System.Net.Dns]::GetHostName()
|
||
|
|
}
|
||
|
|
if (-not $script:CFG_PUBLIC_HOST) { $script:CFG_PUBLIC_HOST = 'localhost' }
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($script:CFG_DOCKER_SOCKET -eq '') {
|
||
|
|
$script:CFG_DOCKER_SOCKET = $DEFAULT_DOCKER_SOCKET
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Detect existing installation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Find-ExistingInstall {
|
||
|
|
$dir = if ($script:CFG_INSTALL_DIR -ne '') { $script:CFG_INSTALL_DIR } else { $DEFAULT_INSTALL_DIR }
|
||
|
|
$confPath = Join-Path $dir 'cameleer.conf'
|
||
|
|
if (Test-Path $confPath -PathType Leaf) {
|
||
|
|
$script:IS_RERUN = $true
|
||
|
|
$script:CFG_INSTALL_DIR = $dir
|
||
|
|
Load-ConfigFile $confPath
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Mode selection
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Select-InstallMode {
|
||
|
|
if ($script:MODE -ne '') { 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' }
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Interactive prompts
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Invoke-SimplePrompts {
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host '--- Simple Installation ---' -ForegroundColor White
|
||
|
|
Write-Host ''
|
||
|
|
|
||
|
|
$script:CFG_INSTALL_DIR = Invoke-Prompt 'Install directory' (if ($script:CFG_INSTALL_DIR -ne '') { $script:CFG_INSTALL_DIR } else { $DEFAULT_INSTALL_DIR })
|
||
|
|
$script:CFG_PUBLIC_HOST = Invoke-Prompt 'Public hostname' (if ($script:CFG_PUBLIC_HOST -ne '') { $script:CFG_PUBLIC_HOST } else { 'localhost' })
|
||
|
|
$script:CFG_ADMIN_USER = Invoke-Prompt 'Admin username' (if ($script:CFG_ADMIN_USER -ne '') { $script:CFG_ADMIN_USER } else { $DEFAULT_ADMIN_USER })
|
||
|
|
|
||
|
|
if (Invoke-YesNoPrompt 'Auto-generate admin password?' 'y') {
|
||
|
|
$script:CFG_ADMIN_PASS = ''
|
||
|
|
} else {
|
||
|
|
$script:CFG_ADMIN_PASS = Invoke-PasswordPrompt 'Admin password'
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
if (Invoke-YesNoPrompt 'Use custom TLS certificates? (no = self-signed)') {
|
||
|
|
$script:CFG_TLS_MODE = 'custom'
|
||
|
|
$script:CFG_CERT_FILE = Invoke-Prompt 'Path to certificate file (PEM)'
|
||
|
|
$script:CFG_KEY_FILE = Invoke-Prompt 'Path to private key file (PEM)'
|
||
|
|
if (Invoke-YesNoPrompt 'Include CA bundle?') {
|
||
|
|
$script:CFG_CA_FILE = Invoke-Prompt 'Path to CA bundle (PEM)'
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$script:CFG_TLS_MODE = 'self-signed'
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
$script:CFG_MONITORING_NETWORK = Invoke-Prompt 'Monitoring network name (empty = skip)'
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-ExpertPrompts {
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host '--- Expert Installation ---' -ForegroundColor White
|
||
|
|
|
||
|
|
Invoke-SimplePrompts
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host ' Credentials:' -ForegroundColor White
|
||
|
|
if (Invoke-YesNoPrompt 'Auto-generate database passwords?' 'y') {
|
||
|
|
$script:CFG_POSTGRES_PASSWORD = ''
|
||
|
|
$script:CFG_CLICKHOUSE_PASSWORD = ''
|
||
|
|
} else {
|
||
|
|
$script:CFG_POSTGRES_PASSWORD = Invoke-PasswordPrompt 'PostgreSQL password'
|
||
|
|
$script:CFG_CLICKHOUSE_PASSWORD = Invoke-PasswordPrompt 'ClickHouse password'
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
if (Invoke-YesNoPrompt 'Enable vendor account?') {
|
||
|
|
$script:CFG_VENDOR_ENABLED = 'true'
|
||
|
|
$script:CFG_VENDOR_USER = Invoke-Prompt 'Vendor username' (if ($script:CFG_VENDOR_USER -ne '') { $script:CFG_VENDOR_USER } else { $DEFAULT_VENDOR_USER })
|
||
|
|
if (Invoke-YesNoPrompt 'Auto-generate vendor password?' 'y') {
|
||
|
|
$script:CFG_VENDOR_PASS = ''
|
||
|
|
} else {
|
||
|
|
$script:CFG_VENDOR_PASS = Invoke-PasswordPrompt 'Vendor password'
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$script:CFG_VENDOR_ENABLED = 'false'
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host ' Networking:' -ForegroundColor White
|
||
|
|
$script:CFG_HTTP_PORT = Invoke-Prompt 'HTTP port' (if ($script:CFG_HTTP_PORT -ne '') { $script:CFG_HTTP_PORT } else { $DEFAULT_HTTP_PORT })
|
||
|
|
$script:CFG_HTTPS_PORT = Invoke-Prompt 'HTTPS port' (if ($script:CFG_HTTPS_PORT -ne '') { $script:CFG_HTTPS_PORT } else { $DEFAULT_HTTPS_PORT })
|
||
|
|
$script:CFG_LOGTO_CONSOLE_PORT = Invoke-Prompt 'Logto admin console port' (if ($script:CFG_LOGTO_CONSOLE_PORT -ne '') { $script:CFG_LOGTO_CONSOLE_PORT } else { $DEFAULT_LOGTO_CONSOLE_PORT })
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host ' Docker:' -ForegroundColor White
|
||
|
|
$script:CFG_VERSION = Invoke-Prompt 'Image version/tag' (if ($script:CFG_VERSION -ne '') { $script:CFG_VERSION } else { $CAMELEER_DEFAULT_VERSION })
|
||
|
|
$script:CFG_COMPOSE_PROJECT = Invoke-Prompt 'Compose project name' (if ($script:CFG_COMPOSE_PROJECT -ne '') { $script:CFG_COMPOSE_PROJECT } else { $DEFAULT_COMPOSE_PROJECT })
|
||
|
|
$script:CFG_DOCKER_SOCKET = Invoke-Prompt 'Docker socket path' (if ($script:CFG_DOCKER_SOCKET -ne '') { $script:CFG_DOCKER_SOCKET } else { $DEFAULT_DOCKER_SOCKET })
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host ' Logto:' -ForegroundColor White
|
||
|
|
if (Invoke-YesNoPrompt 'Expose Logto admin console externally?' 'y') {
|
||
|
|
$script:CFG_LOGTO_CONSOLE_EXPOSED = 'true'
|
||
|
|
} else {
|
||
|
|
$script:CFG_LOGTO_CONSOLE_EXPOSED = 'false'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Config merge and validation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Merge-Config {
|
||
|
|
if ($script:CFG_INSTALL_DIR -eq '') { $script:CFG_INSTALL_DIR = $DEFAULT_INSTALL_DIR }
|
||
|
|
if ($script:CFG_PUBLIC_HOST -eq '') { $script:CFG_PUBLIC_HOST = 'localhost' }
|
||
|
|
if ($script:CFG_PUBLIC_PROTOCOL -eq '') { $script:CFG_PUBLIC_PROTOCOL = $DEFAULT_PUBLIC_PROTOCOL }
|
||
|
|
if ($script:CFG_ADMIN_USER -eq '') { $script:CFG_ADMIN_USER = $DEFAULT_ADMIN_USER }
|
||
|
|
if ($script:CFG_TLS_MODE -eq '') { $script:CFG_TLS_MODE = $DEFAULT_TLS_MODE }
|
||
|
|
if ($script:CFG_HTTP_PORT -eq '') { $script:CFG_HTTP_PORT = $DEFAULT_HTTP_PORT }
|
||
|
|
if ($script:CFG_HTTPS_PORT -eq '') { $script:CFG_HTTPS_PORT = $DEFAULT_HTTPS_PORT }
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_PORT -eq '') { $script:CFG_LOGTO_CONSOLE_PORT = $DEFAULT_LOGTO_CONSOLE_PORT }
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq '') { $script:CFG_LOGTO_CONSOLE_EXPOSED = $DEFAULT_LOGTO_CONSOLE_EXPOSED }
|
||
|
|
if ($script:CFG_VENDOR_ENABLED -eq '') { $script:CFG_VENDOR_ENABLED = $DEFAULT_VENDOR_ENABLED }
|
||
|
|
if ($script:CFG_VENDOR_USER -eq '') { $script:CFG_VENDOR_USER = $DEFAULT_VENDOR_USER }
|
||
|
|
if ($script:CFG_VERSION -eq '') { $script:CFG_VERSION = $CAMELEER_DEFAULT_VERSION }
|
||
|
|
if ($script:CFG_COMPOSE_PROJECT -eq '') { $script:CFG_COMPOSE_PROJECT = $DEFAULT_COMPOSE_PROJECT }
|
||
|
|
if ($script:CFG_DOCKER_SOCKET -eq '') { $script:CFG_DOCKER_SOCKET = $DEFAULT_DOCKER_SOCKET }
|
||
|
|
|
||
|
|
if ($script:CFG_NODE_TLS_REJECT -eq '') {
|
||
|
|
if ($script:CFG_TLS_MODE -eq 'custom') {
|
||
|
|
$script:CFG_NODE_TLS_REJECT = '1'
|
||
|
|
} else {
|
||
|
|
$script:CFG_NODE_TLS_REJECT = '0'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-Config {
|
||
|
|
$errors = 0
|
||
|
|
|
||
|
|
if ($script:CFG_TLS_MODE -eq 'custom') {
|
||
|
|
if (-not (Test-Path $script:CFG_CERT_FILE -PathType Leaf)) {
|
||
|
|
Write-Err "Certificate file not found: $($script:CFG_CERT_FILE)"
|
||
|
|
$errors++
|
||
|
|
}
|
||
|
|
if (-not (Test-Path $script:CFG_KEY_FILE -PathType Leaf)) {
|
||
|
|
Write-Err "Key file not found: $($script:CFG_KEY_FILE)"
|
||
|
|
$errors++
|
||
|
|
}
|
||
|
|
if ($script:CFG_CA_FILE -ne '' -and -not (Test-Path $script:CFG_CA_FILE -PathType Leaf)) {
|
||
|
|
Write-Err "CA bundle not found: $($script:CFG_CA_FILE)"
|
||
|
|
$errors++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($portEntry in @(
|
||
|
|
@{ Name = 'HttpPort'; Value = $script:CFG_HTTP_PORT }
|
||
|
|
@{ Name = 'HttpsPort'; Value = $script:CFG_HTTPS_PORT }
|
||
|
|
@{ Name = 'LogtoConsolePort'; Value = $script:CFG_LOGTO_CONSOLE_PORT }
|
||
|
|
)) {
|
||
|
|
$portVal = $portEntry.Value
|
||
|
|
if ($portVal -notmatch '^\d+$' -or [int]$portVal -lt 1 -or [int]$portVal -gt 65535) {
|
||
|
|
Write-Err "Invalid port for $($portEntry.Name): $portVal"
|
||
|
|
$errors++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($errors -gt 0) {
|
||
|
|
Write-Err 'Configuration validation failed.'
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
Write-Success 'Configuration validated.'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Password generation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function New-Passwords {
|
||
|
|
if ($script:CFG_ADMIN_PASS -eq '') {
|
||
|
|
$script:CFG_ADMIN_PASS = New-SecurePassword
|
||
|
|
Write-Info 'Generated admin password.'
|
||
|
|
}
|
||
|
|
if ($script:CFG_POSTGRES_PASSWORD -eq '') {
|
||
|
|
$script:CFG_POSTGRES_PASSWORD = New-SecurePassword
|
||
|
|
Write-Info 'Generated PostgreSQL password.'
|
||
|
|
}
|
||
|
|
if ($script:CFG_CLICKHOUSE_PASSWORD -eq '') {
|
||
|
|
$script:CFG_CLICKHOUSE_PASSWORD = New-SecurePassword
|
||
|
|
Write-Info 'Generated ClickHouse password.'
|
||
|
|
}
|
||
|
|
if ($script:CFG_VENDOR_ENABLED -eq 'true' -and $script:CFG_VENDOR_PASS -eq '') {
|
||
|
|
$script:CFG_VENDOR_PASS = New-SecurePassword
|
||
|
|
Write-Info 'Generated vendor password.'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Certificate copy
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Copy-Certificates {
|
||
|
|
$certsDir = Join-Path $script:CFG_INSTALL_DIR 'certs'
|
||
|
|
if (-not (Test-Path $certsDir)) { New-Item -ItemType Directory -Path $certsDir -Force | Out-Null }
|
||
|
|
Copy-Item $script:CFG_CERT_FILE (Join-Path $certsDir 'cert.pem') -Force
|
||
|
|
Copy-Item $script:CFG_KEY_FILE (Join-Path $certsDir 'key.pem') -Force
|
||
|
|
if ($script:CFG_CA_FILE -ne '') {
|
||
|
|
Copy-Item $script:CFG_CA_FILE (Join-Path $certsDir 'ca.pem') -Force
|
||
|
|
}
|
||
|
|
Write-Info "Copied TLS certificates to $certsDir/"
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Helper: write file content with LF line endings
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Write-LFFile {
|
||
|
|
param([string]$Path, [string]$Content)
|
||
|
|
# Normalize to LF only (replace CRLF then stray CR)
|
||
|
|
$normalized = $Content -replace "`r`n", "`n" -replace "`r", "`n"
|
||
|
|
[System.IO.File]::WriteAllText($Path, $normalized, [System.Text.Encoding]::UTF8)
|
||
|
|
}
|
||
|
|
|
||
|
|
function Append-LFFile {
|
||
|
|
param([string]$Path, [string]$Content)
|
||
|
|
$normalized = $Content -replace "`r`n", "`n" -replace "`r", "`n"
|
||
|
|
$stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write)
|
||
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($normalized)
|
||
|
|
$stream.Write($bytes, 0, $bytes.Length)
|
||
|
|
$stream.Close()
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# .env file generation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function New-EnvFile {
|
||
|
|
$f = Join-Path $script:CFG_INSTALL_DIR '.env'
|
||
|
|
$utcNow = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss UTC')
|
||
|
|
$ver = $script:CFG_VERSION
|
||
|
|
$host_ = $script:CFG_PUBLIC_HOST
|
||
|
|
$proto = $script:CFG_PUBLIC_PROTOCOL
|
||
|
|
$httpPort = $script:CFG_HTTP_PORT
|
||
|
|
$httpsPort= $script:CFG_HTTPS_PORT
|
||
|
|
$logtoPort= $script:CFG_LOGTO_CONSOLE_PORT
|
||
|
|
$pgPass = $script:CFG_POSTGRES_PASSWORD
|
||
|
|
$chPass = $script:CFG_CLICKHOUSE_PASSWORD
|
||
|
|
$admUser = $script:CFG_ADMIN_USER
|
||
|
|
$admPass = $script:CFG_ADMIN_PASS
|
||
|
|
$nodeTls = $script:CFG_NODE_TLS_REJECT
|
||
|
|
$vendEnabled = $script:CFG_VENDOR_ENABLED
|
||
|
|
$vendUser = $script:CFG_VENDOR_USER
|
||
|
|
$vendPass = $script:CFG_VENDOR_PASS
|
||
|
|
$dockerSock = $script:CFG_DOCKER_SOCKET
|
||
|
|
|
||
|
|
$content = @"
|
||
|
|
# Cameleer SaaS Configuration
|
||
|
|
# Generated by installer v${CAMELEER_INSTALLER_VERSION} on ${utcNow}
|
||
|
|
|
||
|
|
# Image version
|
||
|
|
VERSION=${ver}
|
||
|
|
|
||
|
|
# Public access
|
||
|
|
PUBLIC_HOST=${host_}
|
||
|
|
PUBLIC_PROTOCOL=${proto}
|
||
|
|
|
||
|
|
# Ports
|
||
|
|
HTTP_PORT=${httpPort}
|
||
|
|
HTTPS_PORT=${httpsPort}
|
||
|
|
LOGTO_CONSOLE_PORT=${logtoPort}
|
||
|
|
|
||
|
|
# PostgreSQL
|
||
|
|
POSTGRES_USER=cameleer
|
||
|
|
POSTGRES_PASSWORD=${pgPass}
|
||
|
|
POSTGRES_DB=cameleer_saas
|
||
|
|
|
||
|
|
# ClickHouse
|
||
|
|
CLICKHOUSE_PASSWORD=${chPass}
|
||
|
|
|
||
|
|
# Admin user
|
||
|
|
SAAS_ADMIN_USER=${admUser}
|
||
|
|
SAAS_ADMIN_PASS=${admPass}
|
||
|
|
|
||
|
|
# TLS
|
||
|
|
NODE_TLS_REJECT=${nodeTls}
|
||
|
|
"@
|
||
|
|
|
||
|
|
Write-LFFile $f $content
|
||
|
|
|
||
|
|
if ($script:CFG_TLS_MODE -eq 'custom') {
|
||
|
|
Append-LFFile $f "CERT_FILE=/user-certs/cert.pem`nKEY_FILE=/user-certs/key.pem`n"
|
||
|
|
if ($script:CFG_CA_FILE -ne '') {
|
||
|
|
Append-LFFile $f "CA_FILE=/user-certs/ca.pem`n"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$vendorSection = @"
|
||
|
|
|
||
|
|
# Vendor account
|
||
|
|
VENDOR_SEED_ENABLED=${vendEnabled}
|
||
|
|
VENDOR_USER=${vendUser}
|
||
|
|
VENDOR_PASS=${vendPass}
|
||
|
|
|
||
|
|
# Docker
|
||
|
|
DOCKER_SOCKET=${dockerSock}
|
||
|
|
|
||
|
|
# Provisioning images
|
||
|
|
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer3-server:${ver}
|
||
|
|
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer3-server-ui:${ver}
|
||
|
|
"@
|
||
|
|
Append-LFFile $f $vendorSection
|
||
|
|
|
||
|
|
Write-Info 'Generated .env'
|
||
|
|
Copy-Item $f (Join-Path $script:CFG_INSTALL_DIR '.env.bak') -Force
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# docker-compose.yml generation
|
||
|
|
# The template uses ${VAR} references that Docker Compose expands at runtime.
|
||
|
|
# We use single-quoted PowerShell here-strings (@'...'@) to keep those
|
||
|
|
# references as literal text — they must NOT be expanded by PowerShell.
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function New-ComposeFile {
|
||
|
|
$f = Join-Path $script:CFG_INSTALL_DIR 'docker-compose.yml'
|
||
|
|
if (Test-Path $f) { Remove-Item $f -Force }
|
||
|
|
|
||
|
|
# --- Header + traefik service start ---
|
||
|
|
$header = @'
|
||
|
|
# Cameleer SaaS Platform
|
||
|
|
# Generated by Cameleer installer — do not edit manually
|
||
|
|
|
||
|
|
services:
|
||
|
|
traefik:
|
||
|
|
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
|
||
|
|
restart: unless-stopped
|
||
|
|
ports:
|
||
|
|
- "${HTTP_PORT:-80}:80"
|
||
|
|
- "${HTTPS_PORT:-443}:443"
|
||
|
|
'@
|
||
|
|
Write-LFFile $f $header
|
||
|
|
|
||
|
|
# Conditional: Logto console port exposure
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq 'true') {
|
||
|
|
Append-LFFile $f @'
|
||
|
|
- "${LOGTO_CONSOLE_PORT:-3002}:3002"
|
||
|
|
'@
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f @'
|
||
|
|
environment:
|
||
|
|
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
||
|
|
CERT_FILE: ${CERT_FILE:-}
|
||
|
|
KEY_FILE: ${KEY_FILE:-}
|
||
|
|
CA_FILE: ${CA_FILE:-}
|
||
|
|
volumes:
|
||
|
|
- certs:/certs
|
||
|
|
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
|
||
|
|
'@
|
||
|
|
|
||
|
|
# Conditional: custom cert mount
|
||
|
|
if ($script:CFG_TLS_MODE -eq 'custom') {
|
||
|
|
Append-LFFile $f @'
|
||
|
|
- ./certs:/user-certs:ro
|
||
|
|
'@
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f @'
|
||
|
|
networks:
|
||
|
|
- cameleer
|
||
|
|
- cameleer-traefik
|
||
|
|
'@
|
||
|
|
|
||
|
|
# Conditional: monitoring network + labels for traefik
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -ne '') {
|
||
|
|
Append-LFFile $f " - $($script:CFG_MONITORING_NETWORK)`n"
|
||
|
|
Append-LFFile $f @'
|
||
|
|
labels:
|
||
|
|
- "prometheus.io/scrape=true"
|
||
|
|
- "prometheus.io/port=8082"
|
||
|
|
- "prometheus.io/path=/metrics"
|
||
|
|
'@
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- postgres service ---
|
||
|
|
Append-LFFile $f @'
|
||
|
|
|
||
|
|
postgres:
|
||
|
|
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
|
||
|
|
restart: unless-stopped
|
||
|
|
environment:
|
||
|
|
POSTGRES_DB: cameleer_saas
|
||
|
|
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
|
||
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||
|
|
volumes:
|
||
|
|
- pgdata:/var/lib/postgresql/data
|
||
|
|
healthcheck:
|
||
|
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d cameleer_saas"]
|
||
|
|
interval: 5s
|
||
|
|
timeout: 5s
|
||
|
|
retries: 5
|
||
|
|
networks:
|
||
|
|
- cameleer
|
||
|
|
'@
|
||
|
|
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -ne '') {
|
||
|
|
Append-LFFile $f " - $($script:CFG_MONITORING_NETWORK)`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- clickhouse service ---
|
||
|
|
Append-LFFile $f @'
|
||
|
|
|
||
|
|
clickhouse:
|
||
|
|
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
|
||
|
|
restart: unless-stopped
|
||
|
|
environment:
|
||
|
|
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
|
||
|
|
volumes:
|
||
|
|
- chdata:/var/lib/clickhouse
|
||
|
|
healthcheck:
|
||
|
|
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
|
||
|
|
interval: 10s
|
||
|
|
timeout: 5s
|
||
|
|
retries: 3
|
||
|
|
networks:
|
||
|
|
- cameleer
|
||
|
|
'@
|
||
|
|
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -ne '') {
|
||
|
|
Append-LFFile $f " - $($script:CFG_MONITORING_NETWORK)`n"
|
||
|
|
Append-LFFile $f @'
|
||
|
|
labels:
|
||
|
|
- "prometheus.io/scrape=true"
|
||
|
|
- "prometheus.io/port=9363"
|
||
|
|
- "prometheus.io/path=/metrics"
|
||
|
|
'@
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- logto service ---
|
||
|
|
Append-LFFile $f @'
|
||
|
|
|
||
|
|
logto:
|
||
|
|
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
|
||
|
|
restart: unless-stopped
|
||
|
|
depends_on:
|
||
|
|
postgres:
|
||
|
|
condition: service_healthy
|
||
|
|
environment:
|
||
|
|
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@postgres:5432/logto
|
||
|
|
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||
|
|
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
|
||
|
|
TRUST_PROXY_HEADER: 1
|
||
|
|
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
|
||
|
|
LOGTO_ENDPOINT: http://logto:3001
|
||
|
|
LOGTO_ADMIN_ENDPOINT: http://logto:3002
|
||
|
|
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||
|
|
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
||
|
|
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
||
|
|
PG_HOST: 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:-admin}
|
||
|
|
VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}"
|
||
|
|
VENDOR_USER: ${VENDOR_USER:-vendor}
|
||
|
|
VENDOR_PASS: ${VENDOR_PASS:-vendor}
|
||
|
|
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.logto.rule=PathPrefix(`/`)
|
||
|
|
- traefik.http.routers.logto.priority=1
|
||
|
|
- traefik.http.routers.logto.entrypoints=websecure
|
||
|
|
- traefik.http.routers.logto.tls=true
|
||
|
|
- traefik.http.routers.logto.service=logto
|
||
|
|
- traefik.http.routers.logto.middlewares=logto-cors
|
||
|
|
- "traefik.http.middlewares.logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
|
||
|
|
- traefik.http.middlewares.logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
||
|
|
- traefik.http.middlewares.logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
|
||
|
|
- traefik.http.middlewares.logto-cors.headers.accessControlAllowCredentials=true
|
||
|
|
- traefik.http.services.logto.loadbalancer.server.port=3001
|
||
|
|
'@
|
||
|
|
|
||
|
|
# Conditional: Logto console router labels
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq 'true') {
|
||
|
|
Append-LFFile $f @'
|
||
|
|
- traefik.http.routers.logto-console.rule=PathPrefix(`/`)
|
||
|
|
- traefik.http.routers.logto-console.entrypoints=admin-console
|
||
|
|
- traefik.http.routers.logto-console.tls=true
|
||
|
|
- traefik.http.routers.logto-console.service=logto-console
|
||
|
|
- traefik.http.services.logto-console.loadbalancer.server.port=3002
|
||
|
|
'@
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f @'
|
||
|
|
volumes:
|
||
|
|
- bootstrapdata:/data
|
||
|
|
networks:
|
||
|
|
- cameleer
|
||
|
|
|
||
|
|
cameleer-saas:
|
||
|
|
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
|
||
|
|
restart: unless-stopped
|
||
|
|
depends_on:
|
||
|
|
logto:
|
||
|
|
condition: service_healthy
|
||
|
|
environment:
|
||
|
|
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/cameleer_saas
|
||
|
|
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
|
||
|
|
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
|
||
|
|
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://logto:3001
|
||
|
|
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
||
|
|
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
||
|
|
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
|
||
|
|
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
|
||
|
|
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
|
||
|
|
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer3-server:latest}
|
||
|
|
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer3-server-ui: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
|
||
|
|
'@
|
||
|
|
|
||
|
|
# Conditional: monitoring labels for cameleer-saas
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -ne '') {
|
||
|
|
Append-LFFile $f @'
|
||
|
|
- "prometheus.io/scrape=true"
|
||
|
|
- "prometheus.io/port=8080"
|
||
|
|
- "prometheus.io/path=/platform/actuator/prometheus"
|
||
|
|
'@
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f @'
|
||
|
|
volumes:
|
||
|
|
- bootstrapdata:/data/bootstrap:ro
|
||
|
|
- certs:/certs
|
||
|
|
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
|
||
|
|
networks:
|
||
|
|
- cameleer
|
||
|
|
'@
|
||
|
|
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -ne '') {
|
||
|
|
Append-LFFile $f " - $($script:CFG_MONITORING_NETWORK)`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f @'
|
||
|
|
group_add:
|
||
|
|
- "0"
|
||
|
|
|
||
|
|
volumes:
|
||
|
|
pgdata:
|
||
|
|
chdata:
|
||
|
|
certs:
|
||
|
|
bootstrapdata:
|
||
|
|
|
||
|
|
networks:
|
||
|
|
cameleer:
|
||
|
|
driver: bridge
|
||
|
|
cameleer-traefik:
|
||
|
|
name: cameleer-traefik
|
||
|
|
driver: bridge
|
||
|
|
'@
|
||
|
|
|
||
|
|
# Conditional: external monitoring network declaration
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -ne '') {
|
||
|
|
Append-LFFile $f " $($script:CFG_MONITORING_NETWORK):`n external: true`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Info 'Generated docker-compose.yml'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# cameleer.conf generation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function New-ConfigFile {
|
||
|
|
$f = Join-Path $script:CFG_INSTALL_DIR 'cameleer.conf'
|
||
|
|
$utcNow = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss UTC')
|
||
|
|
|
||
|
|
$content = @"
|
||
|
|
# Cameleer installation config
|
||
|
|
# Generated by installer v${CAMELEER_INSTALLER_VERSION} on ${utcNow}
|
||
|
|
|
||
|
|
install_dir=$($script:CFG_INSTALL_DIR)
|
||
|
|
public_host=$($script:CFG_PUBLIC_HOST)
|
||
|
|
public_protocol=$($script:CFG_PUBLIC_PROTOCOL)
|
||
|
|
admin_user=$($script:CFG_ADMIN_USER)
|
||
|
|
tls_mode=$($script:CFG_TLS_MODE)
|
||
|
|
http_port=$($script:CFG_HTTP_PORT)
|
||
|
|
https_port=$($script:CFG_HTTPS_PORT)
|
||
|
|
logto_console_port=$($script:CFG_LOGTO_CONSOLE_PORT)
|
||
|
|
logto_console_exposed=$($script:CFG_LOGTO_CONSOLE_EXPOSED)
|
||
|
|
vendor_enabled=$($script:CFG_VENDOR_ENABLED)
|
||
|
|
vendor_user=$($script:CFG_VENDOR_USER)
|
||
|
|
monitoring_network=$($script:CFG_MONITORING_NETWORK)
|
||
|
|
version=$($script:CFG_VERSION)
|
||
|
|
compose_project=$($script:CFG_COMPOSE_PROJECT)
|
||
|
|
docker_socket=$($script:CFG_DOCKER_SOCKET)
|
||
|
|
node_tls_reject=$($script:CFG_NODE_TLS_REJECT)
|
||
|
|
"@
|
||
|
|
|
||
|
|
Write-LFFile $f $content
|
||
|
|
Write-Info 'Saved installer config to cameleer.conf'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Docker operations
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Invoke-DockerPull {
|
||
|
|
Write-Info 'Pulling Docker images...'
|
||
|
|
Push-Location $script:CFG_INSTALL_DIR
|
||
|
|
try {
|
||
|
|
& docker compose -p $script:CFG_COMPOSE_PROJECT pull
|
||
|
|
if ($LASTEXITCODE -ne 0) { throw 'docker compose pull failed' }
|
||
|
|
} finally {
|
||
|
|
Pop-Location
|
||
|
|
}
|
||
|
|
Write-Success 'All images pulled.'
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-DockerUp {
|
||
|
|
Write-Info 'Starting Cameleer SaaS platform...'
|
||
|
|
Push-Location $script:CFG_INSTALL_DIR
|
||
|
|
try {
|
||
|
|
& docker compose -p $script:CFG_COMPOSE_PROJECT up -d
|
||
|
|
if ($LASTEXITCODE -ne 0) { throw 'docker compose up failed' }
|
||
|
|
} finally {
|
||
|
|
Pop-Location
|
||
|
|
}
|
||
|
|
Write-Info 'Containers started.'
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-DockerDown {
|
||
|
|
Write-Info 'Stopping Cameleer SaaS platform...'
|
||
|
|
Push-Location $script:CFG_INSTALL_DIR
|
||
|
|
try {
|
||
|
|
& docker compose -p $script:CFG_COMPOSE_PROJECT down
|
||
|
|
} finally {
|
||
|
|
Pop-Location
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Health verification
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Test-ServiceHealth {
|
||
|
|
param(
|
||
|
|
[string]$Name,
|
||
|
|
[scriptblock]$CheckScript,
|
||
|
|
[int]$TimeoutSecs = 120
|
||
|
|
)
|
||
|
|
$startTime = Get-Date
|
||
|
|
while ($true) {
|
||
|
|
try {
|
||
|
|
$result = & $CheckScript
|
||
|
|
if ($result -eq $true -or $LASTEXITCODE -eq 0) {
|
||
|
|
$elapsed = [int]((Get-Date) - $startTime).TotalSeconds
|
||
|
|
Write-Host (" [ok] {0,-20} ready ({1}s)" -f $Name, $elapsed) -ForegroundColor Green
|
||
|
|
return $true
|
||
|
|
}
|
||
|
|
} catch { }
|
||
|
|
|
||
|
|
$elapsed = [int]((Get-Date) - $startTime).TotalSeconds
|
||
|
|
if ($elapsed -ge $TimeoutSecs) {
|
||
|
|
Write-Host (" [FAIL] {0,-20} not ready after {1}s" -f $Name, $TimeoutSecs) -ForegroundColor Red
|
||
|
|
$svcName = $Name.ToLower() -replace ' ', '-'
|
||
|
|
Write-Host " Check: docker compose -p $($script:CFG_COMPOSE_PROJECT) logs $svcName"
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
Start-Sleep -Seconds 5
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-HealthVerification {
|
||
|
|
Write-Host ''
|
||
|
|
Write-Info 'Verifying installation...'
|
||
|
|
$failed = $false
|
||
|
|
$proj = $script:CFG_COMPOSE_PROJECT
|
||
|
|
$dir = $script:CFG_INSTALL_DIR
|
||
|
|
$httpsPort = $script:CFG_HTTPS_PORT
|
||
|
|
|
||
|
|
if (-not $failed) {
|
||
|
|
$ok = Test-ServiceHealth 'PostgreSQL' {
|
||
|
|
Push-Location $dir
|
||
|
|
try { & docker compose -p $proj exec -T postgres pg_isready -U cameleer 2>$null; $LASTEXITCODE -eq 0 }
|
||
|
|
finally { Pop-Location }
|
||
|
|
} 120
|
||
|
|
if (-not $ok) { $failed = $true }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $failed) {
|
||
|
|
$chPass = $script:CFG_CLICKHOUSE_PASSWORD
|
||
|
|
$ok = Test-ServiceHealth 'ClickHouse' {
|
||
|
|
Push-Location $dir
|
||
|
|
try {
|
||
|
|
& docker compose -p $proj exec -T clickhouse clickhouse-client --password $chPass --query 'SELECT 1' 2>$null
|
||
|
|
$LASTEXITCODE -eq 0
|
||
|
|
} finally { Pop-Location }
|
||
|
|
} 120
|
||
|
|
if (-not $ok) { $failed = $true }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $failed) {
|
||
|
|
$ok = Test-ServiceHealth 'Logto' {
|
||
|
|
Push-Location $dir
|
||
|
|
try {
|
||
|
|
& docker compose -p $proj exec -T logto 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))" 2>$null
|
||
|
|
$LASTEXITCODE -eq 0
|
||
|
|
} finally { Pop-Location }
|
||
|
|
} 300
|
||
|
|
if (-not $ok) { $failed = $true }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $failed) {
|
||
|
|
$ok = Test-ServiceHealth 'Bootstrap' {
|
||
|
|
Push-Location $dir
|
||
|
|
try {
|
||
|
|
& docker compose -p $proj exec -T logto test -f /data/logto-bootstrap.json 2>$null
|
||
|
|
$LASTEXITCODE -eq 0
|
||
|
|
} finally { Pop-Location }
|
||
|
|
} 300
|
||
|
|
if (-not $ok) { $failed = $true }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $failed) {
|
||
|
|
$ok = Test-ServiceHealth 'Cameleer SaaS' {
|
||
|
|
try {
|
||
|
|
$response = Invoke-WebRequest -Uri "https://localhost:$httpsPort/platform/api/config" `
|
||
|
|
-UseBasicParsing -SkipCertificateCheck -ErrorAction SilentlyContinue
|
||
|
|
$response.StatusCode -lt 400
|
||
|
|
} catch { $false }
|
||
|
|
} 120
|
||
|
|
if (-not $ok) { $failed = $true }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (-not $failed) {
|
||
|
|
$ok = Test-ServiceHealth 'Traefik routing' {
|
||
|
|
try {
|
||
|
|
$response = Invoke-WebRequest -Uri "https://localhost:$httpsPort/" `
|
||
|
|
-UseBasicParsing -SkipCertificateCheck -ErrorAction SilentlyContinue
|
||
|
|
$response.StatusCode -lt 500
|
||
|
|
} catch { $false }
|
||
|
|
} 120
|
||
|
|
if (-not $ok) { $failed = $true }
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host ''
|
||
|
|
if ($failed) {
|
||
|
|
Write-Err 'Installation verification failed. Stack is running — check logs.'
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
Write-Success 'All services healthy.'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# credentials.txt generation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function New-CredentialsFile {
|
||
|
|
$f = Join-Path $script:CFG_INSTALL_DIR 'credentials.txt'
|
||
|
|
$utcNow = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss UTC')
|
||
|
|
$proto = $script:CFG_PUBLIC_PROTOCOL
|
||
|
|
$host_ = $script:CFG_PUBLIC_HOST
|
||
|
|
|
||
|
|
$content = @"
|
||
|
|
===========================================
|
||
|
|
CAMELEER PLATFORM CREDENTIALS
|
||
|
|
Generated: ${utcNow}
|
||
|
|
|
||
|
|
SECURE THIS FILE AND DELETE AFTER NOTING
|
||
|
|
THESE CREDENTIALS CANNOT BE RECOVERED
|
||
|
|
===========================================
|
||
|
|
|
||
|
|
Admin Console: ${proto}://${host_}/platform/
|
||
|
|
Admin User: $($script:CFG_ADMIN_USER)
|
||
|
|
Admin Password: $($script:CFG_ADMIN_PASS)
|
||
|
|
|
||
|
|
PostgreSQL: cameleer / $($script:CFG_POSTGRES_PASSWORD)
|
||
|
|
ClickHouse: default / $($script:CFG_CLICKHOUSE_PASSWORD)
|
||
|
|
|
||
|
|
"@
|
||
|
|
|
||
|
|
Write-LFFile $f $content
|
||
|
|
|
||
|
|
if ($script:CFG_VENDOR_ENABLED -eq 'true') {
|
||
|
|
Append-LFFile $f "Vendor User: $($script:CFG_VENDOR_USER)`nVendor Password: $($script:CFG_VENDOR_PASS)`n`n"
|
||
|
|
} else {
|
||
|
|
Append-LFFile $f "Vendor User: (not enabled)`n`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq 'true') {
|
||
|
|
Append-LFFile $f "Logto Console: $($script:CFG_PUBLIC_PROTOCOL)://$($script:CFG_PUBLIC_HOST):$($script:CFG_LOGTO_CONSOLE_PORT)`n"
|
||
|
|
} else {
|
||
|
|
Append-LFFile $f "Logto Console: (not exposed)`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Restrict file permissions (Windows: set to owner-only via ACL)
|
||
|
|
try {
|
||
|
|
$acl = Get-Acl $f
|
||
|
|
$acl.SetAccessRuleProtection($true, $false)
|
||
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||
|
|
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name,
|
||
|
|
'FullControl', 'Allow')
|
||
|
|
$acl.SetAccessRule($rule)
|
||
|
|
Set-Acl $f $acl
|
||
|
|
} catch {
|
||
|
|
# Non-fatal — ACL manipulation may fail in containers / WSL scenarios
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Info 'Saved credentials to credentials.txt'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# INSTALL.md generation
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function New-InstallDoc {
|
||
|
|
$f = Join-Path $script:CFG_INSTALL_DIR 'INSTALL.md'
|
||
|
|
$utcNow = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss UTC')
|
||
|
|
$tlsDesc = if ($script:CFG_TLS_MODE -eq 'custom') { 'Custom certificate' } else { 'Self-signed (auto-generated)' }
|
||
|
|
$proto = $script:CFG_PUBLIC_PROTOCOL
|
||
|
|
$host_ = $script:CFG_PUBLIC_HOST
|
||
|
|
$ver = $script:CFG_VERSION
|
||
|
|
$dir = $script:CFG_INSTALL_DIR
|
||
|
|
$proj = $script:CFG_COMPOSE_PROJECT
|
||
|
|
$httpPort = $script:CFG_HTTP_PORT
|
||
|
|
$httpsPort = $script:CFG_HTTPS_PORT
|
||
|
|
$logtoPort = $script:CFG_LOGTO_CONSOLE_PORT
|
||
|
|
|
||
|
|
$content = @"
|
||
|
|
# Cameleer SaaS — Installation Documentation
|
||
|
|
|
||
|
|
## Installation Summary
|
||
|
|
|
||
|
|
| | |
|
||
|
|
|---|---|
|
||
|
|
| **Version** | ${ver} |
|
||
|
|
| **Date** | ${utcNow} |
|
||
|
|
| **Installer** | v${CAMELEER_INSTALLER_VERSION} |
|
||
|
|
| **Install Directory** | ${dir} |
|
||
|
|
| **Hostname** | ${host_} |
|
||
|
|
| **TLS** | ${tlsDesc} |
|
||
|
|
|
||
|
|
## Service URLs
|
||
|
|
|
||
|
|
- **Platform UI:** ${proto}://${host_}/platform/
|
||
|
|
- **API Endpoint:** ${proto}://${host_}/platform/api/
|
||
|
|
"@
|
||
|
|
|
||
|
|
Write-LFFile $f $content
|
||
|
|
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq 'true') {
|
||
|
|
Append-LFFile $f "- **Logto Admin Console:** ${proto}://${host_}:${logtoPort}`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f @'
|
||
|
|
|
||
|
|
## First Steps
|
||
|
|
|
||
|
|
1. Open the Platform UI in your browser
|
||
|
|
2. Log in with the admin credentials from `credentials.txt`
|
||
|
|
3. Create your first tenant via the Vendor console
|
||
|
|
4. The platform will provision a dedicated server instance for the tenant
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
| Container | Purpose |
|
||
|
|
|---|---|
|
||
|
|
| `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) |
|
||
|
|
|
||
|
|
Per-tenant `cameleer3-server` and `cameleer3-server-ui` containers are provisioned dynamically when tenants are created.
|
||
|
|
|
||
|
|
## Networking
|
||
|
|
|
||
|
|
'@
|
||
|
|
|
||
|
|
Append-LFFile $f "| Port | Service |`n|---|---|`n| ${httpPort} | HTTP (redirects to HTTPS) |`n| ${httpsPort} | HTTPS (main entry point) |`n"
|
||
|
|
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq 'true') {
|
||
|
|
Append-LFFile $f "| ${logtoPort} | Logto Admin Console |`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($script:CFG_MONITORING_NETWORK -ne '') {
|
||
|
|
$monNet = $script:CFG_MONITORING_NETWORK
|
||
|
|
Append-LFFile $f "`n### Monitoring`n`nServices are connected to the \`${monNet}\` Docker network with Prometheus labels for auto-discovery.`n"
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f "`n## TLS`n`n**Mode:** ${tlsDesc}`n"
|
||
|
|
|
||
|
|
if ($script:CFG_TLS_MODE -eq 'self-signed') {
|
||
|
|
Append-LFFile $f @'
|
||
|
|
|
||
|
|
The platform generated a self-signed certificate on first boot. To replace it:
|
||
|
|
1. Log in as admin and navigate to **Certificates** in the vendor console
|
||
|
|
2. Upload your certificate and key via the UI
|
||
|
|
3. Activate the new certificate (zero-downtime swap)
|
||
|
|
'@
|
||
|
|
}
|
||
|
|
|
||
|
|
Append-LFFile $f @"
|
||
|
|
|
||
|
|
## Data & Backups
|
||
|
|
|
||
|
|
| Docker Volume | Contains |
|
||
|
|
|---|---|
|
||
|
|
| \`pgdata\` | PostgreSQL data (tenants, licenses, audit) |
|
||
|
|
| \`chdata\` | ClickHouse data (traces, metrics, logs) |
|
||
|
|
| \`certs\` | TLS certificates |
|
||
|
|
| \`bootstrapdata\` | Logto bootstrap results |
|
||
|
|
|
||
|
|
### Backup Commands
|
||
|
|
|
||
|
|
\`\`\`bash
|
||
|
|
# PostgreSQL
|
||
|
|
docker compose -p ${proj} exec postgres pg_dump -U cameleer cameleer_saas > backup.sql
|
||
|
|
|
||
|
|
# ClickHouse
|
||
|
|
docker compose -p ${proj} exec clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native
|
||
|
|
\`\`\`
|
||
|
|
|
||
|
|
## Upgrading
|
||
|
|
|
||
|
|
Re-run the installer with a new version:
|
||
|
|
|
||
|
|
\`\`\`powershell
|
||
|
|
.\install.ps1 -InstallDir ${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 ${proj} logs SERVICE_NAME\` |
|
||
|
|
| Bootstrap failed | \`docker compose -p ${proj} logs logto\` |
|
||
|
|
| Routing issues | \`docker compose -p ${proj} logs traefik\` |
|
||
|
|
| Database issues | \`docker compose -p ${proj} exec postgres psql -U cameleer -d cameleer_saas\` |
|
||
|
|
|
||
|
|
## Uninstalling
|
||
|
|
|
||
|
|
\`\`\`bash
|
||
|
|
# Stop and remove containers
|
||
|
|
cd ${dir} && docker compose -p ${proj} down
|
||
|
|
|
||
|
|
# Remove data volumes (DESTRUCTIVE)
|
||
|
|
cd ${dir} && docker compose -p ${proj} down -v
|
||
|
|
|
||
|
|
# Remove install directory
|
||
|
|
Remove-Item -Recurse -Force ${dir}
|
||
|
|
\`\`\`
|
||
|
|
"@
|
||
|
|
|
||
|
|
Write-Info 'Generated INSTALL.md'
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Print credentials to console
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Show-Credentials {
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host '==========================================' -ForegroundColor White
|
||
|
|
Write-Host ' CAMELEER PLATFORM CREDENTIALS' -ForegroundColor White
|
||
|
|
Write-Host '==========================================' -ForegroundColor White
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host " Admin Console: $($script:CFG_PUBLIC_PROTOCOL)://$($script:CFG_PUBLIC_HOST)/platform/" -ForegroundColor Cyan
|
||
|
|
Write-Host " Admin User: $($script:CFG_ADMIN_USER)" -ForegroundColor White
|
||
|
|
Write-Host " Admin Password: $($script:CFG_ADMIN_PASS)" -ForegroundColor White
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host " PostgreSQL: cameleer / $($script:CFG_POSTGRES_PASSWORD)"
|
||
|
|
Write-Host " ClickHouse: default / $($script:CFG_CLICKHOUSE_PASSWORD)"
|
||
|
|
Write-Host ''
|
||
|
|
if ($script:CFG_VENDOR_ENABLED -eq 'true') {
|
||
|
|
Write-Host " Vendor User: $($script:CFG_VENDOR_USER)" -ForegroundColor White
|
||
|
|
Write-Host " Vendor Password: $($script:CFG_VENDOR_PASS)" -ForegroundColor White
|
||
|
|
Write-Host ''
|
||
|
|
}
|
||
|
|
if ($script:CFG_LOGTO_CONSOLE_EXPOSED -eq 'true') {
|
||
|
|
Write-Host " Logto Console: $($script:CFG_PUBLIC_PROTOCOL)://$($script:CFG_PUBLIC_HOST):$($script:CFG_LOGTO_CONSOLE_PORT)" -ForegroundColor Cyan
|
||
|
|
Write-Host ''
|
||
|
|
}
|
||
|
|
Write-Host " Credentials saved to: $($script:CFG_INSTALL_DIR)\credentials.txt"
|
||
|
|
Write-Host ' Secure this file and delete after noting credentials.' -ForegroundColor Yellow
|
||
|
|
Write-Host ''
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Summary
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Show-Summary {
|
||
|
|
Write-Host '==========================================' -ForegroundColor Green
|
||
|
|
Write-Host ' Installation complete!' -ForegroundColor Green
|
||
|
|
Write-Host '==========================================' -ForegroundColor Green
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host " Install directory: $($script:CFG_INSTALL_DIR)"
|
||
|
|
Write-Host " Documentation: $($script:CFG_INSTALL_DIR)\INSTALL.md"
|
||
|
|
Write-Host ''
|
||
|
|
Write-Host ' To manage the stack:'
|
||
|
|
Write-Host " cd $($script:CFG_INSTALL_DIR)"
|
||
|
|
Write-Host " docker compose -p $($script:CFG_COMPOSE_PROJECT) ps # status"
|
||
|
|
Write-Host " docker compose -p $($script:CFG_COMPOSE_PROJECT) logs -f # logs"
|
||
|
|
Write-Host " docker compose -p $($script:CFG_COMPOSE_PROJECT) down # stop"
|
||
|
|
Write-Host ''
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Re-run / upgrade menu
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Show-RerunMenu {
|
||
|
|
$confPath = Join-Path $script:CFG_INSTALL_DIR 'cameleer.conf'
|
||
|
|
$currentVersion = 'unknown'
|
||
|
|
$currentHost = 'unknown'
|
||
|
|
if (Test-Path $confPath -PathType Leaf) {
|
||
|
|
foreach ($line in [System.IO.File]::ReadAllLines($confPath)) {
|
||
|
|
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 White
|
||
|
|
Write-Host " Install directory: $($script:CFG_INSTALL_DIR)"
|
||
|
|
Write-Host " Public host: $currentHost"
|
||
|
|
Write-Host ''
|
||
|
|
|
||
|
|
if ($script:MODE -eq 'silent') {
|
||
|
|
if ($script:RERUN_ACTION -eq '') { $script:RERUN_ACTION = 'upgrade' }
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($script:RERUN_ACTION -ne '') { return }
|
||
|
|
|
||
|
|
$newVersion = if ($script:CFG_VERSION -ne '') { $script:CFG_VERSION } else { $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) {
|
||
|
|
'1' { $script:RERUN_ACTION = 'upgrade' }
|
||
|
|
'2' { $script:RERUN_ACTION = 'reconfigure' }
|
||
|
|
'3' { $script:RERUN_ACTION = 'reinstall' }
|
||
|
|
'4' { Write-Host 'Cancelled.'; exit 0 }
|
||
|
|
'' { $script:RERUN_ACTION = 'upgrade' }
|
||
|
|
default { Write-Host 'Invalid choice.'; exit 1 }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Invoke-Rerun {
|
||
|
|
switch ($script:RERUN_ACTION) {
|
||
|
|
'upgrade' {
|
||
|
|
Write-Info 'Upgrading installation...'
|
||
|
|
Load-ConfigFile (Join-Path $script:CFG_INSTALL_DIR 'cameleer.conf')
|
||
|
|
Load-EnvOverrides
|
||
|
|
Merge-Config
|
||
|
|
New-ComposeFile
|
||
|
|
Invoke-DockerPull
|
||
|
|
Invoke-DockerDown
|
||
|
|
Invoke-DockerUp
|
||
|
|
Invoke-HealthVerification
|
||
|
|
New-InstallDoc
|
||
|
|
Show-Summary
|
||
|
|
exit 0
|
||
|
|
}
|
||
|
|
'reconfigure' {
|
||
|
|
Write-Info 'Reconfiguring installation...'
|
||
|
|
return
|
||
|
|
}
|
||
|
|
'reinstall' {
|
||
|
|
if (-not $script:CONFIRM_DESTROY) {
|
||
|
|
Write-Host ''
|
||
|
|
Write-Warn 'This will destroy ALL data (databases, certificates, bootstrap).'
|
||
|
|
if (-not (Invoke-YesNoPrompt 'Are you sure? This cannot be undone.')) {
|
||
|
|
Write-Host 'Cancelled.'
|
||
|
|
exit 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Write-Info 'Reinstalling...'
|
||
|
|
try { Invoke-DockerDown } catch { }
|
||
|
|
try {
|
||
|
|
Push-Location $script:CFG_INSTALL_DIR
|
||
|
|
& docker compose -p $script:CFG_COMPOSE_PROJECT down -v 2>$null
|
||
|
|
Pop-Location
|
||
|
|
} catch { }
|
||
|
|
foreach ($file in @('.env', 'docker-compose.yml', 'cameleer.conf', 'credentials.txt', 'INSTALL.md', '.env.bak')) {
|
||
|
|
$fp = Join-Path $script:CFG_INSTALL_DIR $file
|
||
|
|
if (Test-Path $fp) { Remove-Item $fp -Force }
|
||
|
|
}
|
||
|
|
$certsPath = Join-Path $script:CFG_INSTALL_DIR 'certs'
|
||
|
|
if (Test-Path $certsPath) { Remove-Item $certsPath -Recurse -Force }
|
||
|
|
$script:IS_RERUN = $false
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# Main
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function Invoke-Main {
|
||
|
|
if ($Help) { Show-Help; exit 0 }
|
||
|
|
|
||
|
|
Show-Banner
|
||
|
|
|
||
|
|
# Load config sources (CLI already applied via param block)
|
||
|
|
if ($Config -ne '') {
|
||
|
|
Load-ConfigFile $Config
|
||
|
|
}
|
||
|
|
Load-EnvOverrides
|
||
|
|
|
||
|
|
# Check for existing installation
|
||
|
|
Find-ExistingInstall
|
||
|
|
if ($script:IS_RERUN) {
|
||
|
|
Show-RerunMenu
|
||
|
|
Invoke-Rerun
|
||
|
|
# If we reach here, action was 'reconfigure' or 'reinstall' (continues below)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Prerequisites
|
||
|
|
Test-Prerequisites
|
||
|
|
|
||
|
|
# Auto-detect defaults
|
||
|
|
Get-AutoDetectedDefaults
|
||
|
|
|
||
|
|
# Interactive prompts (unless silent)
|
||
|
|
if ($script:MODE -ne 'silent') {
|
||
|
|
Select-InstallMode
|
||
|
|
if ($script:MODE -eq 'expert') {
|
||
|
|
Invoke-ExpertPrompts
|
||
|
|
} else {
|
||
|
|
Invoke-SimplePrompts
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Merge remaining defaults and validate
|
||
|
|
Merge-Config
|
||
|
|
Test-Config
|
||
|
|
|
||
|
|
# Generate passwords for any empty values
|
||
|
|
New-Passwords
|
||
|
|
|
||
|
|
# Create install directory
|
||
|
|
if (-not (Test-Path $script:CFG_INSTALL_DIR)) {
|
||
|
|
New-Item -ItemType Directory -Path $script:CFG_INSTALL_DIR -Force | Out-Null
|
||
|
|
}
|
||
|
|
|
||
|
|
# Copy custom certs if provided
|
||
|
|
if ($script:CFG_TLS_MODE -eq 'custom') {
|
||
|
|
Copy-Certificates
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate configuration files
|
||
|
|
New-EnvFile
|
||
|
|
New-ComposeFile
|
||
|
|
New-ConfigFile
|
||
|
|
|
||
|
|
# Pull and start
|
||
|
|
Invoke-DockerPull
|
||
|
|
Invoke-DockerUp
|
||
|
|
|
||
|
|
# Verify health
|
||
|
|
Invoke-HealthVerification
|
||
|
|
|
||
|
|
# Generate output files
|
||
|
|
New-CredentialsFile
|
||
|
|
New-InstallDoc
|
||
|
|
|
||
|
|
# Print results
|
||
|
|
Show-Credentials
|
||
|
|
Show-Summary
|
||
|
|
}
|
||
|
|
|
||
|
|
# Entry point
|
||
|
|
Invoke-Main
|