491 lines
16 KiB
Plaintext
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"
|
|
}
|
|
}
|
|
}
|