Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
mdBook Build & Deploy / Build mdBook (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
mdBook Build & Deploy / Documentation Quality Check (push) Has been cancelled
mdBook Build & Deploy / Deploy to GitHub Pages (push) Has been cancelled
mdBook Build & Deploy / Notification (push) Has been cancelled
406 lines
12 KiB
Plaintext
Executable File
406 lines
12 KiB
Plaintext
Executable File
#!/usr/bin/env nu
|
||
# VAPORA Deployment Pipeline Orchestration
|
||
# Handles configuration generation, validation, and deployment to all platforms
|
||
# Version: 1.0.0
|
||
|
||
def main [
|
||
--mode: string = "multiuser"
|
||
--output-dir: string = "dist"
|
||
--target: string = "docker"
|
||
--validate-only: bool = false
|
||
--dry-run: bool = false
|
||
] {
|
||
let timestamp = (date now | format date '%Y%m%d-%H%M%S')
|
||
let log_file = ($output_dir | path join $"deploy-($timestamp).log")
|
||
|
||
# Create output directory
|
||
do {
|
||
mkdir ($output_dir | path expand)
|
||
} | complete | if $in.exit_code != 0 {
|
||
error make {msg: $"Failed to create output directory: ($in.stderr)"}
|
||
}
|
||
|
||
print $"🚀 VAPORA Deployment Pipeline - Mode: ($mode), Target: ($target)"
|
||
print $"📝 Logging to: ($log_file)"
|
||
print ""
|
||
|
||
# Step 1: Generate configuration
|
||
print "Step 1️⃣ - Generating configuration from Nickel..."
|
||
let config_json = (generate-config $mode $output_dir)
|
||
if $config_json == null {
|
||
error make {msg: "Configuration generation failed"}
|
||
}
|
||
print "✓ Configuration generated"
|
||
print ""
|
||
|
||
# Step 2: Validate configuration
|
||
print "Step 2️⃣ - Validating configuration..."
|
||
let validation = (validate-config $config_json)
|
||
if not $validation.valid {
|
||
error make {msg: $"Validation failed: ($validation.errors | str join ', ')"}
|
||
}
|
||
print "✓ Configuration valid"
|
||
print ""
|
||
|
||
# Step 3: Render templates based on target
|
||
print "Step 3️⃣ - Rendering output templates..."
|
||
let rendered = (render-templates $config_json $mode $output_dir $target)
|
||
if not $rendered {
|
||
error make {msg: "Template rendering failed"}
|
||
}
|
||
print "✓ Templates rendered"
|
||
print ""
|
||
|
||
# Step 4: Validate rendered outputs
|
||
print "Step 4️⃣ - Validating rendered outputs..."
|
||
let output_validation = (validate-outputs $output_dir $target)
|
||
if not $output_validation.valid {
|
||
error make {msg: $"Output validation failed: ($output_validation.errors | str join ', ')"}
|
||
}
|
||
print "✓ Outputs validated"
|
||
print ""
|
||
|
||
if $validate_only {
|
||
print "✅ Validation complete (--validate-only specified)"
|
||
return
|
||
}
|
||
|
||
# Step 5: Deploy based on target
|
||
print "Step 5️⃣ - Deploying..."
|
||
match $target {
|
||
"docker" => {
|
||
print "📦 Deploying to Docker Compose..."
|
||
deploy-docker $mode $output_dir $dry_run
|
||
}
|
||
"kubernetes" => {
|
||
print "☸️ Deploying to Kubernetes..."
|
||
deploy-kubernetes $mode $output_dir $dry_run
|
||
}
|
||
"both" => {
|
||
print "📦 Deploying to Docker Compose..."
|
||
deploy-docker $mode $output_dir $dry_run
|
||
print "☸️ Deploying to Kubernetes..."
|
||
deploy-kubernetes $mode $output_dir $dry_run
|
||
}
|
||
_ => {
|
||
error make {msg: $"Unknown target: ($target). Use 'docker', 'kubernetes', or 'both'"}
|
||
}
|
||
}
|
||
|
||
print ""
|
||
print "✅ Deployment complete!"
|
||
print $"Outputs saved to: ($output_dir)"
|
||
}
|
||
|
||
def generate-config [mode: string, output_dir: string] {
|
||
let config_file = $"schemas/platform/configs/vapora-($mode).ncl"
|
||
|
||
if not ($config_file | path exists) {
|
||
error make {msg: $"Config not found: ($config_file)"}
|
||
}
|
||
|
||
let output_path = ($output_dir | path join $"config-($mode).json")
|
||
|
||
let result = do {
|
||
nickel export $config_file
|
||
} | complete
|
||
|
||
if $result.exit_code != 0 {
|
||
error make {msg: $"Nickel export failed: ($result.stderr)"}
|
||
}
|
||
|
||
let json_output = $result.stdout
|
||
|
||
do {
|
||
$json_output | save -f $output_path
|
||
} | complete | if $in.exit_code != 0 {
|
||
error make {msg: $"Failed to save config: ($in.stderr)"}
|
||
}
|
||
|
||
$output_path
|
||
}
|
||
|
||
def validate-config [config_path: string] {
|
||
let config = do {
|
||
open $config_path
|
||
} | complete
|
||
|
||
if $config.exit_code != 0 {
|
||
return {
|
||
valid: false
|
||
errors: [
|
||
$"Failed to parse config: ($config.stderr)"
|
||
]
|
||
}
|
||
}
|
||
|
||
let parsed = ($config.stdout | from json)
|
||
let errors = []
|
||
|
||
# Validate required fields
|
||
let required_fields = [
|
||
"deployment_mode"
|
||
"backend"
|
||
"agents"
|
||
"llm_router"
|
||
"database"
|
||
"frontend"
|
||
]
|
||
|
||
let missing_fields = $required_fields | where { |field|
|
||
not ($parsed | has $field)
|
||
}
|
||
|
||
if ($missing_fields | length) > 0 {
|
||
return {
|
||
valid: false
|
||
errors: [
|
||
$"Missing required fields: ($missing_fields | str join ', ')"
|
||
]
|
||
}
|
||
}
|
||
|
||
# Validate deployment mode
|
||
let valid_modes = ["solo", "multiuser", "enterprise"]
|
||
if not ($valid_modes | any { |mode| $mode == $parsed.deployment_mode }) {
|
||
return {
|
||
valid: false
|
||
errors: [
|
||
$"Invalid deployment_mode: ($parsed.deployment_mode)"
|
||
]
|
||
}
|
||
}
|
||
|
||
{valid: true, errors: []}
|
||
}
|
||
|
||
def render-templates [config_path: string, mode: string, output_dir: string, target: string] {
|
||
let config = (open $config_path)
|
||
|
||
# Render TOML
|
||
print " → Rendering TOML configuration..."
|
||
let toml_result = do {
|
||
jinja2 schemas/platform/templates/configs/vapora.toml.j2 $config_path
|
||
} | complete
|
||
|
||
if $toml_result.exit_code != 0 {
|
||
print $" ✗ TOML rendering failed: ($toml_result.stderr)"
|
||
return false
|
||
}
|
||
|
||
do {
|
||
$toml_result.stdout | save -f ($output_dir | path join $"vapora-($mode).toml")
|
||
} | complete | if $in.exit_code != 0 {
|
||
return false
|
||
}
|
||
print " ✓ TOML"
|
||
|
||
# Render YAML
|
||
print " → Rendering YAML configuration..."
|
||
let yaml_result = do {
|
||
jinja2 schemas/platform/templates/configs/vapora.yaml.j2 $config_path
|
||
} | complete
|
||
|
||
if $yaml_result.exit_code != 0 {
|
||
print $" ✗ YAML rendering failed: ($yaml_result.stderr)"
|
||
return false
|
||
}
|
||
|
||
do {
|
||
$yaml_result.stdout | save -f ($output_dir | path join $"vapora-($mode).yaml")
|
||
} | complete | if $in.exit_code != 0 {
|
||
return false
|
||
}
|
||
print " ✓ YAML"
|
||
|
||
# Render Kubernetes templates if needed
|
||
if ($target == "kubernetes") or ($target == "both") {
|
||
print " → Rendering Kubernetes ConfigMap..."
|
||
let cm_result = do {
|
||
jinja2 schemas/platform/templates/kubernetes/configmap.yaml.j2 $config_path
|
||
} | complete
|
||
|
||
if $cm_result.exit_code != 0 {
|
||
print $" ✗ ConfigMap rendering failed: ($cm_result.stderr)"
|
||
return false
|
||
}
|
||
|
||
do {
|
||
$cm_result.stdout | save -f ($output_dir | path join "configmap.yaml")
|
||
} | complete | if $in.exit_code != 0 {
|
||
return false
|
||
}
|
||
print " ✓ ConfigMap"
|
||
|
||
print " → Rendering Kubernetes Deployment..."
|
||
let deploy_result = do {
|
||
jinja2 schemas/platform/templates/kubernetes/deployment.yaml.j2 $config_path
|
||
} | complete
|
||
|
||
if $deploy_result.exit_code != 0 {
|
||
print $" ✗ Deployment rendering failed: ($deploy_result.stderr)"
|
||
return false
|
||
}
|
||
|
||
do {
|
||
$deploy_result.stdout | save -f ($output_dir | path join "deployment.yaml")
|
||
} | complete | if $in.exit_code != 0 {
|
||
return false
|
||
}
|
||
print " ✓ Deployment"
|
||
}
|
||
|
||
# Render Docker Compose if needed
|
||
if ($target == "docker") or ($target == "both") {
|
||
print " → Rendering Docker Compose..."
|
||
let dc_result = do {
|
||
jinja2 schemas/platform/templates/docker-compose/docker-compose.yaml.j2 $config_path
|
||
} | complete
|
||
|
||
if $dc_result.exit_code != 0 {
|
||
print $" ✗ Docker Compose rendering failed: ($dc_result.stderr)"
|
||
return false
|
||
}
|
||
|
||
do {
|
||
$dc_result.stdout | save -f ($output_dir | path join "docker-compose.yml")
|
||
} | complete | if $in.exit_code != 0 {
|
||
return false
|
||
}
|
||
print " ✓ Docker Compose"
|
||
}
|
||
|
||
true
|
||
}
|
||
|
||
def validate-outputs [output_dir: string, target: string] {
|
||
let errors = []
|
||
|
||
# Validate YAML files
|
||
let yaml_files = if ($target == "docker") {
|
||
["vapora-solo.yaml", "vapora-multiuser.yaml", "vapora-enterprise.yaml"]
|
||
} else if ($target == "kubernetes") {
|
||
["configmap.yaml", "deployment.yaml"]
|
||
} else {
|
||
["vapora-solo.yaml", "configmap.yaml", "deployment.yaml"]
|
||
}
|
||
|
||
$yaml_files | each { |file|
|
||
let path = ($output_dir | path join $file)
|
||
if not ($path | path exists) {
|
||
$errors | append $"Missing file: ($file)"
|
||
} else {
|
||
let validate = do {
|
||
yq eval '.' $path
|
||
} | complete
|
||
|
||
if $validate.exit_code != 0 {
|
||
$errors | append $"Invalid YAML in ($file): ($validate.stderr)"
|
||
}
|
||
}
|
||
}
|
||
|
||
{
|
||
valid: ($errors | length) == 0
|
||
errors: $errors
|
||
}
|
||
}
|
||
|
||
def deploy-docker [mode: string, output_dir: string, dry_run: bool] {
|
||
let compose_file = ($output_dir | path join "docker-compose.yml")
|
||
|
||
if not ($compose_file | path exists) {
|
||
error make {msg: "Docker Compose file not found"}
|
||
}
|
||
|
||
print " 📍 Docker Compose file: $compose_file"
|
||
|
||
if $dry_run {
|
||
print " 🔍 [DRY RUN] Would execute: docker compose -f $compose_file up -d"
|
||
return
|
||
}
|
||
|
||
print " 🚀 Starting Docker Compose services..."
|
||
let result = do {
|
||
docker compose -f $compose_file up -d
|
||
} | complete
|
||
|
||
if $result.exit_code != 0 {
|
||
error make {msg: $"Docker Compose failed: ($result.stderr)"}
|
||
}
|
||
|
||
print " ✓ Services started"
|
||
print ""
|
||
print " 📊 Running services:"
|
||
do {
|
||
docker compose -f $compose_file ps
|
||
} | complete | if $in.exit_code == 0 {
|
||
print $in.stdout
|
||
}
|
||
}
|
||
|
||
def deploy-kubernetes [mode: string, output_dir: string, dry_run: bool] {
|
||
let configmap_file = ($output_dir | path join "configmap.yaml")
|
||
let deployment_file = ($output_dir | path join "deployment.yaml")
|
||
|
||
if not ($configmap_file | path exists) {
|
||
error make {msg: "Kubernetes ConfigMap not found"}
|
||
}
|
||
|
||
if not ($deployment_file | path exists) {
|
||
error make {msg: "Kubernetes Deployment not found"}
|
||
}
|
||
|
||
# Ensure namespace exists
|
||
if $dry_run {
|
||
print " 🔍 [DRY RUN] Would create namespace: vapora"
|
||
} else {
|
||
do {
|
||
kubectl create namespace vapora --dry-run=client -o yaml | kubectl apply -f -
|
||
} | complete | if $in.exit_code != 0 {
|
||
print " ⚠️ Namespace creation (may already exist)"
|
||
}
|
||
}
|
||
|
||
# Apply ConfigMap
|
||
print " 📍 Applying ConfigMap..."
|
||
if $dry_run {
|
||
print " 🔍 [DRY RUN] Would apply: ($configmap_file)"
|
||
} else {
|
||
let cm_result = do {
|
||
kubectl apply -f $configmap_file
|
||
} | complete
|
||
|
||
if $cm_result.exit_code != 0 {
|
||
error make {msg: $"ConfigMap deployment failed: ($cm_result.stderr)"}
|
||
}
|
||
print " ✓ ConfigMap applied"
|
||
}
|
||
|
||
# Apply Deployments
|
||
print " 📍 Applying Deployments..."
|
||
if $dry_run {
|
||
print " 🔍 [DRY RUN] Would apply: ($deployment_file)"
|
||
} else {
|
||
let deploy_result = do {
|
||
kubectl apply -f $deployment_file
|
||
} | complete
|
||
|
||
if $deploy_result.exit_code != 0 {
|
||
error make {msg: $"Deployment failed: ($deploy_result.stderr)"}
|
||
}
|
||
print " ✓ Deployments applied"
|
||
}
|
||
|
||
print ""
|
||
print " 📊 Deployment status:"
|
||
do {
|
||
kubectl get deployment -n vapora -o wide
|
||
} | complete | if $in.exit_code == 0 {
|
||
print $in.stdout
|
||
}
|
||
}
|
||
|
||
# Run main function
|
||
main
|