Files
git.stella-ops.org/devops/docker/build-all.ps1

272 lines
8.5 KiB
PowerShell

#!/usr/bin/env pwsh
<#
.SYNOPSIS
Build hardened Docker images for Stella Ops services using the shared matrix.
.DESCRIPTION
The default path publishes .NET services locally and builds hardened runtime
images from small temporary contexts so scratch setup does not keep streaming
the full monorepo into Docker for every backend image.
.PARAMETER Registry
Docker image registry prefix. Default: stellaops
.PARAMETER TagSuffix
Tag suffix for built images. Default: dev
.PARAMETER SdkImage
Reserved for legacy repo-context builds. Default: mcr.microsoft.com/dotnet/sdk:10.0-noble
.PARAMETER RuntimeImage
.NET runtime base image. Default: mcr.microsoft.com/dotnet/aspnet:10.0-noble
.PARAMETER Services
Optional service filter. Only listed services are rebuilt.
.PARAMETER PublishNoRestore
Skip restore during local dotnet publish when a prior solution build already ran.
.PARAMETER UseLegacyRepoContext
Fall back to repo-root docker builds for backend services.
#>
[CmdletBinding()]
param(
[string]$Registry,
[string]$TagSuffix,
[string]$SdkImage,
[string]$RuntimeImage,
[string[]]$Services,
[switch]$PublishNoRestore,
[switch]$UseLegacyRepoContext
)
$ErrorActionPreference = 'Continue'
$previousNativeErrorPreference = $null
if ($PSVersionTable.PSVersion.Major -ge 7) {
$previousNativeErrorPreference = $global:PSNativeCommandUseErrorActionPreference
$global:PSNativeCommandUseErrorActionPreference = $false
}
if ([string]::IsNullOrWhiteSpace($Registry)) {
$Registry = if ([string]::IsNullOrWhiteSpace($env:REGISTRY)) { 'stellaops' } else { $env:REGISTRY }
}
if ([string]::IsNullOrWhiteSpace($TagSuffix)) {
$TagSuffix = if ([string]::IsNullOrWhiteSpace($env:TAG_SUFFIX)) { 'dev' } else { $env:TAG_SUFFIX }
}
if ([string]::IsNullOrWhiteSpace($SdkImage)) {
$SdkImage = if ([string]::IsNullOrWhiteSpace($env:SDK_IMAGE)) { 'mcr.microsoft.com/dotnet/sdk:10.0-noble' } else { $env:SDK_IMAGE }
}
if ([string]::IsNullOrWhiteSpace($RuntimeImage)) {
$RuntimeImage = if ([string]::IsNullOrWhiteSpace($env:RUNTIME_IMAGE)) { 'mcr.microsoft.com/dotnet/aspnet:10.0-noble' } else { $env:RUNTIME_IMAGE }
}
$Root = git rev-parse --show-toplevel 2>$null
if (-not $Root) {
Write-Error 'Not inside a git repository.'
exit 1
}
$Root = $Root.Trim()
$MatrixPath = Join-Path $Root 'devops/docker/services-matrix.env'
if (-not (Test-Path $MatrixPath)) {
Write-Error "Matrix file not found: $MatrixPath"
exit 1
}
$runtimeDockerfile = Join-Path $Root 'devops/docker/Dockerfile.hardened.runtime'
$healthcheckScript = Join-Path $Root 'devops/docker/healthcheck.sh'
$fastContextRoot = Join-Path ([System.IO.Path]::GetTempPath()) 'stellaops-fast-images'
$serviceFilter = @{}
foreach ($serviceName in ($Services | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) {
$serviceFilter[$serviceName.Trim().ToLowerInvariant()] = $true
}
Write-Host "Building services from $MatrixPath -> ${Registry}/<service>:${TagSuffix}" -ForegroundColor Cyan
if ($serviceFilter.Count -gt 0) {
Write-Host "Service filter: $($serviceFilter.Keys -join ', ')" -ForegroundColor Cyan
}
$succeeded = @()
$failed = @()
function Invoke-DockerBuild([string[]]$Arguments) {
& docker @Arguments 2>&1 | ForEach-Object {
$text = if ($_ -is [System.Management.Automation.ErrorRecord]) {
$_.ToString()
}
else {
"$_"
}
Write-Host $text
}
return $LASTEXITCODE
}
function Invoke-NativeCommand([string]$Command, [string[]]$Arguments) {
& $Command @Arguments 2>&1 | ForEach-Object {
$text = if ($_ -is [System.Management.Automation.ErrorRecord]) {
$_.ToString()
}
else {
"$_"
}
Write-Host $text
}
return $LASTEXITCODE
}
function Should-BuildService([string]$ServiceName) {
if ($serviceFilter.Count -eq 0) {
return $true
}
return $serviceFilter.ContainsKey($ServiceName.ToLowerInvariant())
}
function Remove-BuildContext([string]$ContextPath) {
if (Test-Path $ContextPath) {
Remove-Item -Path $ContextPath -Recurse -Force -ErrorAction SilentlyContinue
}
}
function New-PublishedBuildContext([string]$Service, [string]$Project) {
if (-not (Test-Path $runtimeDockerfile)) {
throw "Runtime Dockerfile not found: $runtimeDockerfile"
}
if (-not (Test-Path $healthcheckScript)) {
throw "Healthcheck script not found: $healthcheckScript"
}
$serviceRoot = Join-Path $fastContextRoot $Service
$appDir = Join-Path $serviceRoot 'app'
Remove-BuildContext $serviceRoot
New-Item -ItemType Directory -Path $appDir -Force | Out-Null
$publishArguments = @(
'publish',
(Join-Path $Root $Project),
'-c', 'Release',
'-o', $appDir,
'/p:UseAppHost=false',
'/p:PublishTrimmed=false',
'--nologo'
)
if ($PublishNoRestore) {
$publishArguments += '--no-restore'
}
$publishExitCode = Invoke-NativeCommand 'dotnet' $publishArguments
if ($publishExitCode -ne 0) {
Remove-BuildContext $serviceRoot
throw "dotnet publish failed for $Service"
}
Copy-Item -Path $runtimeDockerfile -Destination (Join-Path $serviceRoot 'Dockerfile') -Force
Copy-Item -Path $healthcheckScript -Destination (Join-Path $serviceRoot 'healthcheck.sh') -Force
return $serviceRoot
}
foreach ($line in Get-Content $MatrixPath) {
$line = $line.Trim()
if (-not $line -or $line.StartsWith('#')) { continue }
$parts = $line -split '\|'
if ($parts.Count -lt 5) { continue }
$service = $parts[0]
$dockerfile = $parts[1]
$project = $parts[2]
$binary = $parts[3]
$port = $parts[4]
if (-not (Should-BuildService $service)) {
continue
}
$image = "${Registry}/${service}:${TagSuffix}"
$dfPath = Join-Path $Root $dockerfile
if (-not (Test-Path $dfPath)) {
Write-Warning "Skipping ${service}: dockerfile missing ($dfPath)"
continue
}
if ($dockerfile -like '*Dockerfile.console*') {
Write-Host "[console] $service -> $image" -ForegroundColor Yellow
$buildExitCode = Invoke-DockerBuild @(
'build',
'-f', $dfPath,
$Root,
'--build-arg', "APP_DIR=$project",
'--build-arg', "APP_PORT=$port",
'-t', $image
)
}
elseif (-not $UseLegacyRepoContext -and $dockerfile -like '*Dockerfile.hardened.template*') {
Write-Host "[service fast] $service -> $image" -ForegroundColor Green
$contextPath = $null
try {
$contextPath = New-PublishedBuildContext -Service $service -Project $project
$buildExitCode = Invoke-DockerBuild @(
'build',
'-f', (Join-Path $contextPath 'Dockerfile'),
$contextPath,
'--build-arg', "RUNTIME_IMAGE=$RuntimeImage",
'--build-arg', "APP_BINARY=$binary",
'--build-arg', "APP_PORT=$port",
'-t', $image
)
}
catch {
Write-Host $_.Exception.Message -ForegroundColor Red
$buildExitCode = 1
}
finally {
if ($contextPath) {
Remove-BuildContext $contextPath
}
}
}
else {
Write-Host "[service] $service -> $image" -ForegroundColor Green
$buildExitCode = Invoke-DockerBuild @(
'build',
'-f', $dfPath,
$Root,
'--build-arg', "SDK_IMAGE=$SdkImage",
'--build-arg', "RUNTIME_IMAGE=$RuntimeImage",
'--build-arg', "APP_PROJECT=$project",
'--build-arg', "APP_BINARY=$binary",
'--build-arg', "APP_PORT=$port",
'-t', $image
)
}
if ($buildExitCode -eq 0) {
$succeeded += $service
}
else {
$failed += $service
Write-Host "FAILED: $service" -ForegroundColor Red
}
}
Write-Host ''
Write-Host '=== BUILD RESULTS ===' -ForegroundColor Cyan
Write-Host "Succeeded ($($succeeded.Count)): $($succeeded -join ', ')" -ForegroundColor Green
Write-Host "Failed ($($failed.Count)): $($failed -join ', ')" -ForegroundColor $(if ($failed.Count -gt 0) { 'Red' } else { 'Green' })
Write-Host ''
if ($failed.Count -gt 0) {
Write-Error 'Some builds failed. Fix the issues and re-run.'
exit 1
}
Write-Host 'Build complete. Remember to enforce readOnlyRootFilesystem at deploy time and run sbom_attest.sh (DOCKER-44-002).' -ForegroundColor Cyan
if ($PSVersionTable.PSVersion.Major -ge 7) {
$global:PSNativeCommandUseErrorActionPreference = $previousNativeErrorPreference
}