utils_devops 0.1.136__py3-none-any.whl → 0.1.137__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.
@@ -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 _perform_health_checks_with_retries(
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 with multiple retries and detailed logging."""
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
- for attempt in range(1, retries + 1):
1276
- logger.info(f"🏥 Health check attempt {attempt}/{retries}...")
1277
-
1278
- # Use the enhanced wait_for_healthy function
1279
- health_success = wait_for_healthy(
1280
- compose_file=compose_file,
1281
- services=services,
1282
- timeout=timeout,
1283
- interval=interval,
1284
- logger=logger,
1285
- env_file=env_file
1286
- )
1287
-
1288
- # Get detailed health status for all services
1289
- detailed_health = _get_detailed_health_status(compose_file, services, logger)
1290
-
1291
- attempt_result = {
1292
- "attempt": attempt,
1293
- "success": health_success,
1294
- "timestamp": dt_ops.current_datetime().isoformat(),
1295
- "service_status": detailed_health
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 False, health_details
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': service_status.get('healthy', False),
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 = 510,
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 retries
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, # <-- Track this in result
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" Compose: {compose_file}")
2199
- logger.info(f" Health timeout: {health_timeout}s, retries: {health_retries}")
2200
- logger.info(f" Keep services after test: {keep_compose_up}") # <-- Log the flag
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 1: Validating configuration...")
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
- # Try to resolve conflicts automatically
2239
- resolve_conflicts(compose_file, project_name=project_name,env_file=env_file)
2240
- logger.info("✅ Conflicts resolved")
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
- up_result = compose_up(
2287
- compose_file,
2288
- services=services,
2289
- project_name=project_name,
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 not up_result.get('success', 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 retries
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 = _perform_health_checks_with_retries(
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 not status.get("healthy", False)
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:]: # Last 10 error lines
2358
- logger.error(f" {line}")
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: # <-- Condition changed
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: # <-- Condition changed
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
- # This would require container inspection to get port mappings
4146
- # Simplified version - we'd need to inspect each container
4147
- pass
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.136
3
+ Version: 0.1.137
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=7941chrSL1OnbvQCOM9Uz7TRtalKDPs4yNFkPre3YUA,175029
12
+ utils_devops/extras/docker_ops.py,sha256=FSckb4TpwCtQW3xqbIru6bkfAWicbpDMOvM6zm010Ps,177442
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.136.dist-info/METADATA,sha256=BVHNdxH50ZdVlV5w2uN21e_klGqdvwg168g7fWVnh90,1903
23
- utils_devops-0.1.136.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
24
- utils_devops-0.1.136.dist-info/entry_points.txt,sha256=ei3B6ZL5yu6dOq-U1r8wsBdkXeg63RAyV7m8_ADaE6k,53
25
- utils_devops-0.1.136.dist-info/RECORD,,
22
+ utils_devops-0.1.137.dist-info/METADATA,sha256=GHgezDlgTJsAvAaC0dCRutrigeUnQwOrDBsi_4JQy6c,1903
23
+ utils_devops-0.1.137.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
24
+ utils_devops-0.1.137.dist-info/entry_points.txt,sha256=ei3B6ZL5yu6dOq-U1r8wsBdkXeg63RAyV7m8_ADaE6k,53
25
+ utils_devops-0.1.137.dist-info/RECORD,,