feat(mc): support for uid/gid, and use default current user

This commit is contained in:
2025-04-01 08:58:54 -06:00
parent 4b7ae39bba
commit 4423f750d5
5 changed files with 168 additions and 38 deletions

View File

@@ -152,6 +152,12 @@ def create_session(
"-m", "-m",
help="Attach MCP servers to the session (can be specified multiple times)", 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: ) -> None:
"""Create a new MC session """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 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. 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 # Use default driver from user configuration
if not driver: if not driver:
driver = user_config.get( driver = user_config.get(
@@ -248,6 +259,8 @@ def create_session(
volumes=volume_mounts, volumes=volume_mounts,
networks=all_networks, networks=all_networks,
mcp=all_mcps, mcp=all_mcps,
uid=target_uid,
gid=target_gid,
) )
if session: if session:

View File

@@ -145,6 +145,8 @@ class ContainerManager:
volumes: Optional[Dict[str, Dict[str, str]]] = None, volumes: Optional[Dict[str, Dict[str, str]]] = None,
networks: Optional[List[str]] = None, networks: Optional[List[str]] = None,
mcp: Optional[List[str]] = None, mcp: Optional[List[str]] = None,
uid: Optional[int] = None,
gid: Optional[int] = None,
) -> Optional[Session]: ) -> Optional[Session]:
"""Create a new MC 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}}) 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 networks: Optional list of additional Docker networks to connect to
mcp: Optional list of MCP server names to attach to the session 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: try:
# Validate driver exists # Validate driver exists
@@ -176,6 +180,12 @@ class ContainerManager:
# Prepare environment variables # Prepare environment variables
env_vars = environment or {} 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 # Add project URL to environment if provided
if project: if project:
env_vars["MC_PROJECT_URL"] = project env_vars["MC_PROJECT_URL"] = project
@@ -534,12 +544,17 @@ class ContainerManager:
created_at=container.attrs["Created"], created_at=container.attrs["Created"],
ports=ports, ports=ports,
mcps=mcp_names, 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 # Save session to the session manager as JSON-compatible dict
self.session_manager.add_session( # Assuming Session model has uid and gid fields added to its definition
session_id, session.model_dump(mode="json") 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 return session
@@ -564,20 +579,72 @@ class ContainerManager:
def connect_session(self, session_id: str) -> bool: def connect_session(self, session_id: str) -> bool:
"""Connect to a running MC session""" """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() sessions = self.list_sessions()
for session in sessions: session_obj = next((s for s in sessions if s.id == session_id), None)
if session.id == session_id and session.container_id: if not session_obj or not session_obj.container_id:
if session.status != SessionStatus.RUNNING: print(f"Session '{session_id}' not found via Docker either.")
print(f"Session '{session_id}' is not running") return False
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 # Check status from Docker directly
# The init-status.sh script will automatically show logs if needed try:
os.system(f"docker exec -it {session.container_id} /bin/bash") container = self.client.containers.get(container_id)
return True 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 return False
except DockerException as e: except DockerException as e:

View File

@@ -3,8 +3,10 @@ FROM python:3.12-slim
LABEL maintainer="team@monadical.com" LABEL maintainer="team@monadical.com"
LABEL description="Goose with MCP servers" LABEL description="Goose with MCP servers"
# Install system dependencies # Install system dependencies including gosu for user switching and shadow for useradd/groupadd
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y --no-install-recommends \
gosu \
passwd \
git \ git \
openssh-server \ openssh-server \
bash \ bash \
@@ -12,22 +14,27 @@ RUN apt-get update && apt-get install -y \
bzip2 \ bzip2 \
iputils-ping \ iputils-ping \
iproute2 \ iproute2 \
libxcb1 \
libdbus-1-3 \
nano \ nano \
vim \ vim \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Set up SSH server # Set up SSH server directory (configuration will be handled by entrypoint if needed)
RUN mkdir /var/run/sshd RUN mkdir -p /var/run/sshd && chmod 0755 /var/run/sshd
RUN echo 'root:root' | chpasswd # Do NOT enable root login or set root password here
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
# Install python dependencies # Install python dependencies
# This is done before copying scripts for better cache management # 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 WORKDIR /tmp
RUN curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh -o download_cli.sh && \ RUN curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh -o download_cli.sh && \
chmod +x 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 # Create app directory
WORKDIR /app WORKDIR /app
@@ -46,15 +53,18 @@ RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh \
/usr/local/bin/update-goose-config.sh /usr/local/bin/update-goose-config.sh
# Set up initialization status check on login # 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 RUN echo '[ -x /init-status.sh ] && /init-status.sh' >> /etc/bash.bashrc
# Set up environment # Set up environment
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
# Set WORKDIR to /app, common practice and expected by mc-init.sh
WORKDIR /app
# Expose ports # Expose ports
EXPOSE 8000 22 EXPOSE 8000 22
# Set entrypoint # Set entrypoint - container starts as root, entrypoint handles user switching
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
# Default command if none is provided (entrypoint will run this via gosu)
CMD ["tail", "-f", "/dev/null"]

View File

@@ -1,17 +1,7 @@
#!/bin/bash #!/bin/bash
# Entrypoint script for Goose driver # 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 exec /mc-init.sh "$@"
/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

View File

@@ -6,6 +6,53 @@ exec > >(tee -a /init.log) 2>&1
# Mark initialization as started # Mark initialization as started
echo "=== MC Initialization started at $(date) ===" 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 echo "INIT_COMPLETE=false" > /init.status
# Project initialization # Project initialization
@@ -70,3 +117,6 @@ echo "MC driver initialization complete"
# Mark initialization as complete # Mark initialization as complete
echo "=== MC Initialization completed at $(date) ===" echo "=== MC Initialization completed at $(date) ==="
echo "INIT_COMPLETE=true" > /init.status echo "INIT_COMPLETE=true" > /init.status
# Switch to the non-root user and execute the container's CMD
exec gosu mcuser "$@"