prvng_core/nulib/workflows/management.nu

412 lines
15 KiB
Text

use std
# Selective imports replacing fat-path (ADR-025 Phase 4).
use lib_provisioning/platform/target.nu [detect-platform-mode]
use lib_provisioning/utils/clean.nu [cleanup]
use lib_provisioning/utils/interface.nu [_print]
use lib_provisioning/utils/service-check.nu [verify-service-or-fail]
use lib_provisioning/utils/simple_validation.nu [check-command]
# Comprehensive workflow management commands
# Get orchestrator endpoint from platform configuration or use provided default
def get-orchestrator-url [--orchestrator: string = ""] {
if ($orchestrator | is-not-empty) {
return $orchestrator
}
# Try to get from environment variable first
if ($env.PROVISIONING_ORCHESTRATOR_URL? | is-not-empty) {
return $env.PROVISIONING_ORCHESTRATOR_URL
}
# Skip slow platform discovery - just use localhost default
# (Platform discovery via nickel export is too slow for CLI responsiveness)
# Users can set PROVISIONING_ORCHESTRATOR_URL to override
"http://localhost:9011"
}
# Detect if orchestrator URL is local (for plugin usage)
def use-local-plugin [orchestrator_url: string] {
# Check if it's a local endpoint
(detect-platform-mode $orchestrator_url) == "local"
}
# List all active workflows - interactive loop
export def "workflow list" [
limit?: int # Number of recent tasks to show (default: 10)
--orchestrator: string = "" # Orchestrator URL (optional, uses platform config if not provided)
--status: string # Filter by status: Pending, Running, Completed, Failed, Cancelled
] {
let orch_url = (get-orchestrator-url --orchestrator=$orchestrator)
let task_limit = ($limit | default 10)
let avail = (verify-service-or-fail $orch_url "Orchestrator"
--check-command "provisioning platform status"
--check-alias "prvng plat st"
--start-command "provisioning platform start orchestrator"
--start-alias "prvng plat start orchestrator"
)
if $avail.status == "error" { return }
mut continue_browsing = true
while $continue_browsing {
# Always use HTTP API - plugin doesn't return tasks reliably
let response = (http get $"($orch_url)/tasks")
if not ($response | get success) {
_print $"Error: (($response | get error))"
break
}
let tasks = ($response | get data)
let filtered_tasks = if ($status | is-not-empty) {
$tasks | where status == $status
} else {
$tasks
}
# Limit to specified number of recent tasks
let limited_tasks = (
if ($filtered_tasks | length) > $task_limit {
$filtered_tasks | reverse | first $task_limit | reverse
} else {
$filtered_tasks
}
)
# Format tasks as numbered table for clean display
mut formatted = []
mut row_num = 1
for task in $limited_tasks {
let status_display = if $task.status == "Failed" {
((ansi red) + $task.status + (ansi reset))
} else {
$task.status
}
$formatted = ($formatted | append {
"#": $row_num,
"Task ID": $task.id,
"Status": $status_display,
"Completed At": ($task.completed_at | default "N/A")
})
$row_num = ($row_num + 1)
}
# Display as native Nushell table without index column
print ($formatted | table --index false)
_print ""
_print "0 = Exit, or enter task number:"
_print ""
# Get task number from user
let task_num_str = (typedialog text "Task number:" --default "0")
# Simple validation - just try to convert
let task_num = ($task_num_str | str trim | into int)
if $task_num == 0 {
$continue_browsing = false
} else if $task_num < 1 or $task_num > ($limited_tasks | length) {
_print $"❌ Invalid task number. Choose 1-($limited_tasks | length), or 0 to exit"
_print ""
} else {
let task_index = ($task_num - 1)
let selected_task = ($limited_tasks | get $task_index)
let task_id = $selected_task.id
_print ""
_print $"📊 Status for: ($task_id)"
_print "════════════════════════════════════════════════"
workflow status $task_id --orchestrator $orch_url
_print ""
_print ""
_print "─────────────────────────────────────────────────"
let continue_choice = (typedialog select "¿Qué deseas hacer?" ["Continuar" "Salir"])
if $continue_choice == "Salir" {
$continue_browsing = false
}
}
}
}
# Get detailed workflow status
export def "workflow status" [
task_id: string # Task ID to check
--orchestrator: string = "" # Orchestrator URL (optional, uses platform config if not provided)
] {
let orch_url = (get-orchestrator-url --orchestrator=$orchestrator)
let avail = (verify-service-or-fail $orch_url "Orchestrator"
--check-command "provisioning platform status"
--check-alias "prvng plat st"
--start-command "provisioning platform start orchestrator"
--start-alias "prvng plat start orchestrator"
)
if $avail.status == "error" { return { error: "Orchestrator not available" } }
# Always use HTTP API - plugin doesn't return tasks reliably
let response = (http get $"($orch_url)/tasks/($task_id)")
if not ($response | get success) {
return { error: ($response | get error) }
}
let task = ($response | get data)
# Convert arrays to strings for display, then transpose to vertical format
let displayable = {
id: $task.id,
name: $task.name,
command: $task.command,
args: ($task.args | str join "\n "),
dependencies: ($task.dependencies | str join "\n "),
status: $task.status,
created_at: $task.created_at,
started_at: ($task.started_at | default "N/A"),
completed_at: ($task.completed_at | default "N/A"),
output: ($task.output | default ""),
error: ($task.error | default "")
}
# Convert to vertical key-value table with proper headers
print ($displayable | transpose | each {|row| {Field: $row.column0, Value: $row.column1}} | table -i false)
}
# Monitor workflow progress in real-time
export def "workflow monitor" [
task_id: string # Task ID to monitor
--orchestrator: string = "" # Orchestrator URL (optional, uses platform config if not provided)
] {
let orch_url = (get-orchestrator-url --orchestrator=$orchestrator)
_print $"Monitoring workflow: ($task_id)"
_print "Press Ctrl+C to stop monitoring"
_print ""
while true {
let task = (workflow status $task_id --orchestrator $orch_url)
let err_result = (do { $task | get error } | complete)
let task_error = if $err_result.exit_code == 0 { $err_result.stdout } else { null }
if ($task_error | is-not-empty) {
_print $"❌ Error getting task status: ($task_error)"
break
}
let status = ($task | get status)
let created = ($task | get created_at)
let start_result = (do { $task | get started_at } | complete)
let started = if $start_result.exit_code == 0 { $start_result.stdout } else { "Not started" }
let comp_result = (do { $task | get completed_at } | complete)
let completed = if $comp_result.exit_code == 0 { $comp_result.stdout } else { "Not completed" }
clear
_print $"📊 Workflow Status: ($task_id)"
_print $"═══════════════════════════════════════════════════════════════"
_print $"Name: (($task | get name))"
_print $"Status: ($status)"
_print $"Created: ($created)"
_print $"Started: ($started)"
_print $"Completed: ($completed)"
_print ""
match $status {
"Completed" => {
_print "✅ Workflow completed successfully!"
let out_result = (do { $task | get output } | complete)
let task_output = if $out_result.exit_code == 0 { $out_result.stdout } else { null }
if ($task_output | is-not-empty) {
_print ""
_print "Output:"
_print "───────"
_print $task_output
}
break
},
"Failed" => {
_print "❌ Workflow failed!"
let err_result = (do { $task | get error } | complete)
let task_error = if $err_result.exit_code == 0 { $err_result.stdout } else { null }
if ($task_error | is-not-empty) {
_print ""
_print "Error:"
_print "──────"
_print $task_error
}
break
},
"Running" => {
_print "🔄 Workflow is running..."
},
"Cancelled" => {
_print "🚫 Workflow was cancelled"
break
},
_ => {
_print $"⏳ Status: ($status)"
}
}
_print ""
_print "Refreshing in 3 seconds... (Ctrl+C to stop)"
sleep 3sec
}
}
# Show workflow statistics
export def "workflow stats" [
--orchestrator: string = "" # Orchestrator URL (optional, uses platform config if not provided)
] {
let orch_url = (get-orchestrator-url --orchestrator=$orchestrator)
let avail = (verify-service-or-fail $orch_url "Orchestrator"
--check-command "provisioning platform status"
--check-alias "prvng plat st"
--start-command "provisioning platform start orchestrator"
--start-alias "prvng plat start orchestrator"
)
if $avail.status == "error" { return }
let response = (http get $"($orch_url)/tasks")
if not ($response | get success) {
_print $"Error: (($response | get error))"
return
}
let tasks = ($response | get data)
let total = ($tasks | length)
let completed = ($tasks | where status == "Completed" | length)
let failed = ($tasks | where status == "Failed" | length)
let running = ($tasks | where status == "Running" | length)
let pending = ($tasks | where status == "Pending" | length)
let cancelled = ($tasks | where status == "Cancelled" | length)
{
total: $total,
completed: $completed,
failed: $failed,
running: $running,
pending: $pending,
cancelled: $cancelled,
success_rate: (if $total > 0 { ($completed / $total * 100) | math round | into int } else { 0 })
}
}
# Clean up old completed workflows
export def "workflow cleanup" [
--orchestrator: string = "" # Orchestrator URL (optional, uses platform config if not provided)
--days: int = 7 # Remove workflows older than this many days
--dry-run # Show what would be removed without actually removing
] {
let orch_url = (get-orchestrator-url --orchestrator=$orchestrator)
_print $"Cleaning up workflows older than ($days) days..."
let cutoff_date = ((date now) - ($days * 86400 | into duration --unit sec))
let tasks = (workflow list --orchestrator $orch_url)
let old_tasks = ($tasks | where {|task|
let task_date = ($task.completed_at | into datetime)
$task_date < $cutoff_date and ($task.status in ["Completed", "Failed", "Cancelled"])
})
if ($old_tasks | length) == 0 {
_print "No old workflows found for cleanup."
return
}
_print $"Found ($old_tasks | length) workflows for cleanup:"
$old_tasks | select id name status completed_at | table
if $dry_run {
_print ""
_print "Dry run mode - no workflows were actually removed."
_print "Run without --dry-run to perform the cleanup."
} else {
_print ""
_print "Note: Actual cleanup would require orchestrator API support for deletion."
_print "This is a placeholder for future implementation."
}
}
# Orchestrator health and info
export def "workflow orchestrator" [
--orchestrator: string = "http://localhost:9011" # Orchestrator URL
] {
let avail = (verify-service-or-fail $orchestrator "Orchestrator"
--check-command "provisioning platform status"
--check-alias "prvng plat st"
--start-command "provisioning platform start orchestrator"
--start-alias "prvng plat start orchestrator"
)
if $avail.status == "error" {
return {
status: "unreachable",
message: "Cannot connect to orchestrator",
orchestrator_url: $orchestrator
}
}
# Always use HTTP API for consistency
let health_response = (http get $"($orchestrator)/health")
let stats = (workflow stats --orchestrator $orchestrator)
if not ($health_response | get success) {
return {
status: "unreachable",
message: "Cannot connect to orchestrator",
orchestrator_url: $orchestrator
}
}
{
status: "healthy",
message: ($health_response | get data),
orchestrator_url: $orchestrator,
workflow_stats: $stats
}
}
# Interactive workflow browser - alias to list
export def "workflow browse" [
limit?: int # Number of recent tasks to show (default: 10)
--orchestrator: string = "" # Orchestrator URL (optional)
] {
if ($limit | is-not-empty) {
workflow list $limit --orchestrator $orchestrator
} else {
workflow list --orchestrator $orchestrator
}
}
# Submit workflows with dependency management
export def "workflow submit" [
workflow_type: string # server, taskserv, cluster
operation: string # create, delete, etc.
target: string # specific target (server name, taskserv name, etc.)
infra?: string # Infrastructure target
settings?: string # Settings file path
--depends-on: list<string> = [] # Task IDs this workflow depends on
--check (-c) # Check mode only
--wait (-w) # Wait for completion
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
] {
match $workflow_type {
"server" => {
_print "Server workflow creation not yet implemented"
},
"taskserv" => {
_print "Taskserv workflow not yet implemented"
},
"cluster" => {
_print "Cluster workflow not yet implemented"
},
_ => {
_print $"Unknown workflow type: ($workflow_type)"
}
}
}