""" Podman client wrapper with security validation. Wraps Podman API with security validation and error handling. All container operations are validated against security policy. """ import os from pathlib import Path from typing import Optional from podman import PodmanClient as BasePodmanClient from mcp_forge.security.allowlist import OperationValidator from mcp_forge.security.audit import AuditLogger class PodmanConnectionError(Exception): """Raised when connection to Podman fails.""" pass class PodmanClient: """ Wrapper around Podman API with security validation. All container operations are validated against security policy before being sent to Podman. Provides lazy connection and proper error handling. """ def __init__( self, socket_path: Path, validator: OperationValidator, audit_logger: AuditLogger ): """ Initialize Podman client wrapper. Args: socket_path: Path to Podman socket validator: Operation validator for security checks audit_logger: Audit logger for operation logging """ self.socket_path = Path(socket_path) self.validator = validator self.audit_logger = audit_logger self._client: Optional[BasePodmanClient] = None def connect(self) -> None: """ Connect to Podman via socket. Raises: PodmanConnectionError: If connection fails """ try: # Verify socket exists and is accessible self.verify_socket_access() # Create Podman client with Unix socket base_url = f"unix://{self.socket_path}" self._client = BasePodmanClient(base_url=base_url) # Test connection with ping (only if client supports it) if hasattr(self._client, 'ping'): self._client.ping() except PodmanConnectionError: # Re-raise our own exceptions raise except Exception as e: raise PodmanConnectionError( f"Failed to connect to Podman at {self.socket_path}: {e}" ) from e def ping(self) -> bool: """ Test connection to Podman. Returns: True if connection is healthy Raises: PodmanConnectionError: If not connected or ping fails """ if self._client is None: raise PodmanConnectionError("Not connected to Podman") try: result = self._client.ping() return result == "OK" or result is True except Exception as e: raise PodmanConnectionError(f"Ping failed: {e}") from e def disconnect(self) -> None: """Disconnect from Podman and cleanup.""" if self._client is not None: try: self._client.close() except Exception: pass # Ignore errors during cleanup finally: self._client = None def verify_socket_access(self) -> None: """ Verify that socket exists and is accessible. Raises: PodmanConnectionError: If socket is not accessible """ if not self.socket_path.exists(): raise PodmanConnectionError( f"Podman socket not found: {self.socket_path}" ) if not os.access(self.socket_path, os.R_OK): raise PodmanConnectionError( f"Podman socket is not readable: {self.socket_path}" ) def check_api_version(self) -> dict: """ Get Podman API version information. Returns: Dictionary with version information Raises: PodmanConnectionError: If not connected """ if self._client is None: raise PodmanConnectionError("Not connected to Podman") try: return self._client.version() except Exception as e: raise PodmanConnectionError( f"Failed to get API version: {e}" ) from e @property def client(self) -> BasePodmanClient: """ Get underlying Podman client (lazy connection). Returns: Connected Podman client Raises: PodmanConnectionError: If connection fails """ if self._client is None: self.connect() assert self._client is not None # Type narrowing for mypy return self._client