From dd5b9ec2133f38ce986d1d00687807a86e4819b1 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Tue, 1 Apr 2025 17:01:25 -0600 Subject: [PATCH] fix(uid): use symlink instead of volume for persistent volume in the container --- .gitignore | 4 ++ mcontainer/container.py | 47 ++++++++++-------- mcontainer/drivers/goose/init-status.sh | 11 ++++- mcontainer/drivers/goose/mc-driver.yaml | 2 +- mcontainer/drivers/goose/mc-init.sh | 64 +++++++++++++++---------- 5 files changed, 81 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index 505a3b1..44087f3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ wheels/ # Virtual environments .venv + +# Aider +.aider* +.goose diff --git a/mcontainer/container.py b/mcontainer/container.py index ccf3e21..201df0a 100644 --- a/mcontainer/container.py +++ b/mcontainer/container.py @@ -188,10 +188,6 @@ class ContainerManager: 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 - # Pass API keys from host environment to container for local development api_keys = [ "OPENAI_API_KEY", @@ -236,6 +232,7 @@ class ContainerManager: # Clear project for container environment since we're mounting project = None elif is_git_repo: + env_vars["MC_PROJECT_URL"] = project print( f"Git repository URL provided - container will clone {project} into /app during initialization" ) @@ -273,6 +270,7 @@ class ContainerManager: # Create driver-specific config directories and set up direct volume mounts if driver.persistent_configs: + persistent_links_data = [] # To store "source:target" pairs for symlinks print("Setting up persistent configuration directories:") for config in driver.persistent_configs: # Get target directory path on host @@ -291,13 +289,21 @@ class ContainerManager: target_dir.parent.mkdir(parents=True, exist_ok=True) # File will be created by the container if needed - # Mount persistent config directly to container path - session_volumes[str(target_dir)] = { - "bind": config.source, - "mode": "rw", - } + # --- REMOVED adding to session_volumes --- + # We will create symlinks inside the container instead of direct mounts + + # Store the source and target paths for the init script + # Note: config.target is the path *within* /mc-config + persistent_links_data.append(f"{config.source}:{config.target}") + print( - f" - Created direct volume mount: {target_dir} -> {config.source}" + f" - Prepared host path {target_dir} for symlink target {config.target}" + ) + # Set environment variable with semicolon-separated link pairs + if persistent_links_data: + env_vars["MC_PERSISTENT_LINKS"] = ";".join(persistent_links_data) + print( + f"Setting MC_PERSISTENT_LINKS={env_vars['MC_PERSISTENT_LINKS']}" ) # Default MC network @@ -634,7 +640,7 @@ class ContainerManager: return False container_id = session_obj.container_id print( - f"[yellow]Warning: Session data missing for {session_id}. Attaching as default container user.[/yellow]" + f"[yellow]Warning: Session data missing for {session_id}. Connecting as default container user.[/yellow]" ) else: container_id = session_data.get("container_id") @@ -660,18 +666,19 @@ class ContainerManager: return False try: - # Attach to the container's main process TTY - # This allows seeing the output of --run command followed by the shell - # The user context (UID/GID) is determined when the container is created, - # attach respects that context. + # Use exec instead of attach to avoid container exit on Ctrl+C print( - f"Attaching to session {session_id} (container: {container_id[:12]})..." + f"Connecting to session {session_id} (container: {container_id[:12]})..." ) - print("Type 'exit' or Ctrl+P, Ctrl+Q (by default) to detach.") - cmd = ["docker", "attach", container_id] + print("Type 'exit' to detach from the session.") - # Use execvp to replace the current process with docker attach - # This provides a more seamless shell experience + # Use docker exec to start a new bash process in the container + # This leverages the init-status.sh script in bash.bashrc + # which will check initialization status + cmd = ["docker", "exec", "-it", container_id, "bash", "-l"] + + # Use execvp to replace the current process with docker exec + # This provides a seamless shell experience os.execvp("docker", cmd) # execvp does not return if successful return True # Should not be reached if execvp succeeds diff --git a/mcontainer/drivers/goose/init-status.sh b/mcontainer/drivers/goose/init-status.sh index b5ae04a..f0db9c1 100644 --- a/mcontainer/drivers/goose/init-status.sh +++ b/mcontainer/drivers/goose/init-status.sh @@ -1,6 +1,11 @@ #!/bin/bash # Script to check and display initialization status - optimized version +# Only proceed if running as root +if [ "$(id -u)" != "0" ]; then + exit 0 +fi + # Quick check instead of full logic if grep -q "INIT_COMPLETE=true" "/init.status" 2>/dev/null; then echo "MC initialization has completed." @@ -12,7 +17,7 @@ else echo "----------------------------------------" tail -f /init.log & tail_pid=$! - + # Check every second if initialization has completed while true; do if grep -q "INIT_COMPLETE=true" "/init.status" 2>/dev/null; then @@ -26,4 +31,6 @@ else else echo "No initialization logs found." fi -fi \ No newline at end of file +fi + +exec gosu mcuser /bin/bash -il diff --git a/mcontainer/drivers/goose/mc-driver.yaml b/mcontainer/drivers/goose/mc-driver.yaml index 0afabdb..ccca196 100644 --- a/mcontainer/drivers/goose/mc-driver.yaml +++ b/mcontainer/drivers/goose/mc-driver.yaml @@ -57,7 +57,7 @@ persistent_configs: target: "/mc-config/goose-app" type: "directory" description: "Goose memory" - - source: "/root/.config/goose" + - source: "/home/mcuser/.config/goose" target: "/mc-config/goose-config" type: "directory" description: "Goose configuration" diff --git a/mcontainer/drivers/goose/mc-init.sh b/mcontainer/drivers/goose/mc-init.sh index 253d301..0f98774 100755 --- a/mcontainer/drivers/goose/mc-init.sh +++ b/mcontainer/drivers/goose/mc-init.sh @@ -38,11 +38,9 @@ else 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 +# Create home directory and set permissions +mkdir -p /home/mcuser +chown $MC_USER_ID:$MC_GROUP_ID /home/mcuser # Ensure /app exists and has correct ownership (important for volume mounts) mkdir -p /app chown $MC_USER_ID:$MC_GROUP_ID /app @@ -112,7 +110,42 @@ else echo "Warning: update-goose-config.sh script not found. Goose configuration will not be updated." fi -echo "MC driver initialization complete" +# Create symlinks for persistent configurations defined in the driver +if [ -n "$MC_PERSISTENT_LINKS" ]; then + echo "Creating persistent configuration symlinks..." + # Split by semicolon + IFS=';' read -ra LINKS <<< "$MC_PERSISTENT_LINKS" + for link_pair in "${LINKS[@]}"; do + # Split by colon + IFS=':' read -r source_path target_path <<< "$link_pair" + + if [ -z "$source_path" ] || [ -z "$target_path" ]; then + echo "Warning: Invalid link pair format '$link_pair', skipping." + continue + fi + + echo "Processing link: $source_path -> $target_path" + parent_dir=$(dirname "$source_path") + + # Ensure parent directory of the link source exists and is owned by mcuser + if [ ! -d "$parent_dir" ]; then + echo "Creating parent directory: $parent_dir" + mkdir -p "$parent_dir" + echo "Changing ownership of parent $parent_dir to $MC_USER_ID:$MC_GROUP_ID" + chown "$MC_USER_ID:$MC_GROUP_ID" "$parent_dir" || echo "Warning: Could not chown parent $parent_dir" + fi + + # Create the symlink (force, no-dereference) + echo "Creating symlink: ln -sfn $target_path $source_path" + ln -sfn "$target_path" "$source_path" + + # Optionally, change ownership of the symlink itself + echo "Changing ownership of symlink $source_path to $MC_USER_ID:$MC_GROUP_ID" + chown -h "$MC_USER_ID:$MC_GROUP_ID" "$source_path" || echo "Warning: Could not chown symlink $source_path" + + done + echo "Persistent configuration symlinks created." +fi # Mark initialization as complete echo "=== MC Initialization completed at $(date) ===" @@ -126,21 +159,4 @@ if [ -n "$MC_RUN_COMMAND" ]; then 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[@]}" +exec gosu mcuser "$@"