146 lines
6.7 KiB
PowerShell
146 lines
6.7 KiB
PowerShell
|
|
#Requires -Version 5.1
|
||
|
|
# install.ps1 — Triton Manage Server installer for Windows.
|
||
|
|
#
|
||
|
|
# Idempotent. Generates secrets on first run, reuses .env afterwards.
|
||
|
|
# Container-based via Docker Desktop or Podman Desktop (auto-detected).
|
||
|
|
# Requires Docker Desktop in Linux container mode (the default).
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# .\install.ps1
|
||
|
|
#
|
||
|
|
# Parameters (all optional):
|
||
|
|
# -GatewayHostname HOST Agent mTLS hostname (defaults to current FQDN).
|
||
|
|
# -ManageHostIP IP Host LAN IP — used for "+ This machine".
|
||
|
|
# -Image TAG Pin a specific manage-server image tag.
|
||
|
|
# -NoTls Skip the TLS-required sanity check (dev).
|
||
|
|
param(
|
||
|
|
[string]$GatewayHostname = '',
|
||
|
|
[string]$ManageHostIP = '',
|
||
|
|
[string]$Image = '',
|
||
|
|
[switch]$NoTls
|
||
|
|
)
|
||
|
|
|
||
|
|
Set-StrictMode -Version Latest
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||
|
|
Set-Location $ScriptDir
|
||
|
|
|
||
|
|
function Write-Info([string]$msg) { Write-Host "[manage-server] $msg" }
|
||
|
|
function Write-Die([string]$msg) { Write-Error "[manage-server] error: $msg"; exit 1 }
|
||
|
|
|
||
|
|
# ── architecture detection ───────────────────────────────────────────────
|
||
|
|
$cpuArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
||
|
|
$arch = switch ($cpuArch.ToString()) {
|
||
|
|
'X64' { 'amd64' }
|
||
|
|
'Arm64' { 'arm64' }
|
||
|
|
default { Write-Die "unsupported architecture: $cpuArch (supported: X64, Arm64)"; 'unknown' }
|
||
|
|
}
|
||
|
|
Write-Info "architecture: windows/$arch"
|
||
|
|
|
||
|
|
# ── runtime detection ────────────────────────────────────────────────────
|
||
|
|
$composeCmd = $null
|
||
|
|
$runtime = $null
|
||
|
|
|
||
|
|
if (Get-Command docker -ErrorAction SilentlyContinue) {
|
||
|
|
& docker compose version 2>&1 | Out-Null
|
||
|
|
if ($LASTEXITCODE -eq 0) { $composeCmd = @('docker','compose'); $runtime = 'docker' }
|
||
|
|
}
|
||
|
|
if (-not $composeCmd -and (Get-Command podman -ErrorAction SilentlyContinue)) {
|
||
|
|
& podman compose version 2>&1 | Out-Null
|
||
|
|
if ($LASTEXITCODE -eq 0) { $composeCmd = @('podman','compose'); $runtime = 'podman' }
|
||
|
|
}
|
||
|
|
if (-not $composeCmd) {
|
||
|
|
Write-Die "no compose runtime found. Install Docker Desktop or Podman Desktop."
|
||
|
|
}
|
||
|
|
Write-Info "using runtime: $runtime"
|
||
|
|
|
||
|
|
# ── random secret generation ─────────────────────────────────────────────
|
||
|
|
function New-RandomHex([int]$byteCount) {
|
||
|
|
$buf = [byte[]]::new($byteCount)
|
||
|
|
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($buf)
|
||
|
|
return ($buf | ForEach-Object { $_.ToString('x2') }) -join ''
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── .env bootstrap ───────────────────────────────────────────────────────
|
||
|
|
$envFile = Join-Path $ScriptDir '.env'
|
||
|
|
|
||
|
|
if (-not (Test-Path $envFile)) {
|
||
|
|
Write-Info "writing .env from env.template"
|
||
|
|
$template = Join-Path $ScriptDir 'env.template'
|
||
|
|
if (-not (Test-Path $template)) { Write-Die "env.template not found in $ScriptDir" }
|
||
|
|
Copy-Item $template $envFile
|
||
|
|
|
||
|
|
$pgPass = New-RandomHex 24
|
||
|
|
$jwtKey = New-RandomHex 32
|
||
|
|
$workerKey = New-RandomHex 16
|
||
|
|
$vaultKey = New-RandomHex 32
|
||
|
|
|
||
|
|
# Read-modify-write in one pass so line endings are preserved.
|
||
|
|
$content = [System.IO.File]::ReadAllText($envFile)
|
||
|
|
$content = $content -replace '(?m)^POSTGRES_PASSWORD=.*', "POSTGRES_PASSWORD=$pgPass"
|
||
|
|
$content = $content -replace '(?m)^TRITON_MANAGE_JWT_SIGNING_KEY=.*', "TRITON_MANAGE_JWT_SIGNING_KEY=$jwtKey"
|
||
|
|
$content = $content -replace '(?m)^TRITON_MANAGE_WORKER_KEY=.*', "TRITON_MANAGE_WORKER_KEY=$workerKey"
|
||
|
|
$content = $content -replace '(?m)^TRITON_VAULT_KEY=.*', "TRITON_VAULT_KEY=$vaultKey"
|
||
|
|
[System.IO.File]::WriteAllText($envFile, $content)
|
||
|
|
Write-Info "vault key generated (PostgreSQL AES-256-GCM)"
|
||
|
|
|
||
|
|
if ($GatewayHostname) {
|
||
|
|
$content = [System.IO.File]::ReadAllText($envFile)
|
||
|
|
$content = $content -replace '(?m)^TRITON_MANAGE_GATEWAY_HOSTNAME=.*', "TRITON_MANAGE_GATEWAY_HOSTNAME=$GatewayHostname"
|
||
|
|
[System.IO.File]::WriteAllText($envFile, $content)
|
||
|
|
}
|
||
|
|
if ($ManageHostIP) {
|
||
|
|
$content = [System.IO.File]::ReadAllText($envFile)
|
||
|
|
$content = $content -replace '(?m)^TRITON_MANAGE_HOST_IP=.*', "TRITON_MANAGE_HOST_IP=$ManageHostIP"
|
||
|
|
[System.IO.File]::WriteAllText($envFile, $content)
|
||
|
|
}
|
||
|
|
# IMAGE has no placeholder line in env.template — append it directly.
|
||
|
|
if ($Image) {
|
||
|
|
Add-Content $envFile "`nTRITON_MANAGE_IMAGE=$Image"
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Info ".env created at $envFile"
|
||
|
|
Write-Info " back this up — it contains the JWT signing key, worker key, and vault key"
|
||
|
|
} else {
|
||
|
|
Write-Info "reusing existing .env at $envFile"
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── start ────────────────────────────────────────────────────────────────
|
||
|
|
Write-Info "starting containers..."
|
||
|
|
& $composeCmd[0] $composeCmd[1] --env-file $envFile up -d
|
||
|
|
if ($LASTEXITCODE -ne 0) { Write-Die "compose up failed (exit $LASTEXITCODE)" }
|
||
|
|
|
||
|
|
# ── wait for health ──────────────────────────────────────────────────────
|
||
|
|
$portLine = Get-Content $envFile | Where-Object { $_ -match '^TRITON_MANAGE_HOST_PORT=' } | Select-Object -First 1
|
||
|
|
$hostPort = if ($portLine) { $portLine -replace '^TRITON_MANAGE_HOST_PORT=', '' } else { '8082' }
|
||
|
|
|
||
|
|
Write-Info "waiting for manage server to become healthy on :$hostPort..."
|
||
|
|
$up = $false
|
||
|
|
for ($i = 1; $i -le 30; $i++) {
|
||
|
|
try {
|
||
|
|
$resp = Invoke-WebRequest "http://localhost:$hostPort/" -MaximumRedirection 0 -UseBasicParsing -ErrorAction Stop
|
||
|
|
if ($resp.StatusCode -in @(200, 302)) { $up = $true; break }
|
||
|
|
} catch [System.Net.WebException] {
|
||
|
|
$code = [int]$_.Exception.Response.StatusCode
|
||
|
|
if ($code -in @(200, 302)) { $up = $true; break }
|
||
|
|
} catch { <# connection refused — keep polling #> }
|
||
|
|
Start-Sleep -Seconds 2
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($up) {
|
||
|
|
Write-Info "manage server is up"
|
||
|
|
} else {
|
||
|
|
Write-Info "warning: health check timed out — check logs with: $($composeCmd -join ' ') logs manage-server"
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Info ""
|
||
|
|
Write-Info "Installation complete. Next steps:"
|
||
|
|
Write-Info " 1. Open http://localhost:$hostPort (or your public URL)"
|
||
|
|
Write-Info " 2. Complete the setup wizard:"
|
||
|
|
Write-Info " - Set your manage server name"
|
||
|
|
Write-Info " - Enter your Triton licence server URL and licence ID"
|
||
|
|
Write-Info " - Or upload an air-gap licence file"
|
||
|
|
Write-Info " 3. Configure TLS via reverse proxy (see docs)"
|
||
|
|
Write-Info ""
|