triton-install/manage-server/install.ps1
amir-climy 5cb8ce2f63 feat(install): macOS + Windows support
install.sh / uninstall.sh (Linux + macOS):
- Detect OS with uname -s; root check is Linux-only (Docker/Podman Desktop
  on macOS runs rootless, no sudo needed).
- Arch detection adds arm64 case for Apple Silicon (uname -m returns "arm64"
  on macOS, "aarch64" on Linux).
- sed_inplace() wrapper handles BSD sed on macOS (requires empty -i suffix).
- Fix --image flag to append TRITON_MANAGE_IMAGE rather than sed-replace a
  line that is commented out in env.template.
- uninstall: re-apply --purge-data fixes (rm installer dir, drop interactive
  prompt, use $RUNTIME for raw cleanup instead of hardcoded podman).

install.ps1 / uninstall.ps1 (Windows):
- Equivalent logic for Docker Desktop / Podman Desktop on Windows.
- Arch via RuntimeInformation.OSArchitecture (X64 → amd64, Arm64 → arm64).
- Secrets via RandomNumberGenerator (no openssl dependency).
- Parameters: -GatewayHostname, -ManageHostIP, -Image, -NoTls / -PurgeData.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:07:13 +08:00

145 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 ""