Update main.py
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user