From c869c1117fa39a36001ac4a8231c582be56e9e12 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Fri, 4 Apr 2025 17:16:26 -0600 Subject: [PATCH] feat(project): explicitely add --project to save information in /mc-config across run. Containers are now isolated by default. --- mcontainer/cli.py | 19 +++- mcontainer/container.py | 141 +++++++++++++++------------- mcontainer/drivers/goose/mc-init.sh | 7 ++ mcontainer/models.py | 1 + 4 files changed, 99 insertions(+), 69 deletions(-) diff --git a/mcontainer/cli.py b/mcontainer/cli.py index d6c4136..8a230b1 100644 --- a/mcontainer/cli.py +++ b/mcontainer/cli.py @@ -86,6 +86,7 @@ def list_sessions() -> None: table.add_column("Status") table.add_column("Ports") table.add_column("Project") + table.add_column("Project Name") table.add_column("MCPs") for session in sessions: @@ -119,6 +120,7 @@ def list_sessions() -> None: f"[{status_color}]{status_name}[/{status_color}]", ports_str, session.project or "", + session.project_name or "", mcps_str, ) @@ -128,11 +130,16 @@ def list_sessions() -> None: @session_app.command("create") def create_session( driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"), - project: Optional[str] = typer.Argument( + path_or_url: Optional[str] = typer.Argument( None, help="Local directory path to mount or repository URL to clone", show_default=False, ), + project: Optional[str] = typer.Option( + None, + "--project", + help="Project name for configuration persistence (if not specified, no persistent configuration will be used)", + ), env: List[str] = typer.Option( [], "--env", "-e", help="Environment variables (KEY=VALUE)" ), @@ -174,6 +181,9 @@ def create_session( If a local directory path is provided, it will be mounted at /app in the container. If a repository URL is provided, it will be cloned into /app during initialization. If no path or URL is provided, no local volume will be mounted. + + Use --project to specify a project name for configuration persistence. + If --project is not specified, no persistent configuration will be used. """ # Determine UID/GID target_uid = uid if uid is not None else os.getuid() @@ -254,15 +264,16 @@ def create_session( console.print(f" {host_path} -> {mount_info['bind']}") with console.status(f"Creating session with driver '{driver}'..."): - # If project is a local directory, we should mount it + # If path_or_url is a local directory, we should mount it # If it's a Git URL or doesn't exist, handle accordingly mount_local = False - if project and os.path.isdir(os.path.expanduser(project)): + if path_or_url and os.path.isdir(os.path.expanduser(path_or_url)): mount_local = True session = container_manager.create_session( driver_name=driver, - project=project, + project=path_or_url, + project_name=project, environment=environment, session_name=name, mount_local=mount_local, diff --git a/mcontainer/container.py b/mcontainer/container.py index 8eb9a26..f5c9611 100644 --- a/mcontainer/container.py +++ b/mcontainer/container.py @@ -51,37 +51,38 @@ class ContainerManager: """Generate a unique session ID""" return str(uuid.uuid4())[:8] - def _get_project_config_path(self, project: Optional[str] = None) -> pathlib.Path: + def _get_project_config_path( + self, project: Optional[str] = None, project_name: Optional[str] = None + ) -> Optional[pathlib.Path]: """Get the path to the project configuration directory Args: - project: Optional project repository URL. If None, uses current directory. + project: Optional project repository URL or path (only used for mounting). + project_name: Optional explicit project name. Only used if specified. Returns: - Path to the project configuration directory + Path to the project configuration directory, or None if no project_name is provided """ # Get home directory for the MC config mc_home = pathlib.Path.home() / ".mc" - # If no project URL is provided, use the current directory path - if not project: - # Use current working directory as project identifier - project_id = os.getcwd() + # Only use project_name if explicitly provided + if project_name: + # Create a hash of the project name to use as directory name + project_hash = hashlib.md5(project_name.encode()).hexdigest() + + # Create the project config directory path + config_path = mc_home / "projects" / project_hash / "config" + + # Create the directory if it doesn't exist + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.mkdir(exist_ok=True) + + return config_path else: - # Use project URL as identifier - project_id = project - - # Create a hash of the project ID to use as directory name - project_hash = hashlib.md5(project_id.encode()).hexdigest() - - # Create the project config directory path - config_path = mc_home / "projects" / project_hash / "config" - - # Create the directory if it doesn't exist - config_path.parent.mkdir(parents=True, exist_ok=True) - config_path.mkdir(exist_ok=True) - - return config_path + # If no project_name is provided, don't create any config directory + # This ensures we don't mount the /mc-config volume for project-less sessions + return None def list_sessions(self) -> List[Session]: """List all active MC sessions""" @@ -113,6 +114,7 @@ class ContainerManager: container_id=container_id, created_at=container.attrs["Created"], project=labels.get("mc.project"), + project_name=labels.get("mc.project_name"), model=labels.get("mc.model"), provider=labels.get("mc.provider"), ) @@ -141,6 +143,7 @@ class ContainerManager: self, driver_name: str, project: Optional[str] = None, + project_name: Optional[str] = None, environment: Optional[Dict[str, str]] = None, session_name: Optional[str] = None, mount_local: bool = False, @@ -159,6 +162,7 @@ class ContainerManager: Args: driver_name: The name of the driver to use project: Optional project repository URL or local directory path + project_name: Optional explicit project name for configuration persistence environment: Optional environment variables session_name: Optional session name mount_local: Whether to mount the specified local directory to /app (ignored if project is None) @@ -262,57 +266,62 @@ class ContainerManager: 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}") + # Set up persistent project configuration if project_name is provided + project_config_path = self._get_project_config_path(project, project_name) + if project_config_path: + print(f"Using project configuration directory: {project_config_path}") - # Mount the project configuration directory - session_volumes[str(project_config_path)] = { - "bind": "/mc-config", - "mode": "rw", - } + # Mount the project configuration directory + session_volumes[str(project_config_path)] = { + "bind": "/mc-config", + "mode": "rw", + } - # Add environment variables for config path - env_vars["MC_CONFIG_DIR"] = "/mc-config" - env_vars["MC_DRIVER_CONFIG_DIR"] = f"/mc-config/{driver_name}" + # Add environment variables for config path + env_vars["MC_CONFIG_DIR"] = "/mc-config" + env_vars["MC_DRIVER_CONFIG_DIR"] = f"/mc-config/{driver_name}" - # Create driver-specific config directories and set up direct volume mounts - if driver.persistent_configs: - persistent_links_data = [] # To store "source:target" pairs for symlinks - print("Setting up persistent configuration directories:") - for config in driver.persistent_configs: - # Get target directory path on host - target_dir = project_config_path / config.target.removeprefix( - "/mc-config/" - ) + # Create driver-specific config directories and set up direct volume mounts + if driver.persistent_configs: + persistent_links_data = [] # To store "source:target" pairs for symlinks + print("Setting up persistent configuration directories:") + for config in driver.persistent_configs: + # Get target directory path on host + target_dir = project_config_path / config.target.removeprefix( + "/mc-config/" + ) - # Create directory if it's a directory type config - if config.type == "directory": - dir_existed = target_dir.exists() - target_dir.mkdir(parents=True, exist_ok=True) - if not dir_existed: - print(f" - Created directory: {target_dir}") - # For files, make sure parent directory exists - elif config.type == "file": - target_dir.parent.mkdir(parents=True, exist_ok=True) - # File will be created by the container if needed + # Create directory if it's a directory type config + if config.type == "directory": + dir_existed = target_dir.exists() + target_dir.mkdir(parents=True, exist_ok=True) + if not dir_existed: + print(f" - Created directory: {target_dir}") + # For files, make sure parent directory exists + elif config.type == "file": + target_dir.parent.mkdir(parents=True, exist_ok=True) + # File will be created by the container if needed - # --- REMOVED adding to session_volumes --- - # We will create symlinks inside the container instead of direct mounts + # Store the source and target paths for the init script + # Note: config.target is the path *within* /mc-config + persistent_links_data.append(f"{config.source}:{config.target}") - # Store the source and target paths for the init script - # Note: config.target is the path *within* /mc-config - persistent_links_data.append(f"{config.source}:{config.target}") + print( + f" - Prepared host path {target_dir} for symlink target {config.target}" + ) - print( - f" - Prepared host path {target_dir} for symlink target {config.target}" - ) - # Set environment variable with semicolon-separated link pairs - if persistent_links_data: - env_vars["MC_PERSISTENT_LINKS"] = ";".join(persistent_links_data) - print( - f"Setting MC_PERSISTENT_LINKS={env_vars['MC_PERSISTENT_LINKS']}" - ) + # Set up persistent links + if persistent_links_data: + env_vars["MC_PERSISTENT_LINKS"] = ";".join( + persistent_links_data + ) + print( + f"Setting MC_PERSISTENT_LINKS={env_vars['MC_PERSISTENT_LINKS']}" + ) + else: + print( + "No project_name provided - skipping configuration directory setup." + ) # Default MC network default_network = self.config_manager.config.docker.get( @@ -504,6 +513,7 @@ class ContainerManager: "mc.session.name": session_name, "mc.driver": driver_name, "mc.project": project or "", + "mc.project_name": project_name or "", "mc.mcps": ",".join(mcp_names) if mcp_names else "", }, network=network_list[0], # Connect to the first network initially @@ -613,6 +623,7 @@ class ContainerManager: container_id=container.id, environment=env_vars, project=project, + project_name=project_name, created_at=container.attrs["Created"], ports=ports, mcps=mcp_names, diff --git a/mcontainer/drivers/goose/mc-init.sh b/mcontainer/drivers/goose/mc-init.sh index 813e584..0519a1b 100755 --- a/mcontainer/drivers/goose/mc-init.sh +++ b/mcontainer/drivers/goose/mc-init.sh @@ -110,6 +110,13 @@ if [ -n "$LANGFUSE_INIT_PROJECT_SECRET_KEY" ] && [ -n "$LANGFUSE_INIT_PROJECT_PU export LANGFUSE_URL="${LANGFUSE_URL:-https://cloud.langfuse.com}" fi +# Ensure /mc-config directory exists (required for symlinks) +if [ ! -d "/mc-config" ]; then + echo "Creating /mc-config directory since it doesn't exist" + mkdir -p /mc-config + chown $MC_USER_ID:$MC_GROUP_ID /mc-config +fi + # Create symlinks for persistent configurations defined in the driver if [ -n "$MC_PERSISTENT_LINKS" ]; then echo "Creating persistent configuration symlinks..." diff --git a/mcontainer/models.py b/mcontainer/models.py index 8a07c2f..0d4e0e6 100644 --- a/mcontainer/models.py +++ b/mcontainer/models.py @@ -102,6 +102,7 @@ class Session(BaseModel): container_id: Optional[str] = None environment: Dict[str, str] = Field(default_factory=dict) project: Optional[str] = None + project_name: Optional[str] = None created_at: str ports: Dict[int, int] = Field(default_factory=dict) mcps: List[str] = Field(default_factory=list)