Skip to content

Commit ea529b7

Browse files
committed
feat(env): Add support for --mount parameter
1 parent 796cbd9 commit ea529b7

File tree

2 files changed

+266
-39
lines changed

2 files changed

+266
-39
lines changed

src/dda/env/dev/types/linux_container.py

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@ class LinuxContainerConfig(DeveloperEnvironmentConfig):
9393
}
9494
),
9595
] = msgspec.field(default_factory=list)
96+
extra_mount_specs: Annotated[
97+
list[str],
98+
msgspec.Meta(
99+
extra={
100+
"params": ["-m", "--mount"],
101+
"help": (
102+
"""\
103+
Additional mounts to be added to the dev env. These can be either bind mounts from the host or Docker volume mounts.
104+
This option may be supplied multiple times, and has the same syntax as the `-m/--mount` flag of `docker run`. Examples:
105+
106+
- `type=bind,src=/tmp/some-location,dst=/location` (bind mount from absolute path on host to container)
107+
- `type=bind,src=./some-location,dst=/location` (bind mount from relative path on host to container)
108+
- `type=volume,src=some-volume,dst=/location` (volume mount from named volume to container)
109+
- `type=bind,src=/tmp/some-location,dst=/location,ro` (bind mount from absolute path on host to container with read-only flag)
110+
- `type=bind,src=/tmp/some-location,dst=/location,bind-propagation=rslave` (bind mount from absolute path on host to container with bind propagation flag)
111+
"""
112+
),
113+
"callback": __validate_extra_mount_specs,
114+
}
115+
),
116+
] = msgspec.field(default_factory=list)
96117

97118

98119
class LinuxContainer(DeveloperEnvironmentInterface[LinuxContainerConfig]):
@@ -412,14 +433,18 @@ def extra_mounts(self) -> list[Mount]:
412433
mounts = []
413434
for spec in self.config.extra_volume_specs:
414435
src, dst, *extra = spec.split(":", 2)
415-
flags = extra[0].split(",") if extra else []
416-
read_only = "ro" in flags
417-
volume_options = {flag.split("=")[0]: flag.split("=")[1] for flag in flags if "=" in flag}
418-
419-
# Check if the source is a path. If not, it's a named volume.
420-
# Note that Docker only recognizes relative paths if they are prefixed with `.`.
421-
mount_type: Literal["bind", "volume"] = "bind" if src.startswith(("/", ".")) else "volume"
422-
436+
read_only = "ro" in extra
437+
438+
# Volume specs are always bind mounts.
439+
mounts.append(Mount(type="bind", path=dst, source=src, read_only=read_only))
440+
441+
for spec in self.config.extra_mount_specs:
442+
type_spec, src_spec, dst_spec, *flag_specs = spec.split(",")
443+
mount_type: Literal["bind", "volume"] = "bind" if type_spec == "type=bind" else "volume"
444+
src = src_spec.split("=")[1]
445+
dst = dst_spec.split("=")[1]
446+
read_only = any(flag in flag_specs for flag in ("readonly", "ro"))
447+
volume_options = dict(flag.split("=", 1) for flag in flag_specs if "=" in flag)
423448
mounts.append(
424449
Mount(type=mount_type, path=dst, source=src, read_only=read_only, volume_options=volume_options)
425450
)
@@ -457,25 +482,81 @@ def repo_path(self, repo: str | None) -> str:
457482
return f"{self.home_dir}/repos/{repo}"
458483

459484

460-
def __validate_extra_volume_specs(_ctx: Context, _param: Option, value: list[str]) -> list[str]:
485+
def __validate_mount_src_dst(mount_type: Literal["bind", "volume"], src: str, dst: str) -> None:
461486
from click import BadParameter
462487

463488
from dda.utils.fs import Path
464489

490+
# Only validate src for bind mounts, since volume mounts sources are pretty much unconstrained.
491+
if mount_type == "bind" and not Path(src).exists():
492+
# NOTE: We have here a slight discrepancy with the behavior of `docker run -v`, which will create the directory if it doesn't exist.
493+
msg = f"Invalid volume source: {src}. Source must be an existing path on the host."
494+
raise BadParameter(msg)
495+
496+
# Destination must always be an absolute path.
497+
if not Path(dst).is_absolute():
498+
msg = f"Invalid volume destination: {dst}. Destination must be an absolute path."
499+
raise BadParameter(msg)
500+
501+
502+
def __validate_extra_volume_specs(_ctx: Context, _param: Option, value: list[str]) -> list[str]:
503+
from click import BadParameter
504+
465505
if not value:
466506
return value
467507

468508
for spec in value:
469-
src, dst, *extra = spec.split(":", 2)
509+
try:
510+
src, dst, *extra = spec.split(":", 2)
511+
except ValueError as e:
512+
msg = f"Invalid volume spec: {spec}. Expected format: <source>:<destination>[:<flag>]"
513+
raise BadParameter(msg) from e
470514
flag = extra[0] if extra else None
471-
if flag not in {None, "ro", "rw"}:
515+
if flag not in {None, "readonly", "ro"}:
472516
msg = f"Invalid volume flag: {flag}. Only the `ro` flag is supported."
473517
raise BadParameter(msg)
474-
if not Path(src).exists():
475-
# NOTE: We have here a slight discrepancy with the behavior of `docker run -v`, which will create the directory if it doesn't exist.
476-
msg = f"Invalid volume source: {src}. Source must be an existing path on the host."
518+
# Volume specs are always bind mounts.
519+
__validate_mount_src_dst("bind", src, dst)
520+
521+
return value
522+
523+
524+
def __validate_extra_mount_specs(_ctx: Context, _param: Option, value: list[str]) -> list[str]:
525+
from click import BadParameter
526+
527+
if not value:
528+
return value
529+
530+
for spec in value:
531+
try:
532+
type_spec, src_spec, dst_spec, *flags = spec.split(",")
533+
except ValueError as e:
534+
msg = f"Invalid mount spec: {spec}. Expected format: type=<type>,src=<source>,dst=<destination>,[<flag>=<value>, ...]"
535+
raise BadParameter(msg) from e
536+
if type_spec not in {"type=bind", "type=volume"}:
537+
msg = f"Invalid mount type: {type_spec}. Only `bind` and `volume` are supported."
538+
raise BadParameter(msg)
539+
if not src_spec.startswith(("src=", "source=")):
540+
msg = f"Invalid mount source: {src_spec}. Source must be prefixed with `src=`."
477541
raise BadParameter(msg)
478-
if not Path(dst).is_absolute():
479-
msg = f"Invalid volume destination: {dst}. Destination must be an absolute path."
542+
if not dst_spec.startswith(("dst=", "destination=", "target=")):
543+
msg = f"Invalid mount destination: {dst_spec}. Destination must be prefixed with `dst=`."
480544
raise BadParameter(msg)
545+
mount_type: Literal["bind", "volume"] = "bind" if type_spec == "type=bind" else "volume"
546+
src = src_spec.removeprefix("src=").removeprefix("source=")
547+
dst = dst_spec.removeprefix("dst=").removeprefix("destination=").removeprefix("target=")
548+
__validate_mount_src_dst(mount_type, src, dst)
549+
550+
for flag in flags:
551+
if flag in {"readonly", "ro"}:
552+
continue
553+
key, _ = flag.split("=", 1)
554+
555+
# Only support the `bind-propagation` flag for bind mounts.
556+
if mount_type == "bind" and key != "bind-propagation":
557+
msg = f"Invalid mount flag: {flag}. Only the `bind-propagation` flag is supported for bind mounts."
558+
raise BadParameter(msg)
559+
if mount_type == "volume" and key not in {"volume-subpath", "volume-opt", "volume-nocopy"}:
560+
msg = f"Invalid mount flag: {flag}. Only the `volume-subpath`, `volume-opt`, and `volume-nocopy` flags are supported for volume mounts."
561+
raise BadParameter(msg)
481562
return value

0 commit comments

Comments
 (0)