Skip to main content
Glama
volume_utils.py12 kB
""" Stack Volume Utils Module Volume and mount handling utilities for Docker Compose stacks. Handles volume path normalization, mount extraction, and volume configuration. """ from pathlib import Path from typing import Any import structlog class StackVolumeUtils: """Volume and mount handling utilities for stack operations.""" def __init__(self): self.logger = structlog.get_logger() def normalize_volume_entry( self, volume: Any, target_appdata: str, stack_name: str ) -> str | None: """Normalize a single volume entry to source:destination format. Args: volume: Volume definition (string or dict) target_appdata: Target appdata path stack_name: Stack name for path resolution Returns: Normalized volume string in "source:destination" format or None """ if isinstance(volume, str) and ":" in volume: parts = volume.split(":", 2) if len(parts) >= 2: source_path = parts[0] container_path = parts[1] # Convert relative paths to absolute if source_path.startswith("."): source_path = f"{target_appdata}/{stack_name}/{source_path[2:]}" elif not source_path.startswith("/"): # Named volume - cannot infer host path deterministically; skip normalization return None return f"{source_path}:{container_path}" elif isinstance(volume, dict) and volume.get("type") == "bind": source = volume.get("source", "") target = volume.get("target", "") if source and target: if not source.startswith("/"): source = f"{target_appdata}/{stack_name}/{source}" return f"{source}:{target}" return None def extract_expected_mounts( self, compose_content: str, target_appdata: str, stack_name: str ) -> list[str]: """Extract expected volume mounts from compose file content. Args: compose_content: Docker Compose YAML content target_appdata: Target appdata path stack_name: Stack name Returns: List of expected mount strings in format "source:destination" """ try: import yaml compose_data = yaml.safe_load(compose_content) expected_mounts = [] # Parse services for volume mounts services = compose_data.get("services", {}) for _service_name, service_config in services.items(): volumes = service_config.get("volumes", []) for volume in volumes: mount = self.normalize_volume_entry(volume, target_appdata, stack_name) if mount and mount not in expected_mounts: expected_mounts.append(mount) if expected_mounts: self.logger.info( "Extracted expected mounts from compose file", stack=stack_name, mounts=expected_mounts, ) return expected_mounts else: # No volumes found - return empty list (don't create fake mounts) self.logger.info( "No volume mounts found in compose file - stack has no persistent data", stack=stack_name, ) return [] except Exception as e: # Fallback on any parsing error - return empty list for safety self.logger.error( "Failed to parse compose file for mounts - assuming no volumes", stack=stack_name, error=str(e), ) return [] def resolve_volume_paths(self, volumes: list[str], base_path: str) -> list[str]: """Resolve volume paths to absolute paths. Args: volumes: List of volume definitions base_path: Base path for resolution Returns: List of resolved absolute paths """ resolved_paths = [] for volume in volumes: if ":" in volume: source_path = volume.split(":")[0] if not source_path.startswith("/"): # Relative path - resolve against base_path source_path = str(Path(base_path) / source_path) resolved_paths.append(source_path) else: # Named volume or single path if volume.startswith("/"): resolved_paths.append(volume) else: resolved_paths.append(str(Path(base_path) / volume)) return resolved_paths def extract_named_volumes(self, compose_content: str) -> list[str]: """Extract named volumes from compose file. Args: compose_content: Docker Compose YAML content Returns: List of named volume names """ try: import yaml compose_data = yaml.safe_load(compose_content) named_volumes = [] # Extract from top-level volumes section if "volumes" in compose_data: named_volumes.extend(list(compose_data["volumes"].keys())) # Extract named volumes used in services services = compose_data.get("services", {}) for _service_name, service_config in services.items(): volumes = service_config.get("volumes", []) for volume in volumes: if isinstance(volume, str) and ":" in volume: source = volume.split(":")[0] if not source.startswith("/") and not source.startswith("."): # This is likely a named volume if source not in named_volumes: named_volumes.append(source) elif isinstance(volume, dict) and volume.get("type") == "volume": vol_name = volume.get("source") if vol_name and vol_name not in named_volumes: named_volumes.append(vol_name) return named_volumes except Exception as e: self.logger.warning("Failed to extract named volumes", error=str(e)) return [] def extract_bind_mounts(self, compose_content: str) -> list[dict]: """Extract bind mount configurations from compose file. Args: compose_content: Docker Compose YAML content Returns: List of bind mount dictionaries with source, target, and options """ try: import yaml compose_data = yaml.safe_load(compose_content) bind_mounts = [] services = compose_data.get("services", {}) for service_name, service_config in services.items(): volumes = service_config.get("volumes", []) for volume in volumes: bind_mount = None if isinstance(volume, str) and ":" in volume: parts = volume.split(":", 2) if len(parts) >= 2 and parts[0].startswith("/"): # This is a bind mount (absolute path) bind_mount = { "service": service_name, "source": parts[0], "target": parts[1], "options": parts[2] if len(parts) > 2 else None, "type": "bind", } elif isinstance(volume, dict) and volume.get("type") == "bind": bind_mount = { "service": service_name, "source": volume.get("source", ""), "target": volume.get("target", ""), "options": volume.get("bind", {}).get("propagation"), "type": "bind", } if bind_mount: bind_mounts.append(bind_mount) return bind_mounts except Exception as e: self.logger.warning("Failed to extract bind mounts", error=str(e)) return [] def get_volume_size_estimate(self, volumes: list[str]) -> int: """Estimate total size of volumes (placeholder for actual implementation). Args: volumes: List of volume paths Returns: Estimated size in bytes (placeholder value) """ # In a real implementation, this would: # 1. Connect to the source host # 2. Use 'du' command to get actual sizes # 3. Sum up all volume sizes # For now, return a reasonable placeholder return len(volumes) * 1024 * 1024 * 100 # 100MB per volume def validate_volume_permissions(self, volume_paths: list[str], user: str = "docker") -> dict: """Validate volume path permissions (placeholder for actual implementation). Args: volume_paths: List of volume paths to validate user: User that needs access (default: docker) Returns: Dict with validation results per path """ results = {} for path in volume_paths: # In a real implementation, this would: # 1. Check if path exists # 2. Check read/write permissions for the user # 3. Check if parent directory is writable results[path] = { "exists": True, # Placeholder "readable": True, # Placeholder "writable": True, # Placeholder "owner": "docker", # Placeholder "permissions": "755", # Placeholder } return results def suggest_volume_optimizations(self, compose_content: str) -> list[str]: """Suggest optimizations for volume configuration. Args: compose_content: Docker Compose YAML content Returns: List of optimization suggestions """ suggestions = [] try: import yaml compose_data = yaml.safe_load(compose_content) bind_mounts = self.extract_bind_mounts(compose_content) named_volumes = self.extract_named_volumes(compose_content) # Check for excessive bind mounts if len(bind_mounts) > 5: suggestions.append( f"Consider consolidating {len(bind_mounts)} bind mounts - " "too many mounts can complicate deployment" ) # Check for missing named volume definitions volume_section = compose_data.get("volumes", {}) for vol_name in named_volumes: if vol_name not in volume_section: suggestions.append( f"Named volume '{vol_name}' is used but not defined in volumes section" ) # Check for potentially problematic paths for mount in bind_mounts: source = mount.get("source", "") if source.startswith("/home/"): suggestions.append( f"Bind mount from home directory ({source}) may have permission issues" ) elif source.startswith("/tmp/"): # noqa: S108 suggestions.append( f"Bind mount from temp directory ({source}) - data may not persist across reboots" ) except Exception as e: self.logger.warning("Failed to analyze volume configuration", error=str(e)) return suggestions

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/docker-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server