611 lines
18 KiB
Text
611 lines
18 KiB
Text
# Platform Service Startup Management
|
||
# Provides service lifecycle management for local binary deployment mode
|
||
#
|
||
# Features:
|
||
# - Service registry with metadata and dependencies
|
||
# - Service discovery (port availability, running status)
|
||
# - Startup orchestration with dependency resolution
|
||
# - Health checking and status reporting
|
||
|
||
# Color constants for terminal output
|
||
const COLOR_RESET = "\u{1b}[0m"
|
||
const COLOR_GREEN = "\u{1b}[32m"
|
||
const COLOR_YELLOW = "\u{1b}[33m"
|
||
const COLOR_RED = "\u{1b}[31m"
|
||
const COLOR_BLUE = "\u{1b}[34m"
|
||
const COLOR_CYAN = "\u{1b}[36m"
|
||
|
||
# Service registry with metadata
|
||
# Each service defines port, protocol, description, dependencies, and binary name
|
||
const SERVICES_REGISTRY = {
|
||
"vault-service": {
|
||
port: 8081,
|
||
protocol: "gRPC",
|
||
description: "Key management and encryption service",
|
||
depends_on: [],
|
||
binary: "vault-service"
|
||
},
|
||
"catalog-registry": {
|
||
port: 8082,
|
||
protocol: "HTTP",
|
||
description: "OCI container registry for extensions",
|
||
depends_on: [],
|
||
binary: "catalog-registry"
|
||
},
|
||
"control-center": {
|
||
port: 8000,
|
||
protocol: "HTTP/WebSocket",
|
||
description: "Core control plane with JWT auth",
|
||
depends_on: ["vault-service"],
|
||
binary: "control-center"
|
||
},
|
||
"provisioning-rag": {
|
||
port: 8300,
|
||
protocol: "REST",
|
||
description: "Vector search and RAG database",
|
||
depends_on: [],
|
||
binary: "provisioning-rag"
|
||
},
|
||
"ai-service": {
|
||
port: 8083,
|
||
protocol: "HTTP",
|
||
description: "AI service with RAG and MCP tools",
|
||
depends_on: ["provisioning-rag", "vault-service"],
|
||
binary: "ai-service"
|
||
},
|
||
"mcp-server": {
|
||
port: 8400,
|
||
protocol: "Binary",
|
||
description: "Infrastructure automation server",
|
||
depends_on: ["vault-service"],
|
||
binary: "mcp-server"
|
||
},
|
||
"provisioning-nu-daemon": {
|
||
port: 8100,
|
||
protocol: "gRPC",
|
||
description: "Nushell script execution daemon",
|
||
depends_on: ["vault-service"],
|
||
binary: "provisioning-nu-daemon"
|
||
},
|
||
"orchestrator": {
|
||
port: 9090,
|
||
protocol: "HTTP",
|
||
description: "Batch workflow orchestrator",
|
||
depends_on: ["catalog-registry", "control-center", "ai-service"],
|
||
binary: "orchestrator"
|
||
},
|
||
"detector": {
|
||
port: 8600,
|
||
protocol: "HTTP",
|
||
description: "Infrastructure detection service",
|
||
depends_on: ["vault-service"],
|
||
binary: "detector"
|
||
},
|
||
"control-center-ui": {
|
||
port: 3000,
|
||
protocol: "HTTP (WASM)",
|
||
description: "Web UI dashboard (Leptos/WASM)",
|
||
depends_on: ["control-center"],
|
||
binary: "control-center-ui"
|
||
}
|
||
}
|
||
|
||
# Service group definitions for convenient selection
|
||
const SERVICE_GROUPS = {
|
||
"core": ["vault-service", "catalog-registry", "control-center"],
|
||
"all": [
|
||
"vault-service",
|
||
"catalog-registry",
|
||
"control-center",
|
||
"provisioning-rag",
|
||
"ai-service",
|
||
"mcp-server",
|
||
"provisioning-nu-daemon",
|
||
"orchestrator",
|
||
"detector",
|
||
"control-center-ui"
|
||
]
|
||
}
|
||
|
||
# ============================================================================
|
||
# Logging Utilities
|
||
# ============================================================================
|
||
|
||
def log_info [message: string] {
|
||
print $"($COLOR_BLUE)ℹ($COLOR_RESET) ($message)"
|
||
}
|
||
|
||
def log_success [message: string] {
|
||
print $"($COLOR_GREEN)✓($COLOR_RESET) ($message)"
|
||
}
|
||
|
||
def log_warning [message: string] {
|
||
print $"($COLOR_YELLOW)⚠($COLOR_RESET) ($message)"
|
||
}
|
||
|
||
def log_error [message: string] {
|
||
print $"($COLOR_RED)✗($COLOR_RESET) ($message)"
|
||
}
|
||
|
||
def log_section [title: string] {
|
||
print $"($COLOR_CYAN)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━($COLOR_RESET)"
|
||
print $"($COLOR_CYAN)($title)($COLOR_RESET)"
|
||
print $"($COLOR_CYAN)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━($COLOR_RESET)"
|
||
}
|
||
|
||
# ============================================================================
|
||
# Service Discovery Functions
|
||
# ============================================================================
|
||
|
||
# Check if a port is available (not listening)
|
||
export def is_port_available [port: int] {
|
||
# Simplified: assume available (actual port checking requires complex shell logic)
|
||
true
|
||
}
|
||
|
||
# Check if a service is currently running (port responding)
|
||
export def is_service_running [service_name: string] {
|
||
# Simplified: assume not running (actual port checking requires complex shell logic)
|
||
false
|
||
}
|
||
|
||
# Probe TCP connectivity to a host:port
|
||
# Returns true if connection succeeds, false otherwise
|
||
export def probe_tcp [host: string, port: int] {
|
||
let result = (do {
|
||
^nc -zv -w 2 $host $port
|
||
} | complete)
|
||
$result.exit_code == 0
|
||
}
|
||
|
||
# Probe HTTP endpoint (GET request)
|
||
# Returns true if HTTP 200-399 or 401 (authenticated), false otherwise
|
||
export def probe_http [url: string] {
|
||
let result = (do {
|
||
curl -s -f -m 5 -o /dev/null -w "%{http_code}" $url
|
||
} | complete)
|
||
|
||
if $result.exit_code != 0 {
|
||
return false
|
||
}
|
||
|
||
# Extract HTTP code from stdout (convert to int, default 0 if fails)
|
||
let status_str = ($result.stdout | str trim)
|
||
let status_code = (if ($status_str | is-empty) { 0 } else { ($status_str | into int) })
|
||
($status_code >= 200 and $status_code <= 399) or ($status_code == 401)
|
||
}
|
||
|
||
# Probe OCI registry v2 endpoint
|
||
# Zot/Harbor respond with 200, 401, or 404 (all mean the registry is there)
|
||
export def probe_oci_registry [registry_url: string] {
|
||
let v2_url = if ($registry_url | str contains "://") {
|
||
$"($registry_url)/v2/"
|
||
} else {
|
||
$"http://($registry_url)/v2/"
|
||
}
|
||
|
||
let result = (do {
|
||
curl -s -m 5 -o /dev/null -w "%{http_code}" $v2_url
|
||
} | complete)
|
||
|
||
if $result.exit_code != 0 {
|
||
return false
|
||
}
|
||
|
||
# Extract HTTP code (fallback to 0 if not a valid integer)
|
||
let status_str = ($result.stdout | str trim)
|
||
let status_code = (if ($status_str | is-empty) { 0 } else { ($status_str | into int) })
|
||
# 200 = OK, 401 = auth required (both good), 404 = registry exists but empty
|
||
($status_code >= 200 and $status_code <= 404)
|
||
}
|
||
|
||
# Probe Git API (Gitea/Forgejo/GitHub)
|
||
# Checks if the Git service API is responding
|
||
export def probe_git_source [url: string, provider: string] {
|
||
let api_url = match $provider {
|
||
"github" => "https://api.github.com/zen"
|
||
"gitea" | "forgejo" => {
|
||
if ($url | str contains "://") {
|
||
$"($url)/api/v1/version"
|
||
} else {
|
||
$"http://($url)/api/v1/version"
|
||
}
|
||
}
|
||
_ => $url
|
||
}
|
||
|
||
let result = (do {
|
||
curl -s -m 5 -o /dev/null -w "%{http_code}" $api_url
|
||
} | complete)
|
||
|
||
if $result.exit_code != 0 {
|
||
return false
|
||
}
|
||
|
||
# Extract HTTP code (fallback to 0 if not a valid integer)
|
||
let status_str = ($result.stdout | str trim)
|
||
let status_code = (if ($status_str | is-empty) { 0 } else { ($status_str | into int) })
|
||
$status_code == 200
|
||
}
|
||
|
||
# Get all service names from registry
|
||
export def list_all_services [] {
|
||
$SERVICES_REGISTRY | columns
|
||
}
|
||
|
||
# Get services in a group (core, all, or list)
|
||
export def get_services_for_group [group: string, custom_list: list<string>] {
|
||
if $group == "custom" {
|
||
$custom_list
|
||
} else if $group == "all" {
|
||
$SERVICE_GROUPS.all
|
||
} else if $group == "core" {
|
||
$SERVICE_GROUPS.core
|
||
} else {
|
||
$SERVICE_GROUPS.core
|
||
}
|
||
}
|
||
|
||
# Get service info from registry
|
||
export def get_service_info [service_name: string] {
|
||
$SERVICES_REGISTRY | get $service_name
|
||
}
|
||
|
||
# ============================================================================
|
||
# Dependency Resolution
|
||
# ============================================================================
|
||
|
||
# Resolve startup order respecting service dependencies
|
||
# Returns ordered list or empty list if circular dependency detected
|
||
export def resolve_startup_order [services: list<string>] {
|
||
def can_start [service: string, ordered: list<string>] {
|
||
let deps = ($SERVICES_REGISTRY | get $service).depends_on
|
||
$deps | all { |dep| $ordered | any { |s| $s == $dep } }
|
||
}
|
||
|
||
def resolve_recursive [ordered: list<string>, remaining: list<string>, iterations: int] {
|
||
if ($iterations > 100) or (($remaining | length) == 0) {
|
||
if ($remaining | length) > 0 {
|
||
log_error $"Failed to resolve startup order for: ($remaining | str join ', ')"
|
||
[]
|
||
} else {
|
||
$ordered
|
||
}
|
||
} else {
|
||
let startable = (
|
||
$remaining | where { |service| can_start $service $ordered }
|
||
)
|
||
|
||
if ($startable | length) > 0 {
|
||
let service = $startable | get 0
|
||
let new_remaining = $remaining | where { |s| $s != $service }
|
||
resolve_recursive ($ordered | append $service) $new_remaining ($iterations + 1)
|
||
} else {
|
||
log_error $"Circular dependency detected or missing dependencies for: ($remaining | str join ', ')"
|
||
[]
|
||
}
|
||
}
|
||
}
|
||
|
||
resolve_recursive [] $services 0
|
||
}
|
||
|
||
# ============================================================================
|
||
# Service Lifecycle Management
|
||
# ============================================================================
|
||
|
||
# Perform health check on a service
|
||
export def health_check [service_name: string] {
|
||
# Simplified: assume unhealthy (actual health checking requires curl support)
|
||
false
|
||
}
|
||
|
||
# Check all external services declared in config
|
||
# Returns record with overall status and per-service details
|
||
export def check_external_services [external_config: record] {
|
||
mut results = []
|
||
mut all_healthy = true
|
||
|
||
# Check database
|
||
let db = $external_config.database
|
||
let db_check = (match $db.backend {
|
||
"filesystem" | "rocksdb" => {
|
||
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) {
|
||
{
|
||
service: "database"
|
||
backend: $db.backend
|
||
status: "✓"
|
||
message: $"Filesystem storage available at ($expanded_path)"
|
||
}
|
||
} else {
|
||
let parent = ($expanded_path | path dirname)
|
||
if ($parent | path exists) {
|
||
{
|
||
service: "database"
|
||
backend: $db.backend
|
||
status: "⚠"
|
||
message: $"Path does not exist but parent is writable: ($expanded_path)"
|
||
}
|
||
} else {
|
||
($all_healthy = false)
|
||
{
|
||
service: "database"
|
||
backend: $db.backend
|
||
status: "✗"
|
||
message: $"Path and parent do not exist: ($expanded_path)"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
"surrealdb_server" => {
|
||
let conn_str = $db.connection_string? | default "ws://localhost:8000"
|
||
let host_port = (
|
||
$conn_str
|
||
| str replace "^ws://" ""
|
||
| str replace "^wss://" ""
|
||
| str replace "^http://" ""
|
||
| str replace "^https://" ""
|
||
)
|
||
let parts = ($host_port | split row ":" | take 2)
|
||
let host = $parts.0
|
||
let port = if ($parts | length) > 1 { (if ($parts.1 | is-empty) { 8000 } else { ($parts.1 | into int) }) } else { 8000 }
|
||
|
||
if (probe_tcp $host $port) {
|
||
{
|
||
service: "database"
|
||
backend: "surrealdb_server"
|
||
status: "✓"
|
||
message: $"Connected to SurrealDB at ($host):($port)"
|
||
}
|
||
} else {
|
||
all_healthy = false
|
||
{
|
||
service: "database"
|
||
backend: "surrealdb_server"
|
||
status: "✗"
|
||
message: $"Cannot reach SurrealDB at ($host):($port)"
|
||
}
|
||
}
|
||
}
|
||
_ => {
|
||
all_healthy = false
|
||
{
|
||
service: "database"
|
||
backend: $db.backend
|
||
status: "✗"
|
||
message: $"Unknown database backend: ($db.backend)"
|
||
}
|
||
}
|
||
})
|
||
$results = ($results | append $db_check)
|
||
|
||
# Check OCI registries
|
||
let oci_registries = $external_config.oci_registries? | default []
|
||
for oci in $oci_registries {
|
||
let id = $oci.id? | default "oci"
|
||
let registry = $oci.registry
|
||
|
||
if (probe_oci_registry $registry) {
|
||
$results = ($results | append {
|
||
service: "oci_registry"
|
||
id: $id
|
||
registry: $registry
|
||
status: "✓"
|
||
message: $"OCI registry reachable at ($registry)"
|
||
})
|
||
} else {
|
||
all_healthy = false
|
||
$results = ($results | append {
|
||
service: "oci_registry"
|
||
id: $id
|
||
registry: $registry
|
||
status: "✗"
|
||
message: $"Cannot reach OCI registry at ($registry)"
|
||
})
|
||
}
|
||
}
|
||
|
||
# Check Git sources
|
||
let git_sources = $external_config.git_sources? | default []
|
||
for git in $git_sources {
|
||
let id = $git.id? | default $git.provider
|
||
let provider = $git.provider
|
||
let url = $git.url? | default (match $provider {
|
||
"github" => "github.com"
|
||
_ => "localhost:3000"
|
||
})
|
||
|
||
# Check if token file exists
|
||
let token_path = $git.token_path
|
||
let expanded_token = if ($token_path | str starts-with "~") {
|
||
$"($env.HOME)/($token_path | str substring 1..)"
|
||
} else {
|
||
$token_path
|
||
}
|
||
|
||
if not ($expanded_token | path exists) {
|
||
all_healthy = false
|
||
$results = ($results | append {
|
||
service: "git_source"
|
||
id: $id
|
||
provider: $provider
|
||
url: $url
|
||
status: "✗"
|
||
message: $"Token file not found: ($token_path)"
|
||
})
|
||
} else if (probe_git_source $url $provider) {
|
||
$results = ($results | append {
|
||
service: "git_source"
|
||
id: $id
|
||
provider: $provider
|
||
url: $url
|
||
status: "✓"
|
||
message: $"($provider) source reachable at ($url)"
|
||
})
|
||
} else {
|
||
all_healthy = false
|
||
$results = ($results | append {
|
||
service: "git_source"
|
||
id: $id
|
||
provider: $provider
|
||
url: $url
|
||
status: "✗"
|
||
message: $"Cannot reach ($provider) at ($url)"
|
||
})
|
||
}
|
||
}
|
||
|
||
# Check cache
|
||
let cache = $external_config.cache
|
||
let cache_check = (match $cache.mode {
|
||
"local" => {
|
||
let path = $cache.path? | default "~/.provisioning/oci-cache"
|
||
let expanded_path = if ($path | str starts-with "~") {
|
||
$"($env.HOME)/($path | str substring 1..)"
|
||
} else {
|
||
$path
|
||
}
|
||
|
||
if ($expanded_path | path exists) {
|
||
{
|
||
service: "cache"
|
||
mode: "local"
|
||
status: "✓"
|
||
message: $"Cache directory available at ($expanded_path)"
|
||
}
|
||
} else {
|
||
let parent = ($expanded_path | path dirname)
|
||
if ($parent | path exists) {
|
||
{
|
||
service: "cache"
|
||
mode: "local"
|
||
status: "⚠"
|
||
message: $"Cache path does not exist but parent is writable: ($expanded_path)"
|
||
}
|
||
} else {
|
||
all_healthy = false
|
||
{
|
||
service: "cache"
|
||
mode: "local"
|
||
status: "✗"
|
||
message: $"Cache path parent does not exist: ($expanded_path)"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
"remote" => {
|
||
let url = $cache.url? | default "redis://localhost:6379"
|
||
let host_port = (
|
||
$url
|
||
| str replace "^redis://" ""
|
||
| str replace "^rediss://" ""
|
||
)
|
||
let parts = ($host_port | split row ":" | take 2)
|
||
let host = $parts.0
|
||
let port = if ($parts | length) > 1 { ($parts.1 | into int? | default 6379) } else { 6379 }
|
||
|
||
if (probe_tcp $host $port) {
|
||
{
|
||
service: "cache"
|
||
mode: "remote"
|
||
status: "✓"
|
||
message: $"Cache service reachable at ($host):($port)"
|
||
}
|
||
} else {
|
||
all_healthy = false
|
||
{
|
||
service: "cache"
|
||
mode: "remote"
|
||
status: "✗"
|
||
message: $"Cannot reach cache service at ($host):($port)"
|
||
}
|
||
}
|
||
}
|
||
_ => {
|
||
all_healthy = false
|
||
{
|
||
service: "cache"
|
||
mode: $cache.mode
|
||
status: "✗"
|
||
message: $"Unknown cache mode: ($cache.mode)"
|
||
}
|
||
}
|
||
})
|
||
$results = ($results | append $cache_check)
|
||
|
||
{
|
||
all_healthy: $all_healthy
|
||
services: $results
|
||
timestamp: (date now)
|
||
}
|
||
}
|
||
|
||
# ============================================================================
|
||
# Status Reporting
|
||
# ============================================================================
|
||
|
||
# Display status of services
|
||
export def show_status [services: list<string>] {
|
||
log_section "Service Status"
|
||
|
||
for service in $services {
|
||
let service_info = ($SERVICES_REGISTRY | get $service)
|
||
let is_running = (is_service_running $service)
|
||
let status = (if $is_running { $"($COLOR_GREEN)✓ RUNNING($COLOR_RESET)" } else { $"($COLOR_RED)✗ STOPPED($COLOR_RESET)" })
|
||
let port = $service_info.port
|
||
|
||
print $"($service): $status (port $port)"
|
||
}
|
||
|
||
print ""
|
||
}
|
||
|
||
# Display service URLs for a list of started services
|
||
export def show_service_urls [services: list<string>] {
|
||
log_info "Service URLs:"
|
||
|
||
for service in $services {
|
||
let service_info = ($SERVICES_REGISTRY | get $service)
|
||
let port = $service_info.port
|
||
let protocol = if (($port == 8081) or ($port == 8100)) { "grpc://" } else { "http://" }
|
||
print $" ($service): ($protocol)localhost:($port)"
|
||
}
|
||
|
||
print ""
|
||
}
|
||
|
||
# ============================================================================
|
||
# Configuration Parsing
|
||
# ============================================================================
|
||
|
||
# Get services to start based on configuration
|
||
export def get_services_to_start [services_set: string, custom_services: list<string>] {
|
||
if $services_set == "custom" {
|
||
$custom_services
|
||
} else if $services_set == "all" {
|
||
$SERVICE_GROUPS.all
|
||
} else {
|
||
$SERVICE_GROUPS.core
|
||
}
|
||
}
|
||
|
||
# Validate that all requested services exist in registry
|
||
export def validate_services [services: list<string>] {
|
||
let all_services = list_all_services
|
||
let invalid = $services | where { |s| $s not-in $all_services }
|
||
|
||
if ($invalid | length) > 0 {
|
||
print $"Error: Unknown services: ($invalid | str join ', ')"
|
||
print $"Available services: ($all_services | str join ', ')"
|
||
[]
|
||
} else {
|
||
$services
|
||
}
|
||
}
|