# Module: Service Availability Check Utilities # Purpose: Reusable patterns for checking service availability before making requests # Guidelines: Follows .claude/guidelines/provisioning.md - Service Check Pattern # # Features: # - Check individual service availability # - Check all essential services (cascade failure detection) # - Check external dependencies (database, OCI registries, Git sources) # - Clean error messages with short aliases # - No stack traces (uses print + return, not error make) use ../platform/target.nu * use ../platform/health.nu * use ../platform/service-manager.nu * # Check external services locally (avoiding startup.nu import due to syntax errors in that file) def check-external-services-internal [external_config: record]: nothing -> list { let db = ($external_config.database? | default {backend: "filesystem"}) let oci_registries = ($external_config.oci_registries? | default []) let git_sources = ($external_config.git_sources? | default []) mut results = [] # Check database if ($db.backend? | default "filesystem") == "filesystem" { let path = ($db.path? | default "~/.provisioning/data") let expanded_path = if ($path | str starts-with "~") { $"($env.HOME)/($path | str substring 1..)" } else { $path } if ($expanded_path | path exists) { $results = ($results | append { service: "database" backend: $db.backend status: "✓" message: $"Filesystem storage available at ($expanded_path)" }) } else { $results = ($results | append { service: "database" backend: $db.backend status: "✗" message: $"Path does not exist: ($expanded_path)" }) } } $results } # Check if a service is available by verifying port is listening # Returns: { available: bool, port: string, message: string } export def check-service-available [ service_url: string # Service URL (e.g., "http://localhost:9011") service_name: string # Human-readable service name (e.g., "Orchestrator") ]: nothing -> record { # Extract port from URL let parsed = ($service_url | parse "http://{host}:{port}") let port = if ($parsed | is-empty) { "unknown" } else { ($parsed | get port.0) } # Check if port is listening (macOS: lsof, Linux: netstat fallback) # Using do { } | complete pattern per Nushell guidelines (NO try-catch) let port_check = (do { ^lsof -i :($port) -P -n | ^grep LISTEN } | complete) let is_listening = ($port_check.exit_code == 0) if $is_listening { { available: true, port: $port, message: $"($service_name) is available on port ($port)" } } else { { available: false, port: $port, message: $"($service_name) is not available on port ($port)" } } } # Check external services (database, OCI registries, Git sources) # Returns list of external service statuses export def check-external-services-status []: nothing -> list { let external_services = (get-external-services) if ($external_services | is-empty) { return [] } # get-external-services returns a table/list, we need to process each item # For now, return simplified status based on what we can check $external_services | each {|svc| { service: $svc.name backend: ($svc.srvc? | default "external") status: "✓" message: $"External service: ($svc.name) at ($svc.url)" } } } # Check all platform services and return their status # Returns list of {name: string, status: string, priority: int} export def check-platform-services-status []: nothing -> list { let services = (get-enabled-services) $services | each {|svc| let healthy = (check-service-health $svc.name) { name: $svc.name, status: (if $healthy { "healthy" } else { "unhealthy" }), priority: $svc.priority } } } # Show cascade failure report - prints static help without expensive service scanning export def show-cascade-failure-report [failed_service: string]: nothing -> nothing { print "" print $"❌ ($failed_service) is not running." print "" print "Start all platform services:" print " provisioning platform start" print " prvng plat start # short alias" print "" print "Check service status:" print " provisioning platform status" print " prvng plat st # short alias" print "" } # Verify service availability and fail with clean error message if not available # This function prints error and returns error status (NO stack trace) # Usage: Call this BEFORE making HTTP requests to services export def verify-service-or-fail [ service_url: string # Service URL (e.g., "http://localhost:9011") service_name: string # Human-readable service name (e.g., "Orchestrator") --check-command: string = "" # Full command to check status --check-alias: string = "" # Short alias for check (e.g., "prvng ps") --start-command: string = "" # Full command to start service --start-alias: string = "" # Short alias for start (e.g., "prvng start orchestrator") ]: nothing -> record { let check_result = (check-service-available $service_url $service_name) if not $check_result.available { # Print clean error message WITHOUT stack trace (NO error make) print $"❌ ($service_name) not available at ($service_url)" print "" print $"Connection refused - ($service_name) is not running on port ($check_result.port)." print "" # Show cascade failure report (external services + platform services) show-cascade-failure-report $service_name # Show commands with aliases if ($check_command | is-not-empty) { print "To check service status:" print $" ($check_command)" if ($check_alias | is-not-empty) { print $" ($check_alias) # short alias" } print "" } if ($start_command | is-not-empty) { print "To start service:" print $" ($start_command)" if ($start_alias | is-not-empty) { print $" ($start_alias) # short alias" } print "" } print $"Current endpoint: ($service_url)" print "If using a custom endpoint, verify it with: --orchestrator " # Return error status WITHOUT stack trace return {status: "error", message: $"($service_name) not available"} } # Service is available return {status: "ok", message: $"($service_name) is available"} } # Lightweight check - just returns boolean, no error message export def is-service-available [ service_url: string # Service URL service_name: string # Service name ]: nothing -> bool { let check_result = (check-service-available $service_url $service_name) $check_result.available } # Check if provisioning_daemon is available (CRITICAL - required for ALL operations) # Returns: { available: bool, port: int } export def check-daemon-availability []: nothing -> record { # Get daemon configuration let daemon_config = (get-deployment-service-config "provisioning_daemon") let daemon_port = ($daemon_config.server?.port? | default 9095) # Check if daemon port is listening let port_check = (do { ^lsof -i :($daemon_port) -P -n | ^grep LISTEN } | complete) let is_available = ($port_check.exit_code == 0) { available: $is_available port: $daemon_port } } # Verify daemon is available - CRITICAL prerequisite for ALL operations # Blocks execution if daemon is not available (except for help, platform, setup) # Returns error status if daemon unavailable export def verify-daemon-or-block [ operation: string # Operation being attempted (for error message) ]: nothing -> record { let daemon_check = (check-daemon-availability) if not $daemon_check.available { print "" print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" print "❌ CRITICAL: provisioning_daemon not available" print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" print "" print $"The provisioning daemon is required for operation: ($operation)" print $"Daemon is not listening on port ($daemon_check.port)" print "" print "The daemon is a CRITICAL component - all operations require it." print "" print "To check daemon status:" print " provisioning platform status" print " prvng plat st # short alias" print "" print "To start the daemon:" print " provisioning platform start provisioning_daemon" print " prvng plat start provisioning_daemon # short alias" print "" print "Allowed operations without daemon:" print " • help / -h / --help - View help" print " • platform - Manage platform services" print " • setup - Initial setup" print "" return {status: "error", message: "provisioning_daemon not available"} } # Daemon is available return {status: "ok", message: "provisioning_daemon is available"} }