utils_devops 0.1.136__py3-none-any.whl → 0.1.138__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- utils_devops/extras/docker_ops.py +194 -158
- {utils_devops-0.1.136.dist-info → utils_devops-0.1.138.dist-info}/METADATA +1 -1
- {utils_devops-0.1.136.dist-info → utils_devops-0.1.138.dist-info}/RECORD +5 -5
- {utils_devops-0.1.136.dist-info → utils_devops-0.1.138.dist-info}/WHEEL +0 -0
- {utils_devops-0.1.136.dist-info → utils_devops-0.1.138.dist-info}/entry_points.txt +0 -0
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import re
|
|
9
10
|
from logging import Logger
|
|
10
11
|
import re
|
|
11
12
|
import sys
|
|
@@ -849,6 +850,32 @@ def health_check_docker_compose(compose_file: str) -> bool:
|
|
|
849
850
|
logger.error(f"Health check failed: {e}")
|
|
850
851
|
return False
|
|
851
852
|
|
|
853
|
+
def _compute_overall_status(service_status: Dict[str, Any]) -> str:
|
|
854
|
+
"""Compute overall status from Docker status and health."""
|
|
855
|
+
if not service_status:
|
|
856
|
+
return "not found"
|
|
857
|
+
|
|
858
|
+
status = service_status.get('status', 'not found').lower()
|
|
859
|
+
health = service_status.get('health', 'none').lower()
|
|
860
|
+
|
|
861
|
+
if status == 'restarting':
|
|
862
|
+
return "restarting"
|
|
863
|
+
|
|
864
|
+
if status in ['created', 'paused']: # Treat as starting or unhealthy based on context
|
|
865
|
+
return "starting" if health == 'starting' else "unhealthy"
|
|
866
|
+
|
|
867
|
+
if status != 'running':
|
|
868
|
+
return "unhealthy" # exited, dead, etc.
|
|
869
|
+
|
|
870
|
+
if health == 'starting':
|
|
871
|
+
return "starting"
|
|
872
|
+
elif health == 'healthy':
|
|
873
|
+
return "healthy"
|
|
874
|
+
elif health == 'unhealthy':
|
|
875
|
+
return "unhealthy"
|
|
876
|
+
else:
|
|
877
|
+
return "unhealthy" # unknown or none
|
|
878
|
+
|
|
852
879
|
def check_health_from_compose_ps(compose_file: str) -> Dict[str, Any]:
|
|
853
880
|
"""
|
|
854
881
|
Direct health check by parsing docker compose ps --format json
|
|
@@ -1254,96 +1281,74 @@ def _check_missing_images(
|
|
|
1254
1281
|
|
|
1255
1282
|
return missing_services
|
|
1256
1283
|
|
|
1257
|
-
def
|
|
1284
|
+
def _perform_health_checks(
|
|
1258
1285
|
compose_file: str,
|
|
1259
1286
|
services: List[str],
|
|
1260
1287
|
timeout: int,
|
|
1261
|
-
retries: int,
|
|
1262
1288
|
interval: int,
|
|
1263
1289
|
logger: logger,
|
|
1264
1290
|
env_file: Optional[str] = None
|
|
1265
1291
|
) -> Tuple[bool, Dict[str, Any]]:
|
|
1266
|
-
"""Perform health checks
|
|
1292
|
+
"""Perform health checks in a single phase with total timeout."""
|
|
1267
1293
|
health_details = {
|
|
1268
1294
|
"success": False,
|
|
1269
|
-
"attempts": [],
|
|
1270
1295
|
"service_status": {},
|
|
1271
1296
|
"final_healthy_count": 0,
|
|
1272
1297
|
"final_unhealthy_count": 0
|
|
1273
1298
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
}
|
|
1297
|
-
health_details["attempts"].append(attempt_result)
|
|
1298
|
-
|
|
1299
|
-
healthy_count = sum(1 for status in detailed_health.values() if status.get('healthy', False))
|
|
1300
|
-
unhealthy_count = len(services) - healthy_count
|
|
1301
|
-
|
|
1302
|
-
logger.info(f" Healthy: {healthy_count}/{len(services)}, Unhealthy: {unhealthy_count}")
|
|
1303
|
-
|
|
1304
|
-
if health_success:
|
|
1305
|
-
health_details["success"] = True
|
|
1306
|
-
health_details["service_status"] = detailed_health
|
|
1307
|
-
health_details["final_healthy_count"] = healthy_count
|
|
1308
|
-
health_details["final_unhealthy_count"] = unhealthy_count
|
|
1309
|
-
return True, health_details
|
|
1310
|
-
|
|
1311
|
-
# Wait before retry (except on last attempt)
|
|
1312
|
-
if attempt < retries:
|
|
1313
|
-
retry_interval = interval * 2 # Exponential backoff
|
|
1314
|
-
logger.info(f"⏳ Waiting {retry_interval}s before retry...")
|
|
1315
|
-
dt_ops.time.sleep(retry_interval)
|
|
1316
|
-
|
|
1317
|
-
# All retries failed
|
|
1318
|
-
health_details["success"] = False
|
|
1299
|
+
|
|
1300
|
+
logger.info(f"🏥 Performing health check (timeout: {timeout}s, interval: {interval}s)...")
|
|
1301
|
+
|
|
1302
|
+
# Use the enhanced wait_for_healthy function
|
|
1303
|
+
health_success = wait_for_healthy(
|
|
1304
|
+
compose_file=compose_file,
|
|
1305
|
+
services=services,
|
|
1306
|
+
timeout=timeout,
|
|
1307
|
+
interval=interval,
|
|
1308
|
+
logger=logger,
|
|
1309
|
+
env_file=env_file
|
|
1310
|
+
)
|
|
1311
|
+
|
|
1312
|
+
# Get detailed health status for all services
|
|
1313
|
+
detailed_health = _get_detailed_health_status(compose_file, services, logger)
|
|
1314
|
+
|
|
1315
|
+
healthy_count = sum(1 for status in detailed_health.values() if status.get('overall_status') == 'healthy')
|
|
1316
|
+
unhealthy_count = len(services) - healthy_count
|
|
1317
|
+
|
|
1318
|
+
logger.info(f" Healthy: {healthy_count}/{len(services)}, Unhealthy: {unhealthy_count}")
|
|
1319
|
+
|
|
1320
|
+
health_details["success"] = health_success
|
|
1319
1321
|
health_details["service_status"] = detailed_health
|
|
1320
1322
|
health_details["final_healthy_count"] = healthy_count
|
|
1321
1323
|
health_details["final_unhealthy_count"] = unhealthy_count
|
|
1322
|
-
|
|
1323
|
-
return
|
|
1324
|
+
|
|
1325
|
+
return health_success, health_details
|
|
1324
1326
|
|
|
1325
1327
|
def _get_detailed_health_status(
|
|
1326
1328
|
compose_file: str,
|
|
1327
1329
|
services: List[str],
|
|
1328
1330
|
logger: logger
|
|
1329
1331
|
) -> Dict[str, Dict[str, Any]]:
|
|
1330
|
-
"""Get detailed health status for each service."""
|
|
1332
|
+
"""Get detailed health status for each service with overall_status."""
|
|
1331
1333
|
detailed_status = {}
|
|
1332
|
-
|
|
1334
|
+
|
|
1333
1335
|
try:
|
|
1334
1336
|
# Use the reliable JSON parsing method
|
|
1335
1337
|
health_result = check_health_from_compose_ps(compose_file)
|
|
1336
1338
|
all_services_status = health_result.get('services', {})
|
|
1337
|
-
|
|
1339
|
+
|
|
1338
1340
|
for service in services:
|
|
1339
1341
|
service_status = all_services_status.get(service, {})
|
|
1342
|
+
overall_status = _compute_overall_status(service_status)
|
|
1340
1343
|
detailed_status[service] = {
|
|
1341
|
-
'healthy':
|
|
1344
|
+
'healthy': overall_status == 'healthy',
|
|
1342
1345
|
'health': service_status.get('health', 'unknown'),
|
|
1343
1346
|
'status': service_status.get('status', 'not found'),
|
|
1344
|
-
'error': service_status.get('error', 'No error details')
|
|
1347
|
+
'error': service_status.get('error', 'No error details'),
|
|
1348
|
+
'overall_status': overall_status
|
|
1345
1349
|
}
|
|
1346
|
-
|
|
1350
|
+
logger.info(f" {service}: {overall_status}")
|
|
1351
|
+
|
|
1347
1352
|
except Exception as e:
|
|
1348
1353
|
logger.error(f"Failed to get detailed health status: {e}")
|
|
1349
1354
|
# Fallback: mark all as unhealthy
|
|
@@ -1352,13 +1357,12 @@ def _get_detailed_health_status(
|
|
|
1352
1357
|
'healthy': False,
|
|
1353
1358
|
'health': 'unknown',
|
|
1354
1359
|
'status': 'check failed',
|
|
1355
|
-
'error': str(e)
|
|
1360
|
+
'error': str(e),
|
|
1361
|
+
'overall_status': 'unhealthy'
|
|
1356
1362
|
}
|
|
1357
|
-
|
|
1363
|
+
|
|
1358
1364
|
return detailed_status
|
|
1359
1365
|
|
|
1360
|
-
import os
|
|
1361
|
-
import re
|
|
1362
1366
|
|
|
1363
1367
|
def _expand_env_vars_in_value(value):
|
|
1364
1368
|
"""Expand environment variables in a string value."""
|
|
@@ -2135,49 +2139,55 @@ def test_compose(
|
|
|
2135
2139
|
env_file: str = '.env',
|
|
2136
2140
|
update_version: bool = False,
|
|
2137
2141
|
version_source: str = 'git',
|
|
2138
|
-
|
|
2142
|
+
|
|
2139
2143
|
# Test configuration
|
|
2140
2144
|
services: Optional[List[str]] = None,
|
|
2141
|
-
health_timeout: int =
|
|
2142
|
-
health_retries: int = 5,
|
|
2145
|
+
health_timeout: int = 500, # Changed to represent total max wait time (e.g., 500 seconds)
|
|
2143
2146
|
health_interval: int = 10,
|
|
2144
2147
|
capture_logs: bool = True,
|
|
2145
2148
|
log_tail: int = 100,
|
|
2146
|
-
|
|
2149
|
+
|
|
2147
2150
|
# Startup options
|
|
2148
2151
|
pull_missing: bool = True,
|
|
2149
2152
|
no_build: bool = True,
|
|
2150
2153
|
no_pull: bool = True,
|
|
2151
|
-
|
|
2154
|
+
|
|
2152
2155
|
# Cleanup options
|
|
2153
|
-
keep_compose_up: bool = False,
|
|
2154
|
-
|
|
2156
|
+
keep_compose_up: bool = False,
|
|
2157
|
+
|
|
2155
2158
|
# Additional options
|
|
2156
2159
|
project_name: Optional[str] = None,
|
|
2157
2160
|
dry_run: bool = False,
|
|
2158
2161
|
logger: Optional[logger] = None
|
|
2159
|
-
|
|
2162
|
+
|
|
2160
2163
|
) -> Dict[str, Any]:
|
|
2161
2164
|
"""
|
|
2162
2165
|
Test Docker Compose services with comprehensive health checks and logging.
|
|
2163
|
-
|
|
2166
|
+
|
|
2167
|
+
Improvements:
|
|
2168
|
+
- If compose up fails, capture error and skip health checks.
|
|
2169
|
+
- Enhanced conflict checking: Detailed info for no conflicts, shows conflicts if found, and automatically resolves them (containers and networks; volumes skipped to avoid data loss).
|
|
2170
|
+
- Health statuses expanded to: not found, starting, restarting, healthy, unhealthy.
|
|
2171
|
+
- Simplified health checking: Removed outer retries, now uses a single total health_timeout (e.g., 500s) with checks every health_interval.
|
|
2172
|
+
- Other upgrades: Better error handling around compose_up, more detailed logging, implemented port conflict resolution by removing conflicting containers.
|
|
2173
|
+
|
|
2164
2174
|
Steps:
|
|
2165
2175
|
0. Pre-Test validation
|
|
2166
2176
|
1. Update version (if enabled)
|
|
2167
|
-
2. Check for conflicts
|
|
2177
|
+
2. Check for conflicts and resolve if found
|
|
2168
2178
|
3. Start services with --no-build and --no-pull
|
|
2169
|
-
4. Perform health checks with
|
|
2179
|
+
4. Perform health checks (single phase with total timeout)
|
|
2170
2180
|
5. Capture detailed logs for failing services
|
|
2171
2181
|
6. Bring down services (unless keep_compose_up=True)
|
|
2172
|
-
|
|
2182
|
+
|
|
2173
2183
|
Args:
|
|
2174
2184
|
keep_compose_up: If True, services will not be stopped after test. Default: False.
|
|
2175
|
-
|
|
2185
|
+
|
|
2176
2186
|
Returns: Detailed test results with logs
|
|
2177
2187
|
"""
|
|
2178
2188
|
logger = logger or DEFAULT_LOGGER
|
|
2179
2189
|
test_id = str(uuid.uuid4())[:8]
|
|
2180
|
-
|
|
2190
|
+
|
|
2181
2191
|
result = {
|
|
2182
2192
|
"test_id": test_id,
|
|
2183
2193
|
"success": False,
|
|
@@ -2186,28 +2196,28 @@ def test_compose(
|
|
|
2186
2196
|
"health_status": {},
|
|
2187
2197
|
"failing_services": [],
|
|
2188
2198
|
"logs_captured": {},
|
|
2189
|
-
"keep_compose_up": keep_compose_up,
|
|
2199
|
+
"keep_compose_up": keep_compose_up,
|
|
2190
2200
|
"error": None,
|
|
2191
2201
|
"duration": 0.0
|
|
2192
2202
|
}
|
|
2193
|
-
|
|
2203
|
+
|
|
2194
2204
|
start_time = dt_ops.current_datetime()
|
|
2195
|
-
|
|
2205
|
+
|
|
2196
2206
|
try:
|
|
2197
2207
|
logger.info(f"🧪 Starting test {test_id}")
|
|
2198
|
-
logger.info(f"
|
|
2199
|
-
logger.info(f"
|
|
2200
|
-
logger.info(f"
|
|
2201
|
-
|
|
2202
|
-
|
|
2208
|
+
logger.info(f" Compose: {compose_file}")
|
|
2209
|
+
logger.info(f" Health timeout: {health_timeout}s, interval: {health_interval}s")
|
|
2210
|
+
logger.info(f" Keep services after test: {keep_compose_up}")
|
|
2211
|
+
|
|
2212
|
+
|
|
2203
2213
|
# Step 0: Pre-build validation
|
|
2204
|
-
logger.info("📋 Step
|
|
2214
|
+
logger.info("📋 Step 0: Validating configuration...")
|
|
2205
2215
|
if not files.file_exists(compose_file):
|
|
2206
2216
|
raise DockerOpsError(f"Compose file not found: {compose_file}")
|
|
2207
|
-
|
|
2217
|
+
|
|
2208
2218
|
if not files.file_exists(env_file):
|
|
2209
2219
|
logger.warning(f"Environment file not found: {env_file}")
|
|
2210
|
-
|
|
2220
|
+
|
|
2211
2221
|
# Step 1: Update version (optional)
|
|
2212
2222
|
new_version = None
|
|
2213
2223
|
if update_version and not dry_run:
|
|
@@ -2224,26 +2234,41 @@ def test_compose(
|
|
|
2224
2234
|
logger.info("🔧 Step 1: Version update skipped (dry run)")
|
|
2225
2235
|
else:
|
|
2226
2236
|
logger.info("🔧 Step 1: Version update disabled")
|
|
2227
|
-
|
|
2237
|
+
|
|
2228
2238
|
# Step 2: Check for conflicts
|
|
2229
2239
|
logger.info("🔍 Step 2: Checking for conflicts...")
|
|
2240
|
+
result["steps"]["conflict_check"] = {"status": "no_conflicts", "conflicts": None}
|
|
2230
2241
|
if not dry_run:
|
|
2231
2242
|
conflicts = check_conflicts(compose_file, project_name=project_name, env_file=env_file)
|
|
2232
|
-
if conflicts:
|
|
2243
|
+
if any(conflicts.values()): # Check if any conflict lists are non-empty
|
|
2233
2244
|
logger.warning(f"⚠️ Found conflicts: {conflicts}")
|
|
2234
2245
|
result["steps"]["conflict_check"] = {
|
|
2235
2246
|
"status": "conflicts_found",
|
|
2236
2247
|
"conflicts": conflicts
|
|
2237
2248
|
}
|
|
2238
|
-
#
|
|
2239
|
-
resolve_conflicts(
|
|
2240
|
-
|
|
2249
|
+
# Resolve conflicts (auto-remove containers and networks; skip volumes)
|
|
2250
|
+
resolution = resolve_conflicts(
|
|
2251
|
+
compose_file,
|
|
2252
|
+
remove_conflicting_containers=True,
|
|
2253
|
+
remove_conflicting_networks=True,
|
|
2254
|
+
remove_conflicting_volumes=False,
|
|
2255
|
+
project_name=project_name,
|
|
2256
|
+
env_file=env_file
|
|
2257
|
+
)
|
|
2258
|
+
if resolution["resolved"]:
|
|
2259
|
+
logger.info(f"✅ Resolved conflicts: {resolution['resolved']}")
|
|
2260
|
+
result["steps"]["conflict_resolution"] = {
|
|
2261
|
+
"status": "resolved",
|
|
2262
|
+
"resolved": resolution["resolved"],
|
|
2263
|
+
"errors": resolution["errors"]
|
|
2264
|
+
}
|
|
2265
|
+
if resolution["errors"]:
|
|
2266
|
+
raise DockerOpsError(f"Failed to resolve some conflicts: {resolution['errors']}")
|
|
2241
2267
|
else:
|
|
2242
2268
|
logger.info("✅ No conflicts found")
|
|
2243
|
-
result["steps"]["conflict_check"] = {"status": "no_conflicts"}
|
|
2244
2269
|
else:
|
|
2245
2270
|
logger.info("🔍 Step 2: Conflict check skipped (dry run)")
|
|
2246
|
-
|
|
2271
|
+
|
|
2247
2272
|
# Step 3: Start services
|
|
2248
2273
|
logger.info("🚀 Step 3: Starting services...")
|
|
2249
2274
|
if not dry_run:
|
|
@@ -2251,16 +2276,16 @@ def test_compose(
|
|
|
2251
2276
|
compose_config = read_compose_file(compose_file, env_file)
|
|
2252
2277
|
available_services = list(compose_config.get('services', {}).keys())
|
|
2253
2278
|
target_services = services or available_services
|
|
2254
|
-
|
|
2279
|
+
|
|
2255
2280
|
# Check for missing images
|
|
2256
2281
|
missing_images = _check_missing_images(
|
|
2257
|
-
compose_file,
|
|
2258
|
-
target_services,
|
|
2259
|
-
project_name,
|
|
2282
|
+
compose_file,
|
|
2283
|
+
target_services,
|
|
2284
|
+
project_name,
|
|
2260
2285
|
logger,
|
|
2261
2286
|
env_file
|
|
2262
2287
|
)
|
|
2263
|
-
|
|
2288
|
+
|
|
2264
2289
|
if missing_images and pull_missing:
|
|
2265
2290
|
logger.info(f"⬇️ Pulling {len(missing_images)} missing images...")
|
|
2266
2291
|
for service in missing_images:
|
|
@@ -2270,25 +2295,34 @@ def test_compose(
|
|
|
2270
2295
|
except Exception as e:
|
|
2271
2296
|
logger.error(f"❌ Failed to pull image for {service}: {e}")
|
|
2272
2297
|
raise DockerOpsError(f"Missing image for {service} and pull failed")
|
|
2273
|
-
|
|
2298
|
+
|
|
2274
2299
|
elif missing_images and not pull_missing:
|
|
2275
2300
|
missing_list = ", ".join(missing_images)
|
|
2276
2301
|
raise DockerOpsError(f"Missing images for services: {missing_list}")
|
|
2277
|
-
|
|
2302
|
+
|
|
2278
2303
|
# Start services with no-build and no-pull
|
|
2279
2304
|
up_options = []
|
|
2280
2305
|
if no_build:
|
|
2281
2306
|
up_options.append("--no-build")
|
|
2282
2307
|
if no_pull:
|
|
2283
2308
|
up_options.append("--no-pull")
|
|
2284
|
-
|
|
2309
|
+
|
|
2285
2310
|
logger.info(f"Starting services with options: {up_options}")
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2311
|
+
try:
|
|
2312
|
+
up_result = compose_up(
|
|
2313
|
+
compose_file,
|
|
2314
|
+
services=services,
|
|
2315
|
+
project_name=project_name,
|
|
2316
|
+
)
|
|
2317
|
+
# Assuming compose_up returns a dict with 'success'; adjust if needed
|
|
2318
|
+
if up_result is not True: # If it doesn't raise, check result
|
|
2319
|
+
raise DockerOpsError(f"Compose up failed: {up_result.get('error', 'Unknown error')}")
|
|
2320
|
+
except Exception as e:
|
|
2321
|
+
result["error"] = str(e)
|
|
2322
|
+
logger.error(f"❌ Compose up failed: {e}")
|
|
2323
|
+
result["steps"]["start_services"] = {"success": False, "error": str(e)}
|
|
2324
|
+
raise # Re-raise to skip health checks and go to cleanup
|
|
2325
|
+
|
|
2292
2326
|
result["steps"]["start_services"] = {
|
|
2293
2327
|
"success": True,
|
|
2294
2328
|
"services_started": target_services,
|
|
@@ -2303,37 +2337,36 @@ def test_compose(
|
|
|
2303
2337
|
target_services = services or available_services
|
|
2304
2338
|
result["services_tested"] = target_services
|
|
2305
2339
|
logger.info("🚀 Step 3: Service start skipped (dry run)")
|
|
2306
|
-
|
|
2307
|
-
# Step 4: Health checks with
|
|
2340
|
+
|
|
2341
|
+
# Step 4: Health checks (single phase with total timeout)
|
|
2308
2342
|
if not dry_run:
|
|
2309
2343
|
logger.info("🏥 Step 4: Performing health checks...")
|
|
2310
|
-
health_success, health_details =
|
|
2344
|
+
health_success, health_details = _perform_health_checks(
|
|
2311
2345
|
compose_file=compose_file,
|
|
2312
2346
|
services=target_services,
|
|
2313
2347
|
timeout=health_timeout,
|
|
2314
|
-
retries=health_retries,
|
|
2315
2348
|
interval=health_interval,
|
|
2316
2349
|
logger=logger,
|
|
2317
2350
|
env_file=env_file
|
|
2318
2351
|
)
|
|
2319
|
-
|
|
2352
|
+
|
|
2320
2353
|
result["steps"]["health_check"] = health_details
|
|
2321
2354
|
result["health_status"] = health_details.get("service_status", {})
|
|
2322
|
-
|
|
2323
|
-
# Identify failing services
|
|
2355
|
+
|
|
2356
|
+
# Identify failing services (any not 'healthy')
|
|
2324
2357
|
failing_services = [
|
|
2325
2358
|
svc for svc, status in health_details.get("service_status", {}).items()
|
|
2326
|
-
if
|
|
2359
|
+
if status.get("overall_status") != "healthy"
|
|
2327
2360
|
]
|
|
2328
2361
|
result["failing_services"] = failing_services
|
|
2329
|
-
|
|
2362
|
+
|
|
2330
2363
|
if health_success:
|
|
2331
2364
|
logger.info("✅ All services healthy!")
|
|
2332
2365
|
result["success"] = True
|
|
2333
2366
|
else:
|
|
2334
2367
|
logger.error(f"❌ Health checks failed for {len(failing_services)} services: {failing_services}")
|
|
2335
2368
|
result["success"] = False
|
|
2336
|
-
|
|
2369
|
+
|
|
2337
2370
|
# Step 5: Capture detailed logs for failing services
|
|
2338
2371
|
if capture_logs and failing_services:
|
|
2339
2372
|
logger.info("📋 Step 5: Capturing logs for failing services...")
|
|
@@ -2349,19 +2382,19 @@ def test_compose(
|
|
|
2349
2382
|
"status": "captured",
|
|
2350
2383
|
"services": list(logs.keys())
|
|
2351
2384
|
}
|
|
2352
|
-
|
|
2385
|
+
|
|
2353
2386
|
# Log the most critical errors
|
|
2354
2387
|
for service, log_data in logs.items():
|
|
2355
2388
|
if log_data.get('error_logs'):
|
|
2356
2389
|
logger.error(f"🔴 {service} error logs (last {log_tail} lines):")
|
|
2357
|
-
for line in log_data['error_logs'][-10:]:
|
|
2358
|
-
logger.error(f"
|
|
2390
|
+
for line in log_data['error_logs'][-10:]:
|
|
2391
|
+
logger.error(f" {line}")
|
|
2359
2392
|
else:
|
|
2360
2393
|
logger.info("🏥 Step 4: Health checks skipped (dry run)")
|
|
2361
2394
|
result["success"] = True # Assume success in dry run
|
|
2362
|
-
|
|
2395
|
+
|
|
2363
2396
|
# Step 6: Bring down services (unless keep_compose_up=True)
|
|
2364
|
-
if not dry_run and not keep_compose_up:
|
|
2397
|
+
if not dry_run and not keep_compose_up:
|
|
2365
2398
|
logger.info("🛑 Step 6: Stopping services...")
|
|
2366
2399
|
try:
|
|
2367
2400
|
down_result = compose_down(compose_file, project_name=project_name)
|
|
@@ -2375,10 +2408,10 @@ def test_compose(
|
|
|
2375
2408
|
result["steps"]["cleanup"] = {"status": "skipped", "reason": "keep_compose_up=True"}
|
|
2376
2409
|
elif dry_run:
|
|
2377
2410
|
logger.info("🛑 Step 6: Service cleanup skipped (dry run)")
|
|
2378
|
-
|
|
2411
|
+
|
|
2379
2412
|
# Calculate duration
|
|
2380
2413
|
result["duration"] = (dt_ops.current_datetime() - start_time).total_seconds()
|
|
2381
|
-
|
|
2414
|
+
|
|
2382
2415
|
if result["success"]:
|
|
2383
2416
|
if keep_compose_up:
|
|
2384
2417
|
logger.info(f"🎉 Test {test_id} completed successfully in {result['duration']:.1f}s! Services kept running.")
|
|
@@ -2386,18 +2419,18 @@ def test_compose(
|
|
|
2386
2419
|
logger.info(f"🎉 Test {test_id} completed successfully in {result['duration']:.1f}s!")
|
|
2387
2420
|
else:
|
|
2388
2421
|
logger.error(f"💥 Test {test_id} failed in {result['duration']:.1f}s!")
|
|
2389
|
-
|
|
2422
|
+
|
|
2390
2423
|
return result
|
|
2391
|
-
|
|
2424
|
+
|
|
2392
2425
|
except Exception as e:
|
|
2393
2426
|
# Failure handling
|
|
2394
2427
|
result["duration"] = (dt_ops.current_datetime() - start_time).total_seconds()
|
|
2395
2428
|
result["error"] = str(e)
|
|
2396
|
-
|
|
2429
|
+
|
|
2397
2430
|
logger.error(f"💥 Test {test_id} failed: {e}")
|
|
2398
|
-
|
|
2431
|
+
|
|
2399
2432
|
# Only try to cleanup on failure if keep_compose_up=False
|
|
2400
|
-
if not dry_run and not keep_compose_up:
|
|
2433
|
+
if not dry_run and not keep_compose_up:
|
|
2401
2434
|
try:
|
|
2402
2435
|
logger.info("🛑 Emergency cleanup...")
|
|
2403
2436
|
compose_down(compose_file, project_name=project_name)
|
|
@@ -2405,7 +2438,7 @@ def test_compose(
|
|
|
2405
2438
|
except Exception as cleanup_error:
|
|
2406
2439
|
logger.error(f"💥 Emergency cleanup failed: {cleanup_error}")
|
|
2407
2440
|
result["steps"]["emergency_cleanup"] = {"status": "failed", "error": str(cleanup_error)}
|
|
2408
|
-
|
|
2441
|
+
|
|
2409
2442
|
return result
|
|
2410
2443
|
|
|
2411
2444
|
def build_compose(
|
|
@@ -4139,12 +4172,16 @@ def check_conflicts(
|
|
|
4139
4172
|
return conflicts
|
|
4140
4173
|
|
|
4141
4174
|
def _get_used_ports(containers: List[ContainerInfo]) -> Dict[int, str]:
|
|
4142
|
-
"""Get used host ports from containers"""
|
|
4175
|
+
"""Get used host ports from containers with conflicting container IDs."""
|
|
4143
4176
|
used_ports = {}
|
|
4144
4177
|
for container in containers:
|
|
4145
|
-
#
|
|
4146
|
-
|
|
4147
|
-
|
|
4178
|
+
# Assuming ContainerInfo has ports as dict like {'80/tcp': [{'HostPort': '80'}]}
|
|
4179
|
+
for container_port, host_bindings in container.ports.items():
|
|
4180
|
+
if host_bindings:
|
|
4181
|
+
for binding in host_bindings:
|
|
4182
|
+
host_port = int(binding.get('HostPort', 0))
|
|
4183
|
+
if host_port:
|
|
4184
|
+
used_ports[host_port] = container.id # Use container ID
|
|
4148
4185
|
return used_ports
|
|
4149
4186
|
|
|
4150
4187
|
def _extract_host_port(port_mapping: Union[str, Dict]) -> Optional[int]:
|
|
@@ -4174,33 +4211,22 @@ def resolve_conflicts(
|
|
|
4174
4211
|
) -> Dict[str, Any]:
|
|
4175
4212
|
"""
|
|
4176
4213
|
Detect and resolve conflicts for compose deployment.
|
|
4177
|
-
|
|
4178
|
-
Args:
|
|
4179
|
-
compose_file: Compose file to check
|
|
4180
|
-
remove_conflicting_containers: Remove conflicting containers
|
|
4181
|
-
remove_conflicting_networks: Remove conflicting networks (non-external only)
|
|
4182
|
-
remove_conflicting_volumes: Remove conflicting volumes (non-external only)
|
|
4183
|
-
force: Force removal without confirmation
|
|
4184
|
-
project_name: Project name
|
|
4185
|
-
|
|
4186
|
-
Returns:
|
|
4187
|
-
Resolution results
|
|
4188
4214
|
"""
|
|
4189
4215
|
conflicts = check_conflicts(
|
|
4190
|
-
compose_file,
|
|
4216
|
+
compose_file,
|
|
4191
4217
|
project_name=project_name,
|
|
4192
4218
|
env_file=env_file,
|
|
4193
4219
|
check_volumes=remove_conflicting_volumes
|
|
4194
4220
|
)
|
|
4195
|
-
|
|
4221
|
+
|
|
4196
4222
|
resolution = {
|
|
4197
4223
|
"conflicts_found": conflicts,
|
|
4198
4224
|
"resolved": [],
|
|
4199
4225
|
"errors": [],
|
|
4200
4226
|
"skipped_external": []
|
|
4201
4227
|
}
|
|
4202
|
-
|
|
4203
|
-
# Resolve container conflicts
|
|
4228
|
+
|
|
4229
|
+
# Resolve container conflicts (by name)
|
|
4204
4230
|
for conflict in conflicts.get("container_conflicts", []):
|
|
4205
4231
|
container_id = conflict["conflicting_container"]
|
|
4206
4232
|
try:
|
|
@@ -4211,45 +4237,55 @@ def resolve_conflicts(
|
|
|
4211
4237
|
resolution["errors"].append(f"Failed to remove container {container_id}")
|
|
4212
4238
|
except Exception as e:
|
|
4213
4239
|
resolution["errors"].append(str(e))
|
|
4214
|
-
|
|
4240
|
+
|
|
4241
|
+
# Resolve port conflicts (by removing conflicting containers)
|
|
4242
|
+
for conflict in conflicts.get("port_conflicts", []):
|
|
4243
|
+
container_id = conflict["conflicting_container"]
|
|
4244
|
+
try:
|
|
4245
|
+
if remove_conflicting_containers:
|
|
4246
|
+
if container_remove(container_id, force=force):
|
|
4247
|
+
resolution["resolved"].append(f"port_conflict_container:{container_id}")
|
|
4248
|
+
else:
|
|
4249
|
+
resolution["errors"].append(f"Failed to remove container {container_id} for port conflict")
|
|
4250
|
+
except Exception as e:
|
|
4251
|
+
resolution["errors"].append(str(e))
|
|
4252
|
+
|
|
4215
4253
|
# Resolve network conflicts (only non-external)
|
|
4216
4254
|
for conflict in conflicts.get("network_conflicts", []):
|
|
4217
4255
|
network_name = conflict["network"]
|
|
4218
4256
|
is_external = conflict.get("external", False)
|
|
4219
|
-
|
|
4257
|
+
|
|
4220
4258
|
if is_external:
|
|
4221
4259
|
resolution["skipped_external"].append(f"network:{network_name}")
|
|
4222
4260
|
continue
|
|
4223
|
-
|
|
4261
|
+
|
|
4224
4262
|
try:
|
|
4225
4263
|
if remove_conflicting_networks:
|
|
4226
|
-
# You'll need a function to remove networks
|
|
4227
4264
|
if network_remove(network_name):
|
|
4228
4265
|
resolution["resolved"].append(f"network:{network_name}")
|
|
4229
4266
|
else:
|
|
4230
4267
|
resolution["errors"].append(f"Failed to remove network {network_name}")
|
|
4231
4268
|
except Exception as e:
|
|
4232
4269
|
resolution["errors"].append(str(e))
|
|
4233
|
-
|
|
4270
|
+
|
|
4234
4271
|
# Resolve volume conflicts (only non-external)
|
|
4235
4272
|
for conflict in conflicts.get("volume_conflicts", []):
|
|
4236
4273
|
volume_name = conflict["volume"]
|
|
4237
4274
|
is_external = conflict.get("external", False)
|
|
4238
|
-
|
|
4275
|
+
|
|
4239
4276
|
if is_external:
|
|
4240
4277
|
resolution["skipped_external"].append(f"volume:{volume_name}")
|
|
4241
4278
|
continue
|
|
4242
|
-
|
|
4279
|
+
|
|
4243
4280
|
try:
|
|
4244
4281
|
if remove_conflicting_volumes:
|
|
4245
|
-
# You'll need a function to remove volumes
|
|
4246
4282
|
if volume_remove(volume_name):
|
|
4247
4283
|
resolution["resolved"].append(f"volume:{volume_name}")
|
|
4248
4284
|
else:
|
|
4249
4285
|
resolution["errors"].append(f"Failed to remove volume {volume_name}")
|
|
4250
4286
|
except Exception as e:
|
|
4251
4287
|
resolution["errors"].append(str(e))
|
|
4252
|
-
|
|
4288
|
+
|
|
4253
4289
|
return resolution
|
|
4254
4290
|
|
|
4255
4291
|
# ==================== COMPOSE ENHANCEMENTS ====================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: utils_devops
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.138
|
|
4
4
|
Summary: Lightweight DevOps utilities for automation scripts: config editing (YAML/JSON/INI/.env), templating, diffing, and CLI tools
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: devops,automation,nginx,cli,jinja2,yaml,config,diff,templating,logging,docker,compose,file-ops
|
|
@@ -9,7 +9,7 @@ utils_devops/core/strings.py,sha256=8s0GSjcyTKwLjJjsJ_XfOJxPtyb549icDlU9SUxSvHI,
|
|
|
9
9
|
utils_devops/core/systems.py,sha256=wNbEFUAvbMPdqWN-iXvTzvj5iE9xaWfjZYYvD0EZAH0,47577
|
|
10
10
|
utils_devops/extras/__init__.py,sha256=ZXHeVLHO3_qiW9AY-UQ_YA9cQzmkLGv54a2UbyvtlM0,3571
|
|
11
11
|
utils_devops/extras/aws_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
utils_devops/extras/docker_ops.py,sha256=
|
|
12
|
+
utils_devops/extras/docker_ops.py,sha256=kfu8o_7S7KzAJRTgf-IlD4JjMFcAdUFfNFklvd5EJmY,177429
|
|
13
13
|
utils_devops/extras/git_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
utils_devops/extras/interaction_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
utils_devops/extras/metrics_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -19,7 +19,7 @@ utils_devops/extras/notification_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
|
19
19
|
utils_devops/extras/performance_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
utils_devops/extras/ssh_ops.py,sha256=8I_AF0q76CJOK2qp68w1oro2SVOZ_v7b8OvgDYcE4tg,73741
|
|
21
21
|
utils_devops/extras/vault_ops.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
utils_devops-0.1.
|
|
23
|
-
utils_devops-0.1.
|
|
24
|
-
utils_devops-0.1.
|
|
25
|
-
utils_devops-0.1.
|
|
22
|
+
utils_devops-0.1.138.dist-info/METADATA,sha256=slzjWVVCU_ZM9LWcLfAIY7bdLCNtjYtGFRkTEzIzwWs,1903
|
|
23
|
+
utils_devops-0.1.138.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
24
|
+
utils_devops-0.1.138.dist-info/entry_points.txt,sha256=ei3B6ZL5yu6dOq-U1r8wsBdkXeg63RAyV7m8_ADaE6k,53
|
|
25
|
+
utils_devops-0.1.138.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|