diff --git a/controler/main.py b/controler/main.py index dec2c39..a3bb1ce 100644 --- a/controler/main.py +++ b/controler/main.py @@ -69,9 +69,58 @@ async def lifespan(app: FastAPI): app = FastAPI(title="A/D Infrastructure Controller", lifespan=lifespan) # Helper functions +def find_compose_file(service_path: str) -> Optional[str]: + """Find docker-compose file in service directory""" + possible_names = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"] + for name in possible_names: + compose_file = os.path.join(service_path, name) + if os.path.exists(compose_file): + return compose_file + return None + +def parse_docker_status(ps_output: str) -> dict: + """Parse docker-compose ps output to get container status""" + lines = ps_output.strip().split('\n') + containers = [] + running_count = 0 + stopped_count = 0 + + for line in lines[1:]: # Skip header + if line.strip(): + # Look for status indicators in the line + if 'Up' in line: + running_count += 1 + containers.append({'status': 'running', 'info': line.strip()}) + elif 'Exit' in line or 'Exited' in line: + stopped_count += 1 + containers.append({'status': 'stopped', 'info': line.strip()}) + else: + containers.append({'status': 'unknown', 'info': line.strip()}) + + # Determine overall status + if running_count > 0 and stopped_count == 0: + overall_status = 'running' + elif running_count == 0 and stopped_count > 0: + overall_status = 'stopped' + elif running_count > 0 and stopped_count > 0: + overall_status = 'partial' + else: + overall_status = 'unknown' + + return { + 'overall_status': overall_status, + 'running': running_count, + 'stopped': stopped_count, + 'containers': containers + } + async def run_docker_compose_command(service_path: str, command: List[str]) -> tuple[int, str, str]: """Run docker-compose command and return (returncode, stdout, stderr)""" - full_command = ["docker-compose", "-f", os.path.join(service_path, "docker-compose.yml")] + command + compose_file = find_compose_file(service_path) + if not compose_file: + return 1, "", "docker-compose file not found" + + full_command = ["docker-compose", "-f", compose_file] + command process = await asyncio.create_subprocess_exec( *full_command, @@ -100,23 +149,74 @@ async def run_git_command(service_path: str, command: List[str]) -> tuple[int, s async def health_check(): return {"status": "ok", "timestamp": datetime.utcnow().isoformat()} +@app.get("/services/scan", dependencies=[Depends(verify_token)]) +async def scan_services_directory(): + """Scan /services directory for available services with docker-compose files""" + if not os.path.exists(SERVICES_DIR): + raise HTTPException(status_code=404, detail=f"Services directory not found: {SERVICES_DIR}") + + available_services = [] + + try: + for entry in os.listdir(SERVICES_DIR): + entry_path = os.path.join(SERVICES_DIR, entry) + if os.path.isdir(entry_path): + compose_file = find_compose_file(entry_path) + if compose_file: + # Check if already registered + conn = await get_db() + try: + existing = await conn.fetchrow( + "SELECT id, name, status FROM services WHERE path = $1", entry_path + ) + if existing: + available_services.append({ + "name": entry, + "path": entry_path, + "compose_file": os.path.basename(compose_file), + "registered": True, + "service_id": existing['id'], + "service_name": existing['name'], + "status": existing['status'] + }) + else: + available_services.append({ + "name": entry, + "path": entry_path, + "compose_file": os.path.basename(compose_file), + "registered": False + }) + finally: + await release_db(conn) + + return { + "services_dir": SERVICES_DIR, + "available_services": available_services, + "count": len(available_services) + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error scanning directory: {str(e)}") + @app.post("/services", dependencies=[Depends(verify_token)]) async def create_service(service: ServiceCreate): """Register a new service""" conn = await get_db() try: + # Build full service path - path should be relative to SERVICES_DIR + # Remove leading slash if present + relative_path = service.path.lstrip('/') + service_path = os.path.join(SERVICES_DIR, relative_path) + # Check if service directory exists - service_path = os.path.join(SERVICES_DIR, service.path) if not os.path.exists(service_path): raise HTTPException(status_code=404, detail=f"Service path not found: {service_path}") - # Check if docker-compose file exists (yml or yaml) - compose_file = os.path.join(service_path, "docker-compose.yml") - if not os.path.exists(compose_file): - compose_file = os.path.join(service_path, "docker-compose.yaml") - if not os.path.exists(compose_file): - compose_file = os.path.join(service_path, "compose.yml") - if not os.path.exists(compose_file): + if not os.path.isdir(service_path): + raise HTTPException(status_code=400, detail=f"Service path is not a directory: {service_path}") + + # Check if docker-compose file exists + compose_file = find_compose_file(service_path) + if not compose_file: raise HTTPException(status_code=404, detail=f"docker-compose file not found in {service_path}") service_id = await conn.fetchval( @@ -126,17 +226,48 @@ async def create_service(service: ServiceCreate): await log_service_action(conn, service_id, "register", "success", "Service registered") - return {"id": service_id, "name": service.name, "status": "registered"} + return {"id": service_id, "name": service.name, "status": "registered", "path": service_path} finally: await release_db(conn) @app.get("/services", dependencies=[Depends(verify_token)]) -async def list_services(): - """List all registered services""" +async def list_services(live_status: bool = True): + """List all registered services with optional live status from docker""" conn = await get_db() try: rows = await conn.fetch("SELECT * FROM services ORDER BY name") - return [dict(row) for row in rows] + services = [] + + for row in rows: + service_dict = dict(row) + + # Get live status from docker if requested + if live_status: + returncode, stdout, stderr = await run_docker_compose_command( + service_dict['path'], ["ps"] + ) + + if returncode == 0 and stdout.strip(): + parsed_status = parse_docker_status(stdout) + service_dict['live_status'] = parsed_status['overall_status'] + service_dict['containers_running'] = parsed_status['running'] + service_dict['containers_stopped'] = parsed_status['stopped'] + + # Update DB status if different + if parsed_status['overall_status'] != service_dict['status']: + await conn.execute( + "UPDATE services SET status = $1, last_updated = $2 WHERE id = $3", + parsed_status['overall_status'], datetime.utcnow(), service_dict['id'] + ) + service_dict['status'] = parsed_status['overall_status'] + else: + service_dict['live_status'] = 'unavailable' + service_dict['containers_running'] = 0 + service_dict['containers_stopped'] = 0 + + services.append(service_dict) + + return services finally: await release_db(conn) @@ -266,7 +397,7 @@ async def get_logs(service_id: int, lines: int = 100): @app.get("/services/{service_id}/status", dependencies=[Depends(verify_token)]) async def get_service_status(service_id: int): - """Get real-time service status from docker-compose""" + """Get real-time service status from docker-compose ps""" conn = await get_db() try: service = await conn.fetchrow("SELECT * FROM services WHERE id = $1", service_id) @@ -280,7 +411,25 @@ async def get_service_status(service_id: int): ) if returncode == 0: - return {"status": stdout, "db_status": service['status']} + parsed_status = parse_docker_status(stdout) + + # Update database with current status + if parsed_status['overall_status'] != service['status']: + await conn.execute( + "UPDATE services SET status = $1, last_updated = $2 WHERE id = $3", + parsed_status['overall_status'], datetime.utcnow(), service_id + ) + + return { + "service_id": service_id, + "service_name": service['name'], + "live_status": parsed_status['overall_status'], + "containers_running": parsed_status['running'], + "containers_stopped": parsed_status['stopped'], + "containers": parsed_status['containers'], + "db_status": service['status'], + "raw_output": stdout + } else: raise HTTPException(status_code=500, detail=f"Failed to get status: {stderr}") finally: