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