From 882c42d51c39503a5b5f01f39ef9c74b42029dd6 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Sun, 16 Feb 2025 21:33:54 +0530 Subject: [PATCH 1/7] feat: Add PodmanConnectionError exception Signed-off-by: Kanishk Pachauri --- podman/errors/__init__.py | 2 ++ podman/errors/exceptions.py | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/podman/errors/__init__.py b/podman/errors/__init__.py index ae8d9fa0..d5adb3ea 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..c403d95d 100644 --- a/podman/errors/exceptions.py +++ b/podman/errors/exceptions.py @@ -144,6 +144,59 @@ def __init__( 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): def __init__(self, reason): From 080964d7eadaea81e05fbe8a368677bc0da72a56 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Sun, 16 Feb 2025 21:39:15 +0530 Subject: [PATCH 2/7] feat: Add PodmanConnectionError check in client.py Signed-off-by: Kanishk Pachauri --- podman/client.py | 71 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/podman/client.py b/podman/client.py index f9a023e7..6b95822e 100644 --- a/podman/client.py +++ b/podman/client.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any, Optional +from podman.errors import PodmanConnectionError from podman.api import cached_property from podman.api.client import APIClient from podman.api.path_utils import get_runtime_dir @@ -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 @@ -116,25 +129,45 @@ def from_env( Raises: ValueError when required environment variable is not set """ - 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: From 6bb826f2fda3feae50c95d51be07ddf0e94e33ee Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Sun, 16 Feb 2025 21:40:19 +0530 Subject: [PATCH 3/7] feat: Add tests for PodmanConnectionError Signed-off-by: Kanishk Pachauri --- podman/tests/integration/test_system.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/podman/tests/integration/test_system.py b/podman/tests/integration/test_system.py index bcc38711..08ffd5a8 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") From 5b381df62ad5587770af6dc067b9b6ee3369dacd Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Sun, 16 Feb 2025 21:46:21 +0530 Subject: [PATCH 4/7] chore: run ruff formatter Signed-off-by: Kanishk Pachauri --- podman/client.py | 6 +----- podman/errors/exceptions.py | 1 + podman/tests/integration/test_system.py | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/podman/client.py b/podman/client.py index 6b95822e..546a7fd9 100644 --- a/podman/client.py +++ b/podman/client.py @@ -144,11 +144,7 @@ def from_env( "max_pool_size": max_pool_size, } - host = ( - environment.get("CONTAINER_HOST") - or environment.get("DOCKER_HOST") - or None - ) + host = environment.get("CONTAINER_HOST") or environment.get("DOCKER_HOST") or None if host is not None: kwargs["base_url"] = host diff --git a/podman/errors/exceptions.py b/podman/errors/exceptions.py index c403d95d..aaf2fb43 100644 --- a/podman/errors/exceptions.py +++ b/podman/errors/exceptions.py @@ -144,6 +144,7 @@ def __init__( class InvalidArgument(PodmanError): """Parameter to method/function was not valid.""" + class PodmanConnectionError(PodmanError): """Exception raised when connection to Podman service fails using environment configuration.""" diff --git a/podman/tests/integration/test_system.py b/podman/tests/integration/test_system.py index 08ffd5a8..1cf43af6 100644 --- a/podman/tests/integration/test_system.py +++ b/podman/tests/integration/test_system.py @@ -64,7 +64,7 @@ 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): From d175b7e00047a63b8620ec89aabe3513aee4dc5d Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Mon, 17 Feb 2025 00:45:59 +0530 Subject: [PATCH 5/7] fix: Improved check for the PodmanConnectionError Signed-off-by: Kanishk Pachauri --- podman/client.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/podman/client.py b/podman/client.py index 546a7fd9..87c4d17b 100644 --- a/podman/client.py +++ b/podman/client.py @@ -61,30 +61,38 @@ def __init__(self, **kwargs) -> None: super().__init__() config = PodmanConfig() - api_kwargs = kwargs.copy() + self.api_kwargs = kwargs.copy() - if "connection" in api_kwargs: - connection = config.services[api_kwargs.get("connection")] - api_kwargs["base_url"] = connection.url.geturl() + if "connection" in self.api_kwargs: + connection = config.services[self.api_kwargs.get("connection")] + self.api_kwargs["base_url"] = connection.url.geturl() # Override configured identity, if provided in arguments - api_kwargs["identity"] = kwargs.get("identity", str(connection.identity)) - elif "base_url" not in api_kwargs: + self.api_kwargs["identity"] = kwargs.get("identity", str(connection.identity)) + elif "base_url" not in self.api_kwargs: path = str(Path(get_runtime_dir()) / "podman" / "podman.sock") - api_kwargs["base_url"] = "http+unix://" + path - self.api = APIClient(**api_kwargs) + self.api_kwargs["base_url"] = "http+unix://" + path - # Check if the connection to the Podman service is successful try: - SystemManager(client=self.api).version() + self.api = APIClient(**self.api_kwargs) + response = self.api.get("_ping") + + if response.status_code != 200: + raise PodmanConnectionError( + message=f"Unexpected response from Podman service: {response.status_code}", + environment=os.environ, + host=self.api_kwargs.get("base_url"), + original_error=None, + ) + except PodmanConnectionError: + raise except Exception as e: - error_msg = "Failed to connect to Podman service" raise PodmanConnectionError( - message=error_msg, + message=f"Failed to connect to Podman service: {str(e)}", environment=os.environ, - host=api_kwargs.get("base_url"), + host=self.api_kwargs.get("base_url"), original_error=e, - ) + ) from e def __enter__(self) -> "PodmanClient": return self From a97f38c275c4933bc899e1e30e4a0cb881c4344d Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Mon, 17 Feb 2025 01:23:28 +0530 Subject: [PATCH 6/7] fix: Improve PodmanConnectionError for all methods Signed-off-by: Kanishk Pachauri --- podman/client.py | 118 ++++++++++++++++++------------------ podman/errors/exceptions.py | 50 --------------- 2 files changed, 60 insertions(+), 108 deletions(-) diff --git a/podman/client.py b/podman/client.py index 87c4d17b..8a337ac5 100644 --- a/podman/client.py +++ b/podman/client.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Any, Optional -from podman.errors import PodmanConnectionError +from podman.errors.exceptions import APIError, PodmanConnectionError from podman.api import cached_property from podman.api.client import APIClient from podman.api.path_utils import get_runtime_dir @@ -61,37 +61,42 @@ def __init__(self, **kwargs) -> None: super().__init__() config = PodmanConfig() - self.api_kwargs = kwargs.copy() + api_kwargs = kwargs.copy() - if "connection" in self.api_kwargs: - connection = config.services[self.api_kwargs.get("connection")] - self.api_kwargs["base_url"] = connection.url.geturl() + if "connection" in api_kwargs: + connection = config.services[api_kwargs.get("connection")] + api_kwargs["base_url"] = connection.url.geturl() # Override configured identity, if provided in arguments - self.api_kwargs["identity"] = kwargs.get("identity", str(connection.identity)) - elif "base_url" not in self.api_kwargs: + api_kwargs["identity"] = kwargs.get("identity", str(connection.identity)) + elif "base_url" not in api_kwargs: path = str(Path(get_runtime_dir()) / "podman" / "podman.sock") - self.api_kwargs["base_url"] = "http+unix://" + path + api_kwargs["base_url"] = "http+unix://" + path + self.api = APIClient(**api_kwargs) - try: - self.api = APIClient(**self.api_kwargs) - response = self.api.get("_ping") + self._verify_connection() - if response.status_code != 200: + def _verify_connection(self): + """Verify connection to Podman daemon during initialization. + + Raises: + PodmanException: If unable to connect to Podman daemon + """ + try: + # Attempt to get version info to verify connection + self.version() + except APIError as e: + if "No such file or directory" in str(e): raise PodmanConnectionError( - message=f"Unexpected response from Podman service: {response.status_code}", - environment=os.environ, - host=self.api_kwargs.get("base_url"), - original_error=None, - ) - except PodmanConnectionError: - raise + "Error while connecting to Podman daemon: " + f"Could not find socket file - {str(e)}" + ) from e + raise PodmanConnectionError( + f"Error while connecting to Podman daemon: {str(e)}" + ) from e except Exception as e: raise PodmanConnectionError( - message=f"Failed to connect to Podman service: {str(e)}", - environment=os.environ, - host=self.api_kwargs.get("base_url"), - original_error=e, + f"Error while connecting to Podman daemon: {str(e)}" ) from e def __enter__(self) -> "PodmanClient": @@ -100,6 +105,18 @@ def __enter__(self) -> "PodmanClient": def __exit__(self, exc_type, exc_value, traceback) -> None: self.close() + def __enter__(self) -> "PodmanClient": + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.close() + + def __enter__(self) -> "PodmanClient": + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.close() + @classmethod def from_env( cls, @@ -137,41 +154,26 @@ def from_env( Raises: ValueError when required environment variable is not set """ - 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 - ) + 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) + @cached_property def containers(self) -> ContainersManager: diff --git a/podman/errors/exceptions.py b/podman/errors/exceptions.py index aaf2fb43..1c6149cd 100644 --- a/podman/errors/exceptions.py +++ b/podman/errors/exceptions.py @@ -148,56 +148,6 @@ class InvalidArgument(PodmanError): 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): def __init__(self, reason): From b133b37e5a86d420c32385731322c00aa7fa2317 Mon Sep 17 00:00:00 2001 From: Kanishk Pachauri Date: Wed, 19 Feb 2025 14:44:22 +0530 Subject: [PATCH 7/7] chore: run ruff formatter Signed-off-by: Kanishk Pachauri --- podman/client.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/podman/client.py b/podman/client.py index 8a337ac5..fd1b08ae 100644 --- a/podman/client.py +++ b/podman/client.py @@ -78,7 +78,7 @@ def __init__(self, **kwargs) -> None: def _verify_connection(self): """Verify connection to Podman daemon during initialization. - + Raises: PodmanException: If unable to connect to Podman daemon """ @@ -91,13 +91,9 @@ def _verify_connection(self): "Error while connecting to Podman daemon: " f"Could not find socket file - {str(e)}" ) from e - raise PodmanConnectionError( - f"Error while connecting to Podman daemon: {str(e)}" - ) from e + raise PodmanConnectionError(f"Error while connecting to Podman daemon: {str(e)}") from e except Exception as e: - raise PodmanConnectionError( - f"Error while connecting to Podman daemon: {str(e)}" - ) from e + raise PodmanConnectionError(f"Error while connecting to Podman daemon: {str(e)}") from e def __enter__(self) -> "PodmanClient": return self @@ -174,7 +170,6 @@ def from_env( return PodmanClient(**kwargs) - @cached_property def containers(self) -> ContainersManager: """Returns Manager for operations on containers stored by a Podman service."""