@@ -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
98119class 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