provisioning-core/nulib/domain/workspace/helpers.nu

317 lines
13 KiB
Text
Raw Permalink Normal View History

#!/usr/bin/env nu
# Helper Functions for Provisioning Platform Deployment
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 }
}
}
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}
}
export def build-deployment-config [params: record]: nothing -> record {
let default_services = get-default-services $params.mode
let services = if ($params.services | is-empty) {
$default_services
} else {
$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)
}
}
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: "catalog-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
}
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"
mkdir $config_dir
let config_file = $config_dir | path join $"deployment_($timestamp).toml"
$config | to toml | save -f $config_file
$config_file
}
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)"}
}
open $config_path | from toml
}
export def validate-deployment-config [
config: record
--strict
]: nothing -> record {
let required_fields = ["platform", "mode", "domain", "services"]
mut errors = []
for field in $required_fields {
if $field not-in ($config | columns) {
$errors = ($errors | append $"Missing required field: ($field)")
}
}
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)")
}
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)")
}
if "services" in ($config | columns) {
if ($config.services | is-empty) {
$errors = ($errors | append "No services configured")
}
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 }
}
}
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)?$"
}
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 = (http get $health_url --max-time 5sec | get status? | default "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 }
}
}
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)"} }
}
}
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"
let result = (do --ignore-errors { ^docker-compose -f $base_file down --volumes } | complete)
if $result.exit_code == 0 {
print "✅ Docker deployment rolled back successfully"
{success: true, platform: "docker"}
} else {
{success: false, platform: "docker", error: $result.stderr}
}
}
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"
let result = (do --ignore-errors { ^podman-compose -f $base_file down --volumes } | complete)
if $result.exit_code == 0 {
print "✅ Podman deployment rolled back successfully"
{success: true, platform: "podman"}
} else {
{success: false, platform: "podman", error: $result.stderr}
}
}
def rollback-kubernetes [config: record]: nothing -> record {
let namespace = "provisioning-platform"
let result = (do --ignore-errors { ^kubectl delete namespace $namespace } | complete)
if $result.exit_code == 0 {
print "✅ Kubernetes deployment rolled back successfully"
{success: true, platform: "kubernetes"}
} else {
{success: false, platform: "kubernetes", error: $result.stderr}
}
}
def rollback-orbstack [config: record]: nothing -> record {
rollback-docker $config | update platform "orbstack"
}
export def check-platform-availability [platform: string]: nothing -> record {
match $platform {
"docker" => { {platform: "docker", available: (which docker | is-not-empty)} }
"podman" => { {platform: "podman", available: (which podman | is-not-empty)} }
"kubernetes" => { {platform: "kubernetes", available: (which kubectl | is-not-empty)} }
"orbstack" => { {platform: "orbstack", available: (which orb | is-not-empty)} }
_ => { {platform: $platform, available: false} }
}
}
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)
}
}
export def create-deployment-manifests [config: record, secrets: record]: nothing -> path {
let manifests_dir = $env.PWD | path join "manifests"
mkdir $manifests_dir
let secrets_file = $manifests_dir | path join "secrets.toml"
$secrets | to toml | save -f $secrets_file
print $"📝 Secrets saved to: ($secrets_file)"
$manifests_dir
}
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 }
}
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"
}
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"
}
}
}