provisioning/scripts/docker-validate-builds.nu

265 lines
7.4 KiB
Text
Raw Normal View History

#!/usr/bin/env nu
# Docker Build Validation Script
# Validates cargo-chef optimization by measuring build times and image sizes
#
# Usage:
# docker-validate-builds.nu extension-registry
# docker-validate-builds.nu orchestrator --iterations 3
#
# Environment:
# PROVISIONING_ROOT - Optional: override project root detection
#
# Metrics:
# - Cold build (no cache)
# - Warm build (dependency cache)
# - Incremental build (source change only)
# - Image size comparison
# Search up directory tree for provisioning root (helper)
def search-up-for-provisioning [dir: string]: nothing -> string {
if ($"($dir)/provisioning/schemas/platform" | path exists) {
$"($dir)/provisioning"
} else {
let parent = ($dir | path dirname)
if $parent == $dir {
""
} else {
search-up-for-provisioning $parent
}
}
}
# Detect provisioning project root
def get-provisioning-root []: nothing -> string {
# 1. Check environment variable
if ($env.PROVISIONING_ROOT? != null) {
return $env.PROVISIONING_ROOT
}
# 2. Check if we're in provisioning/ directory
let cwd = (pwd)
if ($cwd | path basename) == "provisioning" {
# We're inside provisioning/, use current dir
if ("schemas/platform" | path exists) {
return "."
}
}
# 3. Check if provisioning/ exists as subdirectory
if ("provisioning/schemas/platform" | path exists) {
return "provisioning"
}
# 4. Search up the directory tree
let found = (search-up-for-provisioning $cwd)
if $found != "" {
return $found
}
# 5. Fallback to "provisioning" (will fail with clear error)
"provisioning"
}
# Run build benchmark for a service
def main [
service: string, # Service to benchmark
--iterations: int = 1, # Number of iterations per test
--skip-cold, # Skip cold build (fastest)
--registry: string = "localhost:5000" # Registry for cache
]: nothing -> record {
print $"Docker Build Validation: ($service)"
print "========================================"
print ""
# Step 1: Generate Dockerfile
print "→ Generating Dockerfile..."
let prov_root = (get-provisioning-root)
let gen_script = $"($prov_root)/scripts/docker-generate-builds.nu"
let gen_result = (nu $gen_script $service --mode solo)
if not $gen_result.ok {
error make {
msg: $"Failed to generate Dockerfile: ($gen_result.err)"
}
}
print $" ✓ Generated: ($gen_result.path)"
print ""
let prov_root_build = (get-provisioning-root)
let build_context = $"($prov_root_build)/platform"
let dockerfile_path = $gen_result.path
let image_tag = $"provisioning-($service):test"
# Step 2: Cold build (no cache)
let cold_results = if not $skip_cold {
print "→ Running COLD build (no cache)..."
print " This measures full build time including dependencies"
let cold_times = (1..$iterations | each {|i|
print $" Iteration ($i)/($iterations)..."
# Clear Docker build cache
docker builder prune --all --force | complete | ignore
let start = (date now)
let result = (
docker buildx build
--file $dockerfile_path
--tag $"($image_tag)-cold"
--no-cache
--progress plain
$build_context
| complete
)
let end = (date now)
let duration = (($end - $start) | into int) / 1_000_000_000
if $result.exit_code != 0 {
error make {
msg: $"Cold build failed: ($result.stderr)"
}
}
print $" ✓ Completed in ($duration)s"
$duration
})
let avg = ($cold_times | math avg)
let min = ($cold_times | math min)
let max = ($cold_times | math max)
print ""
print $" Average: ($avg)s, Min: ($min)s, Max: ($max)s"
print ""
{
avg: $avg,
min: $min,
max: $max,
times: $cold_times
}
} else {
print "→ Skipping COLD build"
print ""
null
}
# Step 3: Warm build (with dependency cache)
print "→ Running WARM build (with cargo-chef cache)..."
print " This measures build time with cached dependencies"
let warm_times = (1..$iterations | each {|i|
print $" Iteration ($i)/($iterations)..."
let start = (date now)
let result = (
docker buildx build
--file $dockerfile_path
--tag $"($image_tag)-warm"
--cache-from $"type=registry,ref=($registry)/cache:($service)"
--cache-to $"type=registry,ref=($registry)/cache:($service),mode=max"
--progress plain
$build_context
| complete
)
let end = (date now)
let duration = (($end - $start) | into int) / 1_000_000_000
if $result.exit_code != 0 {
error make {
msg: $"Warm build failed: ($result.stderr)"
}
}
print $" ✓ Completed in ($duration)s"
$duration
})
let warm_avg = ($warm_times | math avg)
let warm_min = ($warm_times | math min)
let warm_max = ($warm_times | math max)
print ""
print $" Average: ($warm_avg)s, Min: ($warm_min)s, Max: ($warm_max)s"
print ""
# Step 4: Get image sizes
print "→ Measuring image sizes..."
let size_warm = (
docker images $"($image_tag)-warm" --format "{{.Size}}"
| str trim
)
print $" Warm build image: ($size_warm)"
print ""
# Step 5: Calculate savings
let results = if not $skip_cold {
let cache_savings = (($cold_results.avg - $warm_avg) / $cold_results.avg * 100)
print "Summary:"
print "========"
print $" Cold build (no cache): ($cold_results.avg)s"
print $" Warm build (dep cache): ($warm_avg)s"
print $" Cache savings: ($cache_savings | into int)%"
print $" Image size: ($size_warm)"
print ""
{
service: $service,
cold: $cold_results,
warm: {
avg: $warm_avg,
min: $warm_min,
max: $warm_max,
times: $warm_times
},
cache_savings_percent: $cache_savings,
image_size: $size_warm
}
} else {
print "Summary:"
print "========"
print $" Warm build (dep cache): ($warm_avg)s"
print $" Image size: ($size_warm)"
print ""
{
service: $service,
cold: null,
warm: {
avg: $warm_avg,
min: $warm_min,
max: $warm_max,
times: $warm_times
},
cache_savings_percent: null,
image_size: $size_warm
}
}
$results
}
# Quick validation (warm build only, 1 iteration)
export def "main quick" [
service: string
]: nothing -> record {
main $service --iterations 1 --skip-cold
}
# Full benchmark (cold + warm, 3 iterations)
export def "main full" [
service: string
]: nothing -> record {
main $service --iterations 3
}