8dd350286e
- Add Docker Compose and Dockerfile support for tool definitions - Implement readiness probes with configurable command, timeout, interval - Create ConfigFolder model for reusable file collections with project overrides - Add rich tool config fields: port_override, start_command, working_directory, env vars, volumes - Build unified Tool Workshop UI at /tool-workshop replacing /tool-configs and /tool-types - Update instance creation to support dockerfile builds, config folder mounting, readiness probes - Add 3 database migrations for tool_types, tool_configs, and new config_folders table - Create docker_build.py and readiness_probe.py services - Add config_folders API with CRUD and project override endpoints Quality gates: frontend build passes, Python syntax valid, all phases complete Addresses tool-workshop OpenSpec change
67 lines
2.1 KiB
Python
67 lines
2.1 KiB
Python
"""Readiness probe service for checking if containers are ready."""
|
|
|
|
import asyncio
|
|
import logging
|
|
import subprocess
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def execute_probe(
|
|
container_id: str,
|
|
command: str,
|
|
timeout: int = 30,
|
|
interval: int = 2,
|
|
) -> tuple[bool, list[str]]:
|
|
"""Execute a readiness probe command inside a container.
|
|
|
|
Args:
|
|
container_id: Docker container ID or name
|
|
command: Command to execute inside the container
|
|
timeout: Maximum total time to wait (seconds)
|
|
interval: Time between retries (seconds)
|
|
|
|
Returns:
|
|
Tuple of (success, logs)
|
|
"""
|
|
logs = []
|
|
start_time = asyncio.get_event_loop().time()
|
|
attempt = 0
|
|
|
|
while True:
|
|
attempt += 1
|
|
elapsed = asyncio.get_event_loop().time() - start_time
|
|
|
|
if elapsed >= timeout:
|
|
logs.append(f"Probe timed out after {timeout}s ({attempt} attempts)")
|
|
return False, logs
|
|
|
|
try:
|
|
logger.debug("Probe attempt %d: %s", attempt, command)
|
|
|
|
# Execute command inside container
|
|
result = subprocess.run(
|
|
["docker", "exec", container_id, "sh", "-c", command],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=interval, # Each attempt has its own timeout
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
logs.append(f"Attempt {attempt}: Success")
|
|
if result.stdout:
|
|
logs.append(f"Output: {result.stdout.strip()}")
|
|
return True, logs
|
|
else:
|
|
logs.append(f"Attempt {attempt}: Failed (exit code {result.returncode})")
|
|
if result.stderr:
|
|
logs.append(f"Stderr: {result.stderr.strip()[:200]}")
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logs.append(f"Attempt {attempt}: Command timed out")
|
|
except Exception as exc:
|
|
logs.append(f"Attempt {attempt}: Error - {exc}")
|
|
|
|
# Wait before next attempt
|
|
await asyncio.sleep(interval)
|