317 lines
13 KiB
Text
317 lines
13 KiB
Text
|
|
#!/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"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|