mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-20 12:19:07 +00:00
feat(mc): support for uid/gid, and use default current user
This commit is contained in:
@@ -152,6 +152,12 @@ def create_session(
|
||||
"-m",
|
||||
help="Attach MCP servers to the session (can be specified multiple times)",
|
||||
),
|
||||
uid: Optional[int] = typer.Option(
|
||||
None, "--uid", help="User ID to run the container as (defaults to host user)"
|
||||
),
|
||||
gid: Optional[int] = typer.Option(
|
||||
None, "--gid", help="Group ID to run the container as (defaults to host user)"
|
||||
),
|
||||
) -> None:
|
||||
"""Create a new MC session
|
||||
|
||||
@@ -159,6 +165,11 @@ def create_session(
|
||||
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.
|
||||
"""
|
||||
# Determine UID/GID
|
||||
target_uid = uid if uid is not None else os.getuid()
|
||||
target_gid = gid if gid is not None else os.getgid()
|
||||
console.print(f"Using UID: {target_uid}, GID: {target_gid}")
|
||||
|
||||
# Use default driver from user configuration
|
||||
if not driver:
|
||||
driver = user_config.get(
|
||||
@@ -248,6 +259,8 @@ def create_session(
|
||||
volumes=volume_mounts,
|
||||
networks=all_networks,
|
||||
mcp=all_mcps,
|
||||
uid=target_uid,
|
||||
gid=target_gid,
|
||||
)
|
||||
|
||||
if session:
|
||||
|
||||
@@ -145,6 +145,8 @@ class ContainerManager:
|
||||
volumes: Optional[Dict[str, Dict[str, str]]] = None,
|
||||
networks: Optional[List[str]] = None,
|
||||
mcp: Optional[List[str]] = None,
|
||||
uid: Optional[int] = None,
|
||||
gid: Optional[int] = None,
|
||||
) -> Optional[Session]:
|
||||
"""Create a new MC session
|
||||
|
||||
@@ -157,6 +159,8 @@ class ContainerManager:
|
||||
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
|
||||
mcp: Optional list of MCP server names to attach to the session
|
||||
uid: Optional user ID for the container process
|
||||
gid: Optional group ID for the container process
|
||||
"""
|
||||
try:
|
||||
# Validate driver exists
|
||||
@@ -176,6 +180,12 @@ class ContainerManager:
|
||||
# Prepare environment variables
|
||||
env_vars = environment or {}
|
||||
|
||||
# Add TARGET_UID and TARGET_GID for entrypoint script
|
||||
if uid is not None:
|
||||
env_vars["TARGET_UID"] = str(uid)
|
||||
if gid is not None:
|
||||
env_vars["TARGET_GID"] = str(gid)
|
||||
|
||||
# Add project URL to environment if provided
|
||||
if project:
|
||||
env_vars["MC_PROJECT_URL"] = project
|
||||
@@ -534,12 +544,17 @@ class ContainerManager:
|
||||
created_at=container.attrs["Created"],
|
||||
ports=ports,
|
||||
mcps=mcp_names,
|
||||
# Assuming Session model has uid and gid fields
|
||||
uid=uid,
|
||||
gid=gid,
|
||||
)
|
||||
|
||||
# Save session to the session manager as JSON-compatible dict
|
||||
self.session_manager.add_session(
|
||||
session_id, session.model_dump(mode="json")
|
||||
)
|
||||
# Assuming Session model has uid and gid fields added to its definition
|
||||
session_data_to_save = session.model_dump(mode="json")
|
||||
session_data_to_save["uid"] = uid
|
||||
session_data_to_save["gid"] = gid
|
||||
self.session_manager.add_session(session_id, session_data_to_save)
|
||||
|
||||
return session
|
||||
|
||||
@@ -564,20 +579,72 @@ class ContainerManager:
|
||||
|
||||
def connect_session(self, session_id: str) -> bool:
|
||||
"""Connect to a running MC session"""
|
||||
try:
|
||||
# Retrieve full session data which should include uid/gid
|
||||
session_data = self.session_manager.get_session(session_id)
|
||||
|
||||
if not session_data:
|
||||
print(f"Session '{session_id}' not found in session manager.")
|
||||
# Fallback: try listing via Docker labels if session data is missing
|
||||
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
|
||||
session_obj = next((s for s in sessions if s.id == session_id), None)
|
||||
if not session_obj or not session_obj.container_id:
|
||||
print(f"Session '{session_id}' not found via Docker either.")
|
||||
return False
|
||||
container_id = session_obj.container_id
|
||||
# Cannot determine user if session data is missing
|
||||
user_spec = None
|
||||
print(
|
||||
f"[yellow]Warning: Session data missing for {session_id}. Connecting as default container user.[/yellow]"
|
||||
)
|
||||
else:
|
||||
container_id = session_data.get("container_id")
|
||||
if not container_id:
|
||||
print(f"Container ID not found for session {session_id}.")
|
||||
return False
|
||||
|
||||
# Execute interactive shell in container
|
||||
# The init-status.sh script will automatically show logs if needed
|
||||
os.system(f"docker exec -it {session.container_id} /bin/bash")
|
||||
return True
|
||||
# Check status from Docker directly
|
||||
try:
|
||||
container = self.client.containers.get(container_id)
|
||||
if container.status != "running":
|
||||
print(
|
||||
f"Session '{session_id}' container is not running (status: {container.status})."
|
||||
)
|
||||
return False
|
||||
except docker.errors.NotFound:
|
||||
print(f"Container {container_id} for session {session_id} not found.")
|
||||
# Clean up potentially stale session data
|
||||
self.session_manager.remove_session(session_id)
|
||||
return False
|
||||
except DockerException as e:
|
||||
print(f"Error checking container status for session {session_id}: {e}")
|
||||
return False
|
||||
|
||||
print(f"Session '{session_id}' not found")
|
||||
# Determine user spec from stored session data
|
||||
uid = session_data.get("uid")
|
||||
gid = session_data.get("gid")
|
||||
user_spec = f"{uid}:{gid}" if uid is not None and gid is not None else None
|
||||
|
||||
try:
|
||||
# Execute interactive shell in container
|
||||
cmd = ["docker", "exec", "-it"]
|
||||
if user_spec:
|
||||
cmd.extend(["--user", user_spec])
|
||||
print(f"Connecting as user {user_spec}...")
|
||||
else:
|
||||
print("Connecting as default container user...")
|
||||
|
||||
cmd.extend([container_id, "/bin/bash"])
|
||||
|
||||
# Use execvp to replace the current process with docker exec
|
||||
# This provides a more seamless shell experience
|
||||
os.execvp("docker", cmd)
|
||||
# execvp does not return if successful
|
||||
return True # Should not be reached if execvp succeeds
|
||||
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"[red]Error: 'docker' command not found. Is Docker installed and in your PATH?[/red]"
|
||||
)
|
||||
return False
|
||||
|
||||
except DockerException as e:
|
||||
|
||||
@@ -3,8 +3,10 @@ FROM python:3.12-slim
|
||||
LABEL maintainer="team@monadical.com"
|
||||
LABEL description="Goose with MCP servers"
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
# Install system dependencies including gosu for user switching and shadow for useradd/groupadd
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
passwd \
|
||||
git \
|
||||
openssh-server \
|
||||
bash \
|
||||
@@ -12,22 +14,27 @@ RUN apt-get update && apt-get install -y \
|
||||
bzip2 \
|
||||
iputils-ping \
|
||||
iproute2 \
|
||||
libxcb1 \
|
||||
libdbus-1-3 \
|
||||
nano \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set up SSH server
|
||||
RUN mkdir /var/run/sshd
|
||||
RUN echo 'root:root' | chpasswd
|
||||
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
||||
# Set up SSH server directory (configuration will be handled by entrypoint if needed)
|
||||
RUN mkdir -p /var/run/sshd && chmod 0755 /var/run/sshd
|
||||
# Do NOT enable root login or set root password here
|
||||
|
||||
# Install python dependencies
|
||||
# This is done before copying scripts for better cache management
|
||||
# Consider moving this WORKDIR /tmp section if goose CLI isn't strictly needed for base image setup
|
||||
WORKDIR /tmp
|
||||
RUN curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh -o download_cli.sh && \
|
||||
chmod +x download_cli.sh && \
|
||||
./download_cli.sh
|
||||
./download_cli.sh && \
|
||||
# Move goose to a system-wide location
|
||||
mv /root/.local/bin/goose /usr/local/bin/goose && \
|
||||
# Clean up
|
||||
rm -rf /root/.local download_cli.sh /tmp/goose-*
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
@@ -46,15 +53,18 @@ RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh \
|
||||
/usr/local/bin/update-goose-config.sh
|
||||
|
||||
# Set up initialization status check on login
|
||||
RUN echo 'export PATH=/root/.local/bin:$PATH' >> /etc/bash.bashrc
|
||||
RUN echo '[ -x /init-status.sh ] && /init-status.sh' >> /etc/bash.bashrc
|
||||
|
||||
# Set up environment
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
# Set WORKDIR to /app, common practice and expected by mc-init.sh
|
||||
WORKDIR /app
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8000 22
|
||||
|
||||
# Set entrypoint
|
||||
# Set entrypoint - container starts as root, entrypoint handles user switching
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
# Default command if none is provided (entrypoint will run this via gosu)
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Entrypoint script for Goose driver
|
||||
# Executes the standard initialization script, which handles user setup,
|
||||
# service startup (like sshd), and switching to the non-root user
|
||||
# before running the container's command (CMD).
|
||||
|
||||
# Run the standard initialization script
|
||||
/mc-init.sh
|
||||
|
||||
# Start SSH server in the background
|
||||
/usr/sbin/sshd
|
||||
|
||||
# Print welcome message
|
||||
echo "==============================================="
|
||||
echo "Goose driver container started"
|
||||
echo "SSH server running on port 22"
|
||||
echo "==============================================="
|
||||
|
||||
# Keep container running
|
||||
exec tail -f /dev/null
|
||||
exec /mc-init.sh "$@"
|
||||
|
||||
@@ -6,6 +6,53 @@ exec > >(tee -a /init.log) 2>&1
|
||||
|
||||
# Mark initialization as started
|
||||
echo "=== MC Initialization started at $(date) ==="
|
||||
|
||||
# --- START INSERTED BLOCK ---
|
||||
|
||||
# Default UID/GID if not provided (should be passed by mc tool)
|
||||
MC_USER_ID=${MC_USER_ID:-1000}
|
||||
MC_GROUP_ID=${MC_GROUP_ID:-1000}
|
||||
|
||||
echo "Using UID: $MC_USER_ID, GID: $MC_GROUP_ID"
|
||||
|
||||
# Create group if it doesn't exist
|
||||
if ! getent group mcuser > /dev/null; then
|
||||
groupadd -g $MC_GROUP_ID mcuser
|
||||
else
|
||||
# If group exists but has different GID, modify it
|
||||
EXISTING_GID=$(getent group mcuser | cut -d: -f3)
|
||||
if [ "$EXISTING_GID" != "$MC_GROUP_ID" ]; then
|
||||
groupmod -g $MC_GROUP_ID mcuser
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create user if it doesn't exist
|
||||
if ! getent passwd mcuser > /dev/null; then
|
||||
useradd --shell /bin/bash --uid $MC_USER_ID --gid $MC_GROUP_ID --no-create-home mcuser
|
||||
else
|
||||
# If user exists but has different UID/GID, modify it
|
||||
EXISTING_UID=$(getent passwd mcuser | cut -d: -f3)
|
||||
EXISTING_GID=$(getent passwd mcuser | cut -d: -f4)
|
||||
if [ "$EXISTING_UID" != "$MC_USER_ID" ] || [ "$EXISTING_GID" != "$MC_GROUP_ID" ]; then
|
||||
usermod --uid $MC_USER_ID --gid $MC_GROUP_ID mcuser
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create home directory and set permissions if it doesn't exist
|
||||
if [ ! -d "/home/mcuser" ]; then
|
||||
mkdir -p /home/mcuser
|
||||
chown $MC_USER_ID:$MC_GROUP_ID /home/mcuser
|
||||
fi
|
||||
# Ensure /app exists and has correct ownership (important for volume mounts)
|
||||
mkdir -p /app
|
||||
chown $MC_USER_ID:$MC_GROUP_ID /app
|
||||
|
||||
# Start SSH server as root before switching user
|
||||
echo "Starting SSH server..."
|
||||
/usr/sbin/sshd
|
||||
|
||||
# --- END INSERTED BLOCK ---
|
||||
|
||||
echo "INIT_COMPLETE=false" > /init.status
|
||||
|
||||
# Project initialization
|
||||
@@ -70,3 +117,6 @@ echo "MC driver initialization complete"
|
||||
# Mark initialization as complete
|
||||
echo "=== MC Initialization completed at $(date) ==="
|
||||
echo "INIT_COMPLETE=true" > /init.status
|
||||
|
||||
# Switch to the non-root user and execute the container's CMD
|
||||
exec gosu mcuser "$@"
|
||||
|
||||
Reference in New Issue
Block a user