From b72f1eef9af598f2090a0edae8921c16814b3cda Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Tue, 11 Mar 2025 15:58:13 -0600 Subject: [PATCH] feat(volume): add the possibilty to mount local directory into the container (like docker volume) --- README.md | 4 ++++ mcontainer/cli.py | 31 +++++++++++++++++++++++++++++++ mcontainer/container.py | 31 +++++++++++++++++++++++++++---- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e28d3c7..540d3f6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,10 @@ mc session create --driver goose # Create a session with environment variables mc session create -e VAR1=value1 -e VAR2=value2 +# Mount custom volumes (similar to Docker's -v flag) +mc session create -v /local/path:/container/path +mc session create -v ~/data:/data -v ./configs:/etc/app/config + # Shorthand for creating a session with a project repository mc github.com/username/repo ``` diff --git a/mcontainer/cli.py b/mcontainer/cli.py index e4462dc..5dd344a 100644 --- a/mcontainer/cli.py +++ b/mcontainer/cli.py @@ -109,6 +109,9 @@ def create_session( env: List[str] = typer.Option( [], "--env", "-e", help="Environment variables (KEY=VALUE)" ), + volume: List[str] = typer.Option( + [], "--volume", "-v", help="Mount volumes (LOCAL_PATH:CONTAINER_PATH)" + ), name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"), no_connect: bool = typer.Option( False, "--no-connect", help="Don't automatically connect to the session" @@ -139,6 +142,29 @@ def create_session( f"[yellow]Warning: Ignoring invalid environment variable format: {var}[/yellow]" ) + # Parse volume mounts + volume_mounts = {} + for vol in volume: + if ":" in vol: + local_path, container_path = vol.split(":", 1) + # Convert to absolute path if relative + if not os.path.isabs(local_path): + local_path = os.path.abspath(local_path) + + # Validate local path exists + if not os.path.exists(local_path): + console.print( + f"[yellow]Warning: Local path '{local_path}' does not exist. Volume will not be mounted.[/yellow]" + ) + continue + + # Add to volume mounts + volume_mounts[local_path] = {"bind": container_path, "mode": "rw"} + else: + console.print( + f"[yellow]Warning: Ignoring invalid volume format: {vol}. Use LOCAL_PATH:CONTAINER_PATH.[/yellow]" + ) + with console.status(f"Creating session with driver '{driver}'..."): session = container_manager.create_session( driver_name=driver, @@ -146,6 +172,7 @@ def create_session( environment=environment, session_name=name, mount_local=not no_mount and user_config.get("defaults.mount_local", True), + volumes=volume_mounts, ) if session: @@ -284,6 +311,9 @@ def quick_create( env: List[str] = typer.Option( [], "--env", "-e", help="Environment variables (KEY=VALUE)" ), + volume: List[str] = typer.Option( + [], "--volume", "-v", help="Mount volumes (LOCAL_PATH:CONTAINER_PATH)" + ), name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"), no_connect: bool = typer.Option( False, "--no-connect", help="Don't automatically connect to the session" @@ -303,6 +333,7 @@ def quick_create( driver=driver, project=project, env=env, + volume=volume, name=name, no_connect=no_connect, no_mount=no_mount, diff --git a/mcontainer/container.py b/mcontainer/container.py index 574461e..e74b66f 100644 --- a/mcontainer/container.py +++ b/mcontainer/container.py @@ -125,6 +125,7 @@ class ContainerManager: environment: Optional[Dict[str, str]] = None, session_name: Optional[str] = None, mount_local: bool = True, + volumes: Optional[Dict[str, Dict[str, str]]] = None, ) -> Optional[Session]: """Create a new MC session @@ -134,6 +135,7 @@ class ContainerManager: environment: Optional environment variables session_name: Optional session name mount_local: Whether to mount the current directory to /app + volumes: Optional additional volumes to mount (dict of {host_path: {"bind": container_path, "mode": mode}}) """ try: # Validate driver exists @@ -178,25 +180,46 @@ class ContainerManager: self.client.images.pull(driver.image) # Set up volume mounts - volumes = {} + session_volumes = {} + # If project URL is provided, don't mount local directory (will clone into /app) # If no project URL and mount_local is True, mount local directory to /app if not project and mount_local: # Mount current directory to /app in the container current_dir = os.getcwd() - volumes[current_dir] = {"bind": "/app", "mode": "rw"} + session_volumes[current_dir] = {"bind": "/app", "mode": "rw"} print(f"Mounting local directory {current_dir} to /app") elif project: print( f"Project URL provided - container will clone {project} into /app during initialization" ) + # Add user-specified volumes + if volumes: + for host_path, mount_spec in volumes.items(): + container_path = mount_spec["bind"] + # Check for conflicts with /app mount + if container_path == "/app" and not project and mount_local: + print( + "[yellow]Warning: Volume mount to /app conflicts with automatic local directory mount. User-specified mount takes precedence.[/yellow]" + ) + # Remove the automatic mount if there's a conflict + if current_dir in session_volumes: + del session_volumes[current_dir] + + # Add the volume + session_volumes[host_path] = mount_spec + print(f"Mounting volume: {host_path} -> {container_path}") + # Set up persistent project configuration project_config_path = self._get_project_config_path(project) print(f"Using project configuration directory: {project_config_path}") # Mount the project configuration directory - volumes[str(project_config_path)] = {"bind": "/mc-config", "mode": "rw"} + session_volumes[str(project_config_path)] = { + "bind": "/mc-config", + "mode": "rw", + } # Add environment variables for config path env_vars["MC_CONFIG_DIR"] = "/mc-config" @@ -230,7 +253,7 @@ class ContainerManager: tty=True, stdin_open=True, environment=env_vars, - volumes=volumes, + volumes=session_volumes, labels={ "mc.session": "true", "mc.session.id": session_id,