fix: stack profile file mounts onto profile git-mounts to avoid masking
When a config profile declares both a git_mount and a mounts entry for the same directory (e.g. ~/.pi), the generated bind-mounts would mask each other inside the container. Instead, copy the static profile files into the instance-scoped git-mount source directory so the container sees both the cloned repo contents and the static files through a single bind-mount. - Add _stack_profile_mounts_with_git_mounts helper to merge overlapping profile mounts into git-mount sources. - Integrate stacking into start_tool_instance after resolving both mount types. - Add unit tests for exact, descendant, non-overlapping, and file cases.
This commit is contained in:
@@ -5,6 +5,7 @@ import contextlib
|
||||
import glob as glob_module
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
@@ -139,6 +140,79 @@ def _chown_staged_mounts(
|
||||
_chown_path(source, uid, gid)
|
||||
|
||||
|
||||
def _relative_under(parent: str, child: str) -> str | None:
|
||||
"""Return the relative path of ``child`` under ``parent`` if it is inside.
|
||||
|
||||
Returns ``""`` when the paths are equal. Returns ``None`` when ``child``
|
||||
is not under ``parent``.
|
||||
"""
|
||||
parent = os.path.normpath(parent)
|
||||
child = os.path.normpath(child)
|
||||
if child == parent:
|
||||
return ""
|
||||
prefix = parent + os.sep
|
||||
if child.startswith(prefix):
|
||||
return child[len(prefix) :]
|
||||
return None
|
||||
|
||||
|
||||
def _stack_profile_mounts_with_git_mounts(
|
||||
profile_mounts: list[dict],
|
||||
git_mount_volumes: list[dict],
|
||||
) -> list[dict]:
|
||||
"""Merge profile file mounts into overlapping git-mount sources.
|
||||
|
||||
When a config profile mounts static files to the same directory as a
|
||||
git-mount (e.g. ``~/.pi``), a directory-level bind mount for the profile
|
||||
would mask the cloned repository. Instead, copy the profile files into
|
||||
the git-mount source directory so the container sees both sets of files
|
||||
through a single bind mount.
|
||||
|
||||
Profile mounts whose target is a child of a git-mount target are copied
|
||||
into the corresponding subdirectory. Mounts that do not overlap are
|
||||
returned unchanged.
|
||||
"""
|
||||
remaining: list[dict] = []
|
||||
for pvol in profile_mounts:
|
||||
p_source = pvol.get("source", "")
|
||||
p_target = pvol.get("target", "")
|
||||
if not p_source or not os.path.exists(p_source):
|
||||
remaining.append(pvol)
|
||||
continue
|
||||
|
||||
merged = False
|
||||
for gvol in git_mount_volumes:
|
||||
g_source = gvol.get("source", "")
|
||||
g_target = gvol.get("target", "")
|
||||
if not g_source or not os.path.isdir(g_source):
|
||||
continue
|
||||
|
||||
rel = _relative_under(g_target, p_target)
|
||||
if rel is None:
|
||||
continue
|
||||
|
||||
dst = os.path.join(g_source, rel) if rel else g_source
|
||||
if os.path.isdir(p_source):
|
||||
shutil.copytree(p_source, dst, dirs_exist_ok=True)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.copy2(p_source, dst)
|
||||
|
||||
logger.debug(
|
||||
"Stacked profile mount %s into git mount %s at %s",
|
||||
p_target,
|
||||
g_target,
|
||||
dst,
|
||||
)
|
||||
merged = True
|
||||
break
|
||||
|
||||
if not merged:
|
||||
remaining.append(pvol)
|
||||
|
||||
return remaining
|
||||
|
||||
|
||||
async def resolve_git_mounts(
|
||||
session: AsyncSession,
|
||||
resolved: ResolvedProfile,
|
||||
@@ -1323,19 +1397,25 @@ async def start_tool_instance(
|
||||
port_override = profile_hints["port_override"]
|
||||
env_vars.update(profile_env)
|
||||
config_files.update(profile_files)
|
||||
extra_volumes.extend(profile_mounts)
|
||||
git_mount_volumes = await resolve_git_mounts(
|
||||
session, resolved, instance_dir, working_directory, home_dir
|
||||
)
|
||||
# Stack static file mounts on top of git repo mounts so they do
|
||||
# not mask each other when they target the same directory.
|
||||
stacked_profile_mounts = _stack_profile_mounts_with_git_mounts(
|
||||
profile_mounts, git_mount_volumes
|
||||
)
|
||||
extra_volumes.extend(stacked_profile_mounts)
|
||||
extra_volumes.extend(git_mount_volumes)
|
||||
logger.debug(
|
||||
"Applied config profile %s to instance %s (env=%d, files=%d, mounts=%d, git_mounts=%d)",
|
||||
"Applied config profile %s to instance %s (env=%d, files=%d, mounts=%d, git_mounts=%d, stacked=%d)",
|
||||
resolved.profile_name,
|
||||
instance.id,
|
||||
len(profile_env),
|
||||
len(profile_files),
|
||||
len(profile_mounts),
|
||||
len(git_mount_volumes),
|
||||
len(profile_mounts) - len(stacked_profile_mounts),
|
||||
)
|
||||
except ConfigProfileCycleError as exc:
|
||||
logger.error(
|
||||
|
||||
Reference in New Issue
Block a user