mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-21 04:39:07 +00:00
feat(cli): phase 1 - local cli with docker integration
This commit is contained in:
235
mcontainer/container.py
Normal file
235
mcontainer/container.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
import docker
|
||||
from typing import Dict, List, Optional
|
||||
from docker.errors import DockerException, ImageNotFound
|
||||
|
||||
from .models import Session, SessionStatus
|
||||
from .config import ConfigManager
|
||||
|
||||
|
||||
class ContainerManager:
|
||||
def __init__(self, config_manager: Optional[ConfigManager] = None):
|
||||
self.config_manager = config_manager or ConfigManager()
|
||||
try:
|
||||
self.client = docker.from_env()
|
||||
# Test connection
|
||||
self.client.ping()
|
||||
except DockerException as e:
|
||||
print(f"Error connecting to Docker: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def _ensure_network(self) -> None:
|
||||
"""Ensure the MC network exists"""
|
||||
network_name = self.config_manager.config.docker.get("network", "mc-network")
|
||||
networks = self.client.networks.list(names=[network_name])
|
||||
if not networks:
|
||||
self.client.networks.create(network_name, driver="bridge")
|
||||
|
||||
def _generate_session_id(self) -> str:
|
||||
"""Generate a unique session ID"""
|
||||
return str(uuid.uuid4())[:8]
|
||||
|
||||
def list_sessions(self) -> List[Session]:
|
||||
"""List all active MC sessions"""
|
||||
sessions = []
|
||||
try:
|
||||
containers = self.client.containers.list(
|
||||
all=True, filters={"label": "mc.session"}
|
||||
)
|
||||
|
||||
for container in containers:
|
||||
container_id = container.id
|
||||
labels = container.labels
|
||||
|
||||
session_id = labels.get("mc.session.id")
|
||||
if not session_id:
|
||||
continue
|
||||
|
||||
status = SessionStatus.RUNNING
|
||||
if container.status == "exited":
|
||||
status = SessionStatus.STOPPED
|
||||
elif container.status == "created":
|
||||
status = SessionStatus.CREATING
|
||||
|
||||
session = Session(
|
||||
id=session_id,
|
||||
name=labels.get("mc.session.name", f"mc-{session_id}"),
|
||||
driver=labels.get("mc.driver", "unknown"),
|
||||
status=status,
|
||||
container_id=container_id,
|
||||
created_at=container.attrs["Created"],
|
||||
project=labels.get("mc.project"),
|
||||
)
|
||||
|
||||
# Get port mappings
|
||||
if container.attrs.get("NetworkSettings", {}).get("Ports"):
|
||||
ports = {}
|
||||
for container_port, host_ports in container.attrs[
|
||||
"NetworkSettings"
|
||||
]["Ports"].items():
|
||||
if host_ports:
|
||||
# Strip /tcp or /udp suffix and convert to int
|
||||
container_port_num = int(container_port.split("/")[0])
|
||||
host_port = int(host_ports[0]["HostPort"])
|
||||
ports[container_port_num] = host_port
|
||||
session.ports = ports
|
||||
|
||||
sessions.append(session)
|
||||
|
||||
except DockerException as e:
|
||||
print(f"Error listing sessions: {e}")
|
||||
|
||||
return sessions
|
||||
|
||||
def create_session(
|
||||
self,
|
||||
driver_name: str,
|
||||
project: Optional[str] = None,
|
||||
environment: Optional[Dict[str, str]] = None,
|
||||
session_name: Optional[str] = None,
|
||||
) -> Optional[Session]:
|
||||
"""Create a new MC session"""
|
||||
try:
|
||||
# Validate driver exists
|
||||
driver = self.config_manager.get_driver(driver_name)
|
||||
if not driver:
|
||||
print(f"Driver '{driver_name}' not found")
|
||||
return None
|
||||
|
||||
# Generate session ID and name
|
||||
session_id = self._generate_session_id()
|
||||
if not session_name:
|
||||
session_name = f"mc-{session_id}"
|
||||
|
||||
# Ensure network exists
|
||||
self._ensure_network()
|
||||
|
||||
# Prepare environment variables
|
||||
env_vars = environment or {}
|
||||
|
||||
# Pull image if needed
|
||||
try:
|
||||
self.client.images.get(driver.image)
|
||||
except ImageNotFound:
|
||||
print(f"Pulling image {driver.image}...")
|
||||
self.client.images.pull(driver.image)
|
||||
|
||||
# Create container
|
||||
container = self.client.containers.create(
|
||||
image=driver.image,
|
||||
name=session_name,
|
||||
hostname=session_name,
|
||||
detach=True,
|
||||
tty=True,
|
||||
stdin_open=True,
|
||||
environment=env_vars,
|
||||
labels={
|
||||
"mc.session": "true",
|
||||
"mc.session.id": session_id,
|
||||
"mc.session.name": session_name,
|
||||
"mc.driver": driver_name,
|
||||
"mc.project": project or "",
|
||||
},
|
||||
network=self.config_manager.config.docker.get("network", "mc-network"),
|
||||
ports={f"{port}/tcp": None for port in driver.ports},
|
||||
)
|
||||
|
||||
# Start container
|
||||
container.start()
|
||||
|
||||
# Get updated port information
|
||||
container.reload()
|
||||
ports = {}
|
||||
if container.attrs.get("NetworkSettings", {}).get("Ports"):
|
||||
for container_port, host_ports in container.attrs["NetworkSettings"][
|
||||
"Ports"
|
||||
].items():
|
||||
if host_ports:
|
||||
container_port_num = int(container_port.split("/")[0])
|
||||
host_port = int(host_ports[0]["HostPort"])
|
||||
ports[container_port_num] = host_port
|
||||
|
||||
# Create session object
|
||||
session = Session(
|
||||
id=session_id,
|
||||
name=session_name,
|
||||
driver=driver_name,
|
||||
status=SessionStatus.RUNNING,
|
||||
container_id=container.id,
|
||||
environment=env_vars,
|
||||
project=project,
|
||||
created_at=container.attrs["Created"],
|
||||
ports=ports,
|
||||
)
|
||||
|
||||
# Save session to config as JSON-compatible dict
|
||||
self.config_manager.add_session(session_id, session.model_dump(mode="json"))
|
||||
|
||||
return session
|
||||
|
||||
except DockerException as e:
|
||||
print(f"Error creating session: {e}")
|
||||
return None
|
||||
|
||||
def close_session(self, session_id: str) -> bool:
|
||||
"""Close a MC session"""
|
||||
try:
|
||||
sessions = self.list_sessions()
|
||||
for session in sessions:
|
||||
if session.id == session_id and session.container_id:
|
||||
container = self.client.containers.get(session.container_id)
|
||||
container.stop()
|
||||
container.remove()
|
||||
self.config_manager.remove_session(session_id)
|
||||
return True
|
||||
|
||||
print(f"Session '{session_id}' not found")
|
||||
return False
|
||||
|
||||
except DockerException as e:
|
||||
print(f"Error closing session: {e}")
|
||||
return False
|
||||
|
||||
def connect_session(self, session_id: str) -> bool:
|
||||
"""Connect to a running MC session"""
|
||||
try:
|
||||
sessions = self.list_sessions()
|
||||
for session in sessions:
|
||||
if session.id == session_id and session.container_id:
|
||||
if session.status != SessionStatus.RUNNING:
|
||||
print(f"Session '{session_id}' is not running")
|
||||
return False
|
||||
|
||||
# Execute interactive shell in container
|
||||
os.system(f"docker exec -it {session.container_id} /bin/bash")
|
||||
return True
|
||||
|
||||
print(f"Session '{session_id}' not found")
|
||||
return False
|
||||
|
||||
except DockerException as e:
|
||||
print(f"Error connecting to session: {e}")
|
||||
return False
|
||||
|
||||
def get_session_logs(self, session_id: str, follow: bool = False) -> Optional[str]:
|
||||
"""Get logs from a MC session"""
|
||||
try:
|
||||
sessions = self.list_sessions()
|
||||
for session in sessions:
|
||||
if session.id == session_id and session.container_id:
|
||||
container = self.client.containers.get(session.container_id)
|
||||
if follow:
|
||||
for line in container.logs(stream=True, follow=True):
|
||||
print(line.decode().strip())
|
||||
return None
|
||||
else:
|
||||
return container.logs().decode()
|
||||
|
||||
print(f"Session '{session_id}' not found")
|
||||
return None
|
||||
|
||||
except DockerException as e:
|
||||
print(f"Error getting session logs: {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user