diff --git a/README.md b/README.md index 7a01f77..fc8220e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ mc session create -e VAR1=value1 -e VAR2=value2 mc session create -v /local/path:/container/path mc session create -v ~/data:/data -v ./configs:/etc/app/config +# Connect to external Docker networks +mc session create --network teamnet --network dbnet + # Shorthand for creating a session with a project repository mc github.com/username/repo ``` @@ -124,6 +127,51 @@ mc config set anthropic.api_key "sk-ant-..." mc config reset ``` +### Default Networks Configuration + +You can configure default networks that will be applied to every new session: + +```bash +# List default networks +mc config network list + +# Add a network to defaults +mc config network add teamnet + +# Remove a network from defaults +mc config network remove teamnet +``` + +### External Network Connectivity + +MC containers can connect to external Docker networks, allowing them to communicate with other services in those networks: + +```bash +# Create a session connected to external networks +mc session create --network teamnet --network dbnet +``` + +**Important**: Networks must be "attachable" to be joined by MC containers. Here's how to create attachable networks: + +```bash +# Create an attachable network with Docker +docker network create --driver bridge --attachable teamnet + +# Example docker-compose.yml with attachable network +# docker-compose.yml +version: '3' +services: + web: + image: nginx + networks: + - teamnet + +networks: + teamnet: + driver: bridge + attachable: true # This is required for MC containers to connect +``` + ### Service Credentials Service credentials like API keys configured in `~/.config/mc/config.yaml` are automatically passed to containers as environment variables: diff --git a/SPECIFICATIONS.md b/SPECIFICATIONS.md index 93ea7fa..f750dd7 100644 --- a/SPECIFICATIONS.md +++ b/SPECIFICATIONS.md @@ -77,6 +77,7 @@ defaults: driver: "goose" # Default driver to use connect: true # Automatically connect after creating session mount_local: true # Mount local directory by default + networks: [] # Default networks to connect to (besides mc-network) services: # Service credentials with simplified naming @@ -96,7 +97,7 @@ services: api_key: "sk-or-..." docker: - network: "mc-network" # Docker network to use + network: "mc-network" # Default Docker network to use socket: "/var/run/docker.sock" # Docker socket path remote: @@ -153,6 +154,11 @@ mc config get defaults.driver mc config set langfuse.url "https://cloud.langfuse.com" mc config set openai.api_key "sk-..." +# Network configuration +mc config network list # List default networks +mc config network add example-network # Add a network to defaults +mc config network remove example-network # Remove a network from defaults + # Reset configuration to defaults mc config reset ``` @@ -177,6 +183,9 @@ mc session create --driver goose # Create a session with a specific project repository mc session create --driver goose --project github.com/hello/private +# Create a session with external networks +mc session create --network teamnet --network othernetwork + # Create a session with a project (shorthand) mc git@github.com:hello/private @@ -558,12 +567,44 @@ persistent_configs: 3. **claude-code**: Claude Code environment 4. **custom**: Custom Dockerfile support +## Network Management + +### Docker Network Integration + +MC provides flexible network management for containers: + +1. **Default MC Network**: + - Each container is automatically connected to the MC network (`mc-network` by default) + - This ensures containers can communicate with each other + +2. **External Network Connection**: + - Containers can be connected to one or more external Docker networks + - This allows integration with existing infrastructure (e.g., databases, web servers) + - Networks can be specified at session creation time: `mc session create --network mynetwork` + +3. **Default Networks Configuration**: + - Users can configure default networks in their configuration + - These networks will be used for all new sessions unless overridden + - Managed with `mc config network` commands + +4. **Network Command Examples**: + ```bash + # Use with session creation + mc session create --network teamnet + + # Use with multiple networks + mc session create --network teamnet --network dbnet + + # Configure default networks + mc config network add teamnet + ``` + ## Security Considerations 1. **Container Isolation**: Each session runs in an isolated container 2. **Authentication**: Integration with Authentik for secure authentication 3. **Resource Limits**: Configurable CPU, memory, and storage limits -4. **Network Isolation**: Internal Docker network for container-to-container communication +4. **Network Isolation**: Internal Docker network for container-to-container communication with optional external network connections 5. **Encrypted Connections**: TLS for API connections and SSH for terminal access ## Deployment diff --git a/mcontainer/cli.py b/mcontainer/cli.py index 4320d8e..7ca6859 100644 --- a/mcontainer/cli.py +++ b/mcontainer/cli.py @@ -33,6 +33,7 @@ def main(ctx: typer.Context) -> None: project=None, env=[], volume=[], + network=[], name=None, no_connect=False, no_mount=False, @@ -113,6 +114,9 @@ def create_session( volume: List[str] = typer.Option( [], "--volume", "-v", help="Mount volumes (LOCAL_PATH:CONTAINER_PATH)" ), + network: List[str] = typer.Option( + [], "--network", "-N", help="Connect to additional Docker networks" + ), 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" @@ -166,6 +170,15 @@ def create_session( f"[yellow]Warning: Ignoring invalid volume format: {vol}. Use LOCAL_PATH:CONTAINER_PATH.[/yellow]" ) + # Get default networks from user config + default_networks = user_config.get("defaults.networks", []) + + # Combine default networks with user-specified networks, removing duplicates + all_networks = list(set(default_networks + network)) + + if all_networks: + console.print(f"Networks: {', '.join(all_networks)}") + with console.status(f"Creating session with driver '{driver}'..."): session = container_manager.create_session( driver_name=driver, @@ -174,6 +187,7 @@ def create_session( session_name=name, mount_local=not no_mount and user_config.get("defaults.mount_local", True), volumes=volume_mounts, + networks=all_networks, ) if session: @@ -315,6 +329,9 @@ def quick_create( volume: List[str] = typer.Option( [], "--volume", "-v", help="Mount volumes (LOCAL_PATH:CONTAINER_PATH)" ), + network: List[str] = typer.Option( + [], "--network", "-N", help="Connect to additional Docker networks" + ), 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" @@ -335,6 +352,7 @@ def quick_create( project=project, env=env, volume=volume, + network=network, name=name, no_connect=no_connect, no_mount=no_mount, @@ -449,6 +467,11 @@ def driver_info( console.print(f.read()) +# Create a network subcommand for config +network_app = typer.Typer(help="Manage default networks") +config_app.add_typer(network_app, name="network", no_args_is_help=True) + + # Configuration commands @config_app.command("list") def list_config() -> None: @@ -544,5 +567,56 @@ def reset_config( console.print("[green]Configuration reset to defaults[/green]") +# Network configuration commands +@network_app.command("list") +def list_networks() -> None: + """List all default networks""" + networks = user_config.get("defaults.networks", []) + + if not networks: + console.print("No default networks configured") + return + + table = Table(show_header=True, header_style="bold") + table.add_column("Network") + + for network in networks: + table.add_row(network) + + console.print(table) + + +@network_app.command("add") +def add_network( + network: str = typer.Argument(..., help="Network name to add to defaults"), +) -> None: + """Add a network to default networks""" + networks = user_config.get("defaults.networks", []) + + if network in networks: + console.print(f"Network '{network}' is already in defaults") + return + + networks.append(network) + user_config.set("defaults.networks", networks) + console.print(f"[green]Added network '{network}' to defaults[/green]") + + +@network_app.command("remove") +def remove_network( + network: str = typer.Argument(..., help="Network name to remove from defaults"), +) -> None: + """Remove a network from default networks""" + networks = user_config.get("defaults.networks", []) + + if network not in networks: + console.print(f"Network '{network}' is not in defaults") + return + + networks.remove(network) + user_config.set("defaults.networks", networks) + console.print(f"[green]Removed network '{network}' from defaults[/green]") + + if __name__ == "__main__": app() diff --git a/mcontainer/container.py b/mcontainer/container.py index 782a943..5f246cc 100644 --- a/mcontainer/container.py +++ b/mcontainer/container.py @@ -126,6 +126,7 @@ class ContainerManager: session_name: Optional[str] = None, mount_local: bool = True, volumes: Optional[Dict[str, Dict[str, str]]] = None, + networks: Optional[List[str]] = None, ) -> Optional[Session]: """Create a new MC session @@ -136,6 +137,7 @@ class ContainerManager: 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}}) + networks: Optional list of additional Docker networks to connect to """ try: # Validate driver exists @@ -254,7 +256,12 @@ class ContainerManager: f" - Created direct volume mount: {target_dir} -> {config.source}" ) - # Create container + # Default MC network + default_network = self.config_manager.config.docker.get( + "network", "mc-network" + ) + + # Create container with default MC network container = self.client.containers.create( image=driver.image, name=session_name, @@ -271,13 +278,32 @@ class ContainerManager: "mc.driver": driver_name, "mc.project": project or "", }, - network=self.config_manager.config.docker.get("network", "mc-network"), + network=default_network, ports={f"{port}/tcp": None for port in driver.ports}, ) # Start container container.start() + # Connect to additional networks if specified + if networks: + for network_name in networks: + try: + # Get or create the network + try: + network = self.client.networks.get(network_name) + except DockerException: + print(f"Network '{network_name}' not found, creating it...") + network = self.client.networks.create( + network_name, driver="bridge" + ) + + # Connect the container to the network + network.connect(container) + print(f"Connected to network: {network_name}") + except DockerException as e: + print(f"Error connecting to network {network_name}: {e}") + # Get updated port information container.reload() ports = {} diff --git a/mcontainer/user_config.py b/mcontainer/user_config.py index 59516d8..7ae6fb1 100644 --- a/mcontainer/user_config.py +++ b/mcontainer/user_config.py @@ -62,6 +62,7 @@ class UserConfigManager: "driver": "goose", "connect": True, "mount_local": True, + "networks": [], # Default networks to connect to (besides mc-network) }, "services": { "langfuse": {},