Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

| [DO NOT MERGE] 🛑 | fix: Enhance environment connection error handling #472

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 59 additions & 22 deletions podman/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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__
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions podman/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'NotFoundError',
'PodmanError',
'StreamParseError',
'PodmanConnectionError'
]

try:
Expand All @@ -34,6 +35,7 @@
NotFound,
PodmanError,
StreamParseError,
PodmanConnectionError
)
except ImportError:
pass
Expand Down
67 changes: 65 additions & 2 deletions podman/errors/exceptions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
7 changes: 6 additions & 1 deletion podman/tests/integration/test_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Loading