mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-21 04:39:07 +00:00
refactor: rename driver to image, first pass
This commit is contained in:
@@ -5,9 +5,6 @@
|
|||||||
# Install dependencies using uv (Astral)
|
# Install dependencies using uv (Astral)
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
# Run MC service
|
|
||||||
uv run -m mcontainer.service
|
|
||||||
|
|
||||||
# Run MC CLI
|
# Run MC CLI
|
||||||
uv run -m mcontainer.cli
|
uv run -m mcontainer.cli
|
||||||
```
|
```
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -38,7 +38,7 @@ mc --help
|
|||||||
# Show help message (displays available commands)
|
# Show help message (displays available commands)
|
||||||
mc
|
mc
|
||||||
|
|
||||||
# Create a new session with the default driver (using mcx alias)
|
# Create a new session with the default image (using mcx alias)
|
||||||
mcx
|
mcx
|
||||||
|
|
||||||
# Create a session and run an initial command before the shell starts
|
# Create a session and run an initial command before the shell starts
|
||||||
@@ -53,8 +53,8 @@ mc session connect SESSION_ID
|
|||||||
# Close a session when done
|
# Close a session when done
|
||||||
mc session close SESSION_ID
|
mc session close SESSION_ID
|
||||||
|
|
||||||
# Create a session with a specific driver
|
# Create a session with a specific image
|
||||||
mcx --driver goose
|
mcx --image goose
|
||||||
|
|
||||||
# Create a session with environment variables
|
# Create a session with environment variables
|
||||||
mcx -e VAR1=value1 -e VAR2=value2
|
mcx -e VAR1=value1 -e VAR2=value2
|
||||||
@@ -92,33 +92,34 @@ mcx . --run "apt-get update && apt-get install -y my-package"
|
|||||||
mcx --ssh
|
mcx --ssh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Driver Management
|
## Image Management
|
||||||
|
|
||||||
MC includes a driver management system that allows you to build, manage, and use Docker images for different AI tools:
|
MC includes a image management system that allows you to build, manage, and use Docker images for different AI tools:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List available drivers
|
# List available images
|
||||||
mc driver list
|
mc image list
|
||||||
|
|
||||||
# Get detailed information about a driver
|
# Get detailed information about an image
|
||||||
mc driver info goose
|
mc image info goose
|
||||||
|
|
||||||
# Build a driver image
|
# Build an image
|
||||||
mc driver build goose
|
mc image build goose
|
||||||
|
|
||||||
# Build and push a driver image
|
# Build and push an image
|
||||||
mc driver build goose --push
|
mc image build goose --push
|
||||||
```
|
```
|
||||||
|
|
||||||
Drivers are defined in the `mcontainer/drivers/` directory, with each subdirectory containing:
|
Images are defined in the `mcontainer/images/` directory, with each subdirectory containing:
|
||||||
|
|
||||||
- `Dockerfile`: Docker image definition
|
- `Dockerfile`: Docker image definition
|
||||||
- `entrypoint.sh`: Container entrypoint script
|
- `entrypoint.sh`: Container entrypoint script
|
||||||
- `mc-init.sh`: Standardized initialization script
|
- `mc-init.sh`: Standardized initialization script
|
||||||
- `mc-driver.yaml`: Driver metadata and configuration
|
- `mc-image.yaml`: Image metadata and configuration
|
||||||
- `README.md`: Driver documentation
|
- `README.md`: Image documentation
|
||||||
|
|
||||||
MC automatically discovers and loads driver definitions from the YAML files.
|
MC automatically discovers and loads image definitions from the YAML files.
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Docker-in-Docker (DinD) environment.
|
|||||||
|
|
||||||
1. **CLI Tool (`mc`)**: The command-line interface users interact with
|
1. **CLI Tool (`mc`)**: The command-line interface users interact with
|
||||||
2. **MC Service**: A web service that handles remote container execution
|
2. **MC Service**: A web service that handles remote container execution
|
||||||
3. **Container Drivers**: Predefined container templates for various AI tools
|
3. **Container Images**: Predefined container templates for various AI tools
|
||||||
|
|
||||||
### Architecture Diagram
|
### Architecture Diagram
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@ Docker-in-Docker (DinD) environment.
|
|||||||
|
|
||||||
## Core Concepts
|
## Core Concepts
|
||||||
|
|
||||||
- **Session**: An active container instance with a specific driver
|
- **Session**: An active container instance with a specific image
|
||||||
- **Driver**: A predefined container template with specific AI tools installed
|
- **Image**: A predefined container template with specific AI tools installed
|
||||||
- **Remote**: A configured MC service instance
|
- **Remote**: A configured MC service instance
|
||||||
|
|
||||||
## User Configuration
|
## User Configuration
|
||||||
@@ -74,7 +74,7 @@ MC supports user-specific configuration via a YAML file located at `~/.config/mc
|
|||||||
```yaml
|
```yaml
|
||||||
# ~/.config/mc/config.yaml
|
# ~/.config/mc/config.yaml
|
||||||
defaults:
|
defaults:
|
||||||
driver: "goose" # Default driver to use
|
image: "goose" # Default image to use
|
||||||
connect: true # Automatically connect after creating session
|
connect: true # Automatically connect after creating session
|
||||||
mount_local: true # Mount local directory by default
|
mount_local: true # Mount local directory by default
|
||||||
networks: [] # Default networks to connect to (besides mc-network)
|
networks: [] # Default networks to connect to (besides mc-network)
|
||||||
@@ -177,11 +177,11 @@ mc session list
|
|||||||
# Create a new session locally
|
# Create a new session locally
|
||||||
mc session create [OPTIONS]
|
mc session create [OPTIONS]
|
||||||
|
|
||||||
# Create a session with a specific driver
|
# Create a session with a specific image
|
||||||
mc session create --driver goose
|
mc session create --image goose
|
||||||
|
|
||||||
# Create a session with a specific project repository
|
# Create a session with a specific project repository
|
||||||
mc session create --driver goose --project github.com/hello/private
|
mc session create --image goose --project github.com/hello/private
|
||||||
|
|
||||||
# Create a session with external networks
|
# Create a session with external networks
|
||||||
mc session create --network teamnet --network othernetwork
|
mc session create --network teamnet --network othernetwork
|
||||||
@@ -289,11 +289,11 @@ POST /sessions/{id}/connect - Establish connection to session
|
|||||||
GET /sessions/{id}/logs - Stream session logs
|
GET /sessions/{id}/logs - Stream session logs
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Drivers
|
#### Images
|
||||||
|
|
||||||
```
|
```
|
||||||
GET /drivers - List available drivers
|
GET /images - List available images
|
||||||
GET /drivers/{name} - Get driver details
|
GET /images/{name} - Get image details
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Projects
|
#### Projects
|
||||||
@@ -332,7 +332,7 @@ logging:
|
|||||||
public_key: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
|
public_key: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
|
||||||
secret_key: ${LANGFUSE_INIT_PROJECT_SECRET_KEY}
|
secret_key: ${LANGFUSE_INIT_PROJECT_SECRET_KEY}
|
||||||
|
|
||||||
drivers:
|
images:
|
||||||
- name: goose
|
- name: goose
|
||||||
image: monadical/mc-goose:latest
|
image: monadical/mc-goose:latest
|
||||||
- name: aider
|
- name: aider
|
||||||
@@ -385,10 +385,10 @@ MC provides persistent storage for project-specific configurations that need to
|
|||||||
- For local projects, the hash is derived from the absolute path of the local directory
|
- For local projects, the hash is derived from the absolute path of the local directory
|
||||||
- This directory is mounted into the container at `/mc-config`
|
- This directory is mounted into the container at `/mc-config`
|
||||||
|
|
||||||
2. **Driver Configuration**:
|
2. **Image Configuration**:
|
||||||
- Each driver can specify configuration files/directories that should persist across sessions
|
- Each image can specify configuration files/directories that should persist across sessions
|
||||||
- These are defined in the driver's `mc-driver.yaml` file in the `persistent_configs` section
|
- These are defined in the image's `mc-image.yaml` file in the `persistent_configs` section
|
||||||
- Example for Goose driver:
|
- Example for Goose image:
|
||||||
```yaml
|
```yaml
|
||||||
persistent_configs:
|
persistent_configs:
|
||||||
- source: "/app/.goose" # Path in container
|
- source: "/app/.goose" # Path in container
|
||||||
@@ -407,7 +407,7 @@ MC provides persistent storage for project-specific configurations that need to
|
|||||||
- Container has access to configuration location via environment variables:
|
- Container has access to configuration location via environment variables:
|
||||||
```
|
```
|
||||||
MC_CONFIG_DIR=/mc-config
|
MC_CONFIG_DIR=/mc-config
|
||||||
MC_DRIVER_CONFIG_DIR=/mc-config/<driver-name>
|
MC_IMAGE_CONFIG_DIR=/mc-config/<image-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
This ensures that important configurations like Goose's memory store, authentication tokens, and other state information persist between container sessions while maintaining isolation between different projects.
|
This ensures that important configurations like Goose's memory store, authentication tokens, and other state information persist between container sessions while maintaining isolation between different projects.
|
||||||
@@ -448,25 +448,24 @@ auth:
|
|||||||
public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
|
public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Driver Implementation
|
## Image Implementation
|
||||||
|
|
||||||
### Driver Structure
|
### Image Structure
|
||||||
|
|
||||||
Each driver is a Docker image with a standardized structure:
|
Each image is a Docker container with a standardized structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
/
|
/
|
||||||
├── entrypoint.sh # Container initialization
|
├── entrypoint.sh # Container initialization
|
||||||
├── mc-init.sh # Standardized initialization script
|
├── mc-init.sh # Standardized initialization script
|
||||||
├── mc-driver.yaml # Driver metadata and configuration
|
├── mc-image.yaml # Image metadata and configuration
|
||||||
├── tool/ # AI tool installation
|
├── tool/ # AI tool installation
|
||||||
└── ssh/ # SSH server configuration
|
└── ssh/ # SSH server configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standardized Initialization Script
|
### Standardized Initialization Script
|
||||||
|
|
||||||
All drivers include a standardized `mc-init.sh` script that handles common initialization tasks:
|
All images include a standardized `mc-init.sh` script that handles common initialization tasks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
@@ -498,10 +497,10 @@ if [ -n "$MC_PROJECT_URL" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Driver-specific initialization continues...
|
# Image-specific initialization continues...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Driver Configuration (mc-driver.yaml)
|
### Image Configuration (mc-image.yaml)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: goose
|
name: goose
|
||||||
@@ -558,7 +557,7 @@ persistent_configs:
|
|||||||
description: "Goose memory and configuration"
|
description: "Goose memory and configuration"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example Built-in Drivers
|
### Example Built-in images
|
||||||
|
|
||||||
1. **goose**: Goose with MCP servers
|
1. **goose**: Goose with MCP servers
|
||||||
2. **aider**: Aider coding assistant
|
2. **aider**: Aider coding assistant
|
||||||
@@ -677,7 +676,7 @@ networks:
|
|||||||
2. **Phase 2**: MC Service REST API with basic container management
|
2. **Phase 2**: MC Service REST API with basic container management
|
||||||
3. **Phase 3**: Authentication and secure connections
|
3. **Phase 3**: Authentication and secure connections
|
||||||
4. **Phase 4**: Project management functionality
|
4. **Phase 4**: Project management functionality
|
||||||
5. **Phase 5**: Driver implementation (Goose, Aider, Claude Code)
|
5. **Phase 5**: Image implementation (Goose, Aider, Claude Code)
|
||||||
6. **Phase 6**: Logging integration with Fluentd and Langfuse
|
6. **Phase 6**: Logging integration with Fluentd and Langfuse
|
||||||
7. **Phase 7**: CLI remote connectivity improvements
|
7. **Phase 7**: CLI remote connectivity improvements
|
||||||
8. **Phase 8**: Additional drivers and extensibility features
|
8. **Phase 8**: Additional images and extensibility features
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
This document specifies the implementation for Model Control Protocol (MCP) server support in the Monadical Container (MC) system. The MCP server feature allows users to connect, build, and manage external MCP servers that can be attached to MC sessions.
|
This document specifies the implementation for Model Control Protocol (MCP) server support in the Monadical Container (MC) system. The MCP server feature allows users to connect, build, and manage external MCP servers that can be attached to MC sessions.
|
||||||
|
|
||||||
An MCP server is a service that can be accessed by a driver (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls. It can be either:
|
An MCP server is a service that can be accessed by a image (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls. It can be either:
|
||||||
- A local stdio-based MCP server running in a container (accessed via an SSE proxy)
|
- A local stdio-based MCP server running in a container (accessed via an SSE proxy)
|
||||||
- A remote HTTP SSE server accessed directly via its URL
|
- A remote HTTP SSE server accessed directly via its URL
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,20 @@
|
|||||||
CLI for Monadical Container Tool.
|
CLI for Monadical Container Tool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
from .config import ConfigManager
|
from .config import ConfigManager
|
||||||
from .container import ContainerManager
|
from .container import ContainerManager
|
||||||
from .models import SessionStatus
|
|
||||||
from .user_config import UserConfigManager
|
|
||||||
from .session import SessionManager
|
|
||||||
from .mcp import MCPManager
|
from .mcp import MCPManager
|
||||||
|
from .models import SessionStatus
|
||||||
|
from .session import SessionManager
|
||||||
|
from .user_config import UserConfigManager
|
||||||
|
|
||||||
# Configure logging - will only show logs if --verbose flag is used
|
# Configure logging - will only show logs if --verbose flag is used
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -25,11 +26,11 @@ logging.basicConfig(
|
|||||||
|
|
||||||
app = typer.Typer(help="Monadical Container Tool", no_args_is_help=True)
|
app = typer.Typer(help="Monadical Container Tool", no_args_is_help=True)
|
||||||
session_app = typer.Typer(help="Manage MC sessions", no_args_is_help=True)
|
session_app = typer.Typer(help="Manage MC sessions", no_args_is_help=True)
|
||||||
driver_app = typer.Typer(help="Manage MC drivers", no_args_is_help=True)
|
image_app = typer.Typer(help="Manage MC images", no_args_is_help=True)
|
||||||
config_app = typer.Typer(help="Manage MC configuration", no_args_is_help=True)
|
config_app = typer.Typer(help="Manage MC configuration", no_args_is_help=True)
|
||||||
mcp_app = typer.Typer(help="Manage MCP servers", no_args_is_help=True)
|
mcp_app = typer.Typer(help="Manage MCP servers", no_args_is_help=True)
|
||||||
app.add_typer(session_app, name="session", no_args_is_help=True)
|
app.add_typer(session_app, name="session", no_args_is_help=True)
|
||||||
app.add_typer(driver_app, name="driver", no_args_is_help=True)
|
app.add_typer(image_app, name="image", no_args_is_help=True)
|
||||||
app.add_typer(config_app, name="config", no_args_is_help=True)
|
app.add_typer(config_app, name="config", no_args_is_help=True)
|
||||||
app.add_typer(mcp_app, name="mcp", no_args_is_help=True)
|
app.add_typer(mcp_app, name="mcp", no_args_is_help=True)
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ def list_sessions() -> None:
|
|||||||
table = Table(show_header=True, header_style="bold")
|
table = Table(show_header=True, header_style="bold")
|
||||||
table.add_column("ID")
|
table.add_column("ID")
|
||||||
table.add_column("Name")
|
table.add_column("Name")
|
||||||
table.add_column("Driver")
|
table.add_column("Image")
|
||||||
table.add_column("Status")
|
table.add_column("Status")
|
||||||
table.add_column("Ports")
|
table.add_column("Ports")
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ def list_sessions() -> None:
|
|||||||
table.add_row(
|
table.add_row(
|
||||||
session.id,
|
session.id,
|
||||||
session.name,
|
session.name,
|
||||||
session.driver,
|
session.image,
|
||||||
f"[{status_color}]{status_name}[/{status_color}]",
|
f"[{status_color}]{status_name}[/{status_color}]",
|
||||||
ports_str,
|
ports_str,
|
||||||
)
|
)
|
||||||
@@ -120,7 +121,7 @@ def list_sessions() -> None:
|
|||||||
|
|
||||||
@session_app.command("create")
|
@session_app.command("create")
|
||||||
def create_session(
|
def create_session(
|
||||||
driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"),
|
image: Optional[str] = typer.Option(None, "--image", "-i", help="Image to use"),
|
||||||
path_or_url: Optional[str] = typer.Argument(
|
path_or_url: Optional[str] = typer.Argument(
|
||||||
None,
|
None,
|
||||||
help="Local directory path to mount or repository URL to clone",
|
help="Local directory path to mount or repository URL to clone",
|
||||||
@@ -181,11 +182,13 @@ def create_session(
|
|||||||
target_gid = gid if gid is not None else os.getgid()
|
target_gid = gid if gid is not None else os.getgid()
|
||||||
console.print(f"Using UID: {target_uid}, GID: {target_gid}")
|
console.print(f"Using UID: {target_uid}, GID: {target_gid}")
|
||||||
|
|
||||||
# Use default driver from user configuration
|
# Use default image from user configuration
|
||||||
if not driver:
|
if not image:
|
||||||
driver = user_config.get(
|
image_name = user_config.get(
|
||||||
"defaults.driver", config_manager.config.defaults.get("driver", "goose")
|
"defaults.image", config_manager.config.defaults.get("image", "goose")
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
image_name = image
|
||||||
|
|
||||||
# Start with environment variables from user configuration
|
# Start with environment variables from user configuration
|
||||||
environment = user_config.get_environment_variables()
|
environment = user_config.get_environment_variables()
|
||||||
@@ -254,7 +257,7 @@ def create_session(
|
|||||||
for host_path, mount_info in volume_mounts.items():
|
for host_path, mount_info in volume_mounts.items():
|
||||||
console.print(f" {host_path} -> {mount_info['bind']}")
|
console.print(f" {host_path} -> {mount_info['bind']}")
|
||||||
|
|
||||||
with console.status(f"Creating session with driver '{driver}'..."):
|
with console.status(f"Creating session with image '{image_name}'..."):
|
||||||
# If path_or_url 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
|
# If it's a Git URL or doesn't exist, handle accordingly
|
||||||
mount_local = False
|
mount_local = False
|
||||||
@@ -262,7 +265,7 @@ def create_session(
|
|||||||
mount_local = True
|
mount_local = True
|
||||||
|
|
||||||
session = container_manager.create_session(
|
session = container_manager.create_session(
|
||||||
driver_name=driver,
|
image_name=image_name,
|
||||||
project=path_or_url,
|
project=path_or_url,
|
||||||
project_name=project,
|
project_name=project,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
@@ -282,7 +285,7 @@ def create_session(
|
|||||||
if session:
|
if session:
|
||||||
console.print("[green]Session created successfully![/green]")
|
console.print("[green]Session created successfully![/green]")
|
||||||
console.print(f"Session ID: {session.id}")
|
console.print(f"Session ID: {session.id}")
|
||||||
console.print(f"Driver: {session.driver}")
|
console.print(f"Image: {session.image}")
|
||||||
|
|
||||||
if session.ports:
|
if session.ports:
|
||||||
console.print("Ports:")
|
console.print("Ports:")
|
||||||
@@ -402,13 +405,13 @@ def session_logs(
|
|||||||
console.print(logs)
|
console.print(logs)
|
||||||
|
|
||||||
|
|
||||||
@driver_app.command("list")
|
@image_app.command("list")
|
||||||
def list_drivers() -> None:
|
def list_images() -> None:
|
||||||
"""List available MC drivers"""
|
"""List available MC images"""
|
||||||
drivers = config_manager.list_drivers()
|
images = config_manager.list_images()
|
||||||
|
|
||||||
if not drivers:
|
if not images:
|
||||||
console.print("No drivers found")
|
console.print("No images found")
|
||||||
return
|
return
|
||||||
|
|
||||||
table = Table(show_header=True, header_style="bold")
|
table = Table(show_header=True, header_style="bold")
|
||||||
@@ -418,92 +421,92 @@ def list_drivers() -> None:
|
|||||||
table.add_column("Maintainer")
|
table.add_column("Maintainer")
|
||||||
table.add_column("Image")
|
table.add_column("Image")
|
||||||
|
|
||||||
for name, driver in drivers.items():
|
for name, image in images.items():
|
||||||
table.add_row(
|
table.add_row(
|
||||||
driver.name,
|
image.name,
|
||||||
driver.description,
|
image.description,
|
||||||
driver.version,
|
image.version,
|
||||||
driver.maintainer,
|
image.maintainer,
|
||||||
driver.image,
|
image.image,
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
@driver_app.command("build")
|
@image_app.command("build")
|
||||||
def build_driver(
|
def build_image(
|
||||||
driver_name: str = typer.Argument(..., help="Driver name to build"),
|
image_name: str = typer.Argument(..., help="Image name to build"),
|
||||||
tag: str = typer.Option("latest", "--tag", "-t", help="Image tag"),
|
tag: str = typer.Option("latest", "--tag", "-t", help="Image tag"),
|
||||||
push: bool = typer.Option(
|
push: bool = typer.Option(
|
||||||
False, "--push", "-p", help="Push image to registry after building"
|
False, "--push", "-p", help="Push image to registry after building"
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Build a driver Docker image"""
|
"""Build an image Docker image"""
|
||||||
# Get driver path
|
# Get image path
|
||||||
driver_path = config_manager.get_driver_path(driver_name)
|
image_path = config_manager.get_image_path(image_name)
|
||||||
if not driver_path:
|
if not image_path:
|
||||||
console.print(f"[red]Driver '{driver_name}' not found[/red]")
|
console.print(f"[red]Image '{image_name}' not found[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if Dockerfile exists
|
# Check if Dockerfile exists
|
||||||
dockerfile_path = driver_path / "Dockerfile"
|
dockerfile_path = image_path / "Dockerfile"
|
||||||
if not dockerfile_path.exists():
|
if not dockerfile_path.exists():
|
||||||
console.print(f"[red]Dockerfile not found in {driver_path}[/red]")
|
console.print(f"[red]Dockerfile not found in {image_path}[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Build image name
|
# Build image name
|
||||||
image_name = f"monadical/mc-{driver_name}:{tag}"
|
docker_image_name = f"monadical/mc-{image_name}:{tag}"
|
||||||
|
|
||||||
# Build the image
|
# Build the image
|
||||||
with console.status(f"Building image {image_name}..."):
|
with console.status(f"Building image {docker_image_name}..."):
|
||||||
result = os.system(f"cd {driver_path} && docker build -t {image_name} .")
|
result = os.system(f"cd {image_path} && docker build -t {docker_image_name} .")
|
||||||
|
|
||||||
if result != 0:
|
if result != 0:
|
||||||
console.print("[red]Failed to build driver image[/red]")
|
console.print("[red]Failed to build image[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print(f"[green]Successfully built image: {image_name}[/green]")
|
console.print(f"[green]Successfully built image: {docker_image_name}[/green]")
|
||||||
|
|
||||||
# Push if requested
|
# Push if requested
|
||||||
if push:
|
if push:
|
||||||
with console.status(f"Pushing image {image_name}..."):
|
with console.status(f"Pushing image {docker_image_name}..."):
|
||||||
result = os.system(f"docker push {image_name}")
|
result = os.system(f"docker push {docker_image_name}")
|
||||||
|
|
||||||
if result != 0:
|
if result != 0:
|
||||||
console.print("[red]Failed to push driver image[/red]")
|
console.print("[red]Failed to push image[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print(f"[green]Successfully pushed image: {image_name}[/green]")
|
console.print(f"[green]Successfully pushed image: {docker_image_name}[/green]")
|
||||||
|
|
||||||
|
|
||||||
@driver_app.command("info")
|
@image_app.command("info")
|
||||||
def driver_info(
|
def image_info(
|
||||||
driver_name: str = typer.Argument(..., help="Driver name to get info for"),
|
image_name: str = typer.Argument(..., help="Image name to get info for"),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Show detailed information about a driver"""
|
"""Show detailed information about an image"""
|
||||||
driver = config_manager.get_driver(driver_name)
|
image = config_manager.get_image(image_name)
|
||||||
if not driver:
|
if not image:
|
||||||
console.print(f"[red]Driver '{driver_name}' not found[/red]")
|
console.print(f"[red]Image '{image_name}' not found[/red]")
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print(f"[bold]Driver: {driver.name}[/bold]")
|
console.print(f"[bold]Image: {image.name}[/bold]")
|
||||||
console.print(f"Description: {driver.description}")
|
console.print(f"Description: {image.description}")
|
||||||
console.print(f"Version: {driver.version}")
|
console.print(f"Version: {image.version}")
|
||||||
console.print(f"Maintainer: {driver.maintainer}")
|
console.print(f"Maintainer: {image.maintainer}")
|
||||||
console.print(f"Image: {driver.image}")
|
console.print(f"Docker Image: {image.image}")
|
||||||
|
|
||||||
if driver.ports:
|
if image.ports:
|
||||||
console.print("\n[bold]Ports:[/bold]")
|
console.print("\n[bold]Ports:[/bold]")
|
||||||
for port in driver.ports:
|
for port in image.ports:
|
||||||
console.print(f" {port}")
|
console.print(f" {port}")
|
||||||
|
|
||||||
# Get driver path
|
# Get image path
|
||||||
driver_path = config_manager.get_driver_path(driver_name)
|
image_path = config_manager.get_image_path(image_name)
|
||||||
if driver_path:
|
if image_path:
|
||||||
console.print(f"\n[bold]Path:[/bold] {driver_path}")
|
console.print(f"\n[bold]Path:[/bold] {image_path}")
|
||||||
|
|
||||||
# Check for README
|
# Check for README
|
||||||
readme_path = driver_path / "README.md"
|
readme_path = image_path / "README.md"
|
||||||
if readme_path.exists():
|
if readme_path.exists():
|
||||||
console.print("\n[bold]README:[/bold]")
|
console.print("\n[bold]README:[/bold]")
|
||||||
with open(readme_path, "r") as f:
|
with open(readme_path, "r") as f:
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
import yaml
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from .models import Config, Driver
|
import yaml
|
||||||
|
|
||||||
|
from .models import Config, Image
|
||||||
|
|
||||||
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "mc"
|
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "mc"
|
||||||
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / "config.yaml"
|
||||||
DEFAULT_DRIVERS_DIR = Path.home() / ".config" / "mc" / "drivers"
|
DEFAULT_IMAGES_DIR = Path.home() / ".config" / "mc" / "images"
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
BUILTIN_DRIVERS_DIR = Path(__file__).parent / "drivers" # mcontainer/drivers
|
BUILTIN_IMAGES_DIR = Path(__file__).parent / "images" # mcontainer/images
|
||||||
|
|
||||||
# Dynamically loaded from drivers directory at runtime
|
# Dynamically loaded from images directory at runtime
|
||||||
DEFAULT_DRIVERS = {}
|
DEFAULT_IMAGES = {}
|
||||||
|
|
||||||
|
|
||||||
class ConfigManager:
|
class ConfigManager:
|
||||||
def __init__(self, config_path: Optional[Path] = None):
|
def __init__(self, config_path: Optional[Path] = None):
|
||||||
self.config_path = config_path or DEFAULT_CONFIG_FILE
|
self.config_path = config_path or DEFAULT_CONFIG_FILE
|
||||||
self.config_dir = self.config_path.parent
|
self.config_dir = self.config_path.parent
|
||||||
self.drivers_dir = DEFAULT_DRIVERS_DIR
|
self.images_dir = DEFAULT_IMAGES_DIR
|
||||||
self.config = self._load_or_create_config()
|
self.config = self._load_or_create_config()
|
||||||
|
|
||||||
# Always load package drivers on initialization
|
# Always load package images on initialization
|
||||||
# These are separate from the user config
|
# These are separate from the user config
|
||||||
self.builtin_drivers = self._load_package_drivers()
|
self.builtin_images = self._load_package_images()
|
||||||
|
|
||||||
def _load_or_create_config(self) -> Config:
|
def _load_or_create_config(self) -> Config:
|
||||||
"""Load existing config or create a new one with defaults"""
|
"""Load existing config or create a new one with defaults"""
|
||||||
@@ -38,10 +39,10 @@ class ConfigManager:
|
|||||||
defaults=config_data.get("defaults", {}),
|
defaults=config_data.get("defaults", {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add drivers
|
# Add images
|
||||||
if "drivers" in config_data:
|
if "images" in config_data:
|
||||||
for driver_name, driver_data in config_data["drivers"].items():
|
for image_name, image_data in config_data["images"].items():
|
||||||
config.drivers[driver_name] = Driver.model_validate(driver_data)
|
config.images[image_name] = Image.model_validate(image_data)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -53,16 +54,16 @@ class ConfigManager:
|
|||||||
def _create_default_config(self) -> Config:
|
def _create_default_config(self) -> Config:
|
||||||
"""Create a default configuration"""
|
"""Create a default configuration"""
|
||||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.drivers_dir.mkdir(parents=True, exist_ok=True)
|
self.images_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Initial config without drivers
|
# Initial config without images
|
||||||
config = Config(
|
config = Config(
|
||||||
docker={
|
docker={
|
||||||
"socket": "/var/run/docker.sock",
|
"socket": "/var/run/docker.sock",
|
||||||
"network": "mc-network",
|
"network": "mc-network",
|
||||||
},
|
},
|
||||||
defaults={
|
defaults={
|
||||||
"driver": "goose",
|
"image": "goose",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,92 +84,90 @@ class ConfigManager:
|
|||||||
with open(self.config_path, "w") as f:
|
with open(self.config_path, "w") as f:
|
||||||
yaml.dump(config_dict, f)
|
yaml.dump(config_dict, f)
|
||||||
|
|
||||||
def get_driver(self, name: str) -> Optional[Driver]:
|
def get_image(self, name: str) -> Optional[Image]:
|
||||||
"""Get a driver by name, checking builtin drivers first, then user-configured ones"""
|
"""Get an image by name, checking builtin images first, then user-configured ones"""
|
||||||
# Check builtin drivers first (package drivers take precedence)
|
# Check builtin images first (package images take precedence)
|
||||||
if name in self.builtin_drivers:
|
if name in self.builtin_images:
|
||||||
return self.builtin_drivers[name]
|
return self.builtin_images[name]
|
||||||
# If not found, check user-configured drivers
|
# If not found, check user-configured images
|
||||||
return self.config.drivers.get(name)
|
return self.config.images.get(name)
|
||||||
|
|
||||||
def list_drivers(self) -> Dict[str, Driver]:
|
def list_images(self) -> Dict[str, Image]:
|
||||||
"""List all available drivers (both builtin and user-configured)"""
|
"""List all available images (both builtin and user-configured)"""
|
||||||
# Start with user config drivers
|
# Start with user config images
|
||||||
all_drivers = dict(self.config.drivers)
|
all_images = dict(self.config.images)
|
||||||
|
|
||||||
# Add builtin drivers, overriding any user drivers with the same name
|
# Add builtin images, overriding any user images with the same name
|
||||||
# This ensures that package-provided drivers always take precedence
|
# This ensures that package-provided images always take precedence
|
||||||
all_drivers.update(self.builtin_drivers)
|
all_images.update(self.builtin_images)
|
||||||
|
|
||||||
return all_drivers
|
return all_images
|
||||||
|
|
||||||
# Session management has been moved to SessionManager in session.py
|
# Session management has been moved to SessionManager in session.py
|
||||||
|
|
||||||
def load_driver_from_dir(self, driver_dir: Path) -> Optional[Driver]:
|
def load_image_from_dir(self, image_dir: Path) -> Optional[Image]:
|
||||||
"""Load a driver configuration from a directory"""
|
"""Load an image configuration from a directory"""
|
||||||
# Try with mc-driver.yaml first (new format), then mai-driver.yaml (legacy)
|
# Check for image config file
|
||||||
yaml_path = driver_dir / "mc-driver.yaml"
|
yaml_path = image_dir / "mc-image.yaml"
|
||||||
if not yaml_path.exists():
|
|
||||||
yaml_path = driver_dir / "mai-driver.yaml" # Backward compatibility
|
|
||||||
if not yaml_path.exists():
|
if not yaml_path.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(yaml_path, "r") as f:
|
with open(yaml_path, "r") as f:
|
||||||
driver_data = yaml.safe_load(f)
|
image_data = yaml.safe_load(f)
|
||||||
|
|
||||||
# Extract required fields
|
# Extract required fields
|
||||||
if not all(
|
if not all(
|
||||||
k in driver_data
|
k in image_data
|
||||||
for k in ["name", "description", "version", "maintainer"]
|
for k in ["name", "description", "version", "maintainer"]
|
||||||
):
|
):
|
||||||
print(f"Driver config {yaml_path} missing required fields")
|
print(f"Image config {yaml_path} missing required fields")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Use Driver.model_validate to handle all fields from YAML
|
# Use Image.model_validate to handle all fields from YAML
|
||||||
# This will map all fields according to the Driver model structure
|
# This will map all fields according to the Image model structure
|
||||||
try:
|
try:
|
||||||
# Ensure image field is set if not in YAML
|
# Ensure image field is set if not in YAML
|
||||||
if "image" not in driver_data:
|
if "image" not in image_data:
|
||||||
driver_data["image"] = f"monadical/mc-{driver_data['name']}:latest"
|
image_data["image"] = f"monadical/mc-{image_data['name']}:latest"
|
||||||
|
|
||||||
driver = Driver.model_validate(driver_data)
|
image = Image.model_validate(image_data)
|
||||||
return driver
|
return image
|
||||||
except Exception as validation_error:
|
except Exception as validation_error:
|
||||||
print(
|
print(
|
||||||
f"Error validating driver data from {yaml_path}: {validation_error}"
|
f"Error validating image data from {yaml_path}: {validation_error}"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading driver from {yaml_path}: {e}")
|
print(f"Error loading image from {yaml_path}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _load_package_drivers(self) -> Dict[str, Driver]:
|
def _load_package_images(self) -> Dict[str, Image]:
|
||||||
"""Load all package drivers from the mcontainer/drivers directory"""
|
"""Load all package images from the mcontainer/images directory"""
|
||||||
drivers = {}
|
images = {}
|
||||||
|
|
||||||
if not BUILTIN_DRIVERS_DIR.exists():
|
if not BUILTIN_IMAGES_DIR.exists():
|
||||||
return drivers
|
return images
|
||||||
|
|
||||||
# Search for mc-driver.yaml files in each subdirectory
|
# Search for mc-image.yaml files in each subdirectory
|
||||||
for driver_dir in BUILTIN_DRIVERS_DIR.iterdir():
|
for image_dir in BUILTIN_IMAGES_DIR.iterdir():
|
||||||
if driver_dir.is_dir():
|
if image_dir.is_dir():
|
||||||
driver = self.load_driver_from_dir(driver_dir)
|
image = self.load_image_from_dir(image_dir)
|
||||||
if driver:
|
if image:
|
||||||
drivers[driver.name] = driver
|
images[image.name] = image
|
||||||
|
|
||||||
return drivers
|
return images
|
||||||
|
|
||||||
def get_driver_path(self, driver_name: str) -> Optional[Path]:
|
def get_image_path(self, image_name: str) -> Optional[Path]:
|
||||||
"""Get the directory path for a driver"""
|
"""Get the directory path for an image"""
|
||||||
# Check package drivers first (these are the bundled ones)
|
# Check package images first (these are the bundled ones)
|
||||||
package_path = BUILTIN_DRIVERS_DIR / driver_name
|
package_path = BUILTIN_IMAGES_DIR / image_name
|
||||||
if package_path.exists() and package_path.is_dir():
|
if package_path.exists() and package_path.is_dir():
|
||||||
return package_path
|
return package_path
|
||||||
|
|
||||||
# Then check user drivers
|
# Then check user images
|
||||||
user_path = self.drivers_dir / driver_name
|
user_path = self.images_dir / image_name
|
||||||
if user_path.exists() and user_path.is_dir():
|
if user_path.exists() and user_path.is_dir():
|
||||||
return user_path
|
return user_path
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
|
import concurrent.futures
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
import docker
|
|
||||||
import hashlib
|
|
||||||
import pathlib
|
|
||||||
import concurrent.futures
|
|
||||||
import logging
|
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import docker
|
||||||
from docker.errors import DockerException, ImageNotFound
|
from docker.errors import DockerException, ImageNotFound
|
||||||
|
|
||||||
from .models import Session, SessionStatus
|
|
||||||
from .config import ConfigManager
|
from .config import ConfigManager
|
||||||
from .session import SessionManager
|
|
||||||
from .mcp import MCPManager
|
from .mcp import MCPManager
|
||||||
|
from .models import Session, SessionStatus
|
||||||
|
from .session import SessionManager
|
||||||
from .user_config import UserConfigManager
|
from .user_config import UserConfigManager
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
@@ -109,7 +110,7 @@ class ContainerManager:
|
|||||||
session = Session(
|
session = Session(
|
||||||
id=session_id,
|
id=session_id,
|
||||||
name=labels.get("mc.session.name", f"mc-{session_id}"),
|
name=labels.get("mc.session.name", f"mc-{session_id}"),
|
||||||
driver=labels.get("mc.driver", "unknown"),
|
image=labels.get("mc.image", "unknown"),
|
||||||
status=status,
|
status=status,
|
||||||
container_id=container_id,
|
container_id=container_id,
|
||||||
)
|
)
|
||||||
@@ -136,7 +137,7 @@ class ContainerManager:
|
|||||||
|
|
||||||
def create_session(
|
def create_session(
|
||||||
self,
|
self,
|
||||||
driver_name: str,
|
image_name: str,
|
||||||
project: Optional[str] = None,
|
project: Optional[str] = None,
|
||||||
project_name: Optional[str] = None,
|
project_name: Optional[str] = None,
|
||||||
environment: Optional[Dict[str, str]] = None,
|
environment: Optional[Dict[str, str]] = None,
|
||||||
@@ -155,7 +156,7 @@ class ContainerManager:
|
|||||||
"""Create a new MC session
|
"""Create a new MC session
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
driver_name: The name of the driver to use
|
image_name: The name of the image to use
|
||||||
project: Optional project repository URL or local directory path
|
project: Optional project repository URL or local directory path
|
||||||
project_name: Optional explicit project name for configuration persistence
|
project_name: Optional explicit project name for configuration persistence
|
||||||
environment: Optional environment variables
|
environment: Optional environment variables
|
||||||
@@ -170,10 +171,10 @@ class ContainerManager:
|
|||||||
ssh: Whether to start the SSH server in the container (default: False)
|
ssh: Whether to start the SSH server in the container (default: False)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Validate driver exists
|
# Validate image exists
|
||||||
driver = self.config_manager.get_driver(driver_name)
|
image = self.config_manager.get_image(image_name)
|
||||||
if not driver:
|
if not image:
|
||||||
print(f"Driver '{driver_name}' not found")
|
print(f"Image '{image_name}' not found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Generate session ID and name
|
# Generate session ID and name
|
||||||
@@ -210,10 +211,10 @@ class ContainerManager:
|
|||||||
|
|
||||||
# Pull image if needed
|
# Pull image if needed
|
||||||
try:
|
try:
|
||||||
self.client.images.get(driver.image)
|
self.client.images.get(image.image)
|
||||||
except ImageNotFound:
|
except ImageNotFound:
|
||||||
print(f"Pulling image {driver.image}...")
|
print(f"Pulling image {image.image}...")
|
||||||
self.client.images.pull(driver.image)
|
self.client.images.pull(image.image)
|
||||||
|
|
||||||
# Set up volume mounts
|
# Set up volume mounts
|
||||||
session_volumes = {}
|
session_volumes = {}
|
||||||
@@ -274,13 +275,13 @@ class ContainerManager:
|
|||||||
|
|
||||||
# Add environment variables for config path
|
# Add environment variables for config path
|
||||||
env_vars["MC_CONFIG_DIR"] = "/mc-config"
|
env_vars["MC_CONFIG_DIR"] = "/mc-config"
|
||||||
env_vars["MC_DRIVER_CONFIG_DIR"] = f"/mc-config/{driver_name}"
|
env_vars["MC_IMAGE_CONFIG_DIR"] = f"/mc-config/{image_name}"
|
||||||
|
|
||||||
# Create driver-specific config directories and set up direct volume mounts
|
# Create image-specific config directories and set up direct volume mounts
|
||||||
if driver.persistent_configs:
|
if image.persistent_configs:
|
||||||
persistent_links_data = [] # To store "source:target" pairs for symlinks
|
persistent_links_data = [] # To store "source:target" pairs for symlinks
|
||||||
print("Setting up persistent configuration directories:")
|
print("Setting up persistent configuration directories:")
|
||||||
for config in driver.persistent_configs:
|
for config in image.persistent_configs:
|
||||||
# Get target directory path on host
|
# Get target directory path on host
|
||||||
target_dir = project_config_path / config.target.removeprefix(
|
target_dir = project_config_path / config.target.removeprefix(
|
||||||
"/mc-config/"
|
"/mc-config/"
|
||||||
@@ -494,7 +495,7 @@ class ContainerManager:
|
|||||||
|
|
||||||
# Create container
|
# Create container
|
||||||
container = self.client.containers.create(
|
container = self.client.containers.create(
|
||||||
image=driver.image,
|
image=image.image,
|
||||||
name=session_name,
|
name=session_name,
|
||||||
hostname=session_name,
|
hostname=session_name,
|
||||||
detach=True,
|
detach=True,
|
||||||
@@ -506,7 +507,7 @@ class ContainerManager:
|
|||||||
"mc.session": "true",
|
"mc.session": "true",
|
||||||
"mc.session.id": session_id,
|
"mc.session.id": session_id,
|
||||||
"mc.session.name": session_name,
|
"mc.session.name": session_name,
|
||||||
"mc.driver": driver_name,
|
"mc.image": image_name,
|
||||||
"mc.project": project or "",
|
"mc.project": project or "",
|
||||||
"mc.project_name": project_name or "",
|
"mc.project_name": project_name or "",
|
||||||
"mc.mcps": ",".join(mcp_names) if mcp_names else "",
|
"mc.mcps": ",".join(mcp_names) if mcp_names else "",
|
||||||
@@ -514,7 +515,7 @@ class ContainerManager:
|
|||||||
network=network_list[0], # Connect to the first network initially
|
network=network_list[0], # Connect to the first network initially
|
||||||
command=container_command, # Set the command
|
command=container_command, # Set the command
|
||||||
entrypoint=entrypoint, # Set the entrypoint (might be None)
|
entrypoint=entrypoint, # Set the entrypoint (might be None)
|
||||||
ports={f"{port}/tcp": None for port in driver.ports},
|
ports={f"{port}/tcp": None for port in image.ports},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start container
|
# Start container
|
||||||
@@ -613,7 +614,7 @@ class ContainerManager:
|
|||||||
session = Session(
|
session = Session(
|
||||||
id=session_id,
|
id=session_id,
|
||||||
name=session_name,
|
name=session_name,
|
||||||
driver=driver_name,
|
image=image_name,
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
container_id=container.id,
|
container_id=container.id,
|
||||||
ports=ports,
|
ports=ports,
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
"""
|
|
||||||
Base driver implementation for MAI
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict, Optional
|
|
||||||
|
|
||||||
from ..models import Driver
|
|
||||||
|
|
||||||
|
|
||||||
class DriverManager:
|
|
||||||
"""Manager for MAI drivers"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_default_drivers() -> Dict[str, Driver]:
|
|
||||||
"""Get the default built-in drivers"""
|
|
||||||
from ..config import DEFAULT_DRIVERS
|
|
||||||
|
|
||||||
return DEFAULT_DRIVERS
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_driver_metadata(driver_name: str) -> Optional[Dict]:
|
|
||||||
"""Get metadata for a specific driver"""
|
|
||||||
from ..config import DEFAULT_DRIVERS
|
|
||||||
|
|
||||||
if driver_name in DEFAULT_DRIVERS:
|
|
||||||
return DEFAULT_DRIVERS[driver_name].model_dump()
|
|
||||||
|
|
||||||
return None
|
|
||||||
3
mcontainer/images/__init__.py
Normal file
3
mcontainer/images/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
MAI container image management
|
||||||
|
"""
|
||||||
28
mcontainer/images/base.py
Normal file
28
mcontainer/images/base.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
Base image implementation for MAI
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from ..models import Image
|
||||||
|
|
||||||
|
|
||||||
|
class ImageManager:
|
||||||
|
"""Manager for MAI images"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_default_images() -> Dict[str, Image]:
|
||||||
|
"""Get the default built-in images"""
|
||||||
|
from ..config import DEFAULT_IMAGES
|
||||||
|
|
||||||
|
return DEFAULT_IMAGES
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_image_metadata(image_name: str) -> Optional[Dict]:
|
||||||
|
"""Get metadata for a specific image"""
|
||||||
|
from ..config import DEFAULT_IMAGES
|
||||||
|
|
||||||
|
if image_name in DEFAULT_IMAGES:
|
||||||
|
return DEFAULT_IMAGES[image_name].model_dump()
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -43,7 +43,7 @@ WORKDIR /app
|
|||||||
# Copy initialization scripts
|
# Copy initialization scripts
|
||||||
COPY mc-init.sh /mc-init.sh
|
COPY mc-init.sh /mc-init.sh
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
COPY mc-driver.yaml /mc-driver.yaml
|
COPY mc-image.yaml /mc-image.yaml
|
||||||
COPY init-status.sh /init-status.sh
|
COPY init-status.sh /init-status.sh
|
||||||
COPY update-goose-config.py /usr/local/bin/update-goose-config.py
|
COPY update-goose-config.py /usr/local/bin/update-goose-config.py
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Goose Driver for MC
|
# Goose Image for MC
|
||||||
|
|
||||||
This driver provides a containerized environment for running [Goose](https://goose.ai).
|
This image provides a containerized environment for running [Goose](https://goose.ai).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ This driver provides a containerized environment for running [Goose](https://goo
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
To build this driver:
|
To build this image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd drivers/goose
|
cd drivers/goose
|
||||||
@@ -33,7 +33,7 @@ docker build -t monadical/mc-goose:latest .
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new session with this driver
|
# Create a new session with this image
|
||||||
mc session create --driver goose
|
mc session create --driver goose
|
||||||
|
|
||||||
# Create with project repository
|
# Create with project repository
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Entrypoint script for Goose driver
|
# Entrypoint script for Goose image
|
||||||
# Executes the standard initialization script, which handles user setup,
|
# Executes the standard initialization script, which handles user setup,
|
||||||
# service startup (like sshd), and switching to the non-root user
|
# service startup (like sshd), and switching to the non-root user
|
||||||
# before running the container's command (CMD).
|
# before running the container's command (CMD).
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Standardized initialization script for MC drivers
|
# Standardized initialization script for MC images
|
||||||
|
|
||||||
# Redirect all output to both stdout and the log file
|
# Redirect all output to both stdout and the log file
|
||||||
exec > >(tee -a /init.log) 2>&1
|
exec > >(tee -a /init.log) 2>&1
|
||||||
@@ -117,7 +117,7 @@ if [ ! -d "/mc-config" ]; then
|
|||||||
chown $MC_USER_ID:$MC_GROUP_ID /mc-config
|
chown $MC_USER_ID:$MC_GROUP_ID /mc-config
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create symlinks for persistent configurations defined in the driver
|
# Create symlinks for persistent configurations defined in the image
|
||||||
if [ -n "$MC_PERSISTENT_LINKS" ]; then
|
if [ -n "$MC_PERSISTENT_LINKS" ]; then
|
||||||
echo "Creating persistent configuration symlinks..."
|
echo "Creating persistent configuration symlinks..."
|
||||||
# Split by semicolon
|
# Split by semicolon
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
MCP (Model Control Protocol) server management for Monadical Container.
|
MCP (Model Control Protocol) server management for Monadical Container.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import docker
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import docker
|
||||||
from docker.errors import DockerException, ImageNotFound, NotFound
|
from docker.errors import DockerException, ImageNotFound, NotFound
|
||||||
|
|
||||||
from .models import MCPStatus, RemoteMCP, DockerMCP, ProxyMCP, MCPContainer
|
from .models import DockerMCP, MCPContainer, MCPStatus, ProxyMCP, RemoteMCP
|
||||||
from .user_config import UserConfigManager
|
from .user_config import UserConfigManager
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, Optional, Union, Any
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class MCPStatus(str, Enum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
class DriverEnvironmentVariable(BaseModel):
|
class ImageEnvironmentVariable(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
required: bool = False
|
required: bool = False
|
||||||
@@ -37,19 +38,19 @@ class VolumeMount(BaseModel):
|
|||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
class DriverInit(BaseModel):
|
class ImageInit(BaseModel):
|
||||||
pre_command: Optional[str] = None
|
pre_command: Optional[str] = None
|
||||||
command: str
|
command: str
|
||||||
|
|
||||||
|
|
||||||
class Driver(BaseModel):
|
class Image(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
version: str
|
version: str
|
||||||
maintainer: str
|
maintainer: str
|
||||||
image: str
|
image: str
|
||||||
init: Optional[DriverInit] = None
|
init: Optional[ImageInit] = None
|
||||||
environment: List[DriverEnvironmentVariable] = []
|
environment: List[ImageEnvironmentVariable] = []
|
||||||
ports: List[int] = []
|
ports: List[int] = []
|
||||||
volumes: List[VolumeMount] = []
|
volumes: List[VolumeMount] = []
|
||||||
persistent_configs: List[PersistentConfig] = []
|
persistent_configs: List[PersistentConfig] = []
|
||||||
@@ -97,7 +98,7 @@ class MCPContainer(BaseModel):
|
|||||||
class Session(BaseModel):
|
class Session(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
driver: str
|
image: str
|
||||||
status: SessionStatus
|
status: SessionStatus
|
||||||
container_id: Optional[str] = None
|
container_id: Optional[str] = None
|
||||||
ports: Dict[int, int] = Field(default_factory=dict)
|
ports: Dict[int, int] = Field(default_factory=dict)
|
||||||
@@ -105,7 +106,7 @@ class Session(BaseModel):
|
|||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
docker: Dict[str, str] = Field(default_factory=dict)
|
docker: Dict[str, str] = Field(default_factory=dict)
|
||||||
drivers: Dict[str, Driver] = Field(default_factory=dict)
|
images: Dict[str, Image] = Field(default_factory=dict)
|
||||||
defaults: Dict[str, object] = Field(
|
defaults: Dict[str, object] = Field(
|
||||||
default_factory=dict
|
default_factory=dict
|
||||||
) # Can store strings, booleans, or other values
|
) # Can store strings, booleans, or other values
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
"""
|
|
||||||
MC Service - Container Management Web Service
|
|
||||||
(This is a placeholder for Phase 2)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Run the MC service"""
|
|
||||||
print("MC Service - Container Management Web Service")
|
|
||||||
print("This feature will be implemented in Phase 2")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -3,10 +3,11 @@ Session storage management for Monadical Container Tool.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
DEFAULT_SESSIONS_FILE = Path.home() / ".config" / "mc" / "sessions.yaml"
|
DEFAULT_SESSIONS_FILE = Path.home() / ".config" / "mc" / "sessions.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ User configuration manager for Monadical Container Tool.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional, List, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
# Define the environment variable mappings
|
# Define the environment variable mappings
|
||||||
ENV_MAPPINGS = {
|
ENV_MAPPINGS = {
|
||||||
@@ -89,7 +90,7 @@ class UserConfigManager:
|
|||||||
"""Get the default configuration."""
|
"""Get the default configuration."""
|
||||||
return {
|
return {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"driver": "goose",
|
"image": "goose",
|
||||||
"connect": True,
|
"connect": True,
|
||||||
"mount_local": True,
|
"mount_local": True,
|
||||||
"networks": [], # Default networks to connect to (besides mc-network)
|
"networks": [], # Default networks to connect to (besides mc-network)
|
||||||
@@ -133,7 +134,7 @@ class UserConfigManager:
|
|||||||
"""Get a configuration value by dot-notation path.
|
"""Get a configuration value by dot-notation path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key_path: The configuration path (e.g., "defaults.driver")
|
key_path: The configuration path (e.g., "defaults.image")
|
||||||
default: The default value to return if not found
|
default: The default value to return if not found
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -165,7 +166,7 @@ class UserConfigManager:
|
|||||||
"""Set a configuration value by dot-notation path.
|
"""Set a configuration value by dot-notation path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key_path: The configuration path (e.g., "defaults.driver")
|
key_path: The configuration path (e.g., "defaults.image")
|
||||||
value: The value to set
|
value: The value to set
|
||||||
"""
|
"""
|
||||||
# Handle shorthand service paths (e.g., "langfuse.url")
|
# Handle shorthand service paths (e.g., "langfuse.url")
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ def isolated_session_manager(temp_config_dir):
|
|||||||
def isolated_config_manager():
|
def isolated_config_manager():
|
||||||
"""Create an isolated config manager for testing."""
|
"""Create an isolated config manager for testing."""
|
||||||
config_manager = ConfigManager()
|
config_manager = ConfigManager()
|
||||||
# Ensure we're using the built-in drivers, not trying to load from user config
|
# Ensure we're using the built-in images, not trying to load from user config
|
||||||
return config_manager
|
return config_manager
|
||||||
|
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ def mock_container_manager():
|
|||||||
mock_session = Session(
|
mock_session = Session(
|
||||||
id="test-session-id",
|
id="test-session-id",
|
||||||
name="test-session",
|
name="test-session",
|
||||||
driver="goose",
|
image="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
ports={"8080": "8080"},
|
ports={"8080": "8080"},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ def test_config_list(cli_runner, patched_config_manager):
|
|||||||
assert "Value" in result.stdout
|
assert "Value" in result.stdout
|
||||||
|
|
||||||
# Check for default configurations
|
# Check for default configurations
|
||||||
assert "defaults.driver" in result.stdout
|
assert "defaults.image" in result.stdout
|
||||||
assert "defaults.connect" in result.stdout
|
assert "defaults.connect" in result.stdout
|
||||||
assert "defaults.mount_local" in result.stdout
|
assert "defaults.mount_local" in result.stdout
|
||||||
|
|
||||||
@@ -22,10 +22,10 @@ def test_config_list(cli_runner, patched_config_manager):
|
|||||||
def test_config_get(cli_runner, patched_config_manager):
|
def test_config_get(cli_runner, patched_config_manager):
|
||||||
"""Test the 'mc config get' command."""
|
"""Test the 'mc config get' command."""
|
||||||
# Test getting an existing value
|
# Test getting an existing value
|
||||||
result = cli_runner.invoke(app, ["config", "get", "defaults.driver"])
|
result = cli_runner.invoke(app, ["config", "get", "defaults.image"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "defaults.driver" in result.stdout
|
assert "defaults.image" in result.stdout
|
||||||
assert "goose" in result.stdout
|
assert "goose" in result.stdout
|
||||||
|
|
||||||
# Test getting a non-existent value
|
# Test getting a non-existent value
|
||||||
@@ -38,11 +38,11 @@ def test_config_get(cli_runner, patched_config_manager):
|
|||||||
def test_config_set(cli_runner, patched_config_manager):
|
def test_config_set(cli_runner, patched_config_manager):
|
||||||
"""Test the 'mc config set' command."""
|
"""Test the 'mc config set' command."""
|
||||||
# Test setting a string value
|
# Test setting a string value
|
||||||
result = cli_runner.invoke(app, ["config", "set", "defaults.driver", "claude"])
|
result = cli_runner.invoke(app, ["config", "set", "defaults.image", "claude"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Configuration updated" in result.stdout
|
assert "Configuration updated" in result.stdout
|
||||||
assert patched_config_manager.get("defaults.driver") == "claude"
|
assert patched_config_manager.get("defaults.image") == "claude"
|
||||||
|
|
||||||
# Test setting a boolean value
|
# Test setting a boolean value
|
||||||
result = cli_runner.invoke(app, ["config", "set", "defaults.connect", "false"])
|
result = cli_runner.invoke(app, ["config", "set", "defaults.connect", "false"])
|
||||||
@@ -174,7 +174,7 @@ def test_network_remove(cli_runner, patched_config_manager):
|
|||||||
def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
|
def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
|
||||||
"""Test resetting the configuration."""
|
"""Test resetting the configuration."""
|
||||||
# Set a custom value first
|
# Set a custom value first
|
||||||
patched_config_manager.set("defaults.driver", "custom-driver")
|
patched_config_manager.set("defaults.image", "custom-image")
|
||||||
|
|
||||||
# Mock typer.confirm to return True
|
# Mock typer.confirm to return True
|
||||||
monkeypatch.setattr("typer.confirm", lambda message: True)
|
monkeypatch.setattr("typer.confirm", lambda message: True)
|
||||||
@@ -186,7 +186,7 @@ def test_config_reset(cli_runner, patched_config_manager, monkeypatch):
|
|||||||
assert "Configuration reset to defaults" in result.stdout
|
assert "Configuration reset to defaults" in result.stdout
|
||||||
|
|
||||||
# Verify it was reset
|
# Verify it was reset
|
||||||
assert patched_config_manager.get("defaults.driver") == "goose"
|
assert patched_config_manager.get("defaults.image") == "goose"
|
||||||
|
|
||||||
|
|
||||||
# patched_config_manager fixture is now in conftest.py
|
# patched_config_manager fixture is now in conftest.py
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def test_integration_session_create_with_volumes(container_manager, test_file_co
|
|||||||
try:
|
try:
|
||||||
# Create a session with a volume mount
|
# Create a session with a volume mount
|
||||||
session = container_manager.create_session(
|
session = container_manager.create_session(
|
||||||
driver_name="goose",
|
image_name="goose",
|
||||||
session_name=f"mc-test-volume-{uuid.uuid4().hex[:8]}",
|
session_name=f"mc-test-volume-{uuid.uuid4().hex[:8]}",
|
||||||
mount_local=False, # Don't mount current directory
|
mount_local=False, # Don't mount current directory
|
||||||
volumes={str(test_file): {"bind": "/test/volume_test.txt", "mode": "ro"}},
|
volumes={str(test_file): {"bind": "/test/volume_test.txt", "mode": "ro"}},
|
||||||
@@ -65,7 +65,7 @@ def test_integration_session_create_with_networks(
|
|||||||
try:
|
try:
|
||||||
# Create a session with the test network
|
# Create a session with the test network
|
||||||
session = container_manager.create_session(
|
session = container_manager.create_session(
|
||||||
driver_name="goose",
|
image_name="goose",
|
||||||
session_name=f"mc-test-network-{uuid.uuid4().hex[:8]}",
|
session_name=f"mc-test-network-{uuid.uuid4().hex[:8]}",
|
||||||
mount_local=False, # Don't mount current directory
|
mount_local=False, # Don't mount current directory
|
||||||
networks=[docker_test_network],
|
networks=[docker_test_network],
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ def test_mcp_add(cli_runner, patched_config_manager):
|
|||||||
assert "mcp/github:la" in result.stdout # Truncated in table view
|
assert "mcp/github:la" in result.stdout # Truncated in table view
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_remove(cli_runner, patched_config_manager):
|
def test_mcp_remove(cli_runner, patched_config_manager):
|
||||||
"""Test removing an MCP server."""
|
"""Test removing an MCP server."""
|
||||||
# Add a remote MCP server
|
# Add a remote MCP server
|
||||||
@@ -292,16 +290,14 @@ def test_session_with_mcp(cli_runner, patched_config_manager, mock_container_man
|
|||||||
# Mock the session creation with MCP
|
# Mock the session creation with MCP
|
||||||
from mcontainer.models import Session, SessionStatus
|
from mcontainer.models import Session, SessionStatus
|
||||||
|
|
||||||
timestamp = "2023-01-01T00:00:00Z"
|
# timestamp no longer needed since we don't use created_at in Session
|
||||||
mock_container_manager.create_session.return_value = Session(
|
mock_container_manager.create_session.return_value = Session(
|
||||||
id="test-session-id",
|
id="test-session-id",
|
||||||
name="test-session",
|
name="test-session",
|
||||||
driver="goose",
|
image="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
container_id="test-container-id",
|
container_id="test-container-id",
|
||||||
created_at=timestamp,
|
|
||||||
ports={},
|
ports={},
|
||||||
mcps=["test-mcp"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a session with MCP
|
# Create a session with MCP
|
||||||
|
|||||||
@@ -26,12 +26,9 @@ def test_session_list_with_sessions(cli_runner, mock_container_manager):
|
|||||||
mock_session = Session(
|
mock_session = Session(
|
||||||
id="test-session-id",
|
id="test-session-id",
|
||||||
name="test-session",
|
name="test-session",
|
||||||
driver="goose",
|
image="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
ports={"8080": "8080"},
|
ports={"8080": "8080"},
|
||||||
project=None,
|
|
||||||
created_at="2023-01-01T00:00:00Z",
|
|
||||||
mcps=[],
|
|
||||||
)
|
)
|
||||||
mock_container_manager.list_sessions.return_value = [mock_session]
|
mock_container_manager.list_sessions.return_value = [mock_session]
|
||||||
|
|
||||||
@@ -48,7 +45,7 @@ def test_session_create_basic(cli_runner, mock_container_manager):
|
|||||||
with patch("mcontainer.cli.user_config") as mock_user_config:
|
with patch("mcontainer.cli.user_config") as mock_user_config:
|
||||||
# Handle different key requests appropriately
|
# Handle different key requests appropriately
|
||||||
def mock_get_side_effect(key, default=None):
|
def mock_get_side_effect(key, default=None):
|
||||||
if key == "defaults.driver":
|
if key == "defaults.image":
|
||||||
return "goose"
|
return "goose"
|
||||||
elif key == "defaults.volumes":
|
elif key == "defaults.volumes":
|
||||||
return [] # Return empty list for volumes
|
return [] # Return empty list for volumes
|
||||||
@@ -71,10 +68,10 @@ def test_session_create_basic(cli_runner, mock_container_manager):
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Session created successfully" in result.stdout
|
assert "Session created successfully" in result.stdout
|
||||||
|
|
||||||
# Verify container_manager was called with the expected driver
|
# Verify container_manager was called with the expected image
|
||||||
mock_container_manager.create_session.assert_called_once()
|
mock_container_manager.create_session.assert_called_once()
|
||||||
assert (
|
assert (
|
||||||
mock_container_manager.create_session.call_args[1]["driver_name"] == "goose"
|
mock_container_manager.create_session.call_args[1]["image_name"] == "goose"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -94,16 +91,14 @@ def test_session_close_all(cli_runner, mock_container_manager):
|
|||||||
# Set up mock sessions
|
# Set up mock sessions
|
||||||
from mcontainer.models import Session, SessionStatus
|
from mcontainer.models import Session, SessionStatus
|
||||||
|
|
||||||
timestamp = "2023-01-01T00:00:00Z"
|
# timestamp no longer needed since we don't use created_at in Session
|
||||||
mock_sessions = [
|
mock_sessions = [
|
||||||
Session(
|
Session(
|
||||||
id=f"session-{i}",
|
id=f"session-{i}",
|
||||||
name=f"Session {i}",
|
name=f"Session {i}",
|
||||||
driver="goose",
|
image="goose",
|
||||||
status=SessionStatus.RUNNING,
|
status=SessionStatus.RUNNING,
|
||||||
ports={},
|
ports={},
|
||||||
project=None,
|
|
||||||
created_at=timestamp,
|
|
||||||
)
|
)
|
||||||
for i in range(3)
|
for i in range(3)
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user