Speed up scratch image builds with publish-first contexts

This commit is contained in:
master
2026-03-09 07:37:24 +02:00
parent c9686edf07
commit f218ec82ec
8 changed files with 358 additions and 38 deletions

View File

@@ -1,28 +1,43 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Build hardened Docker images for all Stella Ops services using the shared template/matrix.
Build hardened Docker images for Stella Ops services using the shared matrix.
.DESCRIPTION
PowerShell port of build-all.sh. Reads services-matrix.env (pipe-delimited) and builds
each service image using Dockerfile.hardened.template (or Dockerfile.console for Angular).
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
.NET SDK base image. Default: mcr.microsoft.com/dotnet/sdk:10.0-noble
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]$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 }
@@ -53,11 +68,105 @@ if (-not (Test-Path $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 }
@@ -65,11 +174,15 @@ foreach ($line in Get-Content $MatrixPath) {
$parts = $line -split '\|'
if ($parts.Count -lt 5) { continue }
$service = $parts[0]
$service = $parts[0]
$dockerfile = $parts[1]
$project = $parts[2]
$binary = $parts[3]
$port = $parts[4]
$project = $parts[2]
$binary = $parts[3]
$port = $parts[4]
if (-not (Should-BuildService $service)) {
continue
}
$image = "${Registry}/${service}:${TagSuffix}"
$dfPath = Join-Path $Root $dockerfile
@@ -81,25 +194,57 @@ foreach ($line in Get-Content $MatrixPath) {
if ($dockerfile -like '*Dockerfile.console*') {
Write-Host "[console] $service -> $image" -ForegroundColor Yellow
docker build `
-f $dfPath $Root `
--build-arg "APP_DIR=$project" `
--build-arg "APP_PORT=$port" `
-t $image
$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
docker 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
$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 ($LASTEXITCODE -eq 0) {
if ($buildExitCode -eq 0) {
$succeeded += $service
}
else {
@@ -120,3 +265,7 @@ if ($failed.Count -gt 0) {
}
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
}