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:
@@ -0,0 +1,55 @@
|
||||
# Design: Mount Specificity Ordering
|
||||
|
||||
## Helper Function
|
||||
|
||||
Add `sort_volumes_by_specificity` to a shared utilities module. The most appropriate location is `apps/api/src/services/docker.py` since it already contains Docker/Compose helpers, or a new small module. We'll add it to `apps/api/src/services/docker.py` to keep the change minimal.
|
||||
|
||||
## Sorting Logic
|
||||
|
||||
```python
|
||||
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("/")
|
||||
```
|
||||
|
||||
Stable sort: `sorted(volumes, key=_target_depth)`.
|
||||
|
||||
## Warning Logic
|
||||
|
||||
```python
|
||||
def sort_volumes_by_specificity(volumes: list[str]) -> list[str]:
|
||||
from collections import Counter
|
||||
targets = [v.split(":")[1] if ":" in v else "" for v in volumes]
|
||||
duplicates = [t for t, c in Counter(targets).items() if c > 1]
|
||||
if duplicates:
|
||||
logger.warning("Duplicate mount targets detected: %s", duplicates)
|
||||
return sorted(volumes, key=_target_depth)
|
||||
```
|
||||
|
||||
## Call Sites
|
||||
|
||||
### manifest_compiler.py
|
||||
In `compile_compose()`, after building the `volumes` list and before assigning:
|
||||
```python
|
||||
from src.services.docker import sort_volumes_by_specificity
|
||||
volumes = sort_volumes_by_specificity(volumes)
|
||||
service["volumes"] = volumes
|
||||
```
|
||||
|
||||
### tool_instances.py
|
||||
In `_modify_compose_file()`, after appending `extra_volumes` and before write:
|
||||
```python
|
||||
from src.services.docker import sort_volumes_by_specificity
|
||||
service_config["volumes"] = sort_volumes_by_specificity(service_config["volumes"])
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
- `apps/api/src/services/docker.py` — add helper
|
||||
- `apps/api/src/services/manifest_compiler.py` — sort manifest volumes
|
||||
- `apps/api/src/api/tool_instances.py` — sort legacy volumes
|
||||
- `apps/api/tests/unit/test_docker_service.py` — add tests
|
||||
Reference in New Issue
Block a user