mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2025-12-16 05:19:43 +01:00
Fix n8n container health check to use fuzzy name matching
The container health check was failing because Docker Compose v1 and v2 use different naming conventions: - v1: project_service_1 (underscores) - v2: project-service-1 (hyphens) Changes: 1. Replaced hardcoded container name formatting with fuzzy matching 2. Added find_container_by_service() helper method for dynamic lookup 3. Updated monitor_deployment() to use dynamic container discovery 4. Container names are now found by normalizing and matching patterns This fixes "Containers failed to reach healthy state" errors during n8n deployment from CyberPanel UI. Ticket References: XKTFREZUR, XCGF2HQUH
This commit is contained in:
@@ -911,41 +911,59 @@ services:
|
|||||||
|
|
||||||
##### N8N Container
|
##### N8N Container
|
||||||
|
|
||||||
def check_container_health(self, container_name, max_retries=3, delay=80):
|
def check_container_health(self, service_name, max_retries=3, delay=80):
|
||||||
"""
|
"""
|
||||||
Check if a container is running, accepting healthy, unhealthy, and starting states
|
Check if a container is running, accepting healthy, unhealthy, and starting states
|
||||||
Total wait time will be 4 minutes (3 retries * 80 seconds)
|
Total wait time will be 4 minutes (3 retries * 80 seconds)
|
||||||
|
|
||||||
|
Uses fuzzy matching to find containers since Docker Compose naming varies by version:
|
||||||
|
- Docker Compose v1: project_service_1 (underscores)
|
||||||
|
- Docker Compose v2: project-service-1 (hyphens)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Format container name to match Docker's naming convention
|
logging.writeToFile(f'Checking container health for service: {service_name}')
|
||||||
formatted_name = f"{self.data['ServiceName']}-{container_name}-1"
|
|
||||||
logging.writeToFile(f'Checking container health for: {formatted_name}')
|
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
container = client.containers.get(formatted_name)
|
|
||||||
|
# Find container by searching all containers for a name containing the service name
|
||||||
|
# This handles both v1 (underscores) and v2 (hyphens) naming conventions
|
||||||
|
all_containers = client.containers.list(all=True)
|
||||||
|
container = None
|
||||||
|
|
||||||
|
# Normalize service name for matching (handle both - and _)
|
||||||
|
service_pattern = service_name.lower().replace(' ', '').replace('-', '').replace('_', '')
|
||||||
|
|
||||||
|
for c in all_containers:
|
||||||
|
container_pattern = c.name.lower().replace('-', '').replace('_', '')
|
||||||
|
if service_pattern in container_pattern:
|
||||||
|
container = c
|
||||||
|
logging.writeToFile(f'Found matching container: {c.name} for service: {service_name}')
|
||||||
|
break
|
||||||
|
|
||||||
|
if container is None:
|
||||||
|
logging.writeToFile(f'No container found matching service: {service_name}, attempt {attempt + 1}/{max_retries}')
|
||||||
|
time.sleep(delay)
|
||||||
|
continue
|
||||||
|
|
||||||
if container.status == 'running':
|
if container.status == 'running':
|
||||||
health = container.attrs.get('State', {}).get('Health', {}).get('Status')
|
health = container.attrs.get('State', {}).get('Health', {}).get('Status')
|
||||||
|
|
||||||
# Accept healthy, unhealthy, and starting states as long as container is running
|
# Accept healthy, unhealthy, and starting states as long as container is running
|
||||||
if health in ['healthy', 'unhealthy', 'starting'] or health is None:
|
if health in ['healthy', 'unhealthy', 'starting'] or health is None:
|
||||||
logging.writeToFile(f'Container {formatted_name} is running with status: {health}')
|
logging.writeToFile(f'Container {container.name} is running with health status: {health}')
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
health_logs = container.attrs.get('State', {}).get('Health', {}).get('Log', [])
|
health_logs = container.attrs.get('State', {}).get('Health', {}).get('Log', [])
|
||||||
if health_logs:
|
if health_logs:
|
||||||
last_log = health_logs[-1]
|
last_log = health_logs[-1]
|
||||||
logging.writeToFile(f'Container health check failed: {last_log.get("Output", "")}')
|
logging.writeToFile(f'Container health check failed: {last_log.get("Output", "")}')
|
||||||
|
|
||||||
logging.writeToFile(f'Container {formatted_name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}')
|
logging.writeToFile(f'Container {container.name} status: {container.status}, health: {health}, attempt {attempt + 1}/{max_retries}')
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
except docker.errors.NotFound:
|
|
||||||
logging.writeToFile(f'Container {formatted_name} not found')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.writeToFile(f'Error checking container health: {str(e)}')
|
logging.writeToFile(f'Error checking container health: {str(e)}')
|
||||||
return False
|
return False
|
||||||
@@ -1068,12 +1086,39 @@ services:
|
|||||||
logging.writeToFile(f"Cleanup failed: {str(e)}")
|
logging.writeToFile(f"Cleanup failed: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def find_container_by_service(self, service_name):
|
||||||
|
"""
|
||||||
|
Find a container by service name using fuzzy matching.
|
||||||
|
Returns the container object or None if not found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client = docker.from_env()
|
||||||
|
all_containers = client.containers.list(all=True)
|
||||||
|
|
||||||
|
# Normalize service name for matching
|
||||||
|
service_pattern = service_name.lower().replace(' ', '').replace('-', '').replace('_', '')
|
||||||
|
|
||||||
|
for c in all_containers:
|
||||||
|
container_pattern = c.name.lower().replace('-', '').replace('_', '')
|
||||||
|
if service_pattern in container_pattern:
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logging.writeToFile(f'Error finding container: {str(e)}')
|
||||||
|
return None
|
||||||
|
|
||||||
def monitor_deployment(self):
|
def monitor_deployment(self):
|
||||||
try:
|
try:
|
||||||
# Format container names
|
# Find containers dynamically using fuzzy matching
|
||||||
n8n_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-1"
|
n8n_container = self.find_container_by_service(self.data['ServiceName'])
|
||||||
db_container_name = f"{self.data['ServiceName']}-{self.data['ServiceName']}-db-1"
|
db_container = self.find_container_by_service(f"{self.data['ServiceName']}-db")
|
||||||
|
|
||||||
|
if not n8n_container or not db_container:
|
||||||
|
raise DockerDeploymentError("Could not find n8n or database containers")
|
||||||
|
|
||||||
|
n8n_container_name = n8n_container.name
|
||||||
|
db_container_name = db_container.name
|
||||||
|
|
||||||
logging.writeToFile(f'Monitoring containers: {n8n_container_name} and {db_container_name}')
|
logging.writeToFile(f'Monitoring containers: {n8n_container_name} and {db_container_name}')
|
||||||
|
|
||||||
# Check container health
|
# Check container health
|
||||||
@@ -1081,7 +1126,7 @@ services:
|
|||||||
result, status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
result, status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
||||||
|
|
||||||
# Only raise error if container is exited
|
# Only raise error if container is exited
|
||||||
if "exited" in status:
|
if "exited" in status.lower():
|
||||||
# Get container logs
|
# Get container logs
|
||||||
command = f"docker logs {n8n_container_name}"
|
command = f"docker logs {n8n_container_name}"
|
||||||
result, logs = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
result, logs = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
||||||
@@ -1096,19 +1141,16 @@ services:
|
|||||||
# Check if database container is ready
|
# Check if database container is ready
|
||||||
command = f"docker exec {db_container_name} pg_isready -U postgres"
|
command = f"docker exec {db_container_name} pg_isready -U postgres"
|
||||||
result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
result, output = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
||||||
|
|
||||||
if "accepting connections" in output:
|
if "accepting connections" in output:
|
||||||
db_ready = True
|
db_ready = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check container status
|
# Refresh container status
|
||||||
command = f"docker inspect --format='{{{{.State.Status}}}}' {db_container_name}"
|
db_container = self.find_container_by_service(f"{self.data['ServiceName']}-db")
|
||||||
result, db_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
if db_container and db_container.status == 'exited':
|
||||||
|
raise DockerDeploymentError(f"Database container exited")
|
||||||
# Only raise error if database container is in a failed state
|
|
||||||
if db_status == 'exited':
|
|
||||||
raise DockerDeploymentError(f"Database container is in {db_status} state")
|
|
||||||
|
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
logging.writeToFile(f'Waiting for database to be ready, attempt {retry_count}/{max_retries}')
|
logging.writeToFile(f'Waiting for database to be ready, attempt {retry_count}/{max_retries}')
|
||||||
@@ -1117,13 +1159,11 @@ services:
|
|||||||
raise DockerDeploymentError("Database failed to become ready within timeout period")
|
raise DockerDeploymentError("Database failed to become ready within timeout period")
|
||||||
|
|
||||||
# Check n8n container status
|
# Check n8n container status
|
||||||
command = f"docker inspect --format='{{{{.State.Status}}}}' {n8n_container_name}"
|
n8n_container = self.find_container_by_service(self.data['ServiceName'])
|
||||||
result, n8n_status = ProcessUtilities.outputExecutioner(command, None, None, None, 1)
|
if n8n_container and n8n_container.status == 'exited':
|
||||||
|
raise DockerDeploymentError(f"n8n container exited")
|
||||||
# Only raise error if n8n container is in a failed state
|
|
||||||
if n8n_status == 'exited':
|
|
||||||
raise DockerDeploymentError(f"n8n container is in {n8n_status} state")
|
|
||||||
|
|
||||||
|
n8n_status = n8n_container.status if n8n_container else 'unknown'
|
||||||
logging.writeToFile(f'Deployment monitoring completed successfully. n8n status: {n8n_status}, database ready: {db_ready}')
|
logging.writeToFile(f'Deployment monitoring completed successfully. n8n status: {n8n_status}, database ready: {db_ready}')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user