264 lines
7.4 KiB
Text
Executable file
264 lines
7.4 KiB
Text
Executable file
#!/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
|
|
}
|