Speed up scratch image builds with publish-first contexts
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user