fix: sort mount volumes by specificity to prevent parent mounts hiding children
When git repo mounts and regular file mounts have overlapping target paths, broader parent mounts hide deeper child mounts because Docker Compose applies volumes in array order. - Add sort_volumes_by_specificity() to docker.py: - Sorts by target path depth (parent paths first, child paths last) - Logs warnings for duplicate targets - Handles :bind and :ro suffixes correctly - Integrate into manifest flow (compile_compose): - Sorts manifest mounts + EXTRA_VOLUMES before writing compose - Integrate into legacy flow (_modify_compose_file): - Sorts after appending extra_volumes to existing template volumes - Add 6 unit tests covering parent/child ordering, stable sort, type suffixes, empty list, single volume, and duplicate warnings. Quality gates: pytest (214 passed, 6 pre-existing), tsc --noEmit (clean)
This commit is contained in:
@@ -1,12 +1,54 @@
|
||||
"""Docker service for managing tool instances."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sort_volumes_by_specificity(volumes: list[str]) -> list[str]:
|
||||
"""Sort volume strings so parent paths come before child paths.
|
||||
|
||||
Docker Compose mounts volumes in array order. A later mount at a parent
|
||||
path hides earlier mounts at child paths. By sorting shallow paths first
|
||||
and deep paths last, deeper (more specific) mounts overlay correctly.
|
||||
|
||||
Volume format: source:target or source:target:type
|
||||
|
||||
Args:
|
||||
volumes: List of Docker volume mount strings.
|
||||
|
||||
Returns:
|
||||
Sorted list with parent paths before child paths.
|
||||
"""
|
||||
|
||||
def _target_depth(vol: str) -> int:
|
||||
parts = vol.split(":")
|
||||
if len(parts) < 2:
|
||||
return 0
|
||||
target = parts[1].rstrip("/")
|
||||
if not target or target == "/":
|
||||
return 0
|
||||
return target.count("/")
|
||||
|
||||
# Detect duplicate targets and warn
|
||||
targets = []
|
||||
for vol in volumes:
|
||||
parts = vol.split(":")
|
||||
targets.append(parts[1] if len(parts) > 1 else "")
|
||||
dupes = [t for t, c in Counter(targets).items() if c > 1]
|
||||
if dupes:
|
||||
logger.warning("Duplicate mount targets detected: %s", dupes)
|
||||
|
||||
# Stable sort: parent paths first, child paths last
|
||||
return sorted(volumes, key=_target_depth)
|
||||
|
||||
|
||||
def render_compose_template(template: str, variables: dict[str, Any]) -> str:
|
||||
"""Render a Docker Compose template with variable substitution.
|
||||
|
||||
Reference in New Issue
Block a user