2025-10-07 10:59:52 +01:00

491 lines
16 KiB
Plaintext

#!/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<record> {
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"
}
}
}