From 924166d643764e1eeb1e92fde4542be5e4c4d84c Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Wed, 12 Mar 2025 12:00:08 -0600 Subject: [PATCH] feat(volume): add mc config volume command --- README.md | 17 ++++++ mcontainer/cli.py | 123 +++++++++++++++++++++++++++++++++++++- mcontainer/user_config.py | 1 + 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc8220e..c8ada1e 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,23 @@ mc config network add teamnet mc config network remove teamnet ``` +### Default Volumes Configuration + +You can configure default volumes that will be automatically mounted in every new session: + +```bash +# List default volumes +mc config volume list + +# Add a volume to defaults +mc config volume add /local/path:/container/path + +# Remove a volume from defaults (will prompt if multiple matches found) +mc config volume remove /local/path +``` + +Default volumes will be combined with any volumes specified using the `-v` flag when creating a session. + ### External Network Connectivity MC containers can connect to external Docker networks, allowing them to communicate with other services in those networks: diff --git a/mcontainer/cli.py b/mcontainer/cli.py index 9cd7e4f..59bcd54 100644 --- a/mcontainer/cli.py +++ b/mcontainer/cli.py @@ -151,7 +151,14 @@ def create_session( # Parse volume mounts volume_mounts = {} - for vol in volume: + + # Get default volumes from user config + default_volumes = user_config.get("defaults.volumes", []) + + # Combine default volumes with user-specified volumes + all_volumes = default_volumes + list(volume) + + for vol in all_volumes: if ":" in vol: local_path, container_path = vol.split(":", 1) # Convert to absolute path if relative @@ -165,7 +172,7 @@ def create_session( ) continue - # Add to volume mounts + # Add to volume mounts (later entries override earlier ones with same host path) volume_mounts[local_path] = {"bind": container_path, "mode": "rw"} else: console.print( @@ -181,6 +188,12 @@ def create_session( if all_networks: console.print(f"Networks: {', '.join(all_networks)}") + # Show volumes that will be mounted + if volume_mounts: + console.print("Volumes:") + for host_path, mount_info in volume_mounts.items(): + console.print(f" {host_path} -> {mount_info['bind']}") + with console.status(f"Creating session with driver '{driver}'..."): session = container_manager.create_session( driver_name=driver, @@ -473,6 +486,10 @@ def driver_info( network_app = typer.Typer(help="Manage default networks") config_app.add_typer(network_app, name="network", no_args_is_help=True) +# Create a volume subcommand for config +volume_app = typer.Typer(help="Manage default volumes") +config_app.add_typer(volume_app, name="volume", no_args_is_help=True) + # Configuration commands @config_app.command("list") @@ -620,5 +637,107 @@ def remove_network( console.print(f"[green]Removed network '{network}' from defaults[/green]") +# Volume configuration commands +@volume_app.command("list") +def list_volumes() -> None: + """List all default volumes""" + volumes = user_config.get("defaults.volumes", []) + + if not volumes: + console.print("No default volumes configured") + return + + table = Table(show_header=True, header_style="bold") + table.add_column("Local Path") + table.add_column("Container Path") + + for volume in volumes: + if ":" in volume: + local_path, container_path = volume.split(":", 1) + table.add_row(local_path, container_path) + else: + table.add_row(volume, "[yellow]Invalid format[/yellow]") + + console.print(table) + + +@volume_app.command("add") +def add_volume( + volume: str = typer.Argument( + ..., help="Volume to add (format: LOCAL_PATH:CONTAINER_PATH)" + ), +) -> None: + """Add a volume to default volumes""" + volumes = user_config.get("defaults.volumes", []) + + # Validate format + if ":" not in volume: + console.print( + "[red]Invalid volume format. Use LOCAL_PATH:CONTAINER_PATH.[/red]" + ) + return + + local_path, container_path = volume.split(":", 1) + + # Convert to absolute path if relative + if not os.path.isabs(local_path): + local_path = os.path.abspath(local_path) + volume = f"{local_path}:{container_path}" + + # Validate local path exists + if not os.path.exists(local_path): + console.print( + f"[yellow]Warning: Local path '{local_path}' does not exist.[/yellow]" + ) + if not typer.confirm("Add anyway?"): + return + + # Check if volume is already in defaults + if volume in volumes: + console.print(f"Volume '{volume}' is already in defaults") + return + + volumes.append(volume) + user_config.set("defaults.volumes", volumes) + console.print(f"[green]Added volume '{volume}' to defaults[/green]") + + +@volume_app.command("remove") +def remove_volume( + volume: str = typer.Argument( + ..., help="Volume to remove (format: LOCAL_PATH:CONTAINER_PATH)" + ), +) -> None: + """Remove a volume from default volumes""" + volumes = user_config.get("defaults.volumes", []) + + # Handle case where user provides just a prefix to match + matching_volumes = [v for v in volumes if v.startswith(volume)] + + if not matching_volumes: + console.print(f"No volumes matching '{volume}' found in defaults") + return + + if len(matching_volumes) > 1: + console.print(f"Multiple volumes match '{volume}':") + for i, v in enumerate(matching_volumes): + console.print(f" {i + 1}. {v}") + + index = typer.prompt( + "Enter the number of the volume to remove (0 to cancel)", type=int + ) + if index == 0 or index > len(matching_volumes): + console.print("Volume removal canceled") + return + + volume_to_remove = matching_volumes[index - 1] + else: + volume_to_remove = matching_volumes[0] + + volumes.remove(volume_to_remove) + user_config.set("defaults.volumes", volumes) + console.print(f"[green]Removed volume '{volume_to_remove}' from defaults[/green]") + + if __name__ == "__main__": app() diff --git a/mcontainer/user_config.py b/mcontainer/user_config.py index 624ef62..e8c1d60 100644 --- a/mcontainer/user_config.py +++ b/mcontainer/user_config.py @@ -92,6 +92,7 @@ class UserConfigManager: "connect": True, "mount_local": True, "networks": [], # Default networks to connect to (besides mc-network) + "volumes": [], # Default volumes to mount, format: "source:dest" }, "services": { "langfuse": {},