Files
git.stella-ops.org/scripts/setup.ps1
2026-02-21 20:14:23 +02:00

565 lines
19 KiB
PowerShell

#!/usr/bin/env pwsh
<#
.SYNOPSIS
Automated developer environment setup for Stella Ops (Windows).
.DESCRIPTION
Validates prerequisites, starts infrastructure, builds solutions and Docker images,
and launches the full platform.
.PARAMETER SkipBuild
Skip .NET solution builds.
.PARAMETER InfraOnly
Only start infrastructure containers (PostgreSQL, Valkey, SeaweedFS, Rekor, Zot).
.PARAMETER ImagesOnly
Only build Docker images (skip infra start and .NET build).
.PARAMETER SkipImages
Skip Docker image builds.
#>
[CmdletBinding()]
param(
[switch]$SkipBuild,
[switch]$InfraOnly,
[switch]$ImagesOnly,
[switch]$SkipImages
)
$ErrorActionPreference = 'Stop'
$Root = git rev-parse --show-toplevel 2>$null
if (-not $Root) {
Write-Error 'Not inside a git repository. Run this script from within the Stella Ops repo.'
exit 1
}
$Root = $Root.Trim()
$ComposeDir = Join-Path $Root 'devops/compose'
# ─── Helpers ────────────────────────────────────────────────────────────────
function Write-Step([string]$msg) {
Write-Host "`n>> $msg" -ForegroundColor Cyan
}
function Write-Ok([string]$msg) {
Write-Host " [OK] $msg" -ForegroundColor Green
}
function Write-Warn([string]$msg) {
Write-Host " [WARN] $msg" -ForegroundColor Yellow
}
function Write-Fail([string]$msg) {
Write-Host " [FAIL] $msg" -ForegroundColor Red
}
function Test-Command([string]$cmd) {
return [bool](Get-Command $cmd -ErrorAction SilentlyContinue)
}
function Get-ComposeServices([string]$composeFile) {
$services = @()
if (-not (Test-Path $composeFile)) {
return $services
}
$ps = docker compose -f $composeFile ps --format json 2>$null
if (-not $ps) {
return $services
}
foreach ($line in $ps -split "`n") {
$line = $line.Trim()
if (-not $line) { continue }
try {
$services += ($line | ConvertFrom-Json)
} catch {}
}
return $services
}
function Get-ComposeExpectedServices([string]$composeFile) {
$services = @()
if (-not (Test-Path $composeFile)) {
return $services
}
$configured = docker compose -f $composeFile config --services 2>$null
if (-not $configured) {
return $services
}
foreach ($line in ($configured -split "`n")) {
$name = $line.Trim()
if ($name) {
$services += $name
}
}
return $services
}
function Get-RunningContainerByService([string]$serviceName) {
$names = docker ps --filter "label=com.docker.compose.service=$serviceName" --format "{{.Names}}" 2>$null
if (-not $names) {
return $null
}
foreach ($name in ($names -split "`n")) {
$trimmed = $name.Trim()
if ($trimmed) {
return $trimmed
}
}
return $null
}
# ─── 1. Check prerequisites ────────────────────────────────────────────────
function Test-Prerequisites {
Write-Step 'Checking prerequisites'
$allGood = $true
# dotnet
if (Test-Command 'dotnet') {
$v = (dotnet --version 2>$null)
if ($v -match '^10\.') {
Write-Ok "dotnet $v"
} else {
Write-Fail "dotnet $v found, but 10.x is required"
$allGood = $false
}
} else {
Write-Fail 'dotnet SDK not found. Install .NET 10 SDK.'
$allGood = $false
}
# node
if (Test-Command 'node') {
$v = (node --version 2>$null).TrimStart('v')
$major = [int]($v -split '\.')[0]
if ($major -ge 20) {
Write-Ok "node $v"
} else {
Write-Fail "node $v found, but 20+ is required"
$allGood = $false
}
} else {
Write-Fail 'node not found. Install Node.js 20+.'
$allGood = $false
}
# npm
if (Test-Command 'npm') {
$v = (npm --version 2>$null)
$major = [int]($v -split '\.')[0]
if ($major -ge 10) {
Write-Ok "npm $v"
} else {
Write-Fail "npm $v found, but 10+ is required"
$allGood = $false
}
} else {
Write-Fail 'npm not found.'
$allGood = $false
}
# docker
if (Test-Command 'docker') {
$v = (docker --version 2>$null)
Write-Ok "docker: $v"
} else {
Write-Fail 'docker not found. Install Docker Desktop.'
$allGood = $false
}
# docker compose
$composeOk = $false
try {
$null = docker compose version 2>$null
if ($LASTEXITCODE -eq 0) { $composeOk = $true }
} catch {}
if ($composeOk) {
Write-Ok 'docker compose available'
} else {
Write-Fail 'docker compose not available. Ensure Docker Desktop includes Compose V2.'
$allGood = $false
}
# git
if (Test-Command 'git') {
Write-Ok "git $(git --version 2>$null)"
} else {
Write-Fail 'git not found.'
$allGood = $false
}
if (-not $allGood) {
Write-Error 'Prerequisites not met. Install missing tools and re-run.'
exit 1
}
}
# ─── 2. Check and install hosts file ─────────────────────────────────────
function Test-HostsFile {
Write-Step 'Checking hosts file for stella-ops.local entries'
$hostsPath = 'C:\Windows\System32\drivers\etc\hosts'
$hostsSource = Join-Path $Root 'devops/compose/hosts.stellaops.local'
if (-not (Test-Path $hostsPath)) {
Write-Warn "Cannot read hosts file at $hostsPath"
return
}
$content = Get-Content $hostsPath -Raw
if ($content -match 'stella-ops\.local') {
Write-Ok 'stella-ops.local entries found in hosts file'
return
}
Write-Warn 'stella-ops.local entries NOT found in hosts file.'
if (-not (Test-Path $hostsSource)) {
Write-Warn "Hosts source file not found at $hostsSource"
Write-Host ' Add the hosts block from docs/dev/DEV_ENVIRONMENT_SETUP.md section 2' -ForegroundColor Yellow
return
}
# Check if running as Administrator
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if ($isAdmin) {
Write-Host ''
Write-Host ' Stella Ops needs ~50 hosts file entries for local development.' -ForegroundColor Yellow
Write-Host ' Source: devops/compose/hosts.stellaops.local' -ForegroundColor Yellow
Write-Host ''
$answer = Read-Host ' Add entries to hosts file now? (Y/n)'
if ($answer -eq '' -or $answer -match '^[Yy]') {
$hostsBlock = Get-Content $hostsSource -Raw
Add-Content -Path $hostsPath -Value "`n$hostsBlock"
Write-Ok 'Hosts entries added successfully'
} else {
Write-Warn 'Skipped. Add them manually before accessing the platform.'
Write-Host " Copy from: $hostsSource" -ForegroundColor Yellow
}
} else {
Write-Host ''
Write-Host ' Stella Ops needs ~50 hosts file entries for local development.' -ForegroundColor Yellow
Write-Host ' To install them, run this command in an elevated (Administrator) PowerShell:' -ForegroundColor Yellow
Write-Host ''
Write-Host " Get-Content '$hostsSource' | Add-Content '$hostsPath'" -ForegroundColor White
Write-Host ''
Write-Host ' Or re-run this script as Administrator to install them automatically.' -ForegroundColor Yellow
}
}
# ─── 3. Ensure .env ────────────────────────────────────────────────────────
function Initialize-EnvFile {
Write-Step 'Ensuring .env file exists'
$envFile = Join-Path $ComposeDir '.env'
$envExample = Join-Path $ComposeDir 'env/stellaops.env.example'
if (Test-Path $envFile) {
Write-Ok ".env already exists at $envFile"
} elseif (Test-Path $envExample) {
Copy-Item $envExample $envFile
Write-Ok "Copied $envExample -> $envFile"
Write-Warn 'For production, change POSTGRES_PASSWORD in .env.'
} else {
Write-Fail "Neither .env nor env/stellaops.env.example found in $ComposeDir"
exit 1
}
}
# ─── 4. Start infrastructure ───────────────────────────────────────────────
function Start-Infrastructure {
Write-Step 'Starting infrastructure containers (docker-compose.dev.yml)'
Push-Location $ComposeDir
try {
docker compose -f docker-compose.dev.yml up -d
if ($LASTEXITCODE -ne 0) {
Write-Fail 'Failed to start infrastructure containers.'
exit 1
}
Write-Host ' Waiting for containers to become healthy...' -ForegroundColor Gray
$maxWait = 120
$elapsed = 0
while ($elapsed -lt $maxWait) {
$expectedServices = Get-ComposeExpectedServices 'docker-compose.dev.yml'
$services = Get-ComposeServices 'docker-compose.dev.yml'
if ($expectedServices.Count -gt 0) {
$allowed = @{}
foreach ($name in $expectedServices) {
$allowed[$name.ToLowerInvariant()] = $true
}
$services = $services | Where-Object {
$service = "$($_.Service)".ToLowerInvariant()
$service -and $allowed.ContainsKey($service)
}
}
if ($services.Count -gt 0) {
$allHealthy = $true
foreach ($svc in $services) {
$state = "$($svc.State)".ToLowerInvariant()
$health = "$($svc.Health)".ToLowerInvariant()
if ($state -ne 'running') {
$allHealthy = $false
continue
}
if ($health -and $health -ne 'healthy') {
$allHealthy = $false
}
}
if ($allHealthy -and $elapsed -gt 5) {
Write-Ok 'All infrastructure containers healthy'
return
}
}
Start-Sleep -Seconds 5
$elapsed += 5
}
Write-Warn "Timed out waiting for healthy status after ${maxWait}s. Check with: docker compose -f docker-compose.dev.yml ps"
}
finally {
Pop-Location
}
}
# ─── 5. Build .NET solutions ───────────────────────────────────────────────
function Build-Solutions {
Write-Step 'Building all .NET solutions'
$buildScript = Join-Path $Root 'scripts/build-all-solutions.ps1'
if (Test-Path $buildScript) {
& $buildScript
if ($LASTEXITCODE -ne 0) {
Write-Fail '.NET solution build failed.'
exit 1
}
Write-Ok '.NET solutions built successfully'
} else {
Write-Warn "Build script not found at $buildScript. Skipping .NET build."
}
}
# ─── 6. Build Docker images ────────────────────────────────────────────────
function Build-Images {
Write-Step 'Building Docker images'
$buildScript = Join-Path $Root 'devops/docker/build-all.ps1'
if (Test-Path $buildScript) {
& $buildScript
if ($LASTEXITCODE -ne 0) {
Write-Fail 'Docker image build failed.'
exit 1
}
Write-Ok 'Docker images built successfully'
} else {
Write-Warn "Build script not found at $buildScript. Skipping image build."
}
}
# ─── 7. Start full platform ────────────────────────────────────────────────
function Start-Platform {
Write-Step 'Starting full Stella Ops platform'
Push-Location $ComposeDir
try {
docker compose -f docker-compose.stella-ops.yml up -d
if ($LASTEXITCODE -ne 0) {
Write-Fail 'Failed to start platform services.'
exit 1
}
Write-Ok 'Platform services started'
}
finally {
Pop-Location
}
}
# ─── 8. Smoke test ─────────────────────────────────────────────────────────
function Test-Smoke {
Write-Step 'Running smoke tests'
$hasBlockingFailures = $false
# Infrastructure checks
$postgresContainer = Get-RunningContainerByService 'postgres'
if ($postgresContainer) {
docker exec $postgresContainer pg_isready -U stellaops 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Ok "PostgreSQL ($postgresContainer)"
} else {
Write-Fail "PostgreSQL not responding ($postgresContainer)"
$hasBlockingFailures = $true
}
} else {
Write-Fail 'PostgreSQL container not found'
$hasBlockingFailures = $true
}
$valkeyContainer = Get-RunningContainerByService 'valkey'
if ($valkeyContainer) {
$valkeyResponse = (docker exec $valkeyContainer valkey-cli ping 2>$null)
if ($valkeyResponse -and $valkeyResponse.Trim() -eq 'PONG') {
Write-Ok "Valkey ($valkeyContainer)"
} else {
Write-Fail "Valkey not responding ($valkeyContainer)"
$hasBlockingFailures = $true
}
} else {
Write-Fail 'Valkey container not found'
$hasBlockingFailures = $true
}
# Platform container health summary
Write-Step 'Container health summary'
Push-Location $ComposeDir
try {
$composeFiles = if ($InfraOnly) {
@('docker-compose.dev.yml')
} else {
@('docker-compose.stella-ops.yml')
}
if (-not ($composeFiles | Where-Object { Test-Path $_ })) {
$composeFiles = @('docker-compose.dev.yml', 'docker-compose.stella-ops.yml')
}
$totalContainers = 0
$healthyContainers = 0
$warningNames = @()
$blockingNames = @()
$seenContainers = @{}
foreach ($cf in $composeFiles) {
$expectedServices = Get-ComposeExpectedServices $cf
$services = Get-ComposeServices $cf
if ($expectedServices.Count -gt 0) {
$allowed = @{}
foreach ($name in $expectedServices) {
$allowed[$name.ToLowerInvariant()] = $true
}
$services = $services | Where-Object {
$service = "$($_.Service)".ToLowerInvariant()
$service -and $allowed.ContainsKey($service)
}
}
foreach ($svc in $services) {
$name = "$($svc.Name)"
if (-not $name -or $seenContainers.ContainsKey($name)) {
continue
}
$seenContainers[$name] = $true
$totalContainers++
$state = "$($svc.State)".ToLowerInvariant()
$health = "$($svc.Health)".ToLowerInvariant()
if ($state -ne 'running') {
$blockingNames += "$name (state=$state)"
continue
}
if (-not $health -or $health -eq 'healthy') {
$healthyContainers++
} elseif ($health -eq 'starting') {
$warningNames += "$name (health=starting)"
} else {
$blockingNames += "$name (health=$health)"
}
}
}
if ($totalContainers -gt 0) {
if ($blockingNames.Count -eq 0 -and $warningNames.Count -eq 0) {
Write-Ok "$healthyContainers/$totalContainers containers healthy"
} elseif ($blockingNames.Count -eq 0) {
Write-Warn "$healthyContainers/$totalContainers containers healthy ($($warningNames.Count) still starting)"
foreach ($name in $warningNames) {
Write-Warn " Advisory: $name"
}
} else {
Write-Fail "$healthyContainers/$totalContainers containers healthy ($($blockingNames.Count) blocking issue(s))"
foreach ($name in $blockingNames) {
Write-Fail " Blocking: $name"
}
foreach ($name in $warningNames) {
Write-Warn " Advisory: $name"
}
$hasBlockingFailures = $true
}
}
# Platform endpoint check
try {
$tcp = New-Object System.Net.Sockets.TcpClient
$tcp.Connect('stella-ops.local', 443)
$tcp.Close()
Write-Ok 'Platform accessible at https://stella-ops.local'
} catch {
Write-Warn 'Platform not yet accessible at https://stella-ops.local (may still be starting)'
}
}
finally {
Pop-Location
}
return $hasBlockingFailures
}
# ─── Main ───────────────────────────────────────────────────────────────────
Write-Host '=============================================' -ForegroundColor Cyan
Write-Host ' Stella Ops Developer Environment Setup' -ForegroundColor Cyan
Write-Host '=============================================' -ForegroundColor Cyan
Test-Prerequisites
Test-HostsFile
if ($ImagesOnly) {
Build-Images
Write-Host "`nDone (images only)." -ForegroundColor Green
exit 0
}
Initialize-EnvFile
if ($InfraOnly) {
Start-Infrastructure
$infraSmokeFailed = Test-Smoke
if ($infraSmokeFailed) {
Write-Warn 'Infrastructure started with blocking smoke failures. Review output and docker compose logs.'
}
Write-Host "`nDone (infra only). Infrastructure is running." -ForegroundColor Green
exit 0
}
if (-not $SkipBuild) {
Build-Solutions
}
if (-not $SkipImages) {
Build-Images
}
Start-Platform
$platformSmokeFailed = Test-Smoke
if ($platformSmokeFailed) {
Write-Warn 'Setup completed with blocking smoke failures. Review output and docker compose logs.'
}
Write-Host "`n=============================================" -ForegroundColor Green
Write-Host ' Setup complete!' -ForegroundColor Green
Write-Host ' Platform: https://stella-ops.local' -ForegroundColor Green
Write-Host ' Docs: docs/dev/DEV_ENVIRONMENT_SETUP.md' -ForegroundColor Green
Write-Host '=============================================' -ForegroundColor Green