diff --git a/podman/client.py b/podman/client.py index f9a023e7..1b4e71fe 100644 --- a/podman/client.py +++ b/podman/client.py @@ -19,6 +19,7 @@ from podman.domain.secrets import SecretsManager from podman.domain.system import SystemManager from podman.domain.volumes import VolumesManager +from podman.errors.exceptions import PodmanConnectionError logger = logging.getLogger("podman") @@ -73,6 +74,18 @@ def __init__(self, **kwargs) -> None: api_kwargs["base_url"] = "http+unix://" + path self.api = APIClient(**api_kwargs) + # Check if the connection to the Podman service is successful + try: + SystemManager(client=self.api).version() + except Exception as e: + error_msg = "Failed to connect to Podman service" + raise PodmanConnectionError( + message=error_msg, + environment=os.environ, + host=api_kwargs.get("base_url"), + original_error=e, + ) + def __enter__(self) -> "PodmanClient": return self @@ -114,27 +127,47 @@ def from_env( Client used to communicate with a Podman service. Raises: - ValueError when required environment variable is not set + PodmanConnectionError: When connection to service fails or environment is invalid """ - environment = environment or os.environ - credstore_env = credstore_env or {} - - if version == "auto": - version = None - - kwargs = { - 'version': version, - 'timeout': timeout, - 'tls': False, - 'credstore_env': credstore_env, - 'max_pool_size': max_pool_size, - } - - host = environment.get("CONTAINER_HOST") or environment.get("DOCKER_HOST") or None - if host is not None: - kwargs['base_url'] = host - - return PodmanClient(**kwargs) + try: + environment = environment or os.environ + credstore_env = credstore_env or {} + + if version == "auto": + version = None + + kwargs = { + "version": version, + "timeout": timeout, + "tls": False, + "credstore_env": credstore_env, + "max_pool_size": max_pool_size, + } + + host = ( + environment.get("CONTAINER_HOST") + or environment.get("DOCKER_HOST") + or None + ) + if host is not None: + kwargs["base_url"] = host + + return PodmanClient(**kwargs) + except ValueError as e: + error_msg = "Invalid environment configuration for Podman client" + raise PodmanConnectionError( + message=error_msg, environment=environment, host=host, original_error=e + ) + except (ConnectionError, TimeoutError) as e: + error_msg = "Failed to connect to Podman service" + raise PodmanConnectionError( + message=error_msg, environment=environment, host=host, original_error=e + ) + except Exception as e: + error_msg = "Failed to initialize Podman client from environment" + raise PodmanConnectionError( + message=error_msg, environment=environment, host=host, original_error=e + ) @cached_property def containers(self) -> ContainersManager: @@ -175,7 +208,9 @@ def secrets(self): def system(self): return SystemManager(client=self.api) - def df(self) -> dict[str, Any]: # pylint: disable=missing-function-docstring,invalid-name + def df( + self, + ) -> dict[str, Any]: # pylint: disable=missing-function-docstring,invalid-name return self.system.df() df.__doc__ = SystemManager.df.__doc__ @@ -217,7 +252,9 @@ def swarm(self): Raises: NotImplemented: Swarm not supported by Podman service """ - raise NotImplementedError("Swarm operations are not supported by Podman service.") + raise NotImplementedError( + "Swarm operations are not supported by Podman service." + ) # Aliases to cover all swarm methods services = swarm diff --git a/podman/errors/__init__.py b/podman/errors/__init__.py index ae8d9fa0..2e108e96 100644 --- a/podman/errors/__init__.py +++ b/podman/errors/__init__.py @@ -22,6 +22,7 @@ 'NotFoundError', 'PodmanError', 'StreamParseError', + 'PodmanConnectionError' ] try: @@ -34,6 +35,7 @@ NotFound, PodmanError, StreamParseError, + PodmanConnectionError ) except ImportError: pass diff --git a/podman/errors/exceptions.py b/podman/errors/exceptions.py index f92d886c..6bbd5e75 100644 --- a/podman/errors/exceptions.py +++ b/podman/errors/exceptions.py @@ -1,6 +1,6 @@ """Podman API Errors.""" -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING, Dict from collections.abc import Iterable from requests import Response @@ -145,6 +145,69 @@ class InvalidArgument(PodmanError): """Parameter to method/function was not valid.""" +class PodmanConnectionError(PodmanError): + """Exception raised when connection to Podman service fails using environment configuration.""" + + def __init__( + self, + message: str, + environment: Optional[dict[str, str]] = None, + host: Optional[str] = None, + original_error: Optional[Exception] = None, + ): + """Initialize PodmanConnectionError. + + Args: + message: Description of the error + environment: Environment variables used in connection attempt + host: URL to Podman service that failed + original_error: Original exception that caused this error + """ + super().__init__(message) + self.environment = environment + self.host = host + self.original_error = original_error + + def __str__(self) -> str: + """Format error message with details about connection attempt.""" + msg = [super().__str__()] + + if self.host: + msg.append(f"Host: {self.host}") + + if self.environment: + relevant_vars = { + k: v + for k, v in self.environment.items() + if k + in ( + 'DOCKER_HOST', + 'CONTAINER_HOST', + 'DOCKER_TLS_VERIFY', + 'CONTAINER_TLS_VERIFY', + 'DOCKER_CERT_PATH', + 'CONTAINER_CERT_PATH', + ) + } + if relevant_vars: + msg.append("Environment:") + for key, value in relevant_vars.items(): + msg.append(f" {key}={value}") + + if self.original_error: + msg.append(f"Caused by: {str(self.original_error)}") + + return " | ".join(msg) + + class StreamParseError(RuntimeError): + """Error parsing stream data.""" + def __init__(self, reason): - self.msg = reason + """Initialize StreamParseError. + + Args: + reason: Description of the parsing error + """ + super().__init__(reason) + self.msg = reason \ No newline at end of file diff --git a/podman/tests/integration/test_system.py b/podman/tests/integration/test_system.py index bcc38711..9a869847 100644 --- a/podman/tests/integration/test_system.py +++ b/podman/tests/integration/test_system.py @@ -16,7 +16,7 @@ import podman.tests.integration.base as base from podman import PodmanClient -from podman.errors import APIError +from podman.errors import APIError, PodmanConnectionError class SystemIntegrationTest(base.IntegrationTest): @@ -64,3 +64,8 @@ def test_login(self): def test_from_env(self): """integration: from_env() no error""" PodmanClient.from_env() + + def test_from_env_exceptions(self): + """integration: from_env() returns exceptions""" + with self.assertRaises(PodmanConnectionError): + PodmanClient.from_env(base_url="unix:///path/to/nonexistent.sock") \ No newline at end of file