mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-20 20:29:06 +00:00
feat(run): add --run command
This commit is contained in:
@@ -143,6 +143,11 @@ def create_session(
|
|||||||
[], "--network", "-N", help="Connect to additional Docker networks"
|
[], "--network", "-N", help="Connect to additional Docker networks"
|
||||||
),
|
),
|
||||||
name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"),
|
name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"),
|
||||||
|
run_command: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--run",
|
||||||
|
help="Command to execute inside the container before starting the shell",
|
||||||
|
),
|
||||||
no_connect: bool = typer.Option(
|
no_connect: bool = typer.Option(
|
||||||
False, "--no-connect", help="Don't automatically connect to the session"
|
False, "--no-connect", help="Don't automatically connect to the session"
|
||||||
),
|
),
|
||||||
@@ -259,6 +264,7 @@ def create_session(
|
|||||||
volumes=volume_mounts,
|
volumes=volume_mounts,
|
||||||
networks=all_networks,
|
networks=all_networks,
|
||||||
mcp=all_mcps,
|
mcp=all_mcps,
|
||||||
|
run_command=run_command,
|
||||||
uid=target_uid,
|
uid=target_uid,
|
||||||
gid=target_gid,
|
gid=target_gid,
|
||||||
)
|
)
|
||||||
@@ -275,11 +281,21 @@ def create_session(
|
|||||||
|
|
||||||
# Auto-connect based on user config, unless overridden by --no-connect flag
|
# Auto-connect based on user config, unless overridden by --no-connect flag
|
||||||
auto_connect = user_config.get("defaults.connect", True)
|
auto_connect = user_config.get("defaults.connect", True)
|
||||||
if not no_connect and auto_connect:
|
# Connect if auto_connect is enabled and --no-connect wasn't used.
|
||||||
|
# The --run command no longer prevents connection.
|
||||||
|
should_connect = not no_connect and auto_connect
|
||||||
|
if should_connect:
|
||||||
container_manager.connect_session(session.id)
|
container_manager.connect_session(session.id)
|
||||||
else:
|
else:
|
||||||
|
# Explain why connection was skipped
|
||||||
|
if no_connect:
|
||||||
|
console.print("\nConnection skipped due to --no-connect.")
|
||||||
console.print(
|
console.print(
|
||||||
f"\nConnect to the session with:\n mc session connect {session.id}"
|
f"Connect manually with:\n mc session connect {session.id}"
|
||||||
|
)
|
||||||
|
elif not auto_connect:
|
||||||
|
console.print(
|
||||||
|
f"\nAuto-connect disabled. Connect with:\n mc session connect {session.id}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
console.print("[red]Failed to create session[/red]")
|
console.print("[red]Failed to create session[/red]")
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ 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,
|
||||||
|
run_command: Optional[str] = None,
|
||||||
uid: Optional[int] = None,
|
uid: Optional[int] = None,
|
||||||
gid: Optional[int] = None,
|
gid: Optional[int] = None,
|
||||||
) -> Optional[Session]:
|
) -> Optional[Session]:
|
||||||
@@ -157,6 +158,7 @@ class ContainerManager:
|
|||||||
session_name: Optional session name
|
session_name: Optional session name
|
||||||
mount_local: Whether to mount the specified local directory to /app (ignored if project is None)
|
mount_local: Whether to mount the specified local directory to /app (ignored if project is None)
|
||||||
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}})
|
||||||
|
run_command: Optional command to execute before starting the shell
|
||||||
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
|
uid: Optional user ID for the container process
|
||||||
@@ -412,12 +414,51 @@ class ContainerManager:
|
|||||||
env_vars["MCP_NAMES"] = json.dumps(mcp_names)
|
env_vars["MCP_NAMES"] = json.dumps(mcp_names)
|
||||||
|
|
||||||
# Add user-specified networks
|
# Add user-specified networks
|
||||||
|
# Default MC network
|
||||||
|
default_network = self.config_manager.config.docker.get(
|
||||||
|
"network", "mc-network"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get network list, ensuring default is first and no duplicates
|
||||||
|
network_list_set = {default_network}
|
||||||
|
if networks:
|
||||||
|
network_list_set.update(networks)
|
||||||
|
network_list = (
|
||||||
|
[default_network] + [n for n in networks if n != default_network]
|
||||||
|
if networks
|
||||||
|
else [default_network]
|
||||||
|
)
|
||||||
|
|
||||||
if networks:
|
if networks:
|
||||||
for network in networks:
|
for network in networks:
|
||||||
if network not in network_list:
|
if network not in network_list:
|
||||||
|
# This check is slightly redundant now but harmless
|
||||||
network_list.append(network)
|
network_list.append(network)
|
||||||
print(f"Adding network {network} to session")
|
print(f"Adding network {network} to session")
|
||||||
|
|
||||||
|
# Determine container command and entrypoint
|
||||||
|
container_command = None
|
||||||
|
entrypoint = None # Keep this initially None to mean "use Dockerfile default unless overridden"
|
||||||
|
target_shell = "/bin/bash" # Default final shell
|
||||||
|
|
||||||
|
if run_command:
|
||||||
|
# Set environment variable for mc-init.sh to pick up
|
||||||
|
env_vars["MC_RUN_COMMAND"] = run_command
|
||||||
|
# Set the container's command to be the final shell
|
||||||
|
container_command = [target_shell]
|
||||||
|
logger.info(
|
||||||
|
f"Setting MC_RUN_COMMAND and targeting shell {target_shell}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Use default behavior (often defined by image's ENTRYPOINT/CMD)
|
||||||
|
# Set the container's command to be the final shell if none specified by Dockerfile CMD
|
||||||
|
# Note: Dockerfile CMD is ["tail", "-f", "/dev/null"], so this might need adjustment
|
||||||
|
# if we want interactive shell by default without --run. Let's default to bash for now.
|
||||||
|
container_command = [target_shell]
|
||||||
|
logger.info(
|
||||||
|
"Using default container entrypoint/command for interactive shell."
|
||||||
|
)
|
||||||
|
|
||||||
# Create container
|
# Create container
|
||||||
container = self.client.containers.create(
|
container = self.client.containers.create(
|
||||||
image=driver.image,
|
image=driver.image,
|
||||||
@@ -437,6 +478,8 @@ class ContainerManager:
|
|||||||
"mc.mcps": ",".join(mcp_names) if mcp_names else "",
|
"mc.mcps": ",".join(mcp_names) if mcp_names else "",
|
||||||
},
|
},
|
||||||
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
|
||||||
|
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 driver.ports},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -544,16 +587,15 @@ 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
|
run_command=run_command, # Store the command
|
||||||
uid=uid,
|
uid=uid,
|
||||||
gid=gid,
|
gid=gid,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save session to the session manager as JSON-compatible dict
|
# Save session to the session manager
|
||||||
# Assuming Session model has uid and gid fields added to its definition
|
# 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 = session.model_dump(mode="json")
|
||||||
session_data_to_save["uid"] = uid
|
# uid and gid are already part of the model dump now
|
||||||
session_data_to_save["gid"] = gid
|
|
||||||
self.session_manager.add_session(session_id, session_data_to_save)
|
self.session_manager.add_session(session_id, session_data_to_save)
|
||||||
|
|
||||||
return session
|
return session
|
||||||
@@ -591,10 +633,8 @@ class ContainerManager:
|
|||||||
print(f"Session '{session_id}' not found via Docker either.")
|
print(f"Session '{session_id}' not found via Docker either.")
|
||||||
return False
|
return False
|
||||||
container_id = session_obj.container_id
|
container_id = session_obj.container_id
|
||||||
# Cannot determine user if session data is missing
|
|
||||||
user_spec = None
|
|
||||||
print(
|
print(
|
||||||
f"[yellow]Warning: Session data missing for {session_id}. Connecting as default container user.[/yellow]"
|
f"[yellow]Warning: Session data missing for {session_id}. Attaching as default container user.[/yellow]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
container_id = session_data.get("container_id")
|
container_id = session_data.get("container_id")
|
||||||
@@ -619,23 +659,18 @@ class ContainerManager:
|
|||||||
print(f"Error checking container status for session {session_id}: {e}")
|
print(f"Error checking container status for session {session_id}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 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:
|
try:
|
||||||
# Execute interactive shell in container
|
# Attach to the container's main process TTY
|
||||||
cmd = ["docker", "exec", "-it"]
|
# This allows seeing the output of --run command followed by the shell
|
||||||
if user_spec:
|
# The user context (UID/GID) is determined when the container is created,
|
||||||
cmd.extend(["--user", user_spec])
|
# attach respects that context.
|
||||||
print(f"Connecting as user {user_spec}...")
|
print(
|
||||||
else:
|
f"Attaching to session {session_id} (container: {container_id[:12]})..."
|
||||||
print("Connecting as default container user...")
|
)
|
||||||
|
print("Type 'exit' or Ctrl+P, Ctrl+Q (by default) to detach.")
|
||||||
|
cmd = ["docker", "attach", container_id]
|
||||||
|
|
||||||
cmd.extend([container_id, "/bin/bash"])
|
# Use execvp to replace the current process with docker attach
|
||||||
|
|
||||||
# Use execvp to replace the current process with docker exec
|
|
||||||
# This provides a more seamless shell experience
|
# This provides a more seamless shell experience
|
||||||
os.execvp("docker", cmd)
|
os.execvp("docker", cmd)
|
||||||
# execvp does not return if successful
|
# execvp does not return if successful
|
||||||
|
|||||||
@@ -118,5 +118,29 @@ echo "MC driver initialization 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
|
# Run the user command first, if set, as mcuser
|
||||||
exec gosu mcuser "$@"
|
if [ -n "$MC_RUN_COMMAND" ]; then
|
||||||
|
echo '--- Executing initial command: $MC_RUN_COMMAND ---';
|
||||||
|
gosu mcuser sh -c "$MC_RUN_COMMAND"; # Run user command as mcuser
|
||||||
|
COMMAND_EXIT_CODE=$?;
|
||||||
|
echo "--- Initial command finished (exit code: $COMMAND_EXIT_CODE) ---";
|
||||||
|
fi;
|
||||||
|
|
||||||
|
# Determine the final command (the interactive shell)
|
||||||
|
FINAL_CMD=("$@")
|
||||||
|
if [ ${#FINAL_CMD[@]} -eq 0 ]; then
|
||||||
|
# Default to /bin/bash if CMD wasn't passed or was empty
|
||||||
|
FINAL_CMD=("/bin/bash")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If the final command is bash, ensure it runs interactively
|
||||||
|
# Check if the first argument is /bin/bash and -i is not already present
|
||||||
|
if [ "${FINAL_CMD[0]}" = "/bin/bash" ] && [[ ! " ${FINAL_CMD[@]} " =~ " -i " ]]; then
|
||||||
|
# Add the -i flag to the command array
|
||||||
|
FINAL_CMD+=("-i")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- Starting interactive shell (${FINAL_CMD[*]}) ---";
|
||||||
|
# Now exec gosu directly into the final command, replacing this script process
|
||||||
|
# "${FINAL_CMD[@]}" ensures arguments are passed correctly (e.g., /bin/bash -i)
|
||||||
|
exec gosu mcuser "${FINAL_CMD[@]}"
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ class Session(BaseModel):
|
|||||||
created_at: str
|
created_at: str
|
||||||
ports: Dict[int, int] = Field(default_factory=dict)
|
ports: Dict[int, int] = Field(default_factory=dict)
|
||||||
mcps: List[str] = Field(default_factory=list) # List of MCP server names
|
mcps: List[str] = Field(default_factory=list) # List of MCP server names
|
||||||
|
run_command: Optional[str] = None # Command executed on start
|
||||||
|
uid: Optional[int] = None # Store UID used
|
||||||
|
gid: Optional[int] = None # Store GID used
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
|
|||||||
Reference in New Issue
Block a user