#!/usr/bin/env nu # Helper Functions for Provisioning Platform Deployment # # Provides common utilities for configuration management, # validation, health checks, and rollback operations. # Check deployment prerequisites # # Validates that all required tools and dependencies are available # before attempting deployment. # # @returns: Validation result record export def check-prerequisites []: nothing -> record { print "🔍 Checking prerequisites..." let checks = [ {name: "nushell", cmd: "nu", min_version: "0.107.0"} {name: "docker", cmd: "docker", min_version: "20.10.0"} {name: "git", cmd: "git", min_version: "2.30.0"} ] mut failures = [] for check in $checks { let available = (which $check.cmd | is-not-empty) if not $available { $failures = ($failures | append { tool: $check.name reason: "Not found in PATH" }) } } if ($failures | is-empty) { print "✅ All prerequisites satisfied" {success: true, failures: []} } else { print "❌ Missing prerequisites:" for failure in $failures { print $" - ($failure.tool): ($failure.reason)" } { success: false error: "Missing required tools" failures: $failures } } } # Validate deployment parameters # # @param platform: Target platform name # @param mode: Deployment mode name # @returns: Validation result record export def validate-deployment-params [platform: string, mode: string]: nothing -> record { let valid_platforms = ["docker", "podman", "kubernetes", "orbstack"] let valid_modes = ["solo", "multi-user", "cicd", "enterprise"] if $platform not-in $valid_platforms { return { success: false error: $"Invalid platform '($platform)'. Must be one of: ($valid_platforms | str join ', ')" } } if $mode not-in $valid_modes { return { success: false error: $"Invalid mode '($mode)'. Must be one of: ($valid_modes | str join ', ')" } } {success: true} } # Build deployment configuration # # @param params: Configuration parameters record # @returns: Complete deployment configuration export def build-deployment-config [params: record]: nothing -> record { # Get default services for mode let default_services = get-default-services $params.mode # Merge with user-specified services if provided let services = if ($params.services | is-empty) { $default_services } else { # Filter to only user-specified services $default_services | where {|svc| $svc.name in $params.services or $svc.required } } { platform: $params.platform mode: $params.mode domain: $params.domain services: $services auto_generate_secrets: ($params.auto_generate_secrets? | default true) } } # Get default services for deployment mode # # @param mode: Deployment mode (solo, multi-user, cicd, enterprise) # @returns: List of service configuration records def get-default-services [mode: string]: nothing -> list { let base_services = [ {name: "orchestrator", description: "Task coordination", port: 8080, enabled: true, required: true} {name: "control-center", description: "Web UI", port: 8081, enabled: true, required: true} {name: "coredns", description: "DNS service", port: 5353, enabled: true, required: true} ] let mode_services = match $mode { "solo" => [ {name: "oci-registry", description: "OCI Registry (Zot)", port: 5000, enabled: false, required: false} {name: "extension-registry", description: "Extension hosting", port: 8082, enabled: false, required: false} {name: "mcp-server", description: "Model Context Protocol", port: 8084, enabled: false, required: false} {name: "api-gateway", description: "REST API access", port: 8085, enabled: false, required: false} ] "multi-user" => [ {name: "gitea", description: "Git server", port: 3000, enabled: true, required: true} {name: "postgres", description: "Shared database", port: 5432, enabled: true, required: true} {name: "oci-registry", description: "OCI Registry (Zot)", port: 5000, enabled: false, required: false} ] "cicd" => [ {name: "gitea", description: "Git server", port: 3000, enabled: true, required: true} {name: "postgres", description: "Shared database", port: 5432, enabled: true, required: true} {name: "api-server", description: "REST API", port: 8083, enabled: true, required: true} {name: "oci-registry", description: "OCI Registry (Zot)", port: 5000, enabled: false, required: false} ] "enterprise" => [ {name: "gitea", description: "Git server", port: 3000, enabled: true, required: true} {name: "postgres", description: "Shared database", port: 5432, enabled: true, required: true} {name: "api-server", description: "REST API", port: 8083, enabled: true, required: true} {name: "harbor", description: "Harbor OCI Registry", port: 5000, enabled: true, required: true} {name: "kms", description: "Cosmian KMS", port: 9998, enabled: true, required: true} {name: "prometheus", description: "Metrics", port: 9090, enabled: true, required: true} {name: "grafana", description: "Dashboards", port: 3001, enabled: true, required: true} {name: "loki", description: "Log aggregation", port: 3100, enabled: true, required: true} {name: "nginx", description: "Reverse proxy", port: 80, enabled: true, required: true} ] _ => [] } $base_services | append $mode_services } # Save deployment configuration to TOML file # # @param config: Deployment configuration record # @returns: Path to saved configuration file export def save-deployment-config [config: record]: nothing -> path { let timestamp = (date now | format date "%Y%m%d_%H%M%S") let config_dir = $env.PWD | path join "configs" # Create configs directory if it doesn't exist mkdir $config_dir let config_file = $config_dir | path join $"deployment_($timestamp).toml" # Convert to TOML format let toml_content = $config | to toml $toml_content | save -f $config_file $config_file } # Load deployment configuration from TOML file # # @param config_path: Path to TOML configuration file # @returns: Deployment configuration record export def load-config-from-file [config_path: path]: nothing -> record { if not ($config_path | path exists) { error make {msg: $"Config file not found: ($config_path)"} } try { open $config_path | from toml } catch {|err| error make { msg: $"Failed to parse config file: ($config_path)" label: {text: $err.msg} } } } # Validate deployment configuration # # @param config: Deployment configuration record # @param strict: Enable strict validation (default: false) # @returns: Validation result record export def validate-deployment-config [ config: record --strict ]: nothing -> record { # Required fields let required_fields = ["platform", "mode", "domain", "services"] mut errors = [] # Check required fields for field in $required_fields { if $field not-in ($config | columns) { $errors = ($errors | append $"Missing required field: ($field)") } } # Validate platform let valid_platforms = ["docker", "podman", "kubernetes", "orbstack"] if "platform" in ($config | columns) and ($config.platform not-in $valid_platforms) { $errors = ($errors | append $"Invalid platform: ($config.platform)") } # Validate mode let valid_modes = ["solo", "multi-user", "cicd", "enterprise"] if "mode" in ($config | columns) and ($config.mode not-in $valid_modes) { $errors = ($errors | append $"Invalid mode: ($config.mode)") } # Validate services if "services" in ($config | columns) { if ($config.services | is-empty) { $errors = ($errors | append "No services configured") } # In strict mode, validate required services if $strict { let required_services = $config.services | where required | get name let enabled_services = $config.services | where enabled | get name for req_svc in $required_services { if $req_svc not-in $enabled_services { $errors = ($errors | append $"Required service not enabled: ($req_svc)") } } } } if ($errors | is-empty) { {success: true} } else { { success: false error: ($errors | str join "; ") errors: $errors } } } # Confirm deployment with user # # @param config: Deployment configuration record # @returns: Boolean confirmation result export def confirm-deployment [config: record]: nothing -> bool { print " 📋 Deployment Summary ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ " print $"Platform: ($config.platform)" print $"Mode: ($config.mode)" print $"Domain: ($config.domain)" print "" print "Services:" for svc in $config.services { let status = if $svc.enabled { "✅" } else { "⬜" } let req_mark = if $svc.required { "(required)" } else { "" } print $" ($status) ($svc.name):($svc.port) - ($svc.description) ($req_mark)" } print " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ " let response = (input "Proceed with deployment? [y/N]: ") $response =~ "(?i)^y(es)?$" } # Check deployment health # # @param config: Deployment configuration record # @returns: Health check result record export def check-deployment-health [config: record]: nothing -> record { print "🏥 Running health checks..." let enabled_services = $config.services | where enabled let failed_services = ($enabled_services | each {|svc| let health_url = $"http://($config.domain):($svc.port)/health" print $" Checking ($svc.name)..." let result = try { http get $health_url --max-time 5sec | get status? | default "failed" } catch { "failed" } if $result != "ok" { $svc.name } else { null } } | compact) if ($failed_services | is-empty) { print "✅ All health checks passed" {success: true} } else { print $"❌ Health checks failed for: ($failed_services | str join ', ')" { success: false error: $"Health checks failed for: ($failed_services | str join ', ')" failed_services: $failed_services } } } # Rollback deployment # # @param config: Deployment configuration record # @returns: Rollback result record export def rollback-deployment [config: record]: nothing -> record { print "🔄 Rolling back deployment..." match $config.platform { "docker" => { rollback-docker $config } "podman" => { rollback-podman $config } "kubernetes" => { rollback-kubernetes $config } "orbstack" => { rollback-orbstack $config } _ => { error make {msg: $"Unsupported platform for rollback: ($config.platform)"} } } } # Rollback Docker deployment def rollback-docker [config: record]: nothing -> record { let compose_base = get-platform-path "docker-compose" let base_file = $compose_base | path join "docker-compose.yaml" try { ^docker-compose -f $base_file down --volumes print "✅ Docker deployment rolled back successfully" {success: true, platform: "docker"} } catch {|err| {success: false, platform: "docker", error: $err.msg} } } # Rollback Podman deployment def rollback-podman [config: record]: nothing -> record { let compose_base = get-platform-path "docker-compose" let base_file = $compose_base | path join "docker-compose.yaml" try { ^podman-compose -f $base_file down --volumes print "✅ Podman deployment rolled back successfully" {success: true, platform: "podman"} } catch {|err| {success: false, platform: "podman", error: $err.msg} } } # Rollback Kubernetes deployment def rollback-kubernetes [config: record]: nothing -> record { let namespace = "provisioning-platform" try { ^kubectl delete namespace $namespace print "✅ Kubernetes deployment rolled back successfully" {success: true, platform: "kubernetes"} } catch {|err| {success: false, platform: "kubernetes", error: $err.msg} } } # Rollback OrbStack deployment def rollback-orbstack [config: record]: nothing -> record { # OrbStack uses Docker Compose rollback-docker $config | update platform "orbstack" } # Check platform availability # # @param platform: Platform name to check # @returns: Platform availability record export def check-platform-availability [platform: string]: nothing -> record { match $platform { "docker" => { let available = (which docker | is-not-empty) {platform: "docker", available: $available} } "podman" => { let available = (which podman | is-not-empty) {platform: "podman", available: $available} } "kubernetes" => { let available = (which kubectl | is-not-empty) {platform: "kubernetes", available: $available} } "orbstack" => { let available = (which orb | is-not-empty) {platform: "orbstack", available: $available} } _ => { {platform: $platform, available: false} } } } # Generate secrets for deployment # # @param config: Deployment configuration record # @returns: Generated secrets record export def generate-secrets [config: record]: nothing -> record { print "🔐 Generating secrets..." { jwt_secret: (random chars -l 64) postgres_password: (random chars -l 32) admin_password: (random chars -l 16) api_key: (random chars -l 48) encryption_key: (random chars -l 32) } } # Create deployment manifests # # @param config: Deployment configuration record # @param secrets: Generated secrets record # @returns: Path to manifests directory export def create-deployment-manifests [config: record, secrets: record]: nothing -> path { let manifests_dir = $env.PWD | path join "manifests" mkdir $manifests_dir # Save secrets to file (in production, use proper secret management) let secrets_file = $manifests_dir | path join "secrets.toml" $secrets | to toml | save -f $secrets_file print $"📝 Secrets saved to: ($secrets_file)" $manifests_dir } # Get platform base path # # @param subpath: Optional subpath # @returns: Full platform path def get-platform-path [subpath: string = ""]: nothing -> path { let base_path = $env.PWD | path dirname | path dirname if $subpath == "" { $base_path } else { $base_path | path join $subpath } } # Get installer binary path # # @returns: Path to installer binary export def get-installer-path []: nothing -> path { let installer_dir = $env.PWD | path dirname let installer_name = if $nu.os-info.name == "windows" { "provisioning-installer.exe" } else { "provisioning-installer" } # Check target/release first, then target/debug let release_path = $installer_dir | path join "target" "release" $installer_name let debug_path = $installer_dir | path join "target" "debug" $installer_name if ($release_path | path exists) { $release_path } else if ($debug_path | path exists) { $debug_path } else { error make { msg: "Installer binary not found" help: "Build with: cargo build --release" } } }