#!/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 { $VALID_SERVICES }