provisioning/tools/package/build-containers.nu
2025-10-07 11:12:02 +01:00

639 lines
19 KiB
Plaintext

#!/usr/bin/env nu
# Container build tool - builds Docker containers for platform services
#
# Builds:
# - Orchestrator service container
# - Control center container
# - Web UI container
# - All-in-one development container
# - Platform-specific containers
use std log
def main [
--dist-dir: string = "dist" # Distribution directory with built components
--output-registry: string = "local" # Container registry: local, docker.io, ghcr.io, custom
--tag-prefix: string = "provisioning" # Container tag prefix
--version: string = "" # Version tag (auto-detected if empty)
--build-args: string = "" # Comma-separated build arguments
--platforms: string = "linux/amd64" # Target platforms for multi-arch builds
--push: bool = false # Push containers to registry
--cache: bool = true # Use build cache
--verbose: bool = false # Enable verbose logging
--parallel: bool = true # Build containers in parallel
] -> record {
let repo_root = ($env.PWD | path dirname | path dirname | path dirname)
let dist_root = ($dist_dir | path expand)
# Detect version if not provided
let detected_version = if $version == "" {
detect_version $repo_root
} else {
$version
}
let container_config = {
dist_dir: $dist_root
output_registry: $output_registry
tag_prefix: $tag_prefix
version: $detected_version
build_args: ($build_args | if $in == "" { [] } else { $in | split row "," | each { str trim } })
platforms: ($platforms | split row "," | each { str trim })
push: $push
cache: $cache
verbose: $verbose
parallel: $parallel
repo_root: $repo_root
}
log info $"Starting container builds with config: ($container_config)"
# Validate distribution directory
if not ($dist_root | path exists) {
log error $"Distribution directory does not exist: ($dist_root)"
exit 1
}
# Check Docker availability
let docker_available = check_docker_availability
if not $docker_available {
log error "Docker is not available or not running"
exit 1
}
# Define container configurations
let container_definitions = [
{
name: "orchestrator"
dockerfile: "Dockerfile.orchestrator"
context: "."
binary_path: ($dist_root | path join "platform")
dependencies: ["orchestrator"]
},
{
name: "control-center"
dockerfile: "Dockerfile.control-center"
context: "."
binary_path: ($dist_root | path join "platform")
dependencies: ["control-center"]
},
{
name: "web-ui"
dockerfile: "Dockerfile.web-ui"
context: "."
binary_path: ($dist_root | path join "platform")
dependencies: ["control-center-ui"]
},
{
name: "all-in-one"
dockerfile: "Dockerfile.all-in-one"
context: "."
binary_path: ($dist_root | path join "platform")
dependencies: ["orchestrator", "control-center", "control-center-ui"]
}
]
# Create Dockerfiles if they don't exist
ensure_dockerfiles_exist $container_definitions $container_config
# Build containers
let build_results = if $container_config.parallel {
build_containers_parallel $container_definitions $container_config
} else {
build_containers_sequential $container_definitions $container_config
}
# Push containers if requested
let push_results = if $container_config.push {
push_containers $build_results $container_config
} else {
{ status: "skipped", pushed: [] }
}
let summary = {
total_containers: ($container_definitions | length)
successful_builds: ($build_results | where status == "success" | length)
failed_builds: ($build_results | where status == "failed" | length)
push_results: $push_results
container_config: $container_config
build_results: $build_results
}
if $summary.failed_builds > 0 {
log error $"Container build completed with ($summary.failed_builds) failures"
exit 1
} else {
log info $"Container build completed successfully - ($summary.successful_builds) containers built"
}
return $summary
}
# Detect version from git or other sources
def detect_version [repo_root: string] -> string {
try {
cd $repo_root
let git_version = (git describe --tags --always --dirty 2>/dev/null | complete)
if $git_version.exit_code == 0 and ($git_version.stdout | str trim) != "" {
return ($git_version.stdout | str trim)
}
return $"dev-(date now | format date "%Y%m%d")"
} catch {
return "dev-unknown"
}
}
# Check if Docker is available
def check_docker_availability [] -> bool {
try {
let docker_check = (docker --version | complete)
return ($docker_check.exit_code == 0)
} catch {
return false
}
}
# Ensure Dockerfiles exist, create them if they don't
def ensure_dockerfiles_exist [
container_definitions: list
container_config: record
] {
let dockerfile_dir = ($container_config.repo_root | path join "docker")
# Ensure docker directory exists
mkdir $dockerfile_dir
for container in $container_definitions {
let dockerfile_path = ($dockerfile_dir | path join $container.dockerfile)
if not ($dockerfile_path | path exists) {
log info $"Creating Dockerfile for ($container.name): ($dockerfile_path)"
create_dockerfile $container $container_config $dockerfile_path
}
}
}
# Create a Dockerfile for a container
def create_dockerfile [
container: record
container_config: record
dockerfile_path: string
] {
let dockerfile_content = match $container.name {
"orchestrator" => { create_orchestrator_dockerfile $container_config }
"control-center" => { create_control_center_dockerfile $container_config }
"web-ui" => { create_web_ui_dockerfile $container_config }
"all-in-one" => { create_all_in_one_dockerfile $container_config }
_ => { create_generic_dockerfile $container $container_config }
}
$dockerfile_content | save $dockerfile_path
log info $"Created Dockerfile: ($dockerfile_path)"
}
# Create orchestrator Dockerfile
def create_orchestrator_dockerfile [config: record] -> string {
$"# Provisioning Orchestrator Container
# Version: ($config.version)
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && apt-get install -y \\
ca-certificates \\
curl \\
&& rm -rf /var/lib/apt/lists/*
# Create application user
RUN useradd -r -s /bin/false -m -d /app provisioning
# Copy binary
COPY dist/platform/orchestrator-* /usr/local/bin/provisioning-orchestrator
RUN chmod +x /usr/local/bin/provisioning-orchestrator
# Copy core libraries
COPY dist/core /app/core
COPY dist/config /app/config
# Set ownership
RUN chown -R provisioning:provisioning /app
# Switch to application user
USER provisioning
WORKDIR /app
# Expose ports
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD curl -f http://localhost:8080/health || exit 1
# Run orchestrator
CMD [\"/usr/local/bin/provisioning-orchestrator\", \"--host\", \"0.0.0.0\", \"--port\", \"8080\"]
"
}
# Create control center Dockerfile
def create_control_center_dockerfile [config: record] -> string {
$"# Provisioning Control Center Container
# Version: ($config.version)
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && apt-get install -y \\
ca-certificates \\
curl \\
&& rm -rf /var/lib/apt/lists/*
# Create application user
RUN useradd -r -s /bin/false -m -d /app provisioning
# Copy binary
COPY dist/platform/control-center-* /usr/local/bin/control-center
RUN chmod +x /usr/local/bin/control-center
# Copy core libraries
COPY dist/core /app/core
COPY dist/config /app/config
# Set ownership
RUN chown -R provisioning:provisioning /app
# Switch to application user
USER provisioning
WORKDIR /app
# Expose ports
EXPOSE 9080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD curl -f http://localhost:9080/health || exit 1
# Run control center
CMD [\"/usr/local/bin/control-center\", \"--host\", \"0.0.0.0\", \"--port\", \"9080\"]
"
}
# Create web UI Dockerfile
def create_web_ui_dockerfile [config: record] -> string {
$"# Provisioning Web UI Container
# Version: ($config.version)
FROM nginx:alpine
# Copy built UI assets
COPY dist/platform/control-center-ui /usr/share/nginx/html
# Copy nginx configuration
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD curl -f http://localhost/health || exit 1
# Run nginx
CMD [\"nginx\", \"-g\", \"daemon off;\"]
"
}
# Create all-in-one Dockerfile
def create_all_in_one_dockerfile [config: record] -> string {
$"# Provisioning All-in-One Container
# Version: ($config.version)
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && apt-get install -y \\
ca-certificates \\
curl \\
nginx \\
supervisor \\
&& rm -rf /var/lib/apt/lists/*
# Create application user
RUN useradd -r -s /bin/false -m -d /app provisioning
# Copy binaries
COPY dist/platform/orchestrator-* /usr/local/bin/provisioning-orchestrator
COPY dist/platform/control-center-* /usr/local/bin/control-center
RUN chmod +x /usr/local/bin/provisioning-orchestrator /usr/local/bin/control-center
# Copy core libraries and configuration
COPY dist/core /app/core
COPY dist/config /app/config
# Copy web UI
COPY dist/platform/control-center-ui /usr/share/nginx/html
# Copy supervisor configuration
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/nginx-all-in-one.conf /etc/nginx/sites-available/default
# Set ownership
RUN chown -R provisioning:provisioning /app
# Expose ports
EXPOSE 80 8080 9080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD curl -f http://localhost/health && curl -f http://localhost:8080/health || exit 1
# Run supervisor
CMD [\"/usr/bin/supervisord\", \"-c\", \"/etc/supervisor/conf.d/supervisord.conf\"]
"
}
# Create generic Dockerfile
def create_generic_dockerfile [container: record, config: record] -> string {
$"# Generic Provisioning Service Container
# Service: ($container.name)
# Version: ($config.version)
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && apt-get install -y \\
ca-certificates \\
curl \\
&& rm -rf /var/lib/apt/lists/*
# Create application user
RUN useradd -r -s /bin/false -m -d /app provisioning
# Copy binaries
COPY dist/platform/* /usr/local/bin/
RUN chmod +x /usr/local/bin/*
# Copy core libraries
COPY dist/core /app/core
COPY dist/config /app/config
# Set ownership
RUN chown -R provisioning:provisioning /app
# Switch to application user
USER provisioning
WORKDIR /app
# Expose default port
EXPOSE 8080
# Run service
CMD [\"sh\", \"-c\", \"echo 'Container for ($container.name) - configure as needed'\"]
"
}
# Build containers sequentially
def build_containers_sequential [
container_definitions: list
container_config: record
] -> list {
$container_definitions | each {|container|
build_single_container $container $container_config
}
}
# Build containers in parallel
def build_containers_parallel [
container_definitions: list
container_config: record
] -> list {
# For simplicity, using sequential for now
# In a real implementation, you might use background processes
build_containers_sequential $container_definitions $container_config
}
# Build a single container
def build_single_container [
container: record
container_config: record
] -> record {
log info $"Building container: ($container.name)"
let start_time = (date now)
let dockerfile_path = ($container_config.repo_root | path join "docker" $container.dockerfile)
let image_tag = $"($container_config.tag_prefix)/($container.name):($container_config.version)"
try {
# Check if required binaries exist
let missing_deps = check_container_dependencies $container $container_config
if ($missing_deps | length) > 0 {
return {
container: $container.name
status: "failed"
reason: $"Missing dependencies: ($missing_deps | str join ', ')"
duration: ((date now) - $start_time)
}
}
# Build Docker command
let mut docker_cmd = ["docker", "build"]
# Add build arguments
for arg in $container_config.build_args {
$docker_cmd = ($docker_cmd | append ["--build-arg", $arg])
}
# Add cache options
if not $container_config.cache {
$docker_cmd = ($docker_cmd | append "--no-cache")
}
# Add platform support for multi-arch
if ($container_config.platforms | length) > 1 {
$docker_cmd = ($docker_cmd | append ["--platform", ($container_config.platforms | str join ",")])
} else {
$docker_cmd = ($docker_cmd | append ["--platform", ($container_config.platforms | get 0)])
}
# Add tags
$docker_cmd = ($docker_cmd | append ["-t", $image_tag])
# Add dockerfile and context
$docker_cmd = ($docker_cmd | append ["-f", $dockerfile_path, $container.context])
# Execute build
cd ($container_config.repo_root)
if $container_config.verbose {
log info $"Running: ($docker_cmd | str join ' ')"
}
let build_result = (run-external --redirect-combine $docker_cmd.0 ...$docker_cmd.1.. | complete)
if $build_result.exit_code == 0 {
# Get image size
let image_info = get_image_info $image_tag
log info $"Successfully built container: ($container.name) -> ($image_tag)"
{
container: $container.name
status: "success"
image_tag: $image_tag
image_size: $image_info.size
duration: ((date now) - $start_time)
}
} else {
log error $"Failed to build container ($container.name): ($build_result.stderr)"
{
container: $container.name
status: "failed"
reason: $build_result.stderr
duration: ((date now) - $start_time)
}
}
} catch {|err|
log error $"Failed to build container ($container.name): ($err.msg)"
{
container: $container.name
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Check container dependencies
def check_container_dependencies [
container: record
container_config: record
] -> list {
let mut missing_deps = []
for dep in $container.dependencies {
let binary_pattern = $"($dep)*"
let found_binaries = (find $container.binary_path -name $binary_pattern -type f)
if ($found_binaries | length) == 0 {
$missing_deps = ($missing_deps | append $dep)
}
}
return $missing_deps
}
# Get image information
def get_image_info [image_tag: string] -> record {
try {
let inspect_result = (docker inspect $image_tag | from json | get 0)
{
size: $inspect_result.Size
created: $inspect_result.Created
architecture: $inspect_result.Architecture
}
} catch {
{ size: 0, created: "", architecture: "unknown" }
}
}
# Push containers to registry
def push_containers [
build_results: list
container_config: record
] -> record {
log info $"Pushing containers to registry: ($container_config.output_registry)"
let successful_builds = ($build_results | where status == "success")
let mut push_results = []
for build in $successful_builds {
try {
log info $"Pushing: ($build.image_tag)"
let push_result = (docker push $build.image_tag | complete)
if $push_result.exit_code == 0 {
log info $"Successfully pushed: ($build.image_tag)"
$push_results = ($push_results | append {
image: $build.image_tag
status: "success"
})
} else {
log error $"Failed to push ($build.image_tag): ($push_result.stderr)"
$push_results = ($push_results | append {
image: $build.image_tag
status: "failed"
reason: $push_result.stderr
})
}
} catch {|err|
log error $"Failed to push ($build.image_tag): ($err.msg)"
$push_results = ($push_results | append {
image: $build.image_tag
status: "failed"
reason: $err.msg
})
}
}
{
status: (if ($push_results | where status == "failed" | length) > 0 { "partial" } else { "success" })
total_pushed: ($push_results | where status == "success" | length)
failed_pushes: ($push_results | where status == "failed" | length)
pushed: $push_results
}
}
# Show container information
def "main info" [] {
let docker_available = check_docker_availability
let info = {
docker_available: $docker_available
}
if $docker_available {
let docker_info = try {
docker version --format json | from json
} catch {
{}
}
let images = try {
docker images --filter "reference=provisioning/*" --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" | lines | skip 1
} catch {
[]
}
$info | insert docker_info $docker_info | insert provisioning_images $images
} else {
$info
}
}
# List built containers
def "main list" [--all: bool = false] {
let filter = if $all { "" } else { "reference=provisioning/*" }
let docker_available = check_docker_availability
if not $docker_available {
return { error: "Docker not available" }
}
try {
let images = if $all {
docker images --format json | lines | each { from json }
} else {
docker images --filter "reference=provisioning/*" --format json | lines | each { from json }
}
$images | select Repository Tag Size CreatedAt
} catch {
[]
}
}