merge: preserve profile file bind mount updates

This commit is contained in:
2026-07-22 11:37:54 +02:00
2 changed files with 40 additions and 4 deletions
@@ -515,7 +515,13 @@ def apply_resolved_profile(
files_dir = profile_dir / "files"
mounts_dir = profile_dir / "mounts"
def write_canonical_file(root: Path, relative_path: str, content: str) -> Path | None:
def write_canonical_file(
root: Path,
relative_path: str,
content: str,
*,
preserve_inode: bool = False,
) -> Path | None:
path = root / relative_path
try:
path.resolve().relative_to(root.resolve())
@@ -523,6 +529,12 @@ def apply_resolved_profile(
logger.warning("Profile file path escapes canonical storage: %s", relative_path)
return None
path.parent.mkdir(parents=True, exist_ok=True)
if preserve_inode and path.is_file():
# A file bind mount follows its inode, not its directory entry.
# Replacing this path would leave a running container attached to
# the old inode, so overwrite the existing file in place.
path.write_text(content, encoding="utf-8")
return path
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=path.parent, delete=False
) as temporary_file:
@@ -534,7 +546,9 @@ def apply_resolved_profile(
# Top-level profile files are individual bind mounts under the working
# directory. They therefore cannot mask the workspace directory itself.
for file_path, content in resolved.files.items():
canonical_file = write_canonical_file(files_dir, file_path, content)
canonical_file = write_canonical_file(
files_dir, file_path, content, preserve_inode=True
)
if canonical_file is None:
continue
volume_mounts.append(
@@ -36,7 +36,7 @@ class TestMergeFunctions:
def test_merge_env_vars_tracks_overrides(self) -> None:
"""Test that env var overrides are tracked."""
overrides = {}
overrides: dict[str, str] = {}
_merge_env_vars(
{"A": "1"},
{"A": "2"},
@@ -93,7 +93,7 @@ class TestMergeFunctions:
"""Test that mount mode conflicts are resolved (later wins)."""
from src.services.config.config_profile_resolver import ResolvedMount
overrides = {}
overrides: dict[str, str] = {}
result = _merge_mounts(
{"/app": ResolvedMount(target="/app", mode="rw", files={})},
[{"target": "/app", "mode": "ro", "files": {}}],
@@ -562,6 +562,28 @@ class TestApplyResolvedProfile:
]
assert canonical_file.read_text() == "setting = true"
def test_top_level_file_update_preserves_bind_mount_inode(self, tmp_path) -> None:
"""An individually bind-mounted file must update in place."""
profile_id = uuid.uuid4()
instance_root = tmp_path / "instances"
resolved = ResolvedProfile(
profile_id=profile_id,
profile_name="test",
files={"settings.toml": "value = 1"},
)
apply_resolved_profile(str(instance_root / "instance-a"), resolved)
canonical_file = (
instance_root / "config-profiles" / str(profile_id) / "files" / "settings.toml"
)
original_inode = canonical_file.stat().st_ino
resolved.files["settings.toml"] = "value = 2"
apply_resolved_profile(str(instance_root / "instance-a"), resolved)
assert canonical_file.stat().st_ino == original_inode
assert canonical_file.read_text() == "value = 2"
def test_instances_share_profile_scoped_mount_sources(self, tmp_path) -> None:
"""Different instance paths resolve a profile to one canonical source."""
profile_id = uuid.uuid4()