ea006b68c2
The previous sorting fix exposed a deeper bug: ResolvedMount always mounted its staging directory as a single bind mount. When a config profile mount targeted /workspace/x/y and contained a single file z.json, the staging directory (containing only z.json) replaced the ENTIRE /workspace/x/y directory, hiding all sibling files from git repo mounts. - Change apply_resolved_profile to mount each file individually: - source: staging_dir/relative_path - target: expanded_target/relative_path - Sibling files from other mounts are preserved. - Empty mounts produce no volume entries. - Keep volume sorting (parent paths before child paths) which is still necessary for directory mounts and ensures parent dirs exist before file mounts inside them. - Add 4 unit tests for file-level mount behavior. Quality gates: pytest (218 passed, 6 pre-existing), tsc --noEmit (clean)
69 lines
2.5 KiB
Markdown
69 lines
2.5 KiB
Markdown
# Exploration: File-Level Mount Overlays
|
|
|
|
## Follow-up to Mount Specificity Ordering
|
|
|
|
The sorting fix (parent paths before child paths) was correct for directory mounts,
|
|
but it exposed a deeper bug: `ResolvedMount` always mounts its staging directory
|
|
as a single bind mount. When a config profile mount targets `/workspace/x/y` and
|
|
contains a single file `z.json`, the staging directory (containing only `z.json`)
|
|
replaces the ENTIRE `/workspace/x/y` directory, hiding all sibling files from the
|
|
git repo.
|
|
|
|
## Root Cause
|
|
|
|
In `apply_resolved_profile`:
|
|
```python
|
|
volume_mounts.append({
|
|
"source": str(mount_dir), # staging dir with ONLY z.json
|
|
"target": expanded_target, # /workspace/x/y
|
|
"type": "bind",
|
|
})
|
|
```
|
|
|
|
This mounts a directory. Docker bind mounts at a directory path completely replace
|
|
the target directory. There is no merge.
|
|
|
|
## What the User Expects
|
|
|
|
Git repo mount: `/repo/x` → `/workspace/x` (directory with many files)
|
|
Config profile mount: `z.json` → `/workspace/x/y/z.json` (single file overlay)
|
|
|
|
Expected: `/workspace/x/y/` contains all repo files PLUS the overlaid `z.json`.
|
|
Actual (before sorting): parent mount hides child mount (child never visible).
|
|
Actual (after sorting): child directory mount replaces parent subdirectory
|
|
(`/workspace/x/y` now contains ONLY `z.json`).
|
|
|
|
## Solution
|
|
|
|
Mount each file individually instead of the staging directory.
|
|
|
|
```python
|
|
for file_path, content in mount.files.items():
|
|
full_path = mount_dir / file_path
|
|
full_path.write_text(content)
|
|
volume_mounts.append({
|
|
"source": str(full_path),
|
|
"target": os.path.join(expanded_target, file_path),
|
|
"type": "bind",
|
|
})
|
|
```
|
|
|
|
This creates file-level bind mounts. Docker mounts a single file without affecting
|
|
sibling files in the parent directory.
|
|
|
|
## Edge Cases
|
|
|
|
- Empty `files` dict: skip, no mounts created.
|
|
- Nested file paths (`a/b/c.txt`): mount `staging/a/b/c.txt` → `target/a/b/c.txt`.
|
|
Docker creates parent directories as needed.
|
|
- File target already exists in git repo: file mount wins (desired override behavior).
|
|
- No git repo mount (directory doesn't exist in image): Docker creates parent dirs
|
|
for the first file mount.
|
|
|
|
## Sorting Fix Status
|
|
|
|
KEEP the sorting. It is still correct and necessary for cases where directory
|
|
mounts genuinely override parent directories (e.g., a git mount to `/workspace/x`
|
|
and another git mount to `/workspace/x/sub`). File-level mounts also benefit from
|
|
sorting because the parent directory mount must exist before file mounts inside it.
|