#!/usr/bin/env nu # Start local provisioning services via Docker Compose # Usage: nu provisioning/scripts/start-local.nu [--stop|--restart|--logs|--status] # # Options: # --stop Stop all services # --restart Restart all services # --logs Follow logs in real-time # --status Show service status # (default) Start services and show info use std log def result_ok [value: any] { {ok: $value, err: null} } def result_err [message: string] { {ok: null, err: $message} } def is_ok [result: record] { $result.err == null } # Phase 1: Validate environment and preconditions def validate_environment [] { # Guard 1: Docker is installed let docker_check = ( try { which docker | length > 0 } catch { false } ) if not $docker_check { return (result_err "Docker not found. Please install Docker Desktop or Docker Engine.") } # Guard 2: Docker daemon is running let docker_running = ( try { docker ps out> /dev/null 2>&1; true } catch { false } ) if not $docker_running { return (result_err "Docker daemon is not running. Please start Docker and try again.") } # Guard 3: docker-compose.local.yml exists let compose_file = "provisioning/docker-compose.local.yml" if not ($compose_file | path exists) { return (result_err $"Docker Compose file not found: ($compose_file)") } # Guard 4: .env.local exists (or create from template) let env_file = ".env.local" let env_template = "provisioning/.env.local.template" if not ($env_file | path exists) { if ($env_template | path exists) { log info $"Creating ($env_file) from template" cp $env_template $env_file } else { log warning $"(.env.local) not found, using defaults" } } result_ok null } # Phase 2: Check port availability def check_ports [] { let ports = [ {port: 8080, service: "API Gateway"} {port: 8082, service: "Extension Registry"} {port: 9090, service: "Orchestrator"} {port: 2379, service: "ETCD Client"} {port: 2380, service: "ETCD Peer"} {port: 9091, service: "Prometheus"} ] let unavailable = $ports | where {|p| try { let result = (ss -tuln 2>/dev/null | grep $"(:($p.port) )") $result | length > 0 } catch { false } } if ($unavailable | length) > 0 { let port_list = ($unavailable | map { |p| $" - Port ($p.port): ($p.service)" } | str join "\n") return (result_err $"The following ports are already in use:\n($port_list)\n\nEither stop the conflicting services or wait for cleanup.") } result_ok null } # Phase 3: Start Docker Compose services def start_services [] { log info "Starting Docker Compose services..." try { docker compose -f provisioning/docker-compose.local.yml --env-file .env.local up -d result_ok null } catch {|err| result_err $"Failed to start services: ($err.msg)" } } # Phase 4: Wait for health checks def wait_for_services [timeout_secs: int] { log info $"Waiting for services to be healthy (timeout: ($timeout_secs)s)..." let services = [ {name: "etcd", port: 2379} {name: "extension-registry", port: 8082} {name: "orchestrator", port: 9090} {name: "api-gateway", port: 8080} ] let start_time = (date now | into int) mut all_healthy = false loop { let elapsed = ((date now | into int) - $start_time) if $elapsed > $timeout_secs { return (result_err "Services did not become healthy within timeout period. Check docker logs.") } let health_results = $services | map {|s| let healthy = ( try { curl -s -f -m 2 $"http://localhost:($s.port)/api/v1/health" out> /dev/null 2>&1 true } catch { false } ) {service: $s.name, healthy: $healthy} } let healthy_count = ($health_results | where healthy | length) if $healthy_count == ($services | length) { log info "All services are healthy" $all_healthy = true break } let unhealthy = $health_results | where {|h| not $h.healthy} | map {|h| $h.service} log debug $"Waiting for services: ($unhealthy | str join ', ')" sleep 2sec } result_ok $all_healthy } # Phase 5: Verify service endpoints def verify_endpoints [] { log info "Verifying service endpoints..." let endpoints = [ {name: "API Gateway", url: "http://localhost:8080/api/v1/health"} {name: "Extension Registry", url: "http://localhost:8082/api/v1/health"} {name: "Orchestrator", url: "http://localhost:9090/api/v1/health"} {name: "ETCD", url: "http://localhost:2379/version"} ] let results = $endpoints | map {|ep| let response = ( try { curl -s $ep.url | from json } catch { null } ) {service: $ep.name, accessible: ($response != null)} } let all_accessible = ($results | where accessible | length) == ($results | length) if not $all_accessible { let inaccessible = ($results | where {|r| not $r.accessible} | map {|r| $r.service}) log warning $"Some endpoints are not accessible: ($inaccessible | str join ', ')" } else { log info "All endpoints are accessible" } result_ok $results } # Phase 6: Display access information def show_info [] { log info "" log info "==========================================" log info "Provisioning Local Services Started" log info "==========================================" log info "" log info "Service URLs:" log info " API Gateway: http://localhost:8080" log info " Extension Registry: http://localhost:8082" log info " Orchestrator: http://localhost:9090" log info " ETCD Client: http://localhost:2379" log info " Prometheus: http://localhost:9091" log info "" log info "Useful commands:" log info " View logs: docker compose -f provisioning/docker-compose.local.yml logs -f" log info " Stop services: docker compose -f provisioning/docker-compose.local.yml down" log info " Container status: docker ps --filter label=com.docker.compose.project=provisioning" log info "" log info "Test connectivity:" log info " curl http://localhost:8082/api/v1/health" log info " curl http://localhost:9090/api/v1/health" log info "" log info "==========================================" } # Command: Stop services def stop_services [] { log info "Stopping Docker Compose services..." try { docker compose -f provisioning/docker-compose.local.yml --env-file .env.local down log info "Services stopped successfully" result_ok null } catch {|err| result_err $"Failed to stop services: ($err.msg)" } } # Command: Restart services def restart_services [] { let stop_result = stop_services if not (is_ok $stop_result) { return $stop_result } sleep 2sec let start_result = start_services if not (is_ok $start_result) { return $start_result } wait_for_services 60 } # Command: Show logs def show_logs [] { log info "Following Docker Compose logs (Ctrl+C to stop)..." docker compose -f provisioning/docker-compose.local.yml --env-file .env.local logs -f } # Command: Show status def show_status [] { log info "Checking service status..." docker compose -f provisioning/docker-compose.local.yml ps } # Main execution def main [--stop: bool, --restart: bool, --logs: bool, --status: bool] { # Load environment if (".env.local" | path exists) { load-env (.env.local | open --raw | lines | where {|l| $l !~ '^#' and $l | str trim | length > 0} | each {|l| let parts = ($l | split column '=' -c 2) {key: ($parts.0.0 | str trim), value: ($parts.0.1 | str trim)} } | each {|kv| {($kv.key): $kv.value} } | reduce item=null as $item ({}; . + $item)) } # Route to appropriate command if $stop { let result = (stop_services) if not (is_ok $result) { log error $result.err exit 1 } } else if $restart { let result = (restart_services) if not (is_ok $result) { log error $result.err exit 1 } show_info } else if $logs { show_logs } else if $status { show_status } else { # Default: Start services let validate = (validate_environment) if not (is_ok $validate) { log error $validate.err exit 1 } let ports = (check_ports) if not (is_ok $ports) { log error $ports.err exit 1 } let start = (start_services) if not (is_ok $start) { log error $start.err exit 1 } let wait = (wait_for_services 60) if not (is_ok $wait) { log error $wait.err exit 1 } let verify = (verify_endpoints) if not (is_ok $verify) { log error $verify.err exit 1 } show_info } } main ...$nu.args