provisioning/scripts/docker-build.nu

336 lines
9.7 KiB
Text
Raw Normal View History

#!/usr/bin/env nu
# Docker Build Execution Script
# Builds Docker images with BuildKit caching and cargo-chef optimization
#
# Usage:
# docker-build.nu extension-registry --mode solo
# docker-build.nu orchestrator --mode cicd --push
# docker-build.nu --all --mode cicd
#
# Environment:
# PROVISIONING_ROOT - Optional: override project root detection
#
# Patterns:
# - Pipeline let binding
# - Result pattern (NO try-catch)
# - Memory management with unlet for large datasets
# 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"
}
# Import generator script
use docker-generate-builds.nu
# Valid service names (from generator)
const VALID_SERVICES = [
"orchestrator"
"control-center"
"extension-registry"
"mcp-server"
"provisioning-daemon"
"ai-service"
"rag"
"vault-service"
]
# Valid deployment modes
const VALID_MODES = ["solo", "cicd", "enterprise"]
# Service name to package directory mapping
const SERVICE_TO_DIR = {
orchestrator: "orchestrator",
control-center: "control-center",
extension-registry: "extension-registry",
mcp-server: "mcp-server",
provisioning-daemon: "daemon",
ai-service: "ai-service",
rag: "rag",
vault-service: "vault-service",
}
# Service name to Nickel schema key mapping
const SERVICE_TO_SCHEMA_KEY = {
orchestrator: "orchestrator",
control-center: "control_center",
extension-registry: "extension_registry",
mcp-server: "mcp_server",
provisioning-daemon: "provisioning_daemon",
ai-service: "ai_service",
rag: "rag",
vault-service: "vault",
}
# Read build config from Nickel for a service
def get-build-config [
service: string,
mode: string,
]: nothing -> record {
let prov_root = (get-provisioning-root)
let schema_key = $SERVICE_TO_SCHEMA_KEY | get $service
let defaults_file = $"($prov_root)/schemas/platform/defaults/($service)-defaults.ncl"
# Export build config as JSON
let nickel_expr = $"
let defaults = import \"($defaults_file)\" in
defaults.($schema_key).build
"
let result = (
echo $nickel_expr
| nickel export --format json
| complete
)
if $result.exit_code != 0 {
error make {
msg: "Failed to read build config",
label: {
text: $"Nickel export failed: ($result.stderr)",
span: (metadata $service).span
}
}
}
$result.stdout | from json
}
# Build a single service
def main [
...services: string, # Service names to build (or use --all)
--all, # Build all services
--mode: string = "solo", # Deployment mode
--push, # Push to registry after build
--no-cache, # Disable BuildKit cache
--registry: string = "localhost:5000" # Container registry for cache/push
]: nothing -> table {
# Determine services to build
let services_to_build = if $all {
$VALID_SERVICES
} else if ($services | is-empty) {
error make {
msg: "No services specified. Use service names or --all"
}
} else {
$services
}
# Guard: Validate mode
if not ($mode in $VALID_MODES) {
error make {
msg: $"Invalid mode: ($mode). Valid: ($VALID_MODES | str join ', ')"
}
}
# Build each service
let results = ($services_to_build | each {|service|
print $"Building ($service) with mode ($mode)..."
# Step 1: Generate Dockerfile
print " → Generating Dockerfile from Nickel template..."
let prov_root = (get-provisioning-root)
let gen_script = $"($prov_root)/scripts/docker-generate-builds.nu"
let gen_result = (nu $gen_script $service --mode $mode)
if not $gen_result.ok {
print $" ✗ Generation failed: ($gen_result.err)"
return {
service: $service,
ok: false,
stage: "generate",
error: $gen_result.err,
duration: 0
}
}
print $" ✓ Generated: ($gen_result.path)"
# Step 2: Read build config
let build_config = (get-build-config $service $mode)
let package = $build_config.package
let cache_mode = $build_config.buildkit.cache_mode
let parallel_jobs = $build_config.buildkit.parallel_jobs
# Unlet large config after extraction
# (Memory management for large datasets)
# Step 3: Prepare build context
let prov_root_build = (get-provisioning-root)
let target_dir = $SERVICE_TO_DIR | get $service
let dockerfile_path = $"($prov_root_build)/platform/crates/($target_dir)/Dockerfile"
let build_context = $"($prov_root_build)/platform"
let image_tag = if $push {
$"($registry)/provisioning-($service):latest"
} else {
$"provisioning-($service):latest"
}
# Step 4: Construct docker buildx command
let cache_args = if $no_cache {
[]
} else {
match $cache_mode {
"local" => [
"--cache-from" $"type=local,src=/tmp/buildkit-cache/($service)",
"--cache-to" $"type=local,dest=/tmp/buildkit-cache/($service),mode=max"
],
"registry" => [
"--cache-from" $"type=registry,ref=($registry)/cache:($service)",
"--cache-to" $"type=registry,ref=($registry)/cache:($service),mode=max"
],
"inline" => [
"--cache-from" $"type=registry,ref=($image_tag)",
"--cache-to" "type=inline"
],
_ => []
}
}
let build_args = [
"CARGO_BUILD_JOBS" $parallel_jobs
]
# Step 5: Execute docker build
print $" → Building Docker image: ($image_tag)"
print $" Cache mode: ($cache_mode), Parallel jobs: ($parallel_jobs)"
let start_time = (date now)
let docker_result = (
docker buildx build
--file $dockerfile_path
--tag $image_tag
...$cache_args
--build-arg $"CARGO_BUILD_JOBS=($parallel_jobs)"
--progress plain
$build_context
| complete
)
let end_time = (date now)
let duration = ($end_time - $start_time | into int) / 1_000_000_000
if $docker_result.exit_code != 0 {
print $" ✗ Build failed after ($duration)s"
print $" Error: ($docker_result.stderr)"
return {
service: $service,
ok: false,
stage: "build",
error: $docker_result.stderr,
duration: $duration
}
}
print $" ✓ Build completed in ($duration)s"
# Step 6: Push to registry if requested
if $push {
print $" → Pushing to registry: ($registry)"
let push_result = (
docker push $image_tag
| complete
)
if $push_result.exit_code != 0 {
print $" ✗ Push failed"
return {
service: $service,
ok: false,
stage: "push",
error: $push_result.stderr,
duration: $duration
}
}
print $" ✓ Pushed successfully"
}
# Return success
{
service: $service,
ok: true,
stage: "complete",
error: "",
duration: $duration
}
})
# Display summary
print ""
print "Build Summary:"
print "=============="
let successful = ($results | where ok == true | length)
let failed = ($results | where ok == false | length)
let total_duration = ($results | get duration | math sum)
print $" Total: ($results | length) services"
print $" ✓ Successful: ($successful)"
print $" ✗ Failed: ($failed)"
print $" Total time: ($total_duration)s"
$results
}
# Show build configuration for a service
export def "main config" [
service: string,
--mode: string = "solo"
]: nothing -> record {
if not ($service in $VALID_SERVICES) {
error make {
msg: $"Invalid service: ($service). Valid: ($VALID_SERVICES | str join ', ')"
}
}
get-build-config $service $mode
}
# List all services that can be built
export def "main list" []: nothing -> list<string> {
$VALID_SERVICES
}