added podman, json and yaml
This commit is contained in:
10
venv/lib/python3.11/site-packages/podman/__init__.py
Normal file
10
venv/lib/python3.11/site-packages/podman/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""Podman client module."""
|
||||
import sys
|
||||
|
||||
assert sys.version_info >= (3, 6), "Python 3.6 or greater is required."
|
||||
|
||||
from podman.client import PodmanClient, from_env
|
||||
from podman.version import __version__
|
||||
|
||||
# isort: unique-list
|
||||
__all__ = ['PodmanClient', '__version__', 'from_env']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
61
venv/lib/python3.11/site-packages/podman/api/__init__.py
Normal file
61
venv/lib/python3.11/site-packages/podman/api/__init__.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""Tools for connecting to a Podman service."""
|
||||
import re
|
||||
|
||||
from podman.api.cached_property import cached_property
|
||||
from podman.api.client import APIClient
|
||||
from podman.api.http_utils import prepare_body, prepare_filters
|
||||
from podman.api.parse_utils import (
|
||||
decode_header,
|
||||
frames,
|
||||
parse_repository,
|
||||
prepare_cidr,
|
||||
prepare_timestamp,
|
||||
stream_frames,
|
||||
)
|
||||
from podman.api.tar_utils import create_tar, prepare_containerfile, prepare_containerignore
|
||||
from .. import version
|
||||
|
||||
DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024
|
||||
|
||||
|
||||
def _api_version(release: str, significant: int = 3) -> str:
|
||||
"""Return API version removing any additional identifiers from the release version.
|
||||
|
||||
This is a simple lexicographical parsing, no semantics are applied, e.g. semver checking.
|
||||
"""
|
||||
items = re.split(r"\.|-|\+", release)
|
||||
parts = items[0:significant]
|
||||
return ".".join(parts)
|
||||
|
||||
|
||||
VERSION: str = _api_version(version.__version__)
|
||||
COMPATIBLE_VERSION: str = _api_version(version.__compatible_version__, 2)
|
||||
|
||||
try:
|
||||
from typing import Literal
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
try:
|
||||
from typing_extensions import Literal
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
from podman.api.typing_extensions import Literal # pylint: disable=ungrouped-imports
|
||||
|
||||
# isort: unique-list
|
||||
__all__ = [
|
||||
'APIClient',
|
||||
'COMPATIBLE_VERSION',
|
||||
'DEFAULT_CHUNK_SIZE',
|
||||
'Literal',
|
||||
'VERSION',
|
||||
'cached_property',
|
||||
'create_tar',
|
||||
'decode_header',
|
||||
'frames',
|
||||
'parse_repository',
|
||||
'prepare_body',
|
||||
'prepare_cidr',
|
||||
'prepare_containerfile',
|
||||
'prepare_containerignore',
|
||||
'prepare_filters',
|
||||
'prepare_timestamp',
|
||||
'stream_frames',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,49 @@
|
||||
"""Utility functions for working with Adapters."""
|
||||
from typing import NamedTuple, Mapping
|
||||
|
||||
|
||||
def _key_normalizer(key_class: NamedTuple, request_context: Mapping) -> Mapping:
|
||||
"""Create a pool key out of a request context dictionary.
|
||||
|
||||
According to RFC 3986, both the scheme and host are case-insensitive.
|
||||
Therefore, this function normalizes both before constructing the pool
|
||||
key for an HTTPS request. If you wish to change this behaviour, provide
|
||||
alternate callables to ``key_fn_by_scheme``.
|
||||
|
||||
Copied from urllib3.poolmanager._default_key_normalizer.
|
||||
|
||||
Args:
|
||||
key_class: The class to use when constructing the key. This should be a namedtuple
|
||||
with the scheme and host keys at a minimum.
|
||||
request_context: An object that contain the context for a request.
|
||||
|
||||
Returns:
|
||||
A namedtuple that can be used as a connection pool key.
|
||||
"""
|
||||
# Since we mutate the dictionary, make a copy first
|
||||
context = request_context.copy()
|
||||
context["scheme"] = context["scheme"].lower()
|
||||
context["host"] = context["host"].lower()
|
||||
|
||||
# These are both dictionaries and need to be transformed into frozensets
|
||||
for key in ("headers", "_proxy_headers", "_socks_options"):
|
||||
if key in context and context[key] is not None:
|
||||
context[key] = frozenset(context[key].items())
|
||||
|
||||
# The socket_options key may be a list and needs to be transformed into a
|
||||
# tuple.
|
||||
socket_opts = context.get("socket_options")
|
||||
if socket_opts is not None:
|
||||
context["socket_options"] = tuple(socket_opts)
|
||||
|
||||
# Map the kwargs to the names in the namedtuple - this is necessary since
|
||||
# namedtuples can't have fields starting with '_'.
|
||||
for key in list(context.keys()):
|
||||
context["key_" + key] = context.pop(key)
|
||||
|
||||
# Default to ``None`` for keys missing from the context
|
||||
for field in key_class._fields:
|
||||
if field not in context:
|
||||
context[field] = None
|
||||
|
||||
return key_class(**context)
|
||||
@ -0,0 +1,9 @@
|
||||
"""Provide cached_property for Python <=3.8 programs."""
|
||||
import functools
|
||||
|
||||
try:
|
||||
from functools import cached_property # pylint: disable=unused-import
|
||||
except ImportError:
|
||||
|
||||
def cached_property(fn):
|
||||
return property(functools.lru_cache()(fn))
|
||||
415
venv/lib/python3.11/site-packages/podman/api/client.py
Normal file
415
venv/lib/python3.11/site-packages/podman/api/client.py
Normal file
@ -0,0 +1,415 @@
|
||||
"""APIClient for connecting to Podman service."""
|
||||
import json
|
||||
import urllib.parse
|
||||
from typing import Any, ClassVar, IO, Iterable, List, Mapping, Optional, Tuple, Type, Union
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from podman import api
|
||||
from podman.api.ssh import SSHAdapter
|
||||
from podman.api.uds import UDSAdapter
|
||||
from podman.errors import APIError, NotFound
|
||||
from podman.tlsconfig import TLSConfig
|
||||
from podman.version import __version__
|
||||
|
||||
_Data = Union[
|
||||
None,
|
||||
str,
|
||||
bytes,
|
||||
Mapping[str, Any],
|
||||
Iterable[Tuple[str, Optional[str]]],
|
||||
IO,
|
||||
]
|
||||
"""Type alias for request data parameter."""
|
||||
|
||||
_Timeout = Union[None, float, Tuple[float, float], Tuple[float, None]]
|
||||
"""Type alias for request timeout parameter."""
|
||||
|
||||
|
||||
class APIResponse:
|
||||
"""APIResponse proxy requests.Response objects.
|
||||
|
||||
Override raise_for_status() to implement Podman API binding errors.
|
||||
All other methods and attributes forwarded to original Response.
|
||||
"""
|
||||
|
||||
def __init__(self, response: requests.Response):
|
||||
"""Initialize APIResponse.
|
||||
|
||||
Args:
|
||||
response: the requests.Response to provide implementation
|
||||
"""
|
||||
self._response = response
|
||||
|
||||
def __getattr__(self, item: str):
|
||||
"""Forward any query for an attribute not defined in this proxy class to wrapped class."""
|
||||
return getattr(self._response, item)
|
||||
|
||||
def raise_for_status(self, not_found: Type[APIError] = NotFound) -> None:
|
||||
"""Raises exception when Podman service reports one."""
|
||||
if self.status_code < 400:
|
||||
return
|
||||
|
||||
try:
|
||||
body = self.json()
|
||||
cause = body["cause"]
|
||||
message = body["message"]
|
||||
except (json.decoder.JSONDecodeError, KeyError):
|
||||
cause = message = self.text
|
||||
|
||||
if self.status_code == requests.codes.not_found:
|
||||
raise not_found(cause, response=self._response, explanation=message)
|
||||
raise APIError(cause, response=self._response, explanation=message)
|
||||
|
||||
|
||||
class APIClient(requests.Session):
|
||||
"""Client for Podman service API."""
|
||||
|
||||
# Abstract methods (delete,get,head,post) are specialized and pylint cannot walk hierarchy.
|
||||
# pylint: disable=too-many-instance-attributes,arguments-differ,arguments-renamed
|
||||
|
||||
supported_schemes: ClassVar[List[str]] = (
|
||||
"unix",
|
||||
"http+unix",
|
||||
"ssh",
|
||||
"http+ssh",
|
||||
"tcp",
|
||||
"http",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str = None,
|
||||
version: Optional[str] = None,
|
||||
timeout: Optional[float] = None,
|
||||
tls: Union[TLSConfig, bool] = False,
|
||||
user_agent: Optional[str] = None,
|
||||
num_pools: Optional[int] = None,
|
||||
credstore_env: Optional[Mapping[str, str]] = None,
|
||||
use_ssh_client=True,
|
||||
max_pools_size=None,
|
||||
**kwargs,
|
||||
): # pylint: disable=unused-argument
|
||||
"""Instantiate APIClient object.
|
||||
|
||||
Args:
|
||||
base_url: Address to use for connecting to Podman service.
|
||||
version: Override version prefix for Podman resource URLs.
|
||||
timeout: Time in seconds to allow for Podman service operation.
|
||||
tls: Configuration for TLS connections.
|
||||
user_agent: Override User-Agent HTTP header.
|
||||
num_pools: The number of connection pools to cache.
|
||||
credstore_env: Environment for storing credentials.
|
||||
use_ssh_client: Use system ssh agent rather than ssh module. Always, True.
|
||||
max_pool_size: Override number of connections pools to maintain.
|
||||
Default: requests.adapters.DEFAULT_POOLSIZE
|
||||
|
||||
Keyword Args:
|
||||
compatible_version (str): Override version prefix for compatible resource URLs.
|
||||
identity (str): Provide SSH key to authenticate SSH connection.
|
||||
|
||||
Raises:
|
||||
ValueError: when a parameter is incorrect
|
||||
"""
|
||||
super().__init__()
|
||||
self.base_url = self._normalize_url(base_url)
|
||||
|
||||
adapter_kwargs = kwargs.copy()
|
||||
if num_pools is not None:
|
||||
adapter_kwargs["pool_connections"] = num_pools
|
||||
if max_pools_size is not None:
|
||||
adapter_kwargs["pool_maxsize"] = max_pools_size
|
||||
if timeout is not None:
|
||||
adapter_kwargs["timeout"] = timeout
|
||||
|
||||
if self.base_url.scheme == "http+unix":
|
||||
self.mount("http://", UDSAdapter(self.base_url.geturl(), **adapter_kwargs))
|
||||
self.mount("https://", UDSAdapter(self.base_url.geturl(), **adapter_kwargs))
|
||||
|
||||
elif self.base_url.scheme == "http+ssh":
|
||||
self.mount("http://", SSHAdapter(self.base_url.geturl(), **adapter_kwargs))
|
||||
self.mount("https://", SSHAdapter(self.base_url.geturl(), **adapter_kwargs))
|
||||
|
||||
elif self.base_url.scheme == "http":
|
||||
self.mount("http://", HTTPAdapter(**adapter_kwargs))
|
||||
self.mount("https://", HTTPAdapter(**adapter_kwargs))
|
||||
else:
|
||||
assert False, "APIClient.supported_schemes changed without adding a branch here."
|
||||
|
||||
self.version = version or api.VERSION
|
||||
self.path_prefix = f"/v{self.version}/libpod/"
|
||||
self.compatible_version = kwargs.get("compatible_version", api.COMPATIBLE_VERSION)
|
||||
self.compatible_prefix = f"/v{self.compatible_version}/"
|
||||
|
||||
self.timeout = timeout
|
||||
self.pool_maxsize = num_pools or requests.adapters.DEFAULT_POOLSIZE
|
||||
self.credstore_env = credstore_env or {}
|
||||
|
||||
self.user_agent = user_agent or (
|
||||
f"PodmanPy/{__version__} (API v{self.version}; Compatible v{self.compatible_version})"
|
||||
)
|
||||
self.headers.update({"User-Agent": self.user_agent})
|
||||
|
||||
@staticmethod
|
||||
def _normalize_url(base_url: str) -> urllib.parse.ParseResult:
|
||||
uri = urllib.parse.urlparse(base_url)
|
||||
if uri.scheme not in APIClient.supported_schemes:
|
||||
raise ValueError(
|
||||
f"The scheme '{uri.scheme}' must be one of {APIClient.supported_schemes}"
|
||||
)
|
||||
|
||||
# Normalize URL scheme, needs to match up with adapter mounts
|
||||
if uri.scheme == "unix":
|
||||
uri = uri._replace(scheme="http+unix")
|
||||
elif uri.scheme == "ssh":
|
||||
uri = uri._replace(scheme="http+ssh")
|
||||
elif uri.scheme == "tcp":
|
||||
uri = uri._replace(scheme="http")
|
||||
|
||||
# Normalize URL netloc, needs to match up with transport adapters expectations
|
||||
if uri.netloc == "":
|
||||
uri = uri._replace(netloc=uri.path)._replace(path="")
|
||||
if "/" in uri.netloc:
|
||||
uri = uri._replace(netloc=urllib.parse.quote_plus(uri.netloc))
|
||||
|
||||
return uri
|
||||
|
||||
def delete(
|
||||
self,
|
||||
path: Union[str, bytes],
|
||||
params: Union[None, bytes, Mapping[str, str]] = None,
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
timeout: _Timeout = None,
|
||||
stream: Optional[bool] = False,
|
||||
**kwargs,
|
||||
) -> APIResponse:
|
||||
"""HTTP DELETE operation against configured Podman service.
|
||||
|
||||
Args:
|
||||
path: Relative path to RESTful resource.
|
||||
params: Optional parameters to include with URL.
|
||||
headers: Optional headers to include in request.
|
||||
timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple
|
||||
stream: Return iterator for content vs reading all content into memory
|
||||
|
||||
Keyword Args:
|
||||
compatible: Will override the default path prefix with compatible prefix
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
return self._request(
|
||||
"DELETE",
|
||||
path=path,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def get(
|
||||
self,
|
||||
path: Union[str, bytes],
|
||||
params: Union[None, bytes, Mapping[str, List[str]]] = None,
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
timeout: _Timeout = None,
|
||||
stream: Optional[bool] = False,
|
||||
**kwargs,
|
||||
) -> APIResponse:
|
||||
"""HTTP GET operation against configured Podman service.
|
||||
|
||||
Args:
|
||||
path: Relative path to RESTful resource.
|
||||
params: Optional parameters to include with URL.
|
||||
headers: Optional headers to include in request.
|
||||
timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple
|
||||
stream: Return iterator for content vs reading all content into memory
|
||||
|
||||
Keyword Args:
|
||||
compatible: Will override the default path prefix with compatible prefix
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
return self._request(
|
||||
"GET",
|
||||
path=path,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def head(
|
||||
self,
|
||||
path: Union[str, bytes],
|
||||
params: Union[None, bytes, Mapping[str, str]] = None,
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
timeout: _Timeout = None,
|
||||
stream: Optional[bool] = False,
|
||||
**kwargs,
|
||||
) -> APIResponse:
|
||||
"""HTTP HEAD operation against configured Podman service.
|
||||
|
||||
Args:
|
||||
path: Relative path to RESTful resource.
|
||||
params: Optional parameters to include with URL.
|
||||
headers: Optional headers to include in request.
|
||||
timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple
|
||||
stream: Return iterator for content vs reading all content into memory
|
||||
|
||||
Keyword Args:
|
||||
compatible: Will override the default path prefix with compatible prefix
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
return self._request(
|
||||
"HEAD",
|
||||
path=path,
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def post(
|
||||
self,
|
||||
path: Union[str, bytes],
|
||||
params: Union[None, bytes, Mapping[str, str]] = None,
|
||||
data: _Data = None,
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
timeout: _Timeout = None,
|
||||
stream: Optional[bool] = False,
|
||||
**kwargs,
|
||||
) -> APIResponse:
|
||||
"""HTTP POST operation against configured Podman service.
|
||||
|
||||
Args:
|
||||
path: Relative path to RESTful resource.
|
||||
data: HTTP body for operation
|
||||
params: Optional parameters to include with URL.
|
||||
headers: Optional headers to include in request.
|
||||
timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple
|
||||
stream: Return iterator for content vs reading all content into memory
|
||||
|
||||
Keyword Args:
|
||||
compatible: Will override the default path prefix with compatible prefix
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
return self._request(
|
||||
"POST",
|
||||
path=path,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def put(
|
||||
self,
|
||||
path: Union[str, bytes],
|
||||
params: Union[None, bytes, Mapping[str, str]] = None,
|
||||
data: _Data = None,
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
timeout: _Timeout = None,
|
||||
stream: Optional[bool] = False,
|
||||
**kwargs,
|
||||
) -> APIResponse:
|
||||
"""HTTP PUT operation against configured Podman service.
|
||||
|
||||
Args:
|
||||
path: Relative path to RESTful resource.
|
||||
data: HTTP body for operation
|
||||
params: Optional parameters to include with URL.
|
||||
headers: Optional headers to include in request.
|
||||
timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple
|
||||
stream: Return iterator for content vs reading all content into memory
|
||||
|
||||
Keyword Args:
|
||||
compatible: Will override the default path prefix with compatible prefix
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
return self._request(
|
||||
"PUT",
|
||||
path=path,
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
timeout=timeout,
|
||||
stream=stream,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: Union[str, bytes],
|
||||
data: _Data = None,
|
||||
params: Union[None, bytes, Mapping[str, str]] = None,
|
||||
headers: Optional[Mapping[str, str]] = None,
|
||||
timeout: _Timeout = None,
|
||||
stream: Optional[bool] = None,
|
||||
**kwargs,
|
||||
) -> APIResponse:
|
||||
"""HTTP operation against configured Podman service.
|
||||
|
||||
Args:
|
||||
method: HTTP method to use for request
|
||||
path: Relative path to RESTful resource.
|
||||
params: Optional parameters to include with URL.
|
||||
headers: Optional headers to include in request.
|
||||
timeout: Number of seconds to wait on request, or (connect timeout, read timeout) tuple
|
||||
|
||||
Keyword Args:
|
||||
compatible: Will override the default path prefix with compatible prefix
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
# Only set timeout if one is given, lower level APIs will not override None
|
||||
timeout_kw = {}
|
||||
timeout = timeout or self.timeout
|
||||
if timeout_kw is not None:
|
||||
timeout_kw["timeout"] = timeout
|
||||
|
||||
compatible = kwargs.get("compatible", False)
|
||||
path_prefix = self.compatible_prefix if compatible else self.path_prefix
|
||||
|
||||
path = path.lstrip("/") # leading / makes urljoin crazy...
|
||||
|
||||
# TODO should we have an option for HTTPS support?
|
||||
# Build URL for operation from base_url
|
||||
uri = urllib.parse.ParseResult(
|
||||
"http",
|
||||
self.base_url.netloc,
|
||||
urllib.parse.urljoin(path_prefix, path),
|
||||
self.base_url.params,
|
||||
self.base_url.query,
|
||||
self.base_url.fragment,
|
||||
)
|
||||
|
||||
try:
|
||||
return APIResponse(
|
||||
self.request(
|
||||
method.upper(),
|
||||
uri.geturl(),
|
||||
params=params,
|
||||
data=data,
|
||||
headers=(headers or {}),
|
||||
stream=stream,
|
||||
**timeout_kw,
|
||||
)
|
||||
)
|
||||
except OSError as e:
|
||||
raise APIError(uri.geturl(), explanation=f"{method.upper()} operation failed") from e
|
||||
102
venv/lib/python3.11/site-packages/podman/api/http_utils.py
Normal file
102
venv/lib/python3.11/site-packages/podman/api/http_utils.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Utility functions for working with URLs."""
|
||||
import base64
|
||||
import collections.abc
|
||||
import json
|
||||
from typing import Dict, List, Mapping, Optional, Union, Any
|
||||
|
||||
|
||||
def prepare_filters(filters: Union[str, List[str], Mapping[str, str]]) -> Optional[str]:
|
||||
"""Return filters as an URL quoted JSON Dict[str, List[Any]]."""
|
||||
|
||||
if filters is None or len(filters) == 0:
|
||||
return None
|
||||
|
||||
criteria: Dict[str, List[str]] = {}
|
||||
if isinstance(filters, str):
|
||||
_format_string(filters, criteria)
|
||||
elif isinstance(filters, collections.abc.Mapping):
|
||||
_format_dict(filters, criteria)
|
||||
else:
|
||||
_format_list(filters, criteria)
|
||||
|
||||
if len(criteria) == 0:
|
||||
return None
|
||||
|
||||
return json.dumps(criteria, sort_keys=True)
|
||||
|
||||
|
||||
def _format_list(filters, criteria):
|
||||
for item in filters:
|
||||
if item is None:
|
||||
continue
|
||||
|
||||
key, value = item.split("=", 1)
|
||||
if key in criteria:
|
||||
criteria[key].append(value)
|
||||
else:
|
||||
criteria[key] = [value]
|
||||
|
||||
|
||||
def _format_dict(filters, criteria):
|
||||
for key, value in filters.items():
|
||||
if value is None:
|
||||
continue
|
||||
value = str(value)
|
||||
|
||||
if key in criteria:
|
||||
criteria[key].append(value)
|
||||
else:
|
||||
criteria[key] = [value]
|
||||
|
||||
|
||||
def _format_string(filters, criteria):
|
||||
key, value = filters.split("=", 1)
|
||||
criteria[key] = [value]
|
||||
|
||||
|
||||
def prepare_body(body: Mapping[str, Any]) -> str:
|
||||
"""Returns JSON payload to be uploaded to server.
|
||||
|
||||
Values of None and empty Iterables are removed, False and zero-values are retained.
|
||||
"""
|
||||
if body is None:
|
||||
return ""
|
||||
|
||||
body = _filter_values(body)
|
||||
return json.dumps(body, sort_keys=True)
|
||||
|
||||
|
||||
def _filter_values(mapping: Mapping[str, Any], recursion=False) -> Dict[str, Any]:
|
||||
"""Returns a canonical dictionary with values == None or empty Iterables removed.
|
||||
|
||||
Dictionary is walked using recursion.
|
||||
"""
|
||||
canonical = {}
|
||||
|
||||
for key, value in mapping.items():
|
||||
# quick filter if possible...
|
||||
if (
|
||||
value is None
|
||||
or (isinstance(value, collections.abc.Sized) and len(value) <= 0)
|
||||
and not recursion
|
||||
):
|
||||
continue
|
||||
|
||||
# depending on type we need details...
|
||||
if isinstance(value, collections.abc.Mapping):
|
||||
proposal = _filter_values(value, recursion=True)
|
||||
elif isinstance(value, collections.abc.Iterable) and not isinstance(value, str):
|
||||
proposal = [i for i in value if i is not None]
|
||||
else:
|
||||
proposal = value
|
||||
|
||||
if not recursion and proposal not in (None, str(), [], {}):
|
||||
canonical[key] = proposal
|
||||
elif recursion and proposal not in (None, [], {}):
|
||||
canonical[key] = proposal
|
||||
|
||||
return canonical
|
||||
|
||||
|
||||
def encode_auth_header(auth_config: Dict[str, str]) -> str:
|
||||
return base64.b64encode(json.dumps(auth_config).encode('utf-8'))
|
||||
99
venv/lib/python3.11/site-packages/podman/api/parse_utils.py
Normal file
99
venv/lib/python3.11/site-packages/podman/api/parse_utils.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""Helper functions for parsing strings."""
|
||||
import base64
|
||||
import ipaddress
|
||||
import json
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterator, Optional, Tuple, Union
|
||||
|
||||
from requests import Response
|
||||
|
||||
|
||||
def parse_repository(name: str) -> Tuple[str, Optional[str]]:
|
||||
"""Parse repository image name from tag or digest
|
||||
|
||||
Returns:
|
||||
item 1: repository name
|
||||
item 2: Either digest and tag, tag, or None
|
||||
"""
|
||||
# split image name and digest
|
||||
elements = name.split("@", 1)
|
||||
if len(elements) == 2:
|
||||
return elements[0], elements[1]
|
||||
|
||||
# split repository and image name from tag
|
||||
elements = name.split(":", 1)
|
||||
if len(elements) == 2 and "/" not in elements[1]:
|
||||
return elements[0], elements[1]
|
||||
|
||||
return name, None
|
||||
|
||||
|
||||
def decode_header(value: Optional[str]) -> Dict[str, Any]:
|
||||
"""Decode a base64 JSON header value."""
|
||||
if value is None:
|
||||
return {}
|
||||
|
||||
value = base64.b64decode(value)
|
||||
text = value.decode("utf-8")
|
||||
return json.loads(text)
|
||||
|
||||
|
||||
def prepare_timestamp(value: Union[datetime, int, None]) -> Optional[int]:
|
||||
"""Returns a UTC UNIX timestamp from given input."""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
if isinstance(value, datetime):
|
||||
delta = value - datetime.utcfromtimestamp(0)
|
||||
return delta.seconds + delta.days * 24 * 3600
|
||||
|
||||
raise ValueError(f"Type '{type(value)}' is not supported by prepare_timestamp()")
|
||||
|
||||
|
||||
def prepare_cidr(value: Union[ipaddress.IPv4Network, ipaddress.IPv6Network]) -> (str, str):
|
||||
"""Returns network address and Base64 encoded netmask from CIDR.
|
||||
|
||||
The return values are dictated by the Go JSON decoder.
|
||||
"""
|
||||
return str(value.network_address), base64.b64encode(value.netmask.packed).decode("utf-8")
|
||||
|
||||
|
||||
def frames(response: Response) -> Iterator[bytes]:
|
||||
"""Returns each frame from multiplexed payload, all results are expected in the payload.
|
||||
|
||||
The stdout and stderr frames are undifferentiated as they are returned.
|
||||
"""
|
||||
length = len(response.content)
|
||||
index = 0
|
||||
while length - index > 8:
|
||||
header = response.content[index : index + 8]
|
||||
_, frame_length = struct.unpack_from(">BxxxL", header)
|
||||
frame_begin = index + 8
|
||||
frame_end = frame_begin + frame_length
|
||||
index = frame_end
|
||||
yield response.content[frame_begin:frame_end]
|
||||
|
||||
|
||||
def stream_frames(response: Response) -> Iterator[bytes]:
|
||||
"""Returns each frame from multiplexed streamed payload.
|
||||
|
||||
Notes:
|
||||
The stdout and stderr frames are undifferentiated as they are returned.
|
||||
"""
|
||||
while True:
|
||||
header = response.raw.read(8)
|
||||
if not header:
|
||||
return
|
||||
|
||||
_, frame_length = struct.unpack_from(">BxxxL", header)
|
||||
if not frame_length:
|
||||
continue
|
||||
|
||||
data = response.raw.read(frame_length)
|
||||
if not data:
|
||||
return
|
||||
yield data
|
||||
301
venv/lib/python3.11/site-packages/podman/api/ssh.py
Normal file
301
venv/lib/python3.11/site-packages/podman/api/ssh.py
Normal file
@ -0,0 +1,301 @@
|
||||
"""Specialized Transport Adapter for remote Podman access via ssh tunnel.
|
||||
|
||||
See Podman go bindings for more details.
|
||||
"""
|
||||
import collections
|
||||
import functools
|
||||
import http.client
|
||||
import logging
|
||||
import pathlib
|
||||
import random
|
||||
import socket
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
from contextlib import suppress
|
||||
from typing import Optional, Union
|
||||
|
||||
import time
|
||||
import xdg.BaseDirectory
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
except ImportError:
|
||||
from requests.packages import urllib3
|
||||
|
||||
from requests.adapters import DEFAULT_POOLBLOCK, DEFAULT_RETRIES, HTTPAdapter
|
||||
|
||||
from .adapter_utils import _key_normalizer
|
||||
|
||||
logger = logging.getLogger("podman.ssh_adapter")
|
||||
|
||||
|
||||
class SSHSocket(socket.socket):
|
||||
"""Specialization of socket.socket to forward a UNIX domain socket via SSH."""
|
||||
|
||||
def __init__(self, uri: str, identity: Optional[str] = None):
|
||||
"""Initialize SSHSocket.
|
||||
|
||||
Args:
|
||||
uri: Full address of a Podman service including path to remote socket.
|
||||
identity: path to file containing SSH key for authorization
|
||||
|
||||
Examples:
|
||||
SSHSocket("http+ssh://alice@api.example:2222/run/user/1000/podman/podman.sock",
|
||||
"~alice/.ssh/api_ed25519")
|
||||
"""
|
||||
super().__init__(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.uri = uri
|
||||
self.identity = identity
|
||||
self._proc: Optional[subprocess.Popen] = None
|
||||
|
||||
runtime_dir = pathlib.Path(xdg.BaseDirectory.get_runtime_dir(strict=False)) / "podman"
|
||||
runtime_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
|
||||
self.local_sock = runtime_dir / f"podman-forward-{random.getrandbits(80):x}.sock"
|
||||
|
||||
def connect(self, **kwargs): # pylint: disable=unused-argument
|
||||
"""Returns socket for SSH tunneled UNIX domain socket.
|
||||
|
||||
Raises:
|
||||
subprocess.TimeoutExpired: when SSH client fails to create local socket
|
||||
"""
|
||||
uri = urllib.parse.urlparse(self.uri)
|
||||
|
||||
command = [
|
||||
"ssh",
|
||||
"-N",
|
||||
"-o",
|
||||
"StrictHostKeyChecking no",
|
||||
"-L",
|
||||
f"{self.local_sock}:{uri.path}",
|
||||
]
|
||||
|
||||
if self.identity is not None:
|
||||
path = pathlib.Path(self.identity).expanduser()
|
||||
command += ["-i", str(path)]
|
||||
|
||||
command += [f"ssh://{uri.netloc}"]
|
||||
self._proc = subprocess.Popen( # pylint: disable=consider-using-with
|
||||
command,
|
||||
shell=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
)
|
||||
|
||||
expiration = time.monotonic() + 300
|
||||
while not self.local_sock.exists():
|
||||
if time.monotonic() > expiration:
|
||||
cmd = " ".join(command)
|
||||
raise subprocess.TimeoutExpired(cmd, expiration)
|
||||
|
||||
logger.debug("Waiting on %s", self.local_sock)
|
||||
time.sleep(0.2)
|
||||
|
||||
super().connect(str(self.local_sock))
|
||||
|
||||
def send(self, data: bytes, flags=None) -> int: # pylint: disable=unused-argument
|
||||
"""Write data to SSH forwarded UNIX domain socket.
|
||||
|
||||
Args:
|
||||
data: Data to write.
|
||||
flags: Ignored.
|
||||
|
||||
Returns:
|
||||
The number of bytes written.
|
||||
|
||||
Raises:
|
||||
RuntimeError: When socket has not been connected.
|
||||
"""
|
||||
if not self._proc or self._proc.stdin.closed:
|
||||
raise RuntimeError(f"SSHSocket({self.uri}) not connected.")
|
||||
|
||||
count = self._proc.stdin.write(data)
|
||||
self._proc.stdin.flush()
|
||||
return count
|
||||
|
||||
def recv(self, buffersize, flags=None) -> bytes: # pylint: disable=unused-argument
|
||||
"""Read data from SSH forwarded UNIX domain socket.
|
||||
|
||||
Args:
|
||||
buffersize: Maximum number of bytes to read.
|
||||
flags: Ignored.
|
||||
|
||||
Raises:
|
||||
RuntimeError: When socket has not been connected.
|
||||
"""
|
||||
if not self._proc:
|
||||
raise RuntimeError(f"SSHSocket({self.uri}) not connected.")
|
||||
return self._proc.stdout.read(buffersize)
|
||||
|
||||
def close(self):
|
||||
"""Release resources held by SSHSocket.
|
||||
|
||||
The SSH client is first sent SIGTERM, then a SIGKILL 20 seconds later if needed.
|
||||
"""
|
||||
if not self._proc or self._proc.stdin.closed:
|
||||
return
|
||||
|
||||
with suppress(BrokenPipeError):
|
||||
self._proc.stdin.close()
|
||||
self._proc.stdout.close()
|
||||
|
||||
self._proc.terminate()
|
||||
try:
|
||||
self._proc.wait(timeout=20)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.debug("SIGKILL required to stop SSH client.")
|
||||
self._proc.kill()
|
||||
|
||||
self.local_sock.unlink()
|
||||
self._proc = None
|
||||
super().close()
|
||||
|
||||
|
||||
class SSHConnection(http.client.HTTPConnection):
|
||||
"""Specialization of HTTPConnection to use a SSH forwarded socket."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
timeout: Union[float, urllib3.Timeout, None] = None,
|
||||
strict=False,
|
||||
**kwargs, # pylint: disable=unused-argument
|
||||
) -> None:
|
||||
"""Initialize connection to SSHSocket for HTTP client.
|
||||
|
||||
Args:
|
||||
host: Ignored.
|
||||
port: Ignored.
|
||||
timeout: Time to allow for operation.
|
||||
strict: Ignored.
|
||||
|
||||
Keyword Args:
|
||||
uri: Full address of a Podman service including path to remote socket. Required.
|
||||
identity: path to file containing SSH key for authorization.
|
||||
"""
|
||||
self.sock: Optional[socket.socket] = None
|
||||
|
||||
connection_kwargs = kwargs.copy()
|
||||
connection_kwargs["port"] = port
|
||||
|
||||
if timeout is not None:
|
||||
if isinstance(timeout, urllib3.Timeout):
|
||||
try:
|
||||
connection_kwargs["timeout"] = float(timeout.total)
|
||||
except TypeError:
|
||||
pass
|
||||
connection_kwargs["timeout"] = timeout
|
||||
|
||||
self.uri = connection_kwargs.pop("uri")
|
||||
self.identity = connection_kwargs.pop("identity", None)
|
||||
|
||||
super().__init__(host, **connection_kwargs)
|
||||
if logger.getEffectiveLevel() == logging.DEBUG:
|
||||
self.set_debuglevel(1)
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Connect to Podman service via SSHSocket."""
|
||||
sock = SSHSocket(self.uri, self.identity)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect()
|
||||
self.sock = sock
|
||||
|
||||
|
||||
class SSHConnectionPool(urllib3.HTTPConnectionPool):
|
||||
"""Specialized HTTPConnectionPool for holding SSH connections."""
|
||||
|
||||
ConnectionCls = SSHConnection
|
||||
|
||||
|
||||
class SSHPoolManager(urllib3.PoolManager):
|
||||
"""Specialized PoolManager for tracking SSH connections."""
|
||||
|
||||
# pylint's special handling for namedtuple does not cover this usage
|
||||
# pylint: disable=invalid-name
|
||||
_PoolKey = collections.namedtuple(
|
||||
"_PoolKey", urllib3.poolmanager.PoolKey._fields + ("key_uri", "key_identity")
|
||||
)
|
||||
|
||||
# Map supported schemes to Pool Classes
|
||||
_pool_classes_by_scheme = {
|
||||
"http": SSHConnectionPool,
|
||||
"http+ssh": SSHConnectionPool,
|
||||
}
|
||||
|
||||
# Map supported schemes to Pool Key index generator
|
||||
_key_fn_by_scheme = {
|
||||
"http": functools.partial(_key_normalizer, _PoolKey),
|
||||
"http+ssh": functools.partial(_key_normalizer, _PoolKey),
|
||||
}
|
||||
|
||||
def __init__(self, num_pools=10, headers=None, **kwargs):
|
||||
"""Initialize SSHPoolManager.
|
||||
|
||||
Args:
|
||||
num_pools: Number of SSH Connection pools to maintain.
|
||||
headers: Additional headers to add to operations.
|
||||
"""
|
||||
super().__init__(num_pools, headers, **kwargs)
|
||||
self.pool_classes_by_scheme = SSHPoolManager._pool_classes_by_scheme
|
||||
self.key_fn_by_scheme = SSHPoolManager._key_fn_by_scheme
|
||||
|
||||
|
||||
class SSHAdapter(HTTPAdapter):
|
||||
"""Specialization of requests transport adapter for SSH forwarded UNIX domain sockets."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uri: str,
|
||||
pool_connections: int = 9,
|
||||
pool_maxsize: int = 10,
|
||||
max_retries: int = DEFAULT_RETRIES,
|
||||
pool_block: int = DEFAULT_POOLBLOCK,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize SSHAdapter.
|
||||
|
||||
Args:
|
||||
uri: Full address of a Podman service including path to remote socket.
|
||||
Format, ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True
|
||||
pool_connections: The number of connection pools to cache. Should be at least one less
|
||||
than pool_maxsize.
|
||||
pool_maxsize: The maximum number of connections to save in the pool.
|
||||
OpenSSH default is 10.
|
||||
max_retries: The maximum number of retries each connection should attempt.
|
||||
pool_block: Whether the connection pool should block for connections.
|
||||
|
||||
Keyword Args:
|
||||
timeout (float):
|
||||
identity (str): Optional path to ssh identity key
|
||||
"""
|
||||
self.poolmanager: Optional[SSHPoolManager] = None
|
||||
|
||||
# Parsed for fail-fast side effects
|
||||
_ = urllib.parse.urlparse(uri)
|
||||
self._pool_kwargs = {"uri": uri}
|
||||
|
||||
if "identity" in kwargs:
|
||||
path = pathlib.Path(kwargs.get("identity"))
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Identity file '{path}' does not exist.")
|
||||
self._pool_kwargs["identity"] = str(path)
|
||||
|
||||
if "timeout" in kwargs:
|
||||
self._pool_kwargs["timeout"] = kwargs.get("timeout")
|
||||
|
||||
super().__init__(pool_connections, pool_maxsize, max_retries, pool_block)
|
||||
|
||||
def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **kwargs):
|
||||
"""Initialize SSHPoolManager to be used by SSHAdapter.
|
||||
|
||||
Args:
|
||||
connections: The number of urllib3 connection pools to cache.
|
||||
maxsize: The maximum number of connections to save in the pool.
|
||||
block: Block when no free connections are available.
|
||||
"""
|
||||
pool_kwargs = kwargs.copy()
|
||||
pool_kwargs.update(self._pool_kwargs)
|
||||
self.poolmanager = SSHPoolManager(
|
||||
num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs
|
||||
)
|
||||
133
venv/lib/python3.11/site-packages/podman/api/tar_utils.py
Normal file
133
venv/lib/python3.11/site-packages/podman/api/tar_utils.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""Utility functions for working with tarballs."""
|
||||
import pathlib
|
||||
import random
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
from fnmatch import fnmatch
|
||||
from typing import BinaryIO, List, Optional
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def prepare_containerignore(anchor: str) -> List[str]:
|
||||
"""Return the list of patterns for filenames to exclude.
|
||||
|
||||
.containerignore takes precedence over .dockerignore.
|
||||
"""
|
||||
for filename in (".containerignore", ".dockerignore"):
|
||||
ignore = pathlib.Path(anchor) / filename
|
||||
if not ignore.exists():
|
||||
continue
|
||||
|
||||
with ignore.open(encoding='utf-8') as file:
|
||||
return list(
|
||||
filter(
|
||||
lambda l: l and not l.startswith("#"),
|
||||
(line.strip() for line in file.readlines()),
|
||||
)
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
def prepare_containerfile(anchor: str, dockerfile: str) -> str:
|
||||
"""Ensure that Containerfile, or a proxy Containerfile is in context_dir.
|
||||
|
||||
Args:
|
||||
anchor: Build context directory
|
||||
dockerfile: Path to Dockerfile/Containerfile
|
||||
|
||||
Returns:
|
||||
path to Dockerfile/Containerfile in root of context directory
|
||||
"""
|
||||
anchor_path = pathlib.Path(anchor)
|
||||
dockerfile_path = pathlib.Path(dockerfile)
|
||||
|
||||
if dockerfile_path.parent.samefile(anchor_path):
|
||||
return dockerfile_path.name
|
||||
|
||||
proxy_path = anchor_path / f".containerfile.{random.getrandbits(160):x}"
|
||||
shutil.copy2(dockerfile_path, proxy_path, follow_symlinks=False)
|
||||
return proxy_path.name
|
||||
|
||||
|
||||
def create_tar(
|
||||
anchor: str, name: str = None, exclude: List[str] = None, gzip: bool = False
|
||||
) -> BinaryIO:
|
||||
"""Create a tarfile from context_dir to send to Podman service.
|
||||
|
||||
Args:
|
||||
anchor: Directory to use as root of tar file.
|
||||
name: Name of tar file.
|
||||
exclude: List of patterns for files to exclude from tar file.
|
||||
gzip: When True, gzip compress tar file.
|
||||
"""
|
||||
|
||||
def add_filter(info: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
|
||||
"""Filter files targeted to be added to tarfile.
|
||||
|
||||
Args:
|
||||
info: Information on the file targeted to be added
|
||||
|
||||
Returns:
|
||||
None: if file is not to be added
|
||||
TarInfo: when file is to be added. Modified as needed.
|
||||
|
||||
Notes:
|
||||
exclude is captured from parent
|
||||
"""
|
||||
|
||||
if not (info.isfile() or info.isdir() or info.issym()):
|
||||
return None
|
||||
|
||||
if _exclude_matcher(info.name, exclude):
|
||||
return None
|
||||
|
||||
# Workaround https://bugs.python.org/issue32713. Fixed in Python 3.7
|
||||
if info.mtime < 0 or info.mtime > 8**11 - 1:
|
||||
info.mtime = int(info.mtime)
|
||||
|
||||
# do not leak client information to service
|
||||
info.uid = 0
|
||||
info.uname = info.gname = "root"
|
||||
|
||||
if sys.platform == "win32":
|
||||
info.mode = info.mode & 0o755 | 0o111
|
||||
|
||||
return info
|
||||
|
||||
if name is None:
|
||||
# pylint: disable=consider-using-with
|
||||
name = tempfile.NamedTemporaryFile(prefix="podman_context", suffix=".tar")
|
||||
else:
|
||||
name = pathlib.Path(name)
|
||||
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
else:
|
||||
exclude = exclude.copy()
|
||||
|
||||
# FIXME caller needs to add this...
|
||||
# exclude.append(".dockerignore")
|
||||
exclude.append(name.name)
|
||||
|
||||
mode = "w:gz" if gzip else "w"
|
||||
with tarfile.open(name.name, mode) as tar:
|
||||
tar.add(anchor, arcname="", recursive=True, filter=add_filter)
|
||||
|
||||
return open(name.name, "rb") # pylint: disable=consider-using-with
|
||||
|
||||
|
||||
def _exclude_matcher(path: str, exclude: List[str]) -> bool:
|
||||
"""Returns True if path matches an entry in exclude.
|
||||
|
||||
Note:
|
||||
FIXME Not compatible, support !, **, etc
|
||||
"""
|
||||
if not exclude:
|
||||
return False
|
||||
|
||||
for pattern in exclude:
|
||||
if fnmatch(path, pattern):
|
||||
return True
|
||||
return False
|
||||
3037
venv/lib/python3.11/site-packages/podman/api/typing_extensions.py
Normal file
3037
venv/lib/python3.11/site-packages/podman/api/typing_extensions.py
Normal file
File diff suppressed because it is too large
Load Diff
181
venv/lib/python3.11/site-packages/podman/api/uds.py
Normal file
181
venv/lib/python3.11/site-packages/podman/api/uds.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""Specialized Transport Adapter for UNIX domain sockets."""
|
||||
import collections
|
||||
import functools
|
||||
import http.client
|
||||
import logging
|
||||
import socket
|
||||
from typing import Optional, Union
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
except ImportError:
|
||||
from requests.packages import urllib3
|
||||
|
||||
from requests.adapters import DEFAULT_POOLBLOCK, DEFAULT_POOLSIZE, DEFAULT_RETRIES, HTTPAdapter
|
||||
|
||||
from ..errors import APIError
|
||||
|
||||
from .adapter_utils import _key_normalizer
|
||||
|
||||
logger = logging.getLogger("podman.uds_adapter")
|
||||
|
||||
|
||||
class UDSSocket(socket.socket):
|
||||
"""Specialization of socket.socket for a UNIX domain socket."""
|
||||
|
||||
def __init__(self, uds: str):
|
||||
"""Initialize UDSSocket.
|
||||
|
||||
Args:
|
||||
uds: Full address of a Podman service UNIX domain socket.
|
||||
|
||||
Examples:
|
||||
UDSSocket("http+unix:///run/podman/podman.sock")
|
||||
"""
|
||||
super().__init__(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.uds = uds
|
||||
|
||||
def connect(self, **kwargs): # pylint: disable=unused-argument
|
||||
"""Returns socket for UNIX domain socket."""
|
||||
netloc = unquote(urlparse(self.uds).netloc)
|
||||
try:
|
||||
super().connect(netloc)
|
||||
except Exception as e:
|
||||
raise APIError(f"Unable to make connection to UDS '{netloc}'") from e
|
||||
|
||||
|
||||
class UDSConnection(http.client.HTTPConnection):
|
||||
"""Specialization of HTTPConnection to use a UNIX domain sockets."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str,
|
||||
port: int,
|
||||
timeout: Union[float, urllib3.Timeout, None] = None,
|
||||
strict=False,
|
||||
**kwargs, # pylint: disable=unused-argument
|
||||
):
|
||||
"""Initialize connection to UNIX domain socket for HTTP client.
|
||||
|
||||
Args:
|
||||
host: Ignored.
|
||||
port: Ignored.
|
||||
timeout: Time to allow for operation.
|
||||
strict: Ignored.
|
||||
|
||||
Keyword Args:
|
||||
uds: Full address of a Podman service UNIX domain socket. Required.
|
||||
"""
|
||||
connection_kwargs = kwargs.copy()
|
||||
self.sock: Optional[socket.socket] = None
|
||||
|
||||
if timeout is not None:
|
||||
if isinstance(timeout, urllib3.Timeout):
|
||||
try:
|
||||
connection_kwargs["timeout"] = float(timeout.total)
|
||||
except TypeError:
|
||||
pass
|
||||
connection_kwargs["timeout"] = timeout
|
||||
|
||||
self.uds = connection_kwargs.pop("uds")
|
||||
super().__init__(host, **connection_kwargs)
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Connect to Podman service via UNIX domain socket."""
|
||||
sock = UDSSocket(self.uds)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect()
|
||||
self.sock = sock
|
||||
|
||||
|
||||
class UDSConnectionPool(urllib3.HTTPConnectionPool):
|
||||
"""Specialization of HTTPConnectionPool for holding UNIX domain sockets."""
|
||||
|
||||
ConnectionCls = UDSConnection
|
||||
|
||||
|
||||
class UDSPoolManager(urllib3.PoolManager):
|
||||
"""Specialized PoolManager for tracking UNIX domain socket connections."""
|
||||
|
||||
# pylint's special handling for namedtuple does not cover this usage
|
||||
# pylint: disable=invalid-name
|
||||
_PoolKey = collections.namedtuple(
|
||||
"_PoolKey", urllib3.poolmanager.PoolKey._fields + ("key_uds",)
|
||||
)
|
||||
|
||||
# Map supported schemes to Pool Classes
|
||||
_pool_classes_by_scheme = {
|
||||
"http": UDSConnectionPool,
|
||||
"http+ssh": UDSConnectionPool,
|
||||
}
|
||||
|
||||
# Map supported schemes to Pool Key index generator
|
||||
_key_fn_by_scheme = {
|
||||
"http": functools.partial(_key_normalizer, _PoolKey),
|
||||
"http+ssh": functools.partial(_key_normalizer, _PoolKey),
|
||||
}
|
||||
|
||||
def __init__(self, num_pools=10, headers=None, **kwargs):
|
||||
"""Initialize UDSPoolManager.
|
||||
|
||||
Args:
|
||||
num_pools: Number of UDS Connection pools to maintain.
|
||||
headers: Additional headers to add to operations.
|
||||
"""
|
||||
super().__init__(num_pools, headers, **kwargs)
|
||||
self.pool_classes_by_scheme = UDSPoolManager._pool_classes_by_scheme
|
||||
self.key_fn_by_scheme = UDSPoolManager._key_fn_by_scheme
|
||||
|
||||
|
||||
class UDSAdapter(HTTPAdapter):
|
||||
"""Specialization of requests transport adapter for UNIX domain sockets."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
uds: str,
|
||||
pool_connections=DEFAULT_POOLSIZE,
|
||||
pool_maxsize=DEFAULT_POOLSIZE,
|
||||
max_retries=DEFAULT_RETRIES,
|
||||
pool_block=DEFAULT_POOLBLOCK,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize UDSAdapter.
|
||||
|
||||
Args:
|
||||
uds: Full address of a Podman service UNIX domain socket.
|
||||
Format, http+unix:///run/podman/podman.sock
|
||||
max_retries: The maximum number of retries each connection should attempt.
|
||||
pool_block: Whether the connection pool should block for connections.
|
||||
pool_connections: The number of connection pools to cache.
|
||||
pool_maxsize: The maximum number of connections to save in the pool.
|
||||
|
||||
Keyword Args:
|
||||
timeout (float): Time in seconds to wait for response
|
||||
|
||||
Examples:
|
||||
requests.Session.mount(
|
||||
"http://", UDSAdapater("http+unix:///run/user/1000/podman/podman.sock"))
|
||||
"""
|
||||
self.poolmanager: Optional[UDSPoolManager] = None
|
||||
|
||||
self._pool_kwargs = {"uds": uds}
|
||||
|
||||
if "timeout" in kwargs:
|
||||
self._pool_kwargs["timeout"] = kwargs.get("timeout")
|
||||
|
||||
super().__init__(pool_connections, pool_maxsize, max_retries, pool_block)
|
||||
|
||||
def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **kwargs):
|
||||
"""Initialize UDS Pool Manager.
|
||||
|
||||
Args:
|
||||
connections: The number of urllib3 connection pools to cache.
|
||||
maxsize: The maximum number of connections to save in the pool.
|
||||
block: Block when no free connections are available.
|
||||
"""
|
||||
pool_kwargs = kwargs.copy()
|
||||
pool_kwargs.update(self._pool_kwargs)
|
||||
self.poolmanager = UDSPoolManager(
|
||||
num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs
|
||||
)
|
||||
225
venv/lib/python3.11/site-packages/podman/client.py
Normal file
225
venv/lib/python3.11/site-packages/podman/client.py
Normal file
@ -0,0 +1,225 @@
|
||||
"""Client for connecting to Podman service."""
|
||||
import logging
|
||||
import os
|
||||
from contextlib import AbstractContextManager
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import xdg.BaseDirectory
|
||||
|
||||
from podman.api import cached_property
|
||||
from podman.api.client import APIClient
|
||||
from podman.domain.config import PodmanConfig
|
||||
from podman.domain.containers_manager import ContainersManager
|
||||
from podman.domain.events import EventsManager
|
||||
from podman.domain.images_manager import ImagesManager
|
||||
from podman.domain.manifests import ManifestsManager
|
||||
from podman.domain.networks_manager import NetworksManager
|
||||
from podman.domain.pods_manager import PodsManager
|
||||
from podman.domain.secrets import SecretsManager
|
||||
from podman.domain.system import SystemManager
|
||||
from podman.domain.volumes import VolumesManager
|
||||
|
||||
logger = logging.getLogger("podman")
|
||||
|
||||
|
||||
class PodmanClient(AbstractContextManager):
|
||||
"""Client to connect to a Podman service.
|
||||
|
||||
Examples:
|
||||
|
||||
with PodmanClient(base_url="ssh://root@api.example:22/run/podman/podman.sock?secure=True",
|
||||
identity="~alice/.ssh/api_ed25519")
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
"""Initialize PodmanClient.
|
||||
|
||||
Keyword Args:
|
||||
base_url (str): Full URL to Podman service. See examples.
|
||||
version (str): API version to use. Default: auto, use version from server
|
||||
timeout (int): Timeout for API calls, in seconds.
|
||||
Default: socket._GLOBAL_DEFAULT_TIMEOUT.
|
||||
tls: Ignored. SSH connection configuration delegated to SSH Host configuration.
|
||||
user_agent (str): User agent for service connections. Default: PodmanPy/<Code Version>
|
||||
credstore_env (Mapping[str, str]): Dict containing environment for credential store
|
||||
use_ssh_client (True): Always shell out to SSH client for
|
||||
SSH Podman service connections.
|
||||
max_pool_size (int): Number of connections to save in pool
|
||||
connection (str): Identifier of connection to use from
|
||||
XDG_CONFIG_HOME/containers/containers.conf
|
||||
identity (str): Provide SSH key to authenticate SSH connection.
|
||||
|
||||
Examples:
|
||||
base_url:
|
||||
|
||||
- http+ssh://<user>@<host>[:port]</run/podman/podman.sock>[?secure=True]
|
||||
- http+unix://</run/podman/podman.sock>
|
||||
- tcp://<localhost>[:<port>]
|
||||
"""
|
||||
super().__init__()
|
||||
config = PodmanConfig()
|
||||
|
||||
api_kwargs = kwargs.copy()
|
||||
|
||||
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
|
||||
api_kwargs["identity"] = kwargs.get("identity", str(connection.identity))
|
||||
elif "base_url" not in api_kwargs:
|
||||
path = str(
|
||||
Path(xdg.BaseDirectory.get_runtime_dir(strict=False)) / "podman" / "podman.sock"
|
||||
)
|
||||
api_kwargs["base_url"] = "http+unix://" + path
|
||||
self.api = APIClient(**api_kwargs)
|
||||
|
||||
def __enter__(self) -> "PodmanClient":
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||
self.close()
|
||||
|
||||
@classmethod
|
||||
def from_env(
|
||||
cls,
|
||||
version: str = "auto",
|
||||
timeout: Optional[int] = None,
|
||||
max_pool_size: Optional[int] = None,
|
||||
ssl_version: Optional[int] = None, # pylint: disable=unused-argument
|
||||
assert_hostname: bool = False, # pylint: disable=unused-argument
|
||||
environment: Optional[Dict[str, str]] = None,
|
||||
credstore_env: Optional[Dict[str, str]] = None,
|
||||
use_ssh_client: bool = True, # pylint: disable=unused-argument
|
||||
) -> "PodmanClient":
|
||||
"""Returns connection to service using environment variables and parameters.
|
||||
|
||||
Environment variables:
|
||||
|
||||
- DOCKER_HOST, CONTAINER_HOST: URL to Podman service
|
||||
- DOCKER_TLS_VERIFY, CONTAINER_TLS_VERIFY: Verify host against CA certificate
|
||||
- DOCKER_CERT_PATH, CONTAINER_CERT_PATH: Path to TLS certificates for host connection
|
||||
|
||||
Args:
|
||||
version: API version to use. Default: auto, use version from server
|
||||
timeout: Timeout for API calls, in seconds.
|
||||
max_pool_size: Number of connections to save in pool.
|
||||
ssl_version: SSH configuration delegated to SSH client configuration. Ignored.
|
||||
assert_hostname: Ignored.
|
||||
environment: Dict containing input environment. Default: os.environ
|
||||
credstore_env: Dict containing environment for credential store
|
||||
use_ssh_client: Use system ssh client rather than ssh module. Always, True.
|
||||
|
||||
Returns:
|
||||
Client used to communicate with a Podman service.
|
||||
"""
|
||||
environment = environment or os.environ
|
||||
credstore_env = credstore_env or {}
|
||||
|
||||
if version == "auto":
|
||||
version = None
|
||||
|
||||
host = environment.get("CONTAINER_HOST") or environment.get("DOCKER_HOST") or None
|
||||
|
||||
return PodmanClient(
|
||||
base_url=host,
|
||||
version=version,
|
||||
timeout=timeout,
|
||||
tls=False,
|
||||
credstore_env=credstore_env,
|
||||
max_pool_size=max_pool_size,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def containers(self) -> ContainersManager:
|
||||
"""Returns Manager for operations on containers stored by a Podman service."""
|
||||
return ContainersManager(client=self.api)
|
||||
|
||||
@cached_property
|
||||
def images(self) -> ImagesManager:
|
||||
"""Returns Manager for operations on images stored by a Podman service."""
|
||||
return ImagesManager(client=self.api)
|
||||
|
||||
@cached_property
|
||||
def manifests(self) -> ManifestsManager:
|
||||
"""Returns Manager for operations on manifests maintained by a Podman service."""
|
||||
return ManifestsManager(client=self.api)
|
||||
|
||||
@cached_property
|
||||
def networks(self) -> NetworksManager:
|
||||
"""Returns Manager for operations on networks maintained by a Podman service."""
|
||||
return NetworksManager(client=self.api)
|
||||
|
||||
@cached_property
|
||||
def volumes(self) -> VolumesManager:
|
||||
"""Returns Manager for operations on volumes maintained by a Podman service."""
|
||||
return VolumesManager(client=self.api)
|
||||
|
||||
@cached_property
|
||||
def pods(self) -> PodsManager:
|
||||
"""Returns Manager for operations on pods maintained by a Podman service."""
|
||||
return PodsManager(client=self.api)
|
||||
|
||||
@cached_property
|
||||
def secrets(self):
|
||||
"""Returns Manager for operations on secrets maintained by a Podman service."""
|
||||
return SecretsManager(client=self.api)
|
||||
|
||||
@cached_property
|
||||
def system(self):
|
||||
return SystemManager(client=self.api)
|
||||
|
||||
def df(self) -> Dict[str, Any]: # pylint: disable=missing-function-docstring,invalid-name
|
||||
return self.system.df()
|
||||
|
||||
df.__doc__ = SystemManager.df.__doc__
|
||||
|
||||
def events(self, *args, **kwargs): # pylint: disable=missing-function-docstring
|
||||
return EventsManager(client=self.api).list(*args, **kwargs)
|
||||
|
||||
events.__doc__ = EventsManager.list.__doc__
|
||||
|
||||
def info(self, *args, **kwargs): # pylint: disable=missing-function-docstring
|
||||
return self.system.info(*args, **kwargs)
|
||||
|
||||
info.__doc__ = SystemManager.info.__doc__
|
||||
|
||||
def login(self, *args, **kwargs): # pylint: disable=missing-function-docstring
|
||||
return self.system.login(*args, **kwargs)
|
||||
|
||||
login.__doc__ = SystemManager.login.__doc__
|
||||
|
||||
def ping(self) -> bool: # pylint: disable=missing-function-docstring
|
||||
return self.system.ping()
|
||||
|
||||
ping.__doc__ = SystemManager.ping.__doc__
|
||||
|
||||
def version(self, *args, **kwargs): # pylint: disable=missing-function-docstring
|
||||
_ = args
|
||||
return self.system.version(**kwargs)
|
||||
|
||||
version.__doc__ = SystemManager.version.__doc__
|
||||
|
||||
def close(self):
|
||||
"""Release PodmanClient Resources."""
|
||||
return self.api.close()
|
||||
|
||||
@property
|
||||
def swarm(self):
|
||||
"""Swarm not supported.
|
||||
|
||||
Raises:
|
||||
NotImplemented: Swarm not supported by Podman service
|
||||
"""
|
||||
raise NotImplementedError("Swarm operations are not supported by Podman service.")
|
||||
|
||||
# Aliases to cover all swarm methods
|
||||
services = swarm
|
||||
configs = swarm
|
||||
nodes = swarm
|
||||
|
||||
|
||||
# Aliases to minimize effort to port to PodmanPy
|
||||
DockerClient = PodmanClient
|
||||
from_env = PodmanClient.from_env
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
113
venv/lib/python3.11/site-packages/podman/domain/config.py
Normal file
113
venv/lib/python3.11/site-packages/podman/domain/config.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Read containers.conf file."""
|
||||
import urllib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import xdg.BaseDirectory
|
||||
|
||||
try:
|
||||
import toml
|
||||
except ImportError:
|
||||
import pytoml as toml
|
||||
|
||||
from podman.api import cached_property
|
||||
|
||||
|
||||
class ServiceConnection:
|
||||
"""ServiceConnection defines a connection to the Podman service."""
|
||||
|
||||
def __init__(self, name: str, attrs: Dict[str, str]):
|
||||
"""Create a Podman ServiceConnection."""
|
||||
self.name = name
|
||||
self.attrs = attrs
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__}: '{self.id}'>"""
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(tuple(self.name))
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, ServiceConnection):
|
||||
return self.id == other.id and self.attrs == other.attrs
|
||||
return False
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""str: Returns identifier for service connection."""
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
def url(self):
|
||||
"""urllib.parse.ParseResult: Returns URL for service connection."""
|
||||
return urllib.parse.urlparse(self.attrs.get("uri"))
|
||||
|
||||
@cached_property
|
||||
def identity(self):
|
||||
"""Path: Returns Path to identity file for service connection."""
|
||||
return Path(self.attrs.get("identity"))
|
||||
|
||||
|
||||
class PodmanConfig:
|
||||
"""PodmanConfig provides a representation of the containers.conf file."""
|
||||
|
||||
def __init__(self, path: Optional[str] = None):
|
||||
"""Read Podman configuration from users XDG_CONFIG_HOME."""
|
||||
|
||||
if path is None:
|
||||
home = Path(xdg.BaseDirectory.xdg_config_home)
|
||||
self.path = home / "containers" / "containers.conf"
|
||||
else:
|
||||
self.path = Path(path)
|
||||
|
||||
self.attrs = {}
|
||||
if self.path.exists():
|
||||
with self.path.open(encoding='utf-8') as file:
|
||||
buffer = file.read()
|
||||
self.attrs = toml.loads(buffer)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(tuple(self.path.name))
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
if isinstance(other, PodmanConfig):
|
||||
return self.id == other.id and self.attrs == other.attrs
|
||||
return False
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""Path: Returns Path() of container.conf."""
|
||||
return self.path
|
||||
|
||||
@cached_property
|
||||
def services(self):
|
||||
"""Dict[str, ServiceConnection]: Returns list of service connections.
|
||||
|
||||
Examples:
|
||||
podman_config = PodmanConfig()
|
||||
address = podman_config.services["testing"]
|
||||
print(f"Testing service address {address}")
|
||||
"""
|
||||
services: Dict[str, ServiceConnection] = {}
|
||||
|
||||
engine = self.attrs.get("engine")
|
||||
if engine:
|
||||
destinations = engine.get("service_destinations")
|
||||
for key in destinations:
|
||||
connection = ServiceConnection(key, attrs=destinations[key])
|
||||
services[key] = connection
|
||||
|
||||
return services
|
||||
|
||||
@cached_property
|
||||
def active_service(self):
|
||||
"""Optional[ServiceConnection]: Returns active connection."""
|
||||
|
||||
engine = self.attrs.get("engine")
|
||||
if engine:
|
||||
active = engine.get("active_service")
|
||||
destinations = engine.get("service_destinations")
|
||||
for key in destinations:
|
||||
if key == active:
|
||||
return ServiceConnection(key, attrs=destinations[key])
|
||||
return None
|
||||
534
venv/lib/python3.11/site-packages/podman/domain/containers.py
Normal file
534
venv/lib/python3.11/site-packages/podman/domain/containers.py
Normal file
@ -0,0 +1,534 @@
|
||||
"""Model and Manager for Container resources."""
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import shlex
|
||||
from contextlib import suppress
|
||||
from typing import Any, Dict, Iterable, Iterator, List, Mapping, Optional, Sequence, Tuple, Union
|
||||
|
||||
import requests
|
||||
from requests import Response
|
||||
|
||||
from podman import api
|
||||
from podman.api import Literal
|
||||
from podman.domain.images import Image
|
||||
from podman.domain.images_manager import ImagesManager
|
||||
from podman.domain.manager import PodmanResource
|
||||
from podman.errors import APIError
|
||||
|
||||
logger = logging.getLogger("podman.containers")
|
||||
|
||||
|
||||
class Container(PodmanResource):
|
||||
"""Details and configuration for a container managed by the Podman service."""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""str: Returns container's name."""
|
||||
with suppress(KeyError):
|
||||
if 'Name' in self.attrs:
|
||||
return self.attrs["Name"].lstrip("/")
|
||||
return self.attrs["Names"][0].lstrip("/")
|
||||
return None
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""podman.domain.images.Image: Returns Image object used to create Container."""
|
||||
if "Image" in self.attrs:
|
||||
image_id = self.attrs["Image"]
|
||||
|
||||
return ImagesManager(client=self.client).get(image_id)
|
||||
return Image()
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
"""dict[str, str]: Returns labels associated with container."""
|
||||
with suppress(KeyError):
|
||||
if "Labels" in self.attrs:
|
||||
return self.attrs["Labels"]
|
||||
return self.attrs["Config"]["Labels"]
|
||||
return {}
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Literal["running", "stopped", "exited", "unknown"]: Returns status of container."""
|
||||
with suppress(KeyError):
|
||||
return self.attrs["State"]["Status"]
|
||||
return "unknown"
|
||||
|
||||
@property
|
||||
def ports(self):
|
||||
"""dict[str, int]: Return ports exposed by container."""
|
||||
with suppress(KeyError):
|
||||
return self.attrs["NetworkSettings"]["Ports"]
|
||||
return {}
|
||||
|
||||
def attach(self, **kwargs) -> Union[str, Iterator[str]]:
|
||||
"""Attach to container's tty.
|
||||
|
||||
Keyword Args:
|
||||
stdout (bool): Include stdout. Default: True
|
||||
stderr (bool): Include stderr. Default: True
|
||||
stream (bool): Return iterator of string(s) vs single string. Default: False
|
||||
logs (bool): Include previous container output. Default: False
|
||||
|
||||
Raises:
|
||||
NotImplementedError: method not implemented.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def attach_socket(self, **kwargs):
|
||||
"""Not Implemented.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: method not implemented.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def commit(self, repository: str = None, tag: str = None, **kwargs) -> Image:
|
||||
"""Save container to given repository.
|
||||
|
||||
Args:
|
||||
repository: Where to save Image
|
||||
tag: Tag to push with Image
|
||||
|
||||
Keyword Args:
|
||||
author (str): Name of commit author
|
||||
changes (List[str]): Instructions to apply during commit
|
||||
comment (str): Commit message to include with Image, overrides keyword message
|
||||
conf (dict[str, Any]): Ignored.
|
||||
format (str): Format of the image manifest and metadata
|
||||
message (str): Commit message to include with Image
|
||||
pause (bool): Pause the container before committing it
|
||||
"""
|
||||
params = {
|
||||
"author": kwargs.get("author"),
|
||||
"changes": kwargs.get("changes"),
|
||||
"comment": kwargs.get("comment", kwargs.get("message")),
|
||||
"container": self.id,
|
||||
"format": kwargs.get("format"),
|
||||
"pause": kwargs.get("pause"),
|
||||
"repo": repository,
|
||||
"tag": tag,
|
||||
}
|
||||
response = self.client.post("/commit", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.json()
|
||||
return ImagesManager(client=self.client).get(body["Id"])
|
||||
|
||||
def diff(self) -> List[Dict[str, int]]:
|
||||
"""Report changes of a container's filesystem.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.get(f"/containers/{self.id}/changes")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
# pylint: disable=too-many-arguments,unused-argument
|
||||
def exec_run(
|
||||
self,
|
||||
cmd: Union[str, List[str]],
|
||||
stdout: bool = True,
|
||||
stderr: bool = True,
|
||||
stdin: bool = False,
|
||||
tty: bool = True,
|
||||
privileged: bool = False,
|
||||
user=None,
|
||||
detach: bool = False,
|
||||
stream: bool = False,
|
||||
socket: bool = False,
|
||||
environment: Union[Mapping[str, str], List[str]] = None,
|
||||
workdir: str = None,
|
||||
demux: bool = False,
|
||||
) -> Tuple[Optional[int], Union[Iterator[bytes], Any, Tuple[bytes, bytes]]]:
|
||||
"""Run given command inside container and return results.
|
||||
|
||||
Args:
|
||||
cmd: Command to be executed
|
||||
stdout: Attach to stdout. Default: True
|
||||
stderr: Attach to stderr. Default: True
|
||||
stdin: Attach to stdin. Default: False
|
||||
tty: Allocate a pseudo-TTY. Default: False
|
||||
privileged: Run as privileged.
|
||||
user: User to execute command as. Default: root
|
||||
detach: If true, detach from the exec command.
|
||||
Default: False
|
||||
stream: Stream response data. Default: False
|
||||
socket: Return the connection socket to allow custom
|
||||
read/write operations. Default: False
|
||||
environment: A dictionary or a List[str] in
|
||||
the following format ["PASSWORD=xxx"] or
|
||||
{"PASSWORD": "xxx"}.
|
||||
workdir: Path to working directory for this exec session
|
||||
demux: Return stdout and stderr separately
|
||||
|
||||
Returns:
|
||||
First item is the command response code
|
||||
Second item is the requests response content
|
||||
|
||||
Raises:
|
||||
NotImplementedError: method not implemented.
|
||||
APIError: when service reports error
|
||||
"""
|
||||
# pylint: disable-msg=too-many-locals
|
||||
user = user or "root"
|
||||
if isinstance(environment, dict):
|
||||
environment = [f"{k}={v}" for k, v in environment.items()]
|
||||
data = {
|
||||
"AttachStderr": stderr,
|
||||
"AttachStdin": stdin,
|
||||
"AttachStdout": stdout,
|
||||
"Cmd": cmd if isinstance(cmd, list) else shlex.split(cmd),
|
||||
# "DetachKeys": detach, # This is something else
|
||||
"Env": environment,
|
||||
"Privileged": privileged,
|
||||
"Tty": tty,
|
||||
"User": user,
|
||||
"WorkingDir": workdir,
|
||||
}
|
||||
# create the exec instance
|
||||
response = self.client.post(f"/containers/{self.name}/exec", data=json.dumps(data))
|
||||
response.raise_for_status()
|
||||
exec_id = response.json()['Id']
|
||||
# start the exec instance, this will store command output
|
||||
start_resp = self.client.post(
|
||||
f"/exec/{exec_id}/start", data=json.dumps({"Detach": detach, "Tty": tty})
|
||||
)
|
||||
start_resp.raise_for_status()
|
||||
# get and return exec information
|
||||
response = self.client.get(f"/exec/{exec_id}/json")
|
||||
response.raise_for_status()
|
||||
return response.json().get('ExitCode'), start_resp.content
|
||||
|
||||
def export(self, chunk_size: int = api.DEFAULT_CHUNK_SIZE) -> Iterator[bytes]:
|
||||
"""Download container's filesystem contents as a tar archive.
|
||||
|
||||
Args:
|
||||
chunk_size: <= number of bytes to return for each iteration of the generator.
|
||||
|
||||
Yields:
|
||||
tarball in size/chunk_size chunks
|
||||
|
||||
Raises:
|
||||
NotFound: when container has been removed from service
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.get(f"/containers/{self.id}/export", stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
for out in response.iter_content(chunk_size=chunk_size):
|
||||
yield out
|
||||
|
||||
def get_archive(
|
||||
self, path: str, chunk_size: int = api.DEFAULT_CHUNK_SIZE
|
||||
) -> Tuple[Iterable, Dict[str, Any]]:
|
||||
"""Download a file or folder from the container's filesystem.
|
||||
|
||||
Args:
|
||||
path: Path to file or folder.
|
||||
chunk_size: <= number of bytes to return for each iteration of the generator.
|
||||
|
||||
Returns:
|
||||
First item is a raw tar data stream.
|
||||
Second item is a dict containing os.stat() information on the specified path.
|
||||
"""
|
||||
response = self.client.get(f"/containers/{self.id}/archive", params={"path": [path]})
|
||||
response.raise_for_status()
|
||||
|
||||
stat = response.headers.get("x-docker-container-path-stat", None)
|
||||
stat = api.decode_header(stat)
|
||||
return response.iter_content(chunk_size=chunk_size), stat
|
||||
|
||||
def inspect(self) -> Dict:
|
||||
"""Inspect a container.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.get(f"/containers/{self.id}/json")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def kill(self, signal: Union[str, int, None] = None) -> None:
|
||||
"""Send signal to container.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.post(f"/containers/{self.id}/kill", params={"signal": signal})
|
||||
response.raise_for_status()
|
||||
|
||||
def logs(self, **kwargs) -> Union[bytes, Iterator[bytes]]:
|
||||
"""Get logs from the container.
|
||||
|
||||
Keyword Args:
|
||||
stdout (bool): Include stdout. Default: True
|
||||
stderr (bool): Include stderr. Default: True
|
||||
stream (bool): Return generator of strings as the response. Default: False
|
||||
timestamps (bool): Show timestamps in output. Default: False
|
||||
tail (Union[str, int]): Output specified number of lines at the end of
|
||||
logs. Integer representing the number of lines to display, or the string all.
|
||||
Default: all
|
||||
since (Union[datetime, int]): Show logs since a given datetime or
|
||||
integer epoch (in seconds)
|
||||
follow (bool): Follow log output. Default: False
|
||||
until (Union[datetime, int]): Show logs that occurred before the given
|
||||
datetime or integer epoch (in seconds)
|
||||
"""
|
||||
stream = bool(kwargs.get("stream", False))
|
||||
params = {
|
||||
"follow": kwargs.get("follow", kwargs.get("stream", None)),
|
||||
"since": api.prepare_timestamp(kwargs.get("since")),
|
||||
"stderr": kwargs.get("stderr", None),
|
||||
"stdout": kwargs.get("stdout", True),
|
||||
"tail": kwargs.get("tail"),
|
||||
"timestamps": kwargs.get("timestamps"),
|
||||
"until": api.prepare_timestamp(kwargs.get("until")),
|
||||
}
|
||||
|
||||
response = self.client.get(f"/containers/{self.id}/logs", stream=stream, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
if stream:
|
||||
return api.stream_frames(response)
|
||||
return api.frames(response)
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause processes within the container."""
|
||||
response = self.client.post(f"/containers/{self.id}/pause")
|
||||
response.raise_for_status()
|
||||
|
||||
def put_archive(self, path: str, data: bytes = None) -> bool:
|
||||
"""Upload tar archive containing a file or folder to be written into container.
|
||||
|
||||
Args:
|
||||
path: File to write data into
|
||||
data: Contents to write to file, when None path will be read on client to
|
||||
build tarfile.
|
||||
|
||||
Returns:
|
||||
True when successful
|
||||
|
||||
Raises:
|
||||
APIError: when server reports error
|
||||
"""
|
||||
if path is None:
|
||||
raise ValueError("'path' is a required argument.")
|
||||
|
||||
if data is None:
|
||||
data = api.create_tar("/", path)
|
||||
|
||||
response = self.client.put(
|
||||
f"/containers/{self.id}/archive", params={"path": path}, data=data
|
||||
)
|
||||
return response.ok
|
||||
|
||||
def remove(self, **kwargs) -> None:
|
||||
"""Delete container.
|
||||
|
||||
Keyword Args:
|
||||
v (bool): Delete associated volumes as well.
|
||||
link (bool): Ignored.
|
||||
force (bool): Kill a running container before deleting.
|
||||
"""
|
||||
self.manager.remove(self.id, **kwargs)
|
||||
|
||||
def rename(self, name: str) -> None:
|
||||
"""Rename container.
|
||||
|
||||
Container updated in-situ to avoid reload().
|
||||
|
||||
Args:
|
||||
name: New name for container.
|
||||
"""
|
||||
if not name:
|
||||
raise ValueError("'name' is a required argument.")
|
||||
|
||||
response = self.client.post(f"/containers/{self.id}/rename", params={"name": name})
|
||||
response.raise_for_status()
|
||||
|
||||
self.attrs["Name"] = name # shortcut to avoid needing reload()
|
||||
|
||||
def resize(self, height: int = None, width: int = None) -> None:
|
||||
"""Resize the tty session.
|
||||
|
||||
Args:
|
||||
height: New height of tty session.
|
||||
width: New width of tty session.
|
||||
"""
|
||||
params = {
|
||||
"h": height,
|
||||
"w": width,
|
||||
}
|
||||
response = self.client.post(f"/containers/{self.id}/resize", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
def restart(self, **kwargs) -> None:
|
||||
"""Restart processes in container.
|
||||
|
||||
Keyword Args:
|
||||
timeout (int): Seconds to wait for container to stop before killing container.
|
||||
"""
|
||||
params = {"timeout": kwargs.get("timeout")}
|
||||
post_kwargs = {}
|
||||
if kwargs.get("timeout"):
|
||||
post_kwargs["timeout"] = float(params["timeout"]) * 1.5
|
||||
|
||||
response = self.client.post(f"/containers/{self.id}/restart", params=params, **post_kwargs)
|
||||
response.raise_for_status()
|
||||
|
||||
def start(self, **kwargs) -> None:
|
||||
"""Start processes in container.
|
||||
|
||||
Keyword Args:
|
||||
detach_keys: Override the key sequence for detaching a container (Podman only)
|
||||
"""
|
||||
response = self.client.post(
|
||||
f"/containers/{self.id}/start", params={"detachKeys": kwargs.get("detach_keys")}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def stats(self, **kwargs) -> Union[Sequence[Dict[str, bytes]], bytes]:
|
||||
"""Return statistics for container.
|
||||
|
||||
Keyword Args:
|
||||
decode (bool): If True and stream is True, stream will be decoded into dict's.
|
||||
Default: False.
|
||||
stream (bool): Stream statistics until cancelled. Default: True.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
# FIXME Errors in stream are not handled, need content and json to read Errors.
|
||||
stream = kwargs.get("stream", True)
|
||||
decode = kwargs.get("decode", False)
|
||||
|
||||
params = {
|
||||
"containers": self.id,
|
||||
"stream": stream,
|
||||
}
|
||||
|
||||
response = self.client.get("/containers/stats", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
if stream:
|
||||
return self._stats_helper(decode, response.iter_lines())
|
||||
|
||||
with io.StringIO() as buffer:
|
||||
for entry in response.text:
|
||||
buffer.write(json.dumps(entry) + "\n")
|
||||
return buffer.getvalue()
|
||||
|
||||
@staticmethod
|
||||
def _stats_helper(
|
||||
decode: bool, body: List[Dict[str, Any]]
|
||||
) -> Iterator[Union[str, Dict[str, Any]]]:
|
||||
"""Helper needed to allow stats() to return either a generator or a str."""
|
||||
for entry in body:
|
||||
if decode:
|
||||
yield json.loads(entry)
|
||||
else:
|
||||
yield entry
|
||||
|
||||
def stop(self, **kwargs) -> None:
|
||||
"""Stop container.
|
||||
|
||||
Keyword Args:
|
||||
all (bool): When True, stop all containers. Default: False (Podman only)
|
||||
ignore (bool): When True, ignore error if container already stopped (Podman only)
|
||||
timeout (int): Number of seconds to wait on container to stop before killing it.
|
||||
"""
|
||||
params = {"all": kwargs.get("all"), "timeout": kwargs.get("timeout")}
|
||||
|
||||
post_kwargs = {}
|
||||
if kwargs.get("timeout"):
|
||||
post_kwargs["timeout"] = float(params["timeout"]) * 1.5
|
||||
|
||||
response = self.client.post(f"/containers/{self.id}/stop", params=params, **post_kwargs)
|
||||
response.raise_for_status()
|
||||
|
||||
if response.status_code == requests.codes.no_content:
|
||||
return
|
||||
|
||||
if response.status_code == requests.codes.not_modified:
|
||||
if kwargs.get("ignore", False):
|
||||
return
|
||||
|
||||
body = response.json()
|
||||
raise APIError(body["cause"], response=response, explanation=body["message"])
|
||||
|
||||
def top(self, **kwargs) -> Union[Iterator[Dict[str, Any]], Dict[str, Any]]:
|
||||
"""Report on running processes in the container.
|
||||
|
||||
Keyword Args:
|
||||
ps_args (str): When given, arguments will be passed to ps
|
||||
stream (bool): When True, repeatedly return results. Default: False
|
||||
|
||||
Raises:
|
||||
NotFound: when the container no longer exists
|
||||
APIError: when the service reports an error
|
||||
"""
|
||||
params = {
|
||||
"ps_args": kwargs.get("ps_args"),
|
||||
"stream": kwargs.get("stream", False),
|
||||
}
|
||||
response = self.client.get(f"/containers/{self.id}/top", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
if params["stream"]:
|
||||
self._top_helper(response)
|
||||
|
||||
return response.json()
|
||||
|
||||
@staticmethod
|
||||
def _top_helper(response: Response) -> Iterator[Dict[str, Any]]:
|
||||
for line in response.iter_lines():
|
||||
yield line
|
||||
|
||||
def unpause(self) -> None:
|
||||
"""Unpause processes in container."""
|
||||
response = self.client.post(f"/containers/{self.id}/unpause")
|
||||
response.raise_for_status()
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Update resource configuration of the containers.
|
||||
|
||||
Raises:
|
||||
NotImplementedError: Podman service unsupported operation.
|
||||
"""
|
||||
raise NotImplementedError("Container.update() is not supported by Podman service.")
|
||||
|
||||
def wait(self, **kwargs) -> Dict[Literal["StatusCode", "Error"], Any]:
|
||||
"""Block until the container enters given state.
|
||||
|
||||
Keyword Args:
|
||||
condition (Union[str, List[str]]): Container state on which to release.
|
||||
One or more of: "configured", "created", "running", "stopped",
|
||||
"paused", "exited", "removing", "stopping".
|
||||
interval (int): Time interval to wait before polling for completion.
|
||||
|
||||
Returns:
|
||||
"Error" key has a dictionary value with the key "Message".
|
||||
|
||||
Raises:
|
||||
NotFound: when Container not found
|
||||
ReadTimeoutError: when timeout is exceeded
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
condition = kwargs.get("condition")
|
||||
if isinstance(condition, str):
|
||||
condition = [condition]
|
||||
|
||||
interval = kwargs.get("interval")
|
||||
|
||||
params = {}
|
||||
if condition != []:
|
||||
params["condition"] = condition
|
||||
if interval != "":
|
||||
params["interval"] = interval
|
||||
response = self.client.post(f"/containers/{self.id}/wait", params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@ -0,0 +1,602 @@
|
||||
"""Mixin to provide Container create() method."""
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
from contextlib import suppress
|
||||
from typing import Any, Dict, List, MutableMapping, Union
|
||||
|
||||
from podman import api
|
||||
from podman.domain.containers import Container
|
||||
from podman.domain.images import Image
|
||||
from podman.domain.pods import Pod
|
||||
from podman.errors import ImageNotFound
|
||||
|
||||
logger = logging.getLogger("podman.containers")
|
||||
|
||||
|
||||
class CreateMixin: # pylint: disable=too-few-public-methods
|
||||
"""Class providing create method for ContainersManager."""
|
||||
|
||||
def create(
|
||||
self, image: Union[Image, str], command: Union[str, List[str], None] = None, **kwargs
|
||||
) -> Container:
|
||||
"""Create a container.
|
||||
|
||||
Args:
|
||||
image: Image to run.
|
||||
command: Command to run in the container.
|
||||
|
||||
Keyword Args:
|
||||
auto_remove (bool): Enable auto-removal of the container on daemon side when the
|
||||
container's process exits.
|
||||
blkio_weight_device (Dict[str, Any]): Block IO weight (relative device weight)
|
||||
in the form of: [{"Path": "device_path", "Weight": weight}].
|
||||
blkio_weight (int): Block IO weight (relative weight), accepts a weight value
|
||||
between 10 and 1000.
|
||||
cap_add (List[str]): Add kernel capabilities. For example: ["SYS_ADMIN", "MKNOD"]
|
||||
cap_drop (List[str]): Drop kernel capabilities.
|
||||
cgroup_parent (str): Override the default parent cgroup.
|
||||
cpu_count (int): Number of usable CPUs (Windows only).
|
||||
cpu_percent (int): Usable percentage of the available CPUs (Windows only).
|
||||
cpu_period (int): The length of a CPU period in microseconds.
|
||||
cpu_quota (int): Microseconds of CPU time that the container can get in a CPU period.
|
||||
cpu_rt_period (int): Limit CPU real-time period in microseconds.
|
||||
cpu_rt_runtime (int): Limit CPU real-time runtime in microseconds.
|
||||
cpu_shares (int): CPU shares (relative weight).
|
||||
cpuset_cpus (str): CPUs in which to allow execution (0-3, 0,1).
|
||||
cpuset_mems (str): Memory nodes (MEMs) in which to allow execution (0-3, 0,1).
|
||||
Only effective on NUMA systems.
|
||||
detach (bool): Run container in the background and return a Container object.
|
||||
device_cgroup_rules (List[str]): A list of cgroup rules to apply to the container.
|
||||
device_read_bps: Limit read rate (bytes per second) from a device in the form of:
|
||||
`[{"Path": "device_path", "Rate": rate}]`
|
||||
device_read_iops: Limit read rate (IO per second) from a device.
|
||||
device_write_bps: Limit write rate (bytes per second) from a device.
|
||||
device_write_iops: Limit write rate (IO per second) from a device.
|
||||
devices (List[str]): Expose host devices to the container, as a List[str] in the form
|
||||
<path_on_host>:<path_in_container>:<cgroup_permissions>.
|
||||
|
||||
For example:
|
||||
/dev/sda:/dev/xvda:rwm allows the container to have read-write access to the
|
||||
host's /dev/sda via a node named /dev/xvda inside the container.
|
||||
|
||||
dns (List[str]): Set custom DNS servers.
|
||||
dns_opt (List[str]): Additional options to be added to the container's resolv.conf file.
|
||||
dns_search (List[str]): DNS search domains.
|
||||
domainname (Union[str, List[str]]): Set custom DNS search domains.
|
||||
entrypoint (Union[str, List[str]]): The entrypoint for the container.
|
||||
environment (Union[Dict[str, str], List[str]): Environment variables to set inside
|
||||
the container, as a dictionary or a List[str] in the format
|
||||
["SOMEVARIABLE=xxx", "SOMEOTHERVARIABLE=xyz"].
|
||||
extra_hosts (Dict[str, str]): Additional hostnames to resolve inside the container,
|
||||
as a mapping of hostname to IP address.
|
||||
group_add (List[str]): List of additional group names and/or IDs that the container
|
||||
process will run as.
|
||||
healthcheck (Dict[str,Any]): Specify a test to perform to check that the
|
||||
container is healthy.
|
||||
health_check_on_failure_action (int): Specify an action if a healthcheck fails.
|
||||
hostname (str): Optional hostname for the container.
|
||||
init (bool): Run an init inside the container that forwards signals and reaps processes
|
||||
init_path (str): Path to the docker-init binary
|
||||
ipc_mode (str): Set the IPC mode for the container.
|
||||
isolation (str): Isolation technology to use. Default: `None`.
|
||||
kernel_memory (int or str): Kernel memory limit
|
||||
labels (Union[Dict[str, str], List[str]): A dictionary of name-value labels (e.g.
|
||||
{"label1": "value1", "label2": "value2"}) or a list of names of labels to set
|
||||
with empty values (e.g. ["label1", "label2"])
|
||||
links (Optional[Dict[str, str]]): Mapping of links using the {'container': 'alias'}
|
||||
format. The alias is optional. Containers declared in this dict will be linked to
|
||||
the new container using the provided alias. Default: None.
|
||||
log_config (LogConfig): Logging configuration.
|
||||
lxc_config (Dict[str, str]): LXC config.
|
||||
mac_address (str): MAC address to assign to the container.
|
||||
mem_limit (Union[int, str]): Memory limit. Accepts float values (which represent the
|
||||
memory limit of the created container in bytes) or a string with a units
|
||||
identification char (100000b, 1000k, 128m, 1g). If a string is specified without
|
||||
a units character, bytes are assumed as an intended unit.
|
||||
mem_reservation (Union[int, str]): Memory soft limit.
|
||||
mem_swappiness (int): Tune a container's memory swappiness behavior. Accepts number
|
||||
between 0 and 100.
|
||||
memswap_limit (Union[int, str]): Maximum amount of memory + swap a container is allowed
|
||||
to consume.
|
||||
mounts (List[Mount]): Specification for mounts to be added to the container. More
|
||||
powerful alternative to volumes. Each item in the list is expected to be a
|
||||
Mount object.
|
||||
For example :
|
||||
[
|
||||
{
|
||||
"type": "bind",
|
||||
"source": "/a/b/c1",
|
||||
"target" "/d1",
|
||||
"read_only": True,
|
||||
"relabel": "Z"
|
||||
},
|
||||
{
|
||||
"type": "tmpfs",
|
||||
"source": "tmpfs", # If this was not passed, the regular directory
|
||||
# would be created rather than tmpfs mount !!!
|
||||
# as this will cause to have invalid entry
|
||||
# in /proc/self/mountinfo
|
||||
"target" "/d2",
|
||||
"size": "100k",
|
||||
"chown": True
|
||||
}
|
||||
]
|
||||
|
||||
name (str): The name for this container.
|
||||
nano_cpus (int): CPU quota in units of 1e-9 CPUs.
|
||||
networks (Dict[str, Dict[str, Union[str, List[str]]):
|
||||
Networks which will be connected to container during container creation
|
||||
Values of the network configuration can be :
|
||||
- string
|
||||
- list of strings (e.g. Aliases)
|
||||
network_disabled (bool): Disable networking.
|
||||
network_mode (str): One of:
|
||||
|
||||
- bridge: Create a new network stack for the container
|
||||
on the bridge network.
|
||||
- none: No networking for this container.
|
||||
- container:<name|id>: Reuse another container's network
|
||||
stack.
|
||||
- host: Use the host network stack.
|
||||
|
||||
Incompatible with network.
|
||||
oom_kill_disable (bool): Whether to disable OOM killer.
|
||||
oom_score_adj (int): An integer value containing the score given to the container in
|
||||
order to tune OOM killer preferences.
|
||||
pid_mode (str): If set to host, use the host PID namespace
|
||||
inside the container.
|
||||
pids_limit (int): Tune a container's pids limit. Set -1 for unlimited.
|
||||
platform (str): Platform in the format os[/arch[/variant]]. Only used if the method
|
||||
needs to pull the requested image.
|
||||
ports (Dict[str, Union[int, Tuple[str, int], List[int]]]): Ports to bind inside
|
||||
the container.
|
||||
|
||||
The keys of the dictionary are the ports to bind inside the container, either as an
|
||||
integer or a string in the form port/protocol, where the protocol is either
|
||||
tcp, udp, or sctp.
|
||||
|
||||
The values of the dictionary are the corresponding ports to open on the host,
|
||||
which can be either:
|
||||
|
||||
- The port number, as an integer.
|
||||
For example: {'2222/tcp': 3333} will expose port 2222 inside the container
|
||||
as port 3333 on the host.
|
||||
- None, to assign a random host port.
|
||||
For example: {'2222/tcp': None}.
|
||||
- A tuple of (address, port) if you want to specify the host interface.
|
||||
For example: {'1111/tcp': ('127.0.0.1', 1111)}.
|
||||
- A list of integers or tuples of (address, port), if you want to bind
|
||||
multiple host ports to a single container port.
|
||||
For example: {'1111/tcp': [1234, ("127.0.0.1", 4567)]}.
|
||||
|
||||
For example: {'9090': 7878, '10932/tcp': '8781',
|
||||
"8989/tcp": ("127.0.0.1", 9091)}
|
||||
|
||||
privileged (bool): Give extended privileges to this container.
|
||||
publish_all_ports (bool): Publish all ports to the host.
|
||||
read_only (bool): Mount the container's root filesystem as read only.
|
||||
remove (bool): Remove the container when it has finished running. Default: False.
|
||||
restart_policy (Dict[str, Union[str, int]]): Restart the container when it exits.
|
||||
Configured as a dictionary with keys:
|
||||
|
||||
- Name: One of on-failure, or always.
|
||||
- MaximumRetryCount: Number of times to restart the container on failure.
|
||||
|
||||
For example: {"Name": "on-failure", "MaximumRetryCount": 5}
|
||||
|
||||
runtime (str): Runtime to use with this container.
|
||||
security_opt (List[str]): A List[str]ing values to customize labels for MLS systems,
|
||||
such as SELinux.
|
||||
shm_size (Union[str, int]): Size of /dev/shm (e.g. 1G).
|
||||
stdin_open (bool): Keep STDIN open even if not attached.
|
||||
stdout (bool): Return logs from STDOUT when detach=False. Default: True.
|
||||
stderr (bool): Return logs from STDERR when detach=False. Default: False.
|
||||
stop_signal (str): The stop signal to use to stop the container (e.g. SIGINT).
|
||||
storage_opt (Dict[str, str]): Storage driver options per container as a
|
||||
key-value mapping.
|
||||
stream (bool): If true and detach is false, return a log generator instead of a string.
|
||||
Ignored if detach is true. Default: False.
|
||||
sysctls (Dict[str, str]): Kernel parameters to set in the container.
|
||||
tmpfs (Dict[str, str]): Temporary filesystems to mount, as a dictionary mapping a
|
||||
path inside the container to options for that path.
|
||||
|
||||
For example: {'/mnt/vol2': '', '/mnt/vol1': 'size=3G,uid=1000'}
|
||||
|
||||
tty (bool): Allocate a pseudo-TTY.
|
||||
ulimits (List[Ulimit]): Ulimits to set inside the container.
|
||||
use_config_proxy (bool): If True, and if the docker client configuration
|
||||
file (~/.config/containers/config.json by default) contains a proxy configuration,
|
||||
the corresponding environment variables will be set in the container being built.
|
||||
user (Union[str, int]): Username or UID to run commands as inside the container.
|
||||
userns_mode (str): Sets the user namespace mode for the container when user namespace
|
||||
remapping option is enabled. Supported values are: host
|
||||
uts_mode (str): Sets the UTS namespace mode for the container.
|
||||
Supported values are: host
|
||||
version (str): The version of the API to use. Set to auto to automatically detect
|
||||
the server's version. Default: 3.0.0
|
||||
volume_driver (str): The name of a volume driver/plugin.
|
||||
volumes (Dict[str, Dict[str, Union[str, list]]]): A dictionary to configure
|
||||
volumes mounted inside the container.
|
||||
The key is either the host path or a volume name, and the value is
|
||||
a dictionary with the keys:
|
||||
|
||||
- bind: The path to mount the volume inside the container
|
||||
- mode: Either rw to mount the volume read/write, or ro to mount it read-only.
|
||||
Kept for docker-py compatibility
|
||||
- extended_mode: List of options passed to volume mount.
|
||||
|
||||
For example:
|
||||
|
||||
{
|
||||
'test_bind_1':
|
||||
{'bind': '/mnt/vol1', 'mode': 'rw'},
|
||||
'test_bind_2':
|
||||
{'bind': '/mnt/vol2', 'extended_mode': ['ro', 'noexec']},
|
||||
'test_bind_3':
|
||||
{'bind': '/mnt/vol3', 'extended_mode': ['noexec'], 'mode': 'rw'}
|
||||
}
|
||||
|
||||
volumes_from (List[str]): List of container names or IDs to get volumes from.
|
||||
working_dir (str): Path to the working directory.
|
||||
|
||||
Raises:
|
||||
ImageNotFound: when Image not found by Podman service
|
||||
APIError: when Podman service reports an error
|
||||
"""
|
||||
if isinstance(image, Image):
|
||||
image = image.id
|
||||
|
||||
payload = {"image": image, "command": command}
|
||||
payload.update(kwargs)
|
||||
payload = self._render_payload(payload)
|
||||
payload = api.prepare_body(payload)
|
||||
|
||||
response = self.client.post(
|
||||
"/containers/create", headers={"content-type": "application/json"}, data=payload
|
||||
)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
body = response.json()
|
||||
return self.get(body["Id"])
|
||||
|
||||
# pylint: disable=too-many-locals,too-many-statements,too-many-branches
|
||||
@staticmethod
|
||||
def _render_payload(kwargs: MutableMapping[str, Any]) -> Dict[str, Any]:
|
||||
"""Map create/run kwargs into body parameters."""
|
||||
args = copy.copy(kwargs)
|
||||
|
||||
if "links" in args:
|
||||
if len(args["links"]) > 0:
|
||||
raise ValueError("'links' are not supported by Podman service.")
|
||||
del args["links"]
|
||||
|
||||
# Ignore these keywords
|
||||
for key in (
|
||||
"cpu_count",
|
||||
"cpu_percent",
|
||||
"nano_cpus",
|
||||
"platform", # used by caller
|
||||
"remove", # used by caller
|
||||
"stderr", # used by caller
|
||||
"stdout", # used by caller
|
||||
"stream", # used by caller
|
||||
"detach", # used by caller
|
||||
"volume_driver",
|
||||
):
|
||||
with suppress(KeyError):
|
||||
del args[key]
|
||||
|
||||
# These keywords are not supported for various reasons.
|
||||
unsupported_keys = set(args.keys()).intersection(
|
||||
(
|
||||
"blkio_weight",
|
||||
"blkio_weight_device", # FIXME In addition to device Major/Minor include path
|
||||
"device_cgroup_rules", # FIXME Where to map for Podman API?
|
||||
"device_read_bps", # FIXME In addition to device Major/Minor include path
|
||||
"device_read_iops", # FIXME In addition to device Major/Minor include path
|
||||
"device_requests", # FIXME In addition to device Major/Minor include path
|
||||
"device_write_bps", # FIXME In addition to device Major/Minor include path
|
||||
"device_write_iops", # FIXME In addition to device Major/Minor include path
|
||||
"domainname",
|
||||
"network_disabled", # FIXME Where to map for Podman API?
|
||||
"storage_opt", # FIXME Where to map for Podman API?
|
||||
"tmpfs", # FIXME Where to map for Podman API?
|
||||
)
|
||||
)
|
||||
if len(unsupported_keys) > 0:
|
||||
raise TypeError(
|
||||
f"""Keyword(s) '{" ,".join(unsupported_keys)}' are"""
|
||||
f""" currently not supported by Podman API."""
|
||||
)
|
||||
|
||||
def pop(k):
|
||||
return args.pop(k, None)
|
||||
|
||||
def to_bytes(size: Union[int, str, None]) -> Union[int, None]:
|
||||
"""
|
||||
Converts str or int to bytes.
|
||||
Input can be in the following forms :
|
||||
0) None - e.g. None -> returns None
|
||||
1) int - e.g. 100 == 100 bytes
|
||||
2) str - e.g. '100' == 100 bytes
|
||||
3) str with suffix - available suffixes:
|
||||
b | B - bytes
|
||||
k | K = kilobytes
|
||||
m | M = megabytes
|
||||
g | G = gigabytes
|
||||
e.g. '100m' == 104857600 bytes
|
||||
"""
|
||||
size_type = type(size)
|
||||
if size is None:
|
||||
return size
|
||||
if size_type is int:
|
||||
return size
|
||||
if size_type is str:
|
||||
try:
|
||||
return int(size)
|
||||
except ValueError as bad_size:
|
||||
mapping = {'b': 0, 'k': 1, 'm': 2, 'g': 3}
|
||||
mapping_regex = ''.join(mapping.keys())
|
||||
search = re.search(rf'^(\d+)([{mapping_regex}])$', size.lower())
|
||||
if search:
|
||||
return int(search.group(1)) * (1024 ** mapping[search.group(2)])
|
||||
raise TypeError(
|
||||
f"Passed string size {size} should be in format\\d+[bBkKmMgG] (e.g. '100m')"
|
||||
) from bad_size
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Passed size {size} should be a type of unicode, str "
|
||||
f"or int (found : {size_type})"
|
||||
)
|
||||
|
||||
# Transform keywords into parameters
|
||||
params = {
|
||||
"annotations": pop("annotations"), # TODO document, podman only
|
||||
"apparmor_profile": pop("apparmor_profile"), # TODO document, podman only
|
||||
"cap_add": pop("cap_add"),
|
||||
"cap_drop": pop("cap_drop"),
|
||||
"cgroup_parent": pop("cgroup_parent"),
|
||||
"cgroups_mode": pop("cgroups_mode"), # TODO document, podman only
|
||||
"cni_networks": [pop("network")],
|
||||
"command": args.pop("command", args.pop("cmd", None)),
|
||||
"conmon_pid_file": pop("conmon_pid_file"), # TODO document, podman only
|
||||
"containerCreateCommand": pop("containerCreateCommand"), # TODO document, podman only
|
||||
"devices": [],
|
||||
"dns_options": pop("dns_opt"),
|
||||
"dns_search": pop("dns_search"),
|
||||
"dns_server": pop("dns"),
|
||||
"entrypoint": pop("entrypoint"),
|
||||
"env": pop("environment"),
|
||||
"env_host": pop("env_host"), # TODO document, podman only
|
||||
"expose": {},
|
||||
"groups": pop("group_add"),
|
||||
"healthconfig": pop("healthcheck"),
|
||||
"health_check_on_failure_action": pop("health_check_on_failure_action"),
|
||||
"hostadd": [],
|
||||
"hostname": pop("hostname"),
|
||||
"httpproxy": pop("use_config_proxy"),
|
||||
"idmappings": pop("idmappings"), # TODO document, podman only
|
||||
"image": pop("image"),
|
||||
"image_volume_mode": pop("image_volume_mode"), # TODO document, podman only
|
||||
"image_volumes": pop("image_volumes"), # TODO document, podman only
|
||||
"init": pop("init"),
|
||||
"init_path": pop("init_path"),
|
||||
"isolation": pop("isolation"),
|
||||
"labels": pop("labels"),
|
||||
"log_configuration": {},
|
||||
"lxc_config": pop("lxc_config"),
|
||||
"mask": pop("masked_paths"),
|
||||
"mounts": [],
|
||||
"name": pop("name"),
|
||||
"namespace": pop("namespace"), # TODO What is this for?
|
||||
"network_options": pop("network_options"), # TODO document, podman only
|
||||
"networks": pop("networks"),
|
||||
"no_new_privileges": pop("no_new_privileges"), # TODO document, podman only
|
||||
"oci_runtime": pop("runtime"),
|
||||
"oom_score_adj": pop("oom_score_adj"),
|
||||
"overlay_volumes": pop("overlay_volumes"), # TODO document, podman only
|
||||
"portmappings": [],
|
||||
"privileged": pop("privileged"),
|
||||
"procfs_opts": pop("procfs_opts"), # TODO document, podman only
|
||||
"publish_image_ports": pop("publish_all_ports"),
|
||||
"r_limits": [],
|
||||
"raw_image_name": pop("raw_image_name"), # TODO document, podman only
|
||||
"read_only_filesystem": pop("read_only"),
|
||||
"remove": args.pop("remove", args.pop("auto_remove", None)),
|
||||
"resource_limits": {},
|
||||
"rootfs": pop("rootfs"),
|
||||
"rootfs_propagation": pop("rootfs_propagation"),
|
||||
"sdnotifyMode": pop("sdnotifyMode"), # TODO document, podman only
|
||||
"seccomp_policy": pop("seccomp_policy"), # TODO document, podman only
|
||||
"seccomp_profile_path": pop("seccomp_profile_path"), # TODO document, podman only
|
||||
"secrets": pop("secrets"), # TODO document, podman only
|
||||
"selinux_opts": pop("security_opt"),
|
||||
"shm_size": to_bytes(pop("shm_size")),
|
||||
"static_mac": pop("mac_address"),
|
||||
"stdin": pop("stdin_open"),
|
||||
"stop_signal": pop("stop_signal"),
|
||||
"stop_timeout": pop("stop_timeout"), # TODO document, podman only
|
||||
"sysctl": pop("sysctls"),
|
||||
"systemd": pop("systemd"), # TODO document, podman only
|
||||
"terminal": pop("tty"),
|
||||
"timezone": pop("timezone"),
|
||||
"umask": pop("umask"), # TODO document, podman only
|
||||
"unified": pop("unified"), # TODO document, podman only
|
||||
"unmask": pop("unmasked_paths"), # TODO document, podman only
|
||||
"use_image_hosts": pop("use_image_hosts"), # TODO document, podman only
|
||||
"use_image_resolve_conf": pop("use_image_resolve_conf"), # TODO document, podman only
|
||||
"user": pop("user"),
|
||||
"version": pop("version"),
|
||||
"volumes": [],
|
||||
"volumes_from": pop("volumes_from"),
|
||||
"work_dir": pop("working_dir"),
|
||||
}
|
||||
|
||||
for device in args.pop("devices", []):
|
||||
params["devices"].append({"path": device})
|
||||
|
||||
for item in args.pop("exposed_ports", []):
|
||||
port, protocol = item.split("/")
|
||||
params["expose"][int(port)] = protocol
|
||||
|
||||
for hostname, ip in args.pop("extra_hosts", {}).items():
|
||||
params["hostadd"].append(f"{hostname}:{ip}")
|
||||
|
||||
if "log_config" in args:
|
||||
params["log_configuration"]["driver"] = args["log_config"].get("Type")
|
||||
|
||||
if "Config" in args["log_config"]:
|
||||
params["log_configuration"]["path"] = args["log_config"]["Config"].get("path")
|
||||
params["log_configuration"]["size"] = args["log_config"]["Config"].get("size")
|
||||
params["log_configuration"]["options"] = args["log_config"]["Config"].get("options")
|
||||
args.pop("log_config")
|
||||
|
||||
for item in args.pop("mounts", []):
|
||||
mount_point = {
|
||||
"destination": item.get("target"),
|
||||
"options": [],
|
||||
"source": item.get("source"),
|
||||
"type": item.get("type"),
|
||||
}
|
||||
|
||||
# some names are different for podman-py vs REST API due to compatibility with docker
|
||||
# some (e.g. chown) despite listed in podman-run documentation fails with error
|
||||
names_dict = {"read_only": "ro", "chown": "U"}
|
||||
|
||||
options = []
|
||||
simple_options = ["propagation", "relabel"]
|
||||
bool_options = ["read_only", "U", "chown"]
|
||||
regular_options = ["consistency", "mode", "size"]
|
||||
|
||||
for k, v in item.items():
|
||||
option_name = names_dict.get(k, k)
|
||||
if k in bool_options and v is True:
|
||||
options.append(option_name)
|
||||
elif k in regular_options:
|
||||
options.append(f'{option_name}={v}')
|
||||
elif k in simple_options:
|
||||
options.append(v)
|
||||
|
||||
mount_point["options"] = options
|
||||
|
||||
params["mounts"].append(mount_point)
|
||||
|
||||
if "pod" in args:
|
||||
pod = args.pop("pod")
|
||||
if isinstance(pod, Pod):
|
||||
pod = pod.id
|
||||
params["pod"] = pod # TODO document, podman only
|
||||
|
||||
for container, host in args.pop("ports", {}).items():
|
||||
if "/" in container:
|
||||
container_port, protocol = container.split("/")
|
||||
else:
|
||||
container_port, protocol = container, "tcp"
|
||||
|
||||
port_map = {"container_port": int(container_port), "protocol": protocol}
|
||||
if host is None:
|
||||
pass
|
||||
elif isinstance(host, int) or isinstance(host, str) and host.isdigit():
|
||||
port_map["host_port"] = int(host)
|
||||
elif isinstance(host, tuple):
|
||||
port_map["host_ip"] = host[0]
|
||||
port_map["host_port"] = int(host[1])
|
||||
elif isinstance(host, list):
|
||||
for host_list in host:
|
||||
port_map = {"container_port": int(container_port), "protocol": protocol}
|
||||
if (
|
||||
isinstance(host_list, int)
|
||||
or isinstance(host_list, str)
|
||||
and host_list.isdigit()
|
||||
):
|
||||
port_map["host_port"] = int(host_list)
|
||||
elif isinstance(host_list, tuple):
|
||||
port_map["host_ip"] = host_list[0]
|
||||
port_map["host_port"] = int(host_list[1])
|
||||
else:
|
||||
raise ValueError(f"'ports' value of '{host_list}' is not supported.")
|
||||
params["portmappings"].append(port_map)
|
||||
continue
|
||||
else:
|
||||
raise ValueError(f"'ports' value of '{host}' is not supported.")
|
||||
|
||||
params["portmappings"].append(port_map)
|
||||
|
||||
if "restart_policy" in args:
|
||||
params["restart_policy"] = args["restart_policy"].get("Name")
|
||||
params["restart_tries"] = args["restart_policy"].get("MaximumRetryCount")
|
||||
args.pop("restart_policy")
|
||||
|
||||
params["resource_limits"]["pids"] = {"limit": args.pop("pids_limit", None)}
|
||||
|
||||
params["resource_limits"]["cpu"] = {
|
||||
"cpus": args.pop("cpuset_cpus", None),
|
||||
"mems": args.pop("cpuset_mems", None),
|
||||
"period": args.pop("cpu_period", None),
|
||||
"quota": args.pop("cpu_quota", None),
|
||||
"realtimePeriod": args.pop("cpu_rt_period", None),
|
||||
"realtimeRuntime": args.pop("cpu_rt_runtime", None),
|
||||
"shares": args.pop("cpu_shares", None),
|
||||
}
|
||||
|
||||
params["resource_limits"]["memory"] = {
|
||||
"disableOOMKiller": args.pop("oom_kill_disable", None),
|
||||
"kernel": to_bytes(args.pop("kernel_memory", None)),
|
||||
"kernelTCP": args.pop("kernel_memory_tcp", None),
|
||||
"limit": to_bytes(args.pop("mem_limit", None)),
|
||||
"reservation": to_bytes(args.pop("mem_reservation", None)),
|
||||
"swap": to_bytes(args.pop("memswap_limit", None)),
|
||||
"swappiness": args.pop("mem_swappiness", None),
|
||||
"useHierarchy": args.pop("mem_use_hierarchy", None),
|
||||
}
|
||||
|
||||
for item in args.pop("ulimits", []):
|
||||
params["r_limits"].append(
|
||||
{
|
||||
"type": item["Name"],
|
||||
"hard": item["Hard"],
|
||||
"soft": item["Soft"],
|
||||
}
|
||||
)
|
||||
|
||||
for item in args.pop("volumes", {}).items():
|
||||
key, value = item
|
||||
extended_mode = value.get('extended_mode', [])
|
||||
if not isinstance(extended_mode, list):
|
||||
raise ValueError("'extended_mode' value should be a list")
|
||||
|
||||
options = extended_mode
|
||||
mode = value.get('mode')
|
||||
if mode is not None:
|
||||
if not isinstance(mode, str):
|
||||
raise ValueError("'mode' value should be a str")
|
||||
options.append(mode)
|
||||
|
||||
volume = {"Name": key, "Dest": value["bind"], "Options": options}
|
||||
params["volumes"].append(volume)
|
||||
|
||||
if "cgroupns" in args:
|
||||
params["cgroupns"] = {"nsmode": args.pop("cgroupns")}
|
||||
|
||||
if "ipc_mode" in args:
|
||||
params["ipcns"] = {"nsmode": args.pop("ipc_mode")}
|
||||
|
||||
if "network_mode" in args:
|
||||
params["netns"] = {"nsmode": args.pop("network_mode")}
|
||||
|
||||
if "pid_mode" in args:
|
||||
params["pidns"] = {"nsmode": args.pop("pid_mode")}
|
||||
|
||||
if "userns_mode" in args:
|
||||
params["userns"] = {"nsmode": args.pop("userns_mode")}
|
||||
|
||||
if "uts_mode" in args:
|
||||
params["utsns"] = {"nsmode": args.pop("uts_mode")}
|
||||
|
||||
if len(args) > 0:
|
||||
raise TypeError(
|
||||
"Unknown keyword argument(s): " + " ,".join(f"'{k}'" for k in args.keys())
|
||||
)
|
||||
|
||||
return params
|
||||
@ -0,0 +1,140 @@
|
||||
"""PodmanResource manager subclassed for Containers."""
|
||||
import logging
|
||||
import urllib
|
||||
from typing import Any, Dict, List, Mapping, Union
|
||||
|
||||
from podman import api
|
||||
from podman.domain.containers import Container
|
||||
from podman.domain.containers_create import CreateMixin
|
||||
from podman.domain.containers_run import RunMixin
|
||||
from podman.domain.manager import Manager
|
||||
from podman.errors import APIError
|
||||
|
||||
logger = logging.getLogger("podman.containers")
|
||||
|
||||
|
||||
class ContainersManager(RunMixin, CreateMixin, Manager):
|
||||
"""Specialized Manager for Container resources."""
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Type[Container]: prepare_model() will create Container classes."""
|
||||
return Container
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
response = self.client.get(f"/containers/{key}/exists")
|
||||
return response.ok
|
||||
|
||||
def get(self, key: str) -> Container:
|
||||
"""Get container by name or id.
|
||||
|
||||
Args:
|
||||
container_id: Container name or id.
|
||||
|
||||
Raises:
|
||||
NotFound: when Container does not exist
|
||||
APIError: when an error return by service
|
||||
"""
|
||||
container_id = urllib.parse.quote_plus(key)
|
||||
response = self.client.get(f"/containers/{container_id}/json")
|
||||
response.raise_for_status()
|
||||
return self.prepare_model(attrs=response.json())
|
||||
|
||||
def list(self, **kwargs) -> List[Container]:
|
||||
"""Report on containers.
|
||||
|
||||
Keyword Args:
|
||||
all: If False, only show running containers. Default: False.
|
||||
since: Show containers created after container name or id given.
|
||||
before: Show containers created before container name or id given.
|
||||
limit: Show last N created containers.
|
||||
filters: Filter container reported.
|
||||
Available filters:
|
||||
|
||||
- exited (int): Only containers with specified exit code
|
||||
- status (str): One of restarting, running, paused, exited
|
||||
- label (Union[str, List[str]]): Format either "key", "key=value" or a list of such.
|
||||
- id (str): The id of the container.
|
||||
- name (str): The name of the container.
|
||||
- ancestor (str): Filter by container ancestor. Format of
|
||||
<image-name>[:tag], <image-id>, or <image@digest>.
|
||||
- before (str): Only containers created before a particular container.
|
||||
Give the container name or id.
|
||||
- since (str): Only containers created after a particular container.
|
||||
Give container name or id.
|
||||
sparse: Ignored
|
||||
ignore_removed: If True, ignore failures due to missing containers.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
params = {
|
||||
"all": kwargs.get("all"),
|
||||
"filters": kwargs.get("filters", {}),
|
||||
"limit": kwargs.get("limit"),
|
||||
}
|
||||
if "before" in kwargs:
|
||||
params["filters"]["before"] = kwargs.get("before")
|
||||
if "since" in kwargs:
|
||||
params["filters"]["since"] = kwargs.get("since")
|
||||
|
||||
# filters formatted last because some kwargs may need to be mapped into filters
|
||||
params["filters"] = api.prepare_filters(params["filters"])
|
||||
|
||||
response = self.client.get("/containers/json", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
return [self.prepare_model(attrs=i) for i in response.json()]
|
||||
|
||||
def prune(self, filters: Mapping[str, str] = None) -> Dict[str, Any]:
|
||||
"""Delete stopped containers.
|
||||
|
||||
Args:
|
||||
filters: Criteria for determining containers to remove. Available keys are:
|
||||
- until (str): Delete containers before this time
|
||||
- label (List[str]): Labels associated with containers
|
||||
|
||||
Returns:
|
||||
Keys:
|
||||
- ContainersDeleted (List[str]): Identifiers of deleted containers.
|
||||
- SpaceReclaimed (int): Amount of disk space reclaimed in bytes.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
params = {"filters": api.prepare_filters(filters)}
|
||||
response = self.client.post("/containers/prune", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
results = {"ContainersDeleted": [], "SpaceReclaimed": 0}
|
||||
for entry in response.json():
|
||||
if entry.get("error") is not None:
|
||||
raise APIError(entry["error"], response=response, explanation=entry["error"])
|
||||
|
||||
results["ContainersDeleted"].append(entry["Id"])
|
||||
results["SpaceReclaimed"] += entry["Size"]
|
||||
return results
|
||||
|
||||
def remove(self, container_id: Union[Container, str], **kwargs):
|
||||
"""Delete container.
|
||||
|
||||
Podman only
|
||||
|
||||
Args:
|
||||
container_id: identifier of Container to delete.
|
||||
|
||||
Keyword Args:
|
||||
v (bool): Delete associated volumes as well.
|
||||
link (bool): Ignored.
|
||||
force (bool): Kill a running container before deleting.
|
||||
"""
|
||||
if isinstance(container_id, Container):
|
||||
container_id = container_id.id
|
||||
|
||||
params = {
|
||||
"v": kwargs.get("v"),
|
||||
"force": kwargs.get("force"),
|
||||
}
|
||||
|
||||
response = self.client.delete(f"/containers/{container_id}", params=params)
|
||||
response.raise_for_status()
|
||||
@ -0,0 +1,92 @@
|
||||
"""Mixin to provide Container run() method."""
|
||||
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from typing import Generator, Iterator, List, Union
|
||||
|
||||
from podman.domain.containers import Container
|
||||
from podman.domain.images import Image
|
||||
from podman.errors import ContainerError, ImageNotFound
|
||||
|
||||
logger = logging.getLogger("podman.containers")
|
||||
|
||||
|
||||
class RunMixin: # pylint: disable=too-few-public-methods
|
||||
"""Class providing run() method for ContainersManager."""
|
||||
|
||||
def run(
|
||||
self,
|
||||
image: Union[str, Image],
|
||||
command: Union[str, List[str], None] = None,
|
||||
stdout=True,
|
||||
stderr=False,
|
||||
remove: bool = False,
|
||||
**kwargs,
|
||||
) -> Union[Container, Union[Generator[str, None, None], Iterator[str]]]:
|
||||
"""Run a container.
|
||||
|
||||
By default, run() will wait for the container to finish and return its logs.
|
||||
|
||||
If detach=True, run() will start the container and return a Container object rather
|
||||
than logs.
|
||||
|
||||
Args:
|
||||
image: Image to run.
|
||||
command: Command to run in the container.
|
||||
stdout: Include stdout. Default: True.
|
||||
stderr: Include stderr. Default: False.
|
||||
remove: Delete container when the container's processes exit. Default: False.
|
||||
|
||||
Keyword Args:
|
||||
- See the create() method for keyword arguments.
|
||||
|
||||
Returns:
|
||||
- When detach is True, return a Container
|
||||
- If stdout is True, include stdout from container in output
|
||||
- If stderr is True, include stderr from container in output
|
||||
- When stream is True, output from container is returned as a generator
|
||||
- Otherwise, an iterator is returned after container has finished
|
||||
|
||||
Raises:
|
||||
ContainerError: when Container exists with a non-zero code
|
||||
ImageNotFound: when Image not found by Podman service
|
||||
APIError: when Podman service reports an error
|
||||
"""
|
||||
if isinstance(image, Image):
|
||||
image = image.id
|
||||
if isinstance(command, str):
|
||||
command = [command]
|
||||
|
||||
try:
|
||||
container = self.create(image=image, command=command, **kwargs)
|
||||
except ImageNotFound:
|
||||
self.client.images.pull(image, platform=kwargs.get("platform"))
|
||||
container = self.create(image=image, command=command, **kwargs)
|
||||
|
||||
container.start()
|
||||
container.wait(condition=["running", "exited"])
|
||||
container.reload()
|
||||
|
||||
if kwargs.get("detach", False):
|
||||
return container
|
||||
|
||||
with suppress(KeyError):
|
||||
log_type = container.attrs["HostConfig"]["LogConfig"]["Type"]
|
||||
|
||||
log_iter = None
|
||||
if log_type in ("json-file", "journald"):
|
||||
log_iter = container.logs(stdout=stdout, stderr=stderr, stream=True, follow=True)
|
||||
|
||||
exit_status = container.wait()["StatusCode"]
|
||||
if exit_status != 0:
|
||||
log_iter = None
|
||||
if not kwargs.get("auto_remove", False):
|
||||
log_iter = container.logs(stdout=False, stderr=True)
|
||||
|
||||
if remove:
|
||||
container.remove()
|
||||
|
||||
if exit_status != 0:
|
||||
raise ContainerError(container, exit_status, command, image, log_iter)
|
||||
|
||||
return log_iter if kwargs.get("stream", False) or log_iter is None else b"".join(log_iter)
|
||||
57
venv/lib/python3.11/site-packages/podman/domain/events.py
Normal file
57
venv/lib/python3.11/site-packages/podman/domain/events.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Model and Manager for Event resources."""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional, Union, Iterator
|
||||
|
||||
from podman import api
|
||||
from podman.api.client import APIClient
|
||||
|
||||
logger = logging.getLogger("podman.events")
|
||||
|
||||
|
||||
class EventsManager: # pylint: disable=too-few-public-methods
|
||||
"""Specialized Manager for Event resources."""
|
||||
|
||||
def __init__(self, client: APIClient) -> None:
|
||||
"""Initialize EventManager object.
|
||||
|
||||
Args:
|
||||
client: Connection to Podman service.
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
def list(
|
||||
self,
|
||||
since: Union[datetime, int, None] = None,
|
||||
until: Union[datetime, int, None] = None,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
decode: bool = False,
|
||||
) -> Iterator[Union[str, Dict[str, Any]]]:
|
||||
"""Report on networks.
|
||||
|
||||
Args:
|
||||
decode: When True, decode stream into dict's. Default: False
|
||||
filters: Criteria for including events.
|
||||
since: Get events newer than this time.
|
||||
until: Get events older than this time.
|
||||
|
||||
Yields:
|
||||
When decode is True, Iterator[Dict[str, Any]]
|
||||
|
||||
When decode is False, Iterator[str]
|
||||
"""
|
||||
params = {
|
||||
"filters": api.prepare_filters(filters),
|
||||
"since": api.prepare_timestamp(since),
|
||||
"stream": True,
|
||||
"until": api.prepare_timestamp(until),
|
||||
}
|
||||
response = self.client.get("/events", params=params, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
for item in response.iter_lines():
|
||||
if decode:
|
||||
yield json.loads(item)
|
||||
else:
|
||||
yield item
|
||||
119
venv/lib/python3.11/site-packages/podman/domain/images.py
Normal file
119
venv/lib/python3.11/site-packages/podman/domain/images.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Model and Manager for Image resources."""
|
||||
import logging
|
||||
from typing import Any, Dict, Iterator, List, Optional, Union
|
||||
|
||||
from podman import api
|
||||
from podman.domain.manager import PodmanResource
|
||||
from podman.errors import ImageNotFound
|
||||
|
||||
logger = logging.getLogger("podman.images")
|
||||
|
||||
|
||||
class Image(PodmanResource):
|
||||
"""Details and configuration for an Image managed by the Podman service."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__}: '{"', '".join(self.tags)}'>"""
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
"""dict[str, str]: Return labels associated with Image."""
|
||||
image_labels = self.attrs.get("Labels")
|
||||
if image_labels is None or len(image_labels) == 0:
|
||||
return {}
|
||||
|
||||
return image_labels
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
"""list[str]: Return tags from Image."""
|
||||
repo_tags = self.attrs.get("RepoTags")
|
||||
if repo_tags is None or len(repo_tags) == 0:
|
||||
return []
|
||||
|
||||
return [tag for tag in repo_tags if tag != "<none>:<none>"]
|
||||
|
||||
def history(self) -> List[Dict[str, Any]]:
|
||||
"""Returns history of the Image.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
|
||||
response = self.client.get(f"/images/{self.id}/history")
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
return response.json()
|
||||
|
||||
def remove(
|
||||
self, **kwargs
|
||||
) -> List[Dict[api.Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]:
|
||||
"""Delete image from Podman service.
|
||||
|
||||
Podman only
|
||||
|
||||
Keyword Args:
|
||||
force: Delete Image even if in use
|
||||
noprune: Ignored.
|
||||
|
||||
Returns:
|
||||
Report on which images were deleted and untagged, including any reported errors.
|
||||
|
||||
Raises:
|
||||
ImageNotFound: when image does not exist
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
return self.manager.remove(self.id, **kwargs)
|
||||
|
||||
def save(
|
||||
self,
|
||||
chunk_size: Optional[int] = api.DEFAULT_CHUNK_SIZE,
|
||||
named: Union[str, bool] = False, # pylint: disable=unused-argument
|
||||
) -> Iterator[bytes]:
|
||||
"""Returns Image as tarball.
|
||||
|
||||
Format is set to docker-archive, this allows load() to import this tarball.
|
||||
|
||||
Args:
|
||||
chunk_size: If None, data will be streamed in received buffer size.
|
||||
If not None, data will be returned in sized buffers. Default: 2MB
|
||||
named: Ignored.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
response = self.client.get(
|
||||
f"/images/{self.id}/get", params={"format": ["docker-archive"]}, stream=True
|
||||
)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
return response.iter_content(chunk_size=chunk_size)
|
||||
|
||||
def tag(
|
||||
self,
|
||||
repository: str,
|
||||
tag: Optional[str],
|
||||
force: bool = False, # pylint: disable=unused-argument
|
||||
) -> bool:
|
||||
"""Tag Image into repository.
|
||||
|
||||
Args:
|
||||
repository: The repository for tagging Image.
|
||||
tag: optional tag name.
|
||||
force: Ignore client errors
|
||||
|
||||
Returns:
|
||||
True, when operational succeeds.
|
||||
|
||||
Raises:
|
||||
ImageNotFound: when service cannot find image
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
params = {"repo": repository, "tag": tag}
|
||||
response = self.client.post(f"/images/{self.id}/tag", params=params)
|
||||
if response.ok:
|
||||
return True
|
||||
|
||||
if force and response.status_code <= 500:
|
||||
return False
|
||||
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
return False
|
||||
195
venv/lib/python3.11/site-packages/podman/domain/images_build.py
Normal file
195
venv/lib/python3.11/site-packages/podman/domain/images_build.py
Normal file
@ -0,0 +1,195 @@
|
||||
"""Mixin for Image build support."""
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
from typing import Any, Dict, Iterator, List, Tuple
|
||||
|
||||
import itertools
|
||||
|
||||
from podman import api
|
||||
from podman.domain.images import Image
|
||||
from podman.errors import BuildError, PodmanError, ImageNotFound
|
||||
|
||||
logger = logging.getLogger("podman.images")
|
||||
|
||||
|
||||
class BuildMixin:
|
||||
"""Class providing build method for ImagesManager."""
|
||||
|
||||
# pylint: disable=too-many-locals,too-many-branches,too-few-public-methods,too-many-statements
|
||||
def build(self, **kwargs) -> Tuple[Image, Iterator[bytes]]:
|
||||
"""Returns built image.
|
||||
|
||||
Keyword Args:
|
||||
path (str) – Path to the directory containing the Dockerfile
|
||||
fileobj – A file object to use as the Dockerfile. (Or an IO object)
|
||||
tag (str) – A tag to add to the final image
|
||||
quiet (bool) – Whether to return the status
|
||||
nocache (bool) – Don’t use the cache when set to True
|
||||
rm (bool) – Remove intermediate containers. Default True
|
||||
timeout (int) – HTTP timeout
|
||||
custom_context (bool) – Optional if using fileobj (ignored)
|
||||
encoding (str) – The encoding for a stream. Set to gzip for compressing (ignored)
|
||||
pull (bool) – Downloads any updates to the FROM image in Dockerfile
|
||||
forcerm (bool) – Always remove intermediate containers, even after unsuccessful builds
|
||||
dockerfile (str) – full path to the Dockerfile / Containerfile
|
||||
buildargs (Mapping[str,str) – A dictionary of build arguments
|
||||
container_limits (Dict[str, Union[int,str]]) –
|
||||
A dictionary of limits applied to each container created by the build process.
|
||||
Valid keys:
|
||||
|
||||
- memory (int): set memory limit for build
|
||||
- memswap (int): Total memory (memory + swap), -1 to disable swap
|
||||
- cpushares (int): CPU shares (relative weight)
|
||||
- cpusetcpus (str): CPUs in which to allow execution, For example, "0-3", "0,1"
|
||||
- cpuperiod (int): CPU CFS (Completely Fair Scheduler) period (Podman only)
|
||||
- cpuquota (int): CPU CFS (Completely Fair Scheduler) quota (Podman only)
|
||||
shmsize (int) – Size of /dev/shm in bytes. The size must be greater than 0.
|
||||
If omitted the system uses 64MB
|
||||
labels (Mapping[str,str]) – A dictionary of labels to set on the image
|
||||
cache_from (List[str]) – A list of image's identifier used for build cache resolution
|
||||
target (str) – Name of the build-stage to build in a multi-stage Dockerfile
|
||||
network_mode (str) – networking mode for the run commands during build
|
||||
squash (bool) – Squash the resulting images layers into a single layer.
|
||||
extra_hosts (Dict[str,str]) – Extra hosts to add to /etc/hosts in building
|
||||
containers, as a mapping of hostname to IP address.
|
||||
platform (str) – Platform in the format os[/arch[/variant]].
|
||||
isolation (str) – Isolation technology used during build. (ignored)
|
||||
use_config_proxy (bool) – (ignored)
|
||||
http_proxy (bool) - Inject http proxy environment variables into container (Podman only)
|
||||
layers (bool) - Cache intermediate layers during build.
|
||||
output (str) - specifies if any custom build output is selected for following build.
|
||||
outputformat (str) - The format of the output image's manifest and configuration data.
|
||||
|
||||
Returns:
|
||||
first item is the podman.domain.images.Image built
|
||||
|
||||
second item is the build logs
|
||||
|
||||
Raises:
|
||||
BuildError: when there is an error during the build
|
||||
APIError: when service returns an error
|
||||
TypeError: when neither path nor fileobj is not specified
|
||||
"""
|
||||
|
||||
params = self._render_params(kwargs)
|
||||
|
||||
body = None
|
||||
path = None
|
||||
if "fileobj" in kwargs:
|
||||
path = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
|
||||
filename = pathlib.Path(path.name) / params["dockerfile"]
|
||||
|
||||
with open(filename, "w", encoding='utf-8') as file:
|
||||
shutil.copyfileobj(kwargs["fileobj"], file)
|
||||
body = api.create_tar(anchor=path.name, gzip=kwargs.get("gzip", False))
|
||||
elif "path" in kwargs:
|
||||
filename = pathlib.Path(kwargs["path"]) / params["dockerfile"]
|
||||
# The Dockerfile will be copied into the context_dir if needed
|
||||
params["dockerfile"] = api.prepare_containerfile(kwargs["path"], str(filename))
|
||||
|
||||
excludes = api.prepare_containerignore(kwargs["path"])
|
||||
body = api.create_tar(
|
||||
anchor=kwargs["path"], exclude=excludes, gzip=kwargs.get("gzip", False)
|
||||
)
|
||||
|
||||
post_kwargs = {}
|
||||
if kwargs.get("timeout"):
|
||||
post_kwargs["timeout"] = float(kwargs.get("timeout"))
|
||||
|
||||
response = self.client.post(
|
||||
"/build",
|
||||
params=params,
|
||||
data=body,
|
||||
headers={
|
||||
"Content-type": "application/x-tar",
|
||||
# "X-Registry-Config": "TODO",
|
||||
},
|
||||
stream=True,
|
||||
**post_kwargs,
|
||||
)
|
||||
if hasattr(body, "close"):
|
||||
body.close()
|
||||
|
||||
if hasattr(path, "cleanup"):
|
||||
path.cleanup()
|
||||
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
image_id = unknown = None
|
||||
marker = re.compile(r"(^[0-9a-f]+)\n$")
|
||||
report_stream, stream = itertools.tee(response.iter_lines())
|
||||
for line in stream:
|
||||
result = json.loads(line)
|
||||
if "error" in result:
|
||||
raise BuildError(result["error"], report_stream)
|
||||
if "stream" in result:
|
||||
match = marker.match(result["stream"])
|
||||
if match:
|
||||
image_id = match.group(1)
|
||||
unknown = line
|
||||
|
||||
if image_id:
|
||||
return self.get(image_id), report_stream
|
||||
|
||||
raise BuildError(unknown or "Unknown", report_stream)
|
||||
|
||||
@staticmethod
|
||||
def _render_params(kwargs) -> Dict[str, List[Any]]:
|
||||
"""Map kwargs to query parameters.
|
||||
|
||||
All unsupported kwargs are silently ignored.
|
||||
"""
|
||||
if "path" not in kwargs and "fileobj" not in kwargs:
|
||||
raise TypeError("Either path or fileobj must be provided.")
|
||||
|
||||
if "gzip" in kwargs and "encoding" in kwargs:
|
||||
raise PodmanError("Custom encoding not supported when gzip enabled.")
|
||||
|
||||
params = {
|
||||
"dockerfile": kwargs.get("dockerfile"),
|
||||
"forcerm": kwargs.get("forcerm"),
|
||||
"httpproxy": kwargs.get("http_proxy"),
|
||||
"networkmode": kwargs.get("network_mode"),
|
||||
"nocache": kwargs.get("nocache"),
|
||||
"platform": kwargs.get("platform"),
|
||||
"pull": kwargs.get("pull"),
|
||||
"q": kwargs.get("quiet"),
|
||||
"remote": kwargs.get("remote"),
|
||||
"rm": kwargs.get("rm"),
|
||||
"shmsize": kwargs.get("shmsize"),
|
||||
"squash": kwargs.get("squash"),
|
||||
"t": kwargs.get("tag"),
|
||||
"target": kwargs.get("target"),
|
||||
"layers": kwargs.get("layers"),
|
||||
"output": kwargs.get("output"),
|
||||
"outputformat": kwargs.get("outputformat"),
|
||||
}
|
||||
|
||||
if "buildargs" in kwargs:
|
||||
params["buildargs"] = json.dumps(kwargs.get("buildargs"))
|
||||
if "cache_from" in kwargs:
|
||||
params["cachefrom"] = json.dumps(kwargs.get("cache_from"))
|
||||
|
||||
if "container_limits" in kwargs:
|
||||
params["cpuperiod"] = kwargs["container_limits"].get("cpuperiod")
|
||||
params["cpuquota"] = kwargs["container_limits"].get("cpuquota")
|
||||
params["cpusetcpus"] = kwargs["container_limits"].get("cpusetcpus")
|
||||
params["cpushares"] = kwargs["container_limits"].get("cpushares")
|
||||
params["memory"] = kwargs["container_limits"].get("memory")
|
||||
params["memswap"] = kwargs["container_limits"].get("memswap")
|
||||
|
||||
if "extra_hosts" in kwargs:
|
||||
params["extrahosts"] = json.dumps(kwargs.get("extra_hosts"))
|
||||
if "labels" in kwargs:
|
||||
params["labels"] = json.dumps(kwargs.get("labels"))
|
||||
|
||||
if params["dockerfile"] is None:
|
||||
params["dockerfile"] = f".containerfile.{random.getrandbits(160):x}"
|
||||
|
||||
# Remove any unset parameters
|
||||
return dict(filter(lambda i: i[1] is not None, params.items()))
|
||||
@ -0,0 +1,421 @@
|
||||
"""PodmanResource manager subclassed for Images."""
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
from typing import Any, Dict, Generator, Iterator, List, Mapping, Optional, Union
|
||||
|
||||
import requests
|
||||
|
||||
from podman import api
|
||||
from podman.api import Literal
|
||||
from podman.api.http_utils import encode_auth_header
|
||||
from podman.domain.images import Image
|
||||
from podman.domain.images_build import BuildMixin
|
||||
from podman.domain.manager import Manager
|
||||
from podman.domain.registry_data import RegistryData
|
||||
from podman.errors import APIError, ImageNotFound
|
||||
|
||||
logger = logging.getLogger("podman.images")
|
||||
|
||||
|
||||
class ImagesManager(BuildMixin, Manager):
|
||||
"""Specialized Manager for Image resources."""
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Type[podman.domain.images.Image]: prepare_model() will create Image classes."""
|
||||
return Image
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
"""Return true when image exists."""
|
||||
key = urllib.parse.quote_plus(key)
|
||||
response = self.client.get(f"/images/{key}/exists")
|
||||
return response.ok
|
||||
|
||||
def list(self, **kwargs) -> List[Image]:
|
||||
"""Report on images.
|
||||
|
||||
Keyword Args:
|
||||
name (str) – Only show images belonging to the repository name
|
||||
all (bool) – Show intermediate image layers. By default, these are filtered out.
|
||||
filters (Mapping[str, Union[str, List[str]]) – Filters to be used on the image list.
|
||||
Available filters:
|
||||
|
||||
- dangling (bool)
|
||||
- label (Union[str, List[str]]): format either "key" or "key=value"
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
params = {
|
||||
"all": kwargs.get("all"),
|
||||
"name": kwargs.get("name"),
|
||||
"filters": api.prepare_filters(kwargs.get("filters")),
|
||||
}
|
||||
response = self.client.get("/images/json", params=params)
|
||||
if response.status_code == requests.codes.not_found:
|
||||
return []
|
||||
response.raise_for_status()
|
||||
|
||||
return [self.prepare_model(attrs=i) for i in response.json()]
|
||||
|
||||
# pylint is flagging 'name' here vs. 'key' parameter in super.get()
|
||||
def get(self, name: str) -> Image: # pylint: disable=arguments-differ,arguments-renamed
|
||||
"""Returns an image by name or id.
|
||||
|
||||
Args:
|
||||
name: Image id or name for which to search
|
||||
|
||||
Raises:
|
||||
ImageNotFound: when image does not exist
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
name = urllib.parse.quote_plus(name)
|
||||
response = self.client.get(f"/images/{name}/json")
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
return self.prepare_model(response.json())
|
||||
|
||||
def get_registry_data(
|
||||
self,
|
||||
name: str,
|
||||
auth_config=Mapping[str, str], # pylint: disable=unused-argument
|
||||
) -> RegistryData:
|
||||
"""Returns registry data for an image.
|
||||
|
||||
Provided for compatibility
|
||||
|
||||
Args:
|
||||
name: Image name
|
||||
auth_config: Override configured credentials. Keys username and password are required.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
# FIXME populate attrs using auth_config
|
||||
image = self.get(name)
|
||||
return RegistryData(
|
||||
image_name=name,
|
||||
attrs=image.attrs,
|
||||
client=self.client,
|
||||
collection=self,
|
||||
)
|
||||
|
||||
def load(self, data: bytes) -> Generator[Image, None, None]:
|
||||
"""Restore an image previously saved.
|
||||
|
||||
Args:
|
||||
data: Image to be loaded in tarball format.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
# TODO fix podman swagger cannot use this header!
|
||||
# headers = {"Content-type": "application/x-www-form-urlencoded"}
|
||||
|
||||
response = self.client.post(
|
||||
"/images/load", data=data, headers={"Content-type": "application/x-tar"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.json()
|
||||
for item in body["Names"]:
|
||||
yield self.get(item)
|
||||
|
||||
def prune(
|
||||
self, filters: Optional[Mapping[str, Any]] = None
|
||||
) -> Dict[Literal["ImagesDeleted", "SpaceReclaimed"], Any]:
|
||||
"""Delete unused images.
|
||||
|
||||
The Untagged keys will always be "".
|
||||
|
||||
Args:
|
||||
filters: Qualify Images to prune. Available filters:
|
||||
|
||||
- dangling (bool): when true, only delete unused and untagged images.
|
||||
- until (str): Delete images older than this timestamp.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
response = self.client.post(
|
||||
"/images/prune", params={"filters": api.prepare_filters(filters)}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
deleted: List[Dict[str, str]] = []
|
||||
error: List[str] = []
|
||||
reclaimed: int = 0
|
||||
for element in response.json():
|
||||
if "Err" in element and element["Err"] is not None:
|
||||
error.append(element["Err"])
|
||||
else:
|
||||
reclaimed += element["Size"]
|
||||
deleted.append(
|
||||
{
|
||||
"Deleted": element["Id"],
|
||||
"Untagged": "",
|
||||
}
|
||||
)
|
||||
if len(error) > 0:
|
||||
raise APIError(response.url, response=response, explanation="; ".join(error))
|
||||
|
||||
return {
|
||||
"ImagesDeleted": deleted,
|
||||
"SpaceReclaimed": reclaimed,
|
||||
}
|
||||
|
||||
def prune_builds(self) -> Dict[Literal["CachesDeleted", "SpaceReclaimed"], Any]:
|
||||
"""Delete builder cache.
|
||||
|
||||
Method included to complete API, the operation always returns empty
|
||||
CacheDeleted and zero SpaceReclaimed.
|
||||
"""
|
||||
return {"CachesDeleted": [], "SpaceReclaimed": 0}
|
||||
|
||||
def push(
|
||||
self, repository: str, tag: Optional[str] = None, **kwargs
|
||||
) -> Union[str, Iterator[Union[str, Dict[str, Any]]]]:
|
||||
"""Push Image or repository to the registry.
|
||||
|
||||
Args:
|
||||
repository: Target repository for push
|
||||
tag: Tag to push, if given
|
||||
|
||||
Keyword Args:
|
||||
auth_config (Mapping[str, str]: Override configured credentials. Must include
|
||||
username and password keys.
|
||||
decode (bool): return data from server as Dict[str, Any]. Ignored unless stream=True.
|
||||
destination (str): alternate destination for image. (Podman only)
|
||||
stream (bool): return output as blocking generator. Default: False.
|
||||
tlsVerify (bool): Require TLS verification.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
auth_config: Optional[Dict[str, str]] = kwargs.get("auth_config")
|
||||
|
||||
headers = {
|
||||
# A base64url-encoded auth configuration
|
||||
"X-Registry-Auth": encode_auth_header(auth_config)
|
||||
if auth_config
|
||||
else ""
|
||||
}
|
||||
|
||||
params = {
|
||||
"destination": kwargs.get("destination"),
|
||||
"tlsVerify": kwargs.get("tlsVerify"),
|
||||
}
|
||||
|
||||
name = f'{repository}:{tag}' if tag else repository
|
||||
name = urllib.parse.quote_plus(name)
|
||||
response = self.client.post(f"/images/{name}/push", params=params, headers=headers)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
tag_count = 0 if tag is None else 1
|
||||
body = [
|
||||
{
|
||||
"status": f"Pushing repository {repository} ({tag_count} tags)",
|
||||
},
|
||||
{
|
||||
"status": "Pushing",
|
||||
"progressDetail": {},
|
||||
"id": repository,
|
||||
},
|
||||
]
|
||||
|
||||
stream = kwargs.get("stream", False)
|
||||
decode = kwargs.get("decode", False)
|
||||
if stream:
|
||||
return self._push_helper(decode, body)
|
||||
|
||||
with io.StringIO() as buffer:
|
||||
for entry in body:
|
||||
buffer.write(json.dumps(entry) + "\n")
|
||||
return buffer.getvalue()
|
||||
|
||||
@staticmethod
|
||||
def _push_helper(
|
||||
decode: bool, body: List[Dict[str, Any]]
|
||||
) -> Iterator[Union[str, Dict[str, Any]]]:
|
||||
"""Helper needed to allow push() to return either a generator or a str."""
|
||||
for entry in body:
|
||||
if decode:
|
||||
yield entry
|
||||
else:
|
||||
yield json.dumps(entry)
|
||||
|
||||
# pylint: disable=too-many-locals,too-many-branches
|
||||
def pull(
|
||||
self, repository: str, tag: Optional[str] = None, all_tags: bool = False, **kwargs
|
||||
) -> Union[Image, List[Image], Iterator[str]]:
|
||||
"""Request Podman service to pull image(s) from repository.
|
||||
|
||||
Args:
|
||||
repository: Repository to pull from
|
||||
tag: Image tag to pull. Default: "latest".
|
||||
all_tags: pull all image tags from repository.
|
||||
|
||||
Keyword Args:
|
||||
auth_config (Mapping[str, str]) – Override the credentials that are found in the
|
||||
config for this request. auth_config should contain the username and password
|
||||
keys to be valid.
|
||||
platform (str) – Platform in the format os[/arch[/variant]]
|
||||
tls_verify (bool) - Require TLS verification. Default: True.
|
||||
stream (bool) - When True, the pull progress will be published as received.
|
||||
Default: False.
|
||||
|
||||
Returns:
|
||||
When stream is True, return a generator publishing the service pull progress.
|
||||
If all_tags is True, return list of Image's rather than Image pulled.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
if tag is None or len(tag) == 0:
|
||||
tokens = repository.split(":")
|
||||
if len(tokens) == 2:
|
||||
repository = tokens[0]
|
||||
tag = tokens[1]
|
||||
else:
|
||||
tag = "latest"
|
||||
|
||||
auth_config: Optional[Dict[str, str]] = kwargs.get("auth_config")
|
||||
|
||||
headers = {
|
||||
# A base64url-encoded auth configuration
|
||||
"X-Registry-Auth": encode_auth_header(auth_config)
|
||||
if auth_config
|
||||
else ""
|
||||
}
|
||||
|
||||
params = {
|
||||
"reference": repository,
|
||||
"tlsVerify": kwargs.get("tls_verify"),
|
||||
}
|
||||
|
||||
if all_tags:
|
||||
params["allTags"] = True
|
||||
else:
|
||||
params["reference"] = f"{repository}:{tag}"
|
||||
|
||||
if "platform" in kwargs:
|
||||
tokens = kwargs.get("platform").split("/")
|
||||
if 1 < len(tokens) > 3:
|
||||
raise ValueError(f'\'{kwargs.get("platform")}\' is not a legal platform.')
|
||||
|
||||
params["OS"] = tokens[0]
|
||||
if len(tokens) > 1:
|
||||
params["Arch"] = tokens[1]
|
||||
if len(tokens) > 2:
|
||||
params["Variant"] = tokens[2]
|
||||
|
||||
stream = kwargs.get("stream", False)
|
||||
response = self.client.post("/images/pull", params=params, stream=stream, headers=headers)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
if stream:
|
||||
return response.iter_lines()
|
||||
|
||||
for item in response.iter_lines():
|
||||
obj = json.loads(item)
|
||||
if all_tags and "images" in obj:
|
||||
images: List[Image] = []
|
||||
for name in obj["images"]:
|
||||
images.append(self.get(name))
|
||||
return images
|
||||
|
||||
if "id" in obj:
|
||||
return self.get(obj["id"])
|
||||
return self.resource()
|
||||
|
||||
def remove(
|
||||
self,
|
||||
image: Union[Image, str],
|
||||
force: Optional[bool] = None,
|
||||
noprune: bool = False, # pylint: disable=unused-argument
|
||||
) -> List[Dict[Literal["Deleted", "Untagged", "Errors", "ExitCode"], Union[str, int]]]:
|
||||
"""Delete image from Podman service.
|
||||
|
||||
Args:
|
||||
image: Name or Id of Image to remove
|
||||
force: Delete Image even if in use
|
||||
noprune: Ignored.
|
||||
|
||||
Raises:
|
||||
ImageNotFound: when image does not exist
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
if isinstance(image, Image):
|
||||
image = image.id
|
||||
|
||||
response = self.client.delete(f"/images/{image}", params={"force": force})
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
body = response.json()
|
||||
results: List[Dict[str, Union[int, str]]] = []
|
||||
for key in ("Deleted", "Untagged", "Errors"):
|
||||
if key in body:
|
||||
for element in body[key]:
|
||||
results.append({key: element})
|
||||
results.append({"ExitCode": body["ExitCode"]})
|
||||
return results
|
||||
|
||||
def search(self, term: str, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""Search Images on registries.
|
||||
|
||||
Args:
|
||||
term: Used to target Image results.
|
||||
|
||||
Keyword Args:
|
||||
filters (Mapping[str, List[str]): Refine results of search. Available filters:
|
||||
|
||||
- is-automated (bool): Image build is automated.
|
||||
- is-official (bool): Image build is owned by product provider.
|
||||
- stars (int): Image has at least this number of stars.
|
||||
|
||||
noTrunc (bool): Do not truncate any result string. Default: True.
|
||||
limit (int): Maximum number of results.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
params = {
|
||||
"filters": api.prepare_filters(kwargs.get("filters")),
|
||||
"limit": kwargs.get("limit"),
|
||||
"noTrunc": True,
|
||||
"term": [term],
|
||||
}
|
||||
|
||||
response = self.client.get("/images/search", params=params)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
return response.json()
|
||||
|
||||
def scp(
|
||||
self,
|
||||
source: str,
|
||||
dest: Optional[str] = None,
|
||||
quiet: Optional[bool] = False,
|
||||
) -> str:
|
||||
"""Securely copy images between hosts.
|
||||
|
||||
Args:
|
||||
source: source connection/image
|
||||
dest: destination connection/image
|
||||
quiet: do not print save/load output, only the image
|
||||
|
||||
Returns:
|
||||
A string containing the loaded image
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
params = {}
|
||||
if dest is not None and quiet:
|
||||
params = {"destination": dest, "quiet": quiet}
|
||||
elif quiet:
|
||||
params = {"quiet": quiet}
|
||||
response = self.client.post(f"/images/scp/{source}", params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
60
venv/lib/python3.11/site-packages/podman/domain/ipam.py
Normal file
60
venv/lib/python3.11/site-packages/podman/domain/ipam.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Classes to support Internet Protocol Address Management.
|
||||
|
||||
Provided for compatibility
|
||||
"""
|
||||
from typing import Any, List, Mapping, Optional
|
||||
|
||||
|
||||
class IPAMPool(dict):
|
||||
"""Collect IP Network configuration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
subnet: Optional[str] = None,
|
||||
iprange: Optional[str] = None,
|
||||
gateway: Optional[str] = None,
|
||||
aux_addresses: Optional[Mapping[str, str]] = None,
|
||||
):
|
||||
"""Create IPAMPool.
|
||||
|
||||
Args:
|
||||
subnet: IP subnet in CIDR format for this network.
|
||||
iprange: IP range in CIDR format for endpoints on this network.
|
||||
gateway: IP gateway address for this network.
|
||||
aux_addresses: Ignored.
|
||||
"""
|
||||
super().__init__()
|
||||
self.update(
|
||||
{
|
||||
"AuxiliaryAddresses": aux_addresses,
|
||||
"Gateway": gateway,
|
||||
"IPRange": iprange,
|
||||
"Subnet": subnet,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class IPAMConfig(dict):
|
||||
"""Collect IP Address configuration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver: Optional[str] = "default",
|
||||
pool_configs: Optional[List[IPAMPool]] = None,
|
||||
options: Optional[Mapping[str, Any]] = None,
|
||||
):
|
||||
"""Create IPAMConfig.
|
||||
|
||||
Args:
|
||||
driver: Network driver to use with this network.
|
||||
pool_configs: Network and endpoint information. Podman only supports one pool.
|
||||
options: Options to provide to the Network driver.
|
||||
"""
|
||||
super().__init__()
|
||||
self.update(
|
||||
{
|
||||
"Config": pool_configs or [],
|
||||
"Driver": driver,
|
||||
"Options": options or {},
|
||||
}
|
||||
)
|
||||
121
venv/lib/python3.11/site-packages/podman/domain/manager.py
Normal file
121
venv/lib/python3.11/site-packages/podman/domain/manager.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""Base classes for PodmanResources and Manager's."""
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import abc
|
||||
from typing import Any, List, Mapping, Optional, TypeVar, Union
|
||||
|
||||
from podman.api.client import APIClient
|
||||
|
||||
# Methods use this Type when a subclass of PodmanResource is expected.
|
||||
PodmanResourceType: TypeVar = TypeVar("PodmanResourceType", bound="PodmanResource")
|
||||
|
||||
|
||||
class PodmanResource(ABC):
|
||||
"""Base class for representing resource of a Podman service.
|
||||
|
||||
Attributes:
|
||||
attrs: Mapping of attributes for resource from Podman service
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attrs: Optional[Mapping[str, Any]] = None,
|
||||
client: Optional[APIClient] = None,
|
||||
collection: Optional["Manager"] = None,
|
||||
):
|
||||
"""Initialize base class for PodmanResource's.
|
||||
|
||||
Args:
|
||||
attrs: Mapping of attributes for resource from Podman service.
|
||||
client: Configured connection to a Podman service.
|
||||
collection: Manager of this category of resource, named `collection` for compatibility
|
||||
"""
|
||||
super().__init__()
|
||||
self.client = client
|
||||
self.manager = collection
|
||||
|
||||
self.attrs = {}
|
||||
if attrs is not None:
|
||||
self.attrs.update(attrs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}: {self.short_id}>"
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, self.__class__) and self.id == other.id
|
||||
|
||||
def __hash__(self):
|
||||
return hash(f"{self.__class__.__name__}:{self.id}")
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""str: Returns the identifier for the object."""
|
||||
return self.attrs.get("Id")
|
||||
|
||||
@property
|
||||
def short_id(self):
|
||||
"""str: Returns truncated identifier. 'sha256' preserved when included in the id.
|
||||
|
||||
No attempt is made to ensure the returned value is semantically meaningful
|
||||
for all resources.
|
||||
"""
|
||||
if self.id.startswith("sha256:"):
|
||||
return self.id[:17]
|
||||
return self.id[:10]
|
||||
|
||||
def reload(self) -> None:
|
||||
"""Refresh this object's data from the service."""
|
||||
latest = self.manager.get(self.id)
|
||||
self.attrs = latest.attrs
|
||||
|
||||
|
||||
class Manager(ABC):
|
||||
"""Base class for representing a Manager of resources for a Podman service."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def resource(self):
|
||||
"""Type[PodmanResource]: Class which the factory method prepare_model() will use."""
|
||||
|
||||
def __init__(self, client: APIClient = None) -> None:
|
||||
"""Initialize Manager() object.
|
||||
|
||||
Args:
|
||||
client: APIClient() configured to connect to Podman service.
|
||||
"""
|
||||
super().__init__()
|
||||
self.client = client
|
||||
|
||||
@abstractmethod
|
||||
def exists(self, key: str) -> bool:
|
||||
"""Returns True if resource exists.
|
||||
|
||||
Podman only.
|
||||
|
||||
Notes:
|
||||
This method does _not_ provide any mutex mechanism.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get(self, key: str) -> PodmanResourceType:
|
||||
"""Returns representation of resource."""
|
||||
|
||||
@abstractmethod
|
||||
def list(self, **kwargs) -> List[PodmanResourceType]:
|
||||
"""Returns list of resources."""
|
||||
|
||||
def prepare_model(self, attrs: Union[PodmanResource, Mapping[str, Any]]) -> PodmanResourceType:
|
||||
"""Create a model from a set of attributes."""
|
||||
|
||||
# Refresh existing PodmanResource.
|
||||
if isinstance(attrs, PodmanResource):
|
||||
attrs.client = self.client
|
||||
attrs.collection = self
|
||||
return attrs
|
||||
|
||||
# Instantiate new PodmanResource from Mapping[str, Any]
|
||||
if isinstance(attrs, abc.Mapping):
|
||||
# TODO Determine why pylint is reporting typing.Type not callable
|
||||
# pylint: disable=not-callable
|
||||
return self.resource(attrs=attrs, client=self.client, collection=self)
|
||||
|
||||
raise Exception(f"Can't create {self.resource.__name__} from {attrs}")
|
||||
232
venv/lib/python3.11/site-packages/podman/domain/manifests.py
Normal file
232
venv/lib/python3.11/site-packages/podman/domain/manifests.py
Normal file
@ -0,0 +1,232 @@
|
||||
"""Model and Manager for Manifest resources."""
|
||||
import logging
|
||||
import urllib.parse
|
||||
from contextlib import suppress
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from podman import api
|
||||
from podman.domain.images import Image
|
||||
from podman.domain.manager import Manager, PodmanResource
|
||||
from podman.errors import ImageNotFound
|
||||
|
||||
logger = logging.getLogger("podman.manifests")
|
||||
|
||||
|
||||
class Manifest(PodmanResource):
|
||||
"""Details and configuration for a manifest managed by the Podman service."""
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""str: Returns the identifier of the manifest list."""
|
||||
with suppress(KeyError, TypeError, IndexError):
|
||||
digest = self.attrs["manifests"][0]["digest"]
|
||||
if digest.startswith("sha256:"):
|
||||
return digest[7:]
|
||||
return digest
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""str: Returns the human-formatted identifier of the manifest list."""
|
||||
return self.attrs.get("names")
|
||||
|
||||
@property
|
||||
def quoted_name(self):
|
||||
"""str: name quoted as path parameter."""
|
||||
return urllib.parse.quote_plus(self.name)
|
||||
|
||||
@property
|
||||
def names(self):
|
||||
"""List[str]: Returns the identifier of the manifest."""
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def media_type(self):
|
||||
"""Optional[str]: Returns the Media/MIME type for this manifest."""
|
||||
return self.attrs.get("mediaType")
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""int: Returns the schema version type for this manifest."""
|
||||
return self.attrs.get("schemaVersion")
|
||||
|
||||
def add(self, images: List[Union[Image, str]], **kwargs) -> None:
|
||||
"""Add Image to manifest list.
|
||||
|
||||
Args:
|
||||
images: List of Images to be added to manifest.
|
||||
|
||||
Keyword Args:
|
||||
all (bool):
|
||||
annotation (Dict[str, str]):
|
||||
arch (str):
|
||||
features (List[str]):
|
||||
os (str):
|
||||
os_version (str):
|
||||
variant (str):
|
||||
|
||||
Raises:
|
||||
ImageNotFound: when Image(s) could not be found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
data = {
|
||||
"all": kwargs.get("all"),
|
||||
"annotation": kwargs.get("annotation"),
|
||||
"arch": kwargs.get("arch"),
|
||||
"features": kwargs.get("features"),
|
||||
"images": [],
|
||||
"os": kwargs.get("os"),
|
||||
"os_version": kwargs.get("os_version"),
|
||||
"variant": kwargs.get("variant"),
|
||||
"operation": "update",
|
||||
}
|
||||
for item in images:
|
||||
if isinstance(item, Image):
|
||||
item = item.attrs["RepoTags"][0]
|
||||
data["images"].append(item)
|
||||
|
||||
data = api.prepare_body(data)
|
||||
response = self.client.put(f"/manifests/{self.quoted_name}", data=data)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
return self.reload()
|
||||
|
||||
def push(
|
||||
self,
|
||||
destination: str,
|
||||
all: Optional[bool] = None, # pylint: disable=redefined-builtin
|
||||
) -> None:
|
||||
"""Push a manifest list or image index to a registry.
|
||||
|
||||
Args:
|
||||
destination: Target for push.
|
||||
all: Push all images.
|
||||
|
||||
Raises:
|
||||
NotFound: when the Manifest could not be found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
params = {
|
||||
"all": all,
|
||||
"destination": destination,
|
||||
}
|
||||
response = self.client.post(f"/manifests/{self.quoted_name}/push", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
def remove(self, digest: str) -> None:
|
||||
"""Remove Image digest from manifest list.
|
||||
|
||||
Args:
|
||||
digest: Image digest to be removed. Should a full Image reference be provided,
|
||||
the digest will be parsed out.
|
||||
|
||||
Raises:
|
||||
ImageNotFound: when the Image could not be found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
if "@" in digest:
|
||||
digest = digest.split("@", maxsplit=2)[1]
|
||||
|
||||
data = {"operation": "remove", "images": [digest]}
|
||||
data = api.prepare_body(data)
|
||||
|
||||
response = self.client.put(f"/manifests/{self.quoted_name}", data=data)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
return self.reload()
|
||||
|
||||
def reload(self) -> None:
|
||||
"""Refresh this object's data from the service."""
|
||||
latest = self.manager.get(self.name)
|
||||
self.attrs = latest.attrs
|
||||
|
||||
|
||||
class ManifestsManager(Manager):
|
||||
"""Specialized Manager for Manifest resources."""
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Type[Manifest]: prepare_model() will create Manifest classes."""
|
||||
return Manifest
|
||||
|
||||
def create(
|
||||
self,
|
||||
name: str,
|
||||
images: Optional[List[Union[Image, str]]] = None,
|
||||
all: Optional[bool] = None, # pylint: disable=redefined-builtin
|
||||
) -> Manifest:
|
||||
"""Create a Manifest.
|
||||
|
||||
Args:
|
||||
name: Name of manifest list.
|
||||
images: Images or Image identifiers to be included in the manifest.
|
||||
all: When True, add all contents from images given.
|
||||
|
||||
Raises:
|
||||
ValueError: when no names are provided
|
||||
NotFoundImage: when a given image does not exist
|
||||
"""
|
||||
params: Dict[str, Any] = {}
|
||||
if images is not None:
|
||||
params["images"] = []
|
||||
for item in images:
|
||||
if isinstance(item, Image):
|
||||
item = item.attrs["RepoTags"][0]
|
||||
params["images"].append(item)
|
||||
|
||||
if all is not None:
|
||||
params["all"] = all
|
||||
|
||||
name_quoted = urllib.parse.quote_plus(name)
|
||||
response = self.client.post(f"/manifests/{name_quoted}", params=params)
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
body = response.json()
|
||||
manifest = self.get(body["Id"])
|
||||
manifest.attrs["names"] = name
|
||||
|
||||
if manifest.attrs["manifests"] is None:
|
||||
manifest.attrs["manifests"] = []
|
||||
return manifest
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
key = urllib.parse.quote_plus(key)
|
||||
response = self.client.get(f"/manifests/{key}/exists")
|
||||
return response.ok
|
||||
|
||||
def get(self, key: str) -> Manifest:
|
||||
"""Returns the manifest by name.
|
||||
|
||||
To have Manifest conform with other PodmanResource's, we use the key that
|
||||
retrieved the Manifest be its name.
|
||||
|
||||
Args:
|
||||
key: Manifest name for which to search
|
||||
|
||||
Raises:
|
||||
NotFound: when manifest could not be found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
quoted_key = urllib.parse.quote_plus(key)
|
||||
response = self.client.get(f"/manifests/{quoted_key}/json")
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.json()
|
||||
if "names" not in body:
|
||||
body["names"] = key
|
||||
return self.prepare_model(attrs=body)
|
||||
|
||||
def list(self, **kwargs) -> List[Manifest]:
|
||||
"""Not Implemented."""
|
||||
|
||||
raise NotImplementedError("Podman service currently does not support listing manifests.")
|
||||
|
||||
def remove(self, name: Union[Manifest, str]) -> Dict[str, Any]:
|
||||
"""Delete the manifest list from the Podman service."""
|
||||
if isinstance(name, Manifest):
|
||||
name = name.name
|
||||
|
||||
response = self.client.delete(f"/manifests/{name}")
|
||||
response.raise_for_status(not_found=ImageNotFound)
|
||||
|
||||
body = response.json()
|
||||
body["ExitCode"] = response.status_code
|
||||
return body
|
||||
144
venv/lib/python3.11/site-packages/podman/domain/networks.py
Normal file
144
venv/lib/python3.11/site-packages/podman/domain/networks.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""Model for Network resources.
|
||||
|
||||
Example:
|
||||
|
||||
with PodmanClient(base_url="unix:///run/user/1000/podman/podman.sock") as client:
|
||||
net = client.networks.get("db_network")
|
||||
print(net.name, "\n")
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from typing import Optional, Union
|
||||
|
||||
from podman.domain.containers import Container
|
||||
from podman.domain.containers_manager import ContainersManager
|
||||
from podman.domain.manager import PodmanResource
|
||||
|
||||
logger = logging.getLogger("podman.networks")
|
||||
|
||||
|
||||
class Network(PodmanResource):
|
||||
"""Details and configuration for a networks managed by the Podman service.
|
||||
|
||||
Attributes:
|
||||
attrs (Dict[str, Any]): Attributes of Network reported from Podman service
|
||||
"""
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""str: Returns the identifier of the network."""
|
||||
with suppress(KeyError):
|
||||
return self.attrs["Id"]
|
||||
|
||||
with suppress(KeyError):
|
||||
sha256 = hashlib.sha256(self.attrs["name"].encode("ascii"))
|
||||
return sha256.hexdigest()
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def containers(self):
|
||||
"""List[Container]: Returns list of Containers connected to network."""
|
||||
with suppress(KeyError):
|
||||
container_manager = ContainersManager(client=self.client)
|
||||
return [container_manager.get(ident) for ident in self.attrs["Containers"].keys()]
|
||||
return []
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""str: Returns the name of the network."""
|
||||
|
||||
if "Name" in self.attrs:
|
||||
return self.attrs["Name"]
|
||||
|
||||
if "name" in self.attrs:
|
||||
return self.attrs["name"]
|
||||
|
||||
raise KeyError("Neither 'name' or 'Name' attribute found.")
|
||||
|
||||
def reload(self):
|
||||
"""Refresh this object's data from the service."""
|
||||
latest = self.manager.get(self.name)
|
||||
self.attrs = latest.attrs
|
||||
|
||||
def connect(self, container: Union[str, Container], *_, **kwargs) -> None:
|
||||
"""Connect given container to this network.
|
||||
|
||||
Args:
|
||||
container: To add to this Network
|
||||
|
||||
Keyword Args:
|
||||
aliases (List[str]): Aliases to add for this endpoint
|
||||
driver_opt (Dict[str, Any]): Options to provide to network driver
|
||||
ipv4_address (str): IPv4 address for given Container on this network
|
||||
ipv6_address (str): IPv6 address for given Container on this network
|
||||
link_local_ips (List[str]): list of link-local addresses
|
||||
links (List[Union[str, Containers]]): Ignored
|
||||
|
||||
Raises:
|
||||
APIError: when Podman service reports an error
|
||||
"""
|
||||
if isinstance(container, Container):
|
||||
container = container.id
|
||||
|
||||
# TODO Talk with baude on which IPAddress field is needed...
|
||||
ipam = dict(
|
||||
IPv4Address=kwargs.get("ipv4_address"),
|
||||
IPv6Address=kwargs.get("ipv6_address"),
|
||||
Links=kwargs.get("link_local_ips"),
|
||||
)
|
||||
ipam = {k: v for (k, v) in ipam.items() if not (v is None or len(v) == 0)}
|
||||
|
||||
endpoint_config = dict(
|
||||
Aliases=kwargs.get("aliases"),
|
||||
DriverOpts=kwargs.get("driver_opt"),
|
||||
IPAddress=kwargs.get("ipv4_address", kwargs.get("ipv6_address")),
|
||||
IPAMConfig=ipam,
|
||||
Links=kwargs.get("link_local_ips"),
|
||||
NetworkID=self.id,
|
||||
)
|
||||
endpoint_config = {
|
||||
k: v for (k, v) in endpoint_config.items() if not (v is None or len(v) == 0)
|
||||
}
|
||||
|
||||
data = dict(Container=container, EndpointConfig=endpoint_config)
|
||||
data = {k: v for (k, v) in data.items() if not (v is None or len(v) == 0)}
|
||||
|
||||
response = self.client.post(
|
||||
f"/networks/{self.name}/connect",
|
||||
data=json.dumps(data),
|
||||
headers={"Content-type": "application/json"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def disconnect(self, container: Union[str, Container], **kwargs) -> None:
|
||||
"""Disconnect given container from this network.
|
||||
|
||||
Args:
|
||||
container: To remove from this Network
|
||||
|
||||
Keyword Args:
|
||||
force (bool): Force operation
|
||||
|
||||
Raises:
|
||||
APIError: when Podman service reports an error
|
||||
"""
|
||||
if isinstance(container, Container):
|
||||
container = container.id
|
||||
|
||||
data = {"Container": container, "Force": kwargs.get("force")}
|
||||
response = self.client.post(f"/networks/{self.name}/disconnect", data=json.dumps(data))
|
||||
response.raise_for_status()
|
||||
|
||||
def remove(self, force: Optional[bool] = None, **kwargs) -> None:
|
||||
"""Remove this network.
|
||||
|
||||
Args:
|
||||
force: Remove network and any associated containers
|
||||
|
||||
Raises:
|
||||
APIError: when Podman service reports an error
|
||||
"""
|
||||
self.manager.remove(self.name, force=force, **kwargs)
|
||||
@ -0,0 +1,203 @@
|
||||
"""PodmanResource manager subclassed for Network resources.
|
||||
|
||||
Classes and methods for manipulating network resources via Podman API service.
|
||||
|
||||
Example:
|
||||
|
||||
with PodmanClient(base_url="unix:///run/user/1000/podman/podman.sock") as client:
|
||||
for net in client.networks.list():
|
||||
print(net.id, "\n")
|
||||
"""
|
||||
import ipaddress
|
||||
import logging
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from podman import api
|
||||
from podman.api import http_utils
|
||||
from podman.domain.manager import Manager
|
||||
from podman.domain.networks import Network
|
||||
from podman.errors import APIError
|
||||
|
||||
logger = logging.getLogger("podman.networks")
|
||||
|
||||
|
||||
class NetworksManager(Manager):
|
||||
"""Specialized Manager for Network resources."""
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Type[Network]: prepare_model() will create Network classes."""
|
||||
return Network
|
||||
|
||||
def create(self, name: str, **kwargs) -> Network:
|
||||
"""Create a Network resource.
|
||||
|
||||
Args:
|
||||
name: Name of network to be created
|
||||
|
||||
Keyword Args:
|
||||
attachable (bool): Ignored, always False.
|
||||
check_duplicate (bool): Ignored, always False.
|
||||
dns_enabled (bool): When True, do not provision DNS for this network.
|
||||
driver (str): Which network driver to use when creating network.
|
||||
enable_ipv6 (bool): Enable IPv6 on the network.
|
||||
ingress (bool): Ignored, always False.
|
||||
internal (bool): Restrict external access to the network.
|
||||
ipam (IPAMConfig): Optional custom IP scheme for the network.
|
||||
labels (Dict[str, str]): Map of labels to set on the network.
|
||||
options (Dict[str, Any]): Driver options.
|
||||
scope (str): Ignored, always "local".
|
||||
|
||||
Raises:
|
||||
APIError: when Podman service reports an error
|
||||
"""
|
||||
data = {
|
||||
"name": name,
|
||||
"driver": kwargs.get("driver"),
|
||||
"dns_enabled": kwargs.get("dns_enabled"),
|
||||
"subnets": kwargs.get("subnets"),
|
||||
"ipv6_enabled": kwargs.get("enable_ipv6"),
|
||||
"internal": kwargs.get("internal"),
|
||||
"labels": kwargs.get("labels"),
|
||||
"options": kwargs.get("options"),
|
||||
}
|
||||
|
||||
with suppress(KeyError):
|
||||
self._prepare_ipam(data, kwargs["ipam"])
|
||||
|
||||
response = self.client.post(
|
||||
"/networks/create",
|
||||
data=http_utils.prepare_body(data),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
sys.stderr.write(str(response.json()))
|
||||
return self.prepare_model(attrs=response.json())
|
||||
|
||||
def _prepare_ipam(self, data: Dict[str, Any], ipam: Dict[str, Any]):
|
||||
if "Config" not in ipam:
|
||||
return
|
||||
|
||||
data["subnets"] = []
|
||||
for cfg in ipam["Config"]:
|
||||
subnet = {
|
||||
"gateway": cfg.get("Gateway"),
|
||||
"subnet": cfg.get("Subnet"),
|
||||
}
|
||||
|
||||
with suppress(KeyError):
|
||||
net = ipaddress.ip_network(cfg["IPRange"])
|
||||
subnet["lease_range"] = {
|
||||
"start_ip": str(net[1]),
|
||||
"end_ip": str(net[-2]),
|
||||
}
|
||||
|
||||
data["subnets"].append(subnet)
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
response = self.client.get(f"/networks/{key}/exists")
|
||||
return response.ok
|
||||
|
||||
def get(self, key: str) -> Network:
|
||||
"""Return information for the network_id.
|
||||
|
||||
Args:
|
||||
key: Network name or id.
|
||||
|
||||
Raises:
|
||||
NotFound: when Network does not exist
|
||||
APIError: when error returned by service
|
||||
"""
|
||||
response = self.client.get(f"/networks/{key}")
|
||||
response.raise_for_status()
|
||||
|
||||
return self.prepare_model(attrs=response.json())
|
||||
|
||||
def list(self, **kwargs) -> List[Network]:
|
||||
"""Report on networks.
|
||||
|
||||
Keyword Args:
|
||||
names (List[str]): List of names to filter by.
|
||||
ids (List[str]): List of identifiers to filter by.
|
||||
filters (Mapping[str,str]): Criteria for listing networks. Available filters:
|
||||
|
||||
- driver="bridge": Matches a network's driver. Only "bridge" is supported.
|
||||
- label=(Union[str, List[str]]): format either "key", "key=value"
|
||||
or a list of such.
|
||||
- type=(str): Filters networks by type, legal values are:
|
||||
|
||||
- "custom"
|
||||
- "builtin"
|
||||
|
||||
- plugin=(List[str]]): Matches CNI plugins included in a network, legal
|
||||
values are (Podman only):
|
||||
|
||||
- bridge
|
||||
- portmap
|
||||
- firewall
|
||||
- tuning
|
||||
- dnsname
|
||||
- macvlan
|
||||
|
||||
greedy (bool): Fetch more details for each network individually.
|
||||
You might want this to get the containers attached to them. Ignored.
|
||||
|
||||
Raises:
|
||||
APIError: when error returned by service
|
||||
"""
|
||||
filters = kwargs.get("filters", {})
|
||||
filters["name"] = kwargs.get("names")
|
||||
filters["id"] = kwargs.get("ids")
|
||||
filters = api.prepare_filters(filters)
|
||||
|
||||
params = {"filters": filters}
|
||||
response = self.client.get("/networks/json", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
return [self.prepare_model(i) for i in response.json()]
|
||||
|
||||
def prune(
|
||||
self, filters: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[api.Literal["NetworksDeleted", "SpaceReclaimed"], Any]:
|
||||
"""Delete unused Networks.
|
||||
|
||||
SpaceReclaimed always reported as 0
|
||||
|
||||
Args:
|
||||
filters: Criteria for selecting volumes to delete. Ignored.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports error
|
||||
"""
|
||||
response = self.client.post("/networks/prune", filters=api.prepare_filters(filters))
|
||||
response.raise_for_status()
|
||||
|
||||
deleted: List[str] = []
|
||||
for item in response.json():
|
||||
if item["Error"] is not None:
|
||||
raise APIError(
|
||||
item["Error"],
|
||||
response=response,
|
||||
explanation=f"""Failed to prune network '{item["Name"]}'""",
|
||||
)
|
||||
deleted.append(item["Name"])
|
||||
|
||||
return {"NetworksDeleted": deleted, "SpaceReclaimed": 0}
|
||||
|
||||
def remove(self, name: [Network, str], force: Optional[bool] = None) -> None:
|
||||
"""Remove Network resource.
|
||||
|
||||
Args:
|
||||
name: Identifier of Network to delete.
|
||||
force: Remove network and any associated containers
|
||||
|
||||
Raises:
|
||||
APIError: when Podman service reports an error
|
||||
"""
|
||||
if isinstance(name, Network):
|
||||
name = name.name
|
||||
|
||||
response = self.client.delete(f"/networks/{name}", params={"force": force})
|
||||
response.raise_for_status()
|
||||
119
venv/lib/python3.11/site-packages/podman/domain/pods.py
Normal file
119
venv/lib/python3.11/site-packages/podman/domain/pods.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Model and Manager for Pod resources."""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from podman.domain.manager import PodmanResource
|
||||
|
||||
_Timeout = Union[None, float, Tuple[float, float], Tuple[float, None]]
|
||||
|
||||
logger = logging.getLogger("podman.pods")
|
||||
|
||||
|
||||
class Pod(PodmanResource):
|
||||
"""Details and configuration for a pod managed by the Podman service."""
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
return self.attrs.get("ID", self.attrs.get("Id"))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""str: Returns name of pod."""
|
||||
return self.attrs.get("Name")
|
||||
|
||||
def kill(self, signal: Union[str, int, None] = None) -> None:
|
||||
"""Send signal to pod.
|
||||
|
||||
Args:
|
||||
signal: To be sent to pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.post(f"/pods/{self.id}/kill", params={"signal": signal})
|
||||
response.raise_for_status()
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.post(f"/pods/{self.id}/pause")
|
||||
response.raise_for_status()
|
||||
|
||||
def remove(self, force: Optional[bool] = None) -> None:
|
||||
"""Delete pod.
|
||||
|
||||
Args:
|
||||
force: When True, stop and delete all containers in pod before deleting pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
self.manager.remove(self.id, force=force)
|
||||
|
||||
def restart(self) -> None:
|
||||
"""Restart pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.post(f"/pods/{self.id}/restart")
|
||||
response.raise_for_status()
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.post(f"/pods/{self.id}/start")
|
||||
response.raise_for_status()
|
||||
|
||||
def stop(self, timeout: _Timeout = None) -> None:
|
||||
"""Stop pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
params = {"t": timeout}
|
||||
response = self.client.post(f"/pods/{self.id}/stop", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
def top(self, **kwargs) -> Dict[str, Any]:
|
||||
"""Report on running processes in pod.
|
||||
|
||||
Keyword Args:
|
||||
ps_args (str): Optional arguments passed to ps.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
params = {
|
||||
"ps_args": kwargs.get("ps_args"),
|
||||
"stream": False,
|
||||
}
|
||||
response = self.client.get(f"/pods/{self.id}/top", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
if len(response.text) == 0:
|
||||
return {"Processes": [], "Titles": []}
|
||||
return response.json()
|
||||
|
||||
def unpause(self) -> None:
|
||||
"""Unpause pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.post(f"/pods/{self.id}/unpause")
|
||||
response.raise_for_status()
|
||||
151
venv/lib/python3.11/site-packages/podman/domain/pods_manager.py
Normal file
151
venv/lib/python3.11/site-packages/podman/domain/pods_manager.py
Normal file
@ -0,0 +1,151 @@
|
||||
"""PodmanResource manager subclassed for Networks."""
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from podman import api
|
||||
from podman.domain.manager import Manager
|
||||
from podman.domain.pods import Pod
|
||||
from podman.errors import APIError
|
||||
|
||||
logger = logging.getLogger("podman.pods")
|
||||
|
||||
|
||||
class PodsManager(Manager):
|
||||
"""Specialized Manager for Pod resources."""
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Type[Pod]: prepare_model() will create Pod classes."""
|
||||
return Pod
|
||||
|
||||
def create(self, name: str, **kwargs) -> Pod:
|
||||
"""Create a Pod.
|
||||
|
||||
Keyword Args:
|
||||
See (API documentation)[
|
||||
https://docs.podman.io/en/latest/_static/api.html#operation/CreatePod] for
|
||||
complete list of keywords.
|
||||
"""
|
||||
data = {} if kwargs is None else kwargs.copy()
|
||||
data["name"] = name
|
||||
|
||||
response = self.client.post("/pods/create", data=json.dumps(data))
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.json()
|
||||
return self.get(body["Id"])
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
"""Returns True, when pod exists."""
|
||||
response = self.client.get(f"/pods/{key}/exists")
|
||||
return response.ok
|
||||
|
||||
# pylint is flagging 'pod_id' here vs. 'key' parameter in super.get()
|
||||
def get(self, pod_id: str) -> Pod: # pylint: disable=arguments-differ,arguments-renamed
|
||||
"""Return information for Pod by name or id.
|
||||
|
||||
Args:
|
||||
pod_id: Pod name or id.
|
||||
|
||||
Raises:
|
||||
NotFound: when network does not exist
|
||||
APIError: when error returned by service
|
||||
"""
|
||||
response = self.client.get(f"/pods/{pod_id}/json")
|
||||
response.raise_for_status()
|
||||
return self.prepare_model(attrs=response.json())
|
||||
|
||||
def list(self, **kwargs) -> List[Pod]:
|
||||
"""Report on pods.
|
||||
|
||||
Keyword Args:
|
||||
filters (Mapping[str, str]): Criteria for listing pods. Available filters:
|
||||
|
||||
- ctr-ids (List[str]): List of container ids to filter by.
|
||||
- ctr-names (List[str]): List of container names to filter by.
|
||||
- ctr-number (List[int]): list pods with given number of containers.
|
||||
- ctr-status (List[str]): List pods with containers in given state.
|
||||
Legal values are: "created", "running", "paused", "stopped",
|
||||
"exited", or "unknown"
|
||||
- id (str) - List pod with this id.
|
||||
- name (str) - List pod with this name.
|
||||
- status (List[str]): List pods in given state. Legal values are:
|
||||
"created", "running", "paused", "stopped", "exited", or "unknown"
|
||||
- label (List[str]): List pods with given labels.
|
||||
- network (List[str]): List pods associated with given Network Ids (not Names).
|
||||
|
||||
Raises:
|
||||
APIError: when an error returned by service
|
||||
"""
|
||||
params = {"filters": api.prepare_filters(kwargs.get("filters"))}
|
||||
response = self.client.get("/pods/json", params=params)
|
||||
response.raise_for_status()
|
||||
return [self.prepare_model(attrs=i) for i in response.json()]
|
||||
|
||||
def prune(self, filters: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
||||
"""Delete unused Pods.
|
||||
|
||||
Returns:
|
||||
Dictionary Keys:
|
||||
- PodsDeleted (List[str]): List of pod ids deleted.
|
||||
- SpaceReclaimed (int): Always zero.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports error
|
||||
"""
|
||||
response = self.client.post("/pods/prune", params={"filters": api.prepare_filters(filters)})
|
||||
response.raise_for_status()
|
||||
|
||||
deleted: List[str] = []
|
||||
for item in response.json():
|
||||
if item["Err"] is not None:
|
||||
raise APIError(
|
||||
item["Err"],
|
||||
response=response,
|
||||
explanation=f"""Failed to prune network '{item["Id"]}'""",
|
||||
)
|
||||
deleted.append(item["Id"])
|
||||
return {"PodsDeleted": deleted, "SpaceReclaimed": 0}
|
||||
|
||||
def remove(self, pod_id: Union[Pod, str], force: Optional[bool] = None) -> None:
|
||||
"""Delete pod.
|
||||
|
||||
Args:
|
||||
pod_id: Identifier of Pod to delete.
|
||||
force: When True, stop and delete all containers in pod before deleting pod.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
|
||||
Notes:
|
||||
Podman only.
|
||||
"""
|
||||
if isinstance(pod_id, Pod):
|
||||
pod_id = pod_id.id
|
||||
|
||||
response = self.client.delete(f"/pods/{pod_id}", params={"force": force})
|
||||
response.raise_for_status()
|
||||
|
||||
def stats(self, **kwargs) -> Dict[str, Any]:
|
||||
"""Resource usage statistics for the containers in pods.
|
||||
|
||||
Keyword Args:
|
||||
all (bool): Provide statistics for all running pods.
|
||||
name (Union[str, List[str]]): Pods to include in report.
|
||||
|
||||
Raises:
|
||||
NotFound: when pod not found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
if "all" in kwargs and "name" in kwargs:
|
||||
raise ValueError("Keywords 'all' and 'name' are mutually exclusive.")
|
||||
|
||||
params = {
|
||||
"all": kwargs.get("all"),
|
||||
"namesOrIDs": kwargs.get("name"),
|
||||
}
|
||||
response = self.client.get("/pods/stats", params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@ -0,0 +1,86 @@
|
||||
"""Module for tracking registry metadata."""
|
||||
import logging
|
||||
from typing import Any, Mapping, Optional, Union
|
||||
|
||||
from podman import api
|
||||
from podman.domain.images import Image
|
||||
from podman.domain.manager import PodmanResource
|
||||
from podman.errors import InvalidArgument
|
||||
|
||||
logger = logging.getLogger("podman.images")
|
||||
|
||||
|
||||
class RegistryData(PodmanResource):
|
||||
"""Registry metadata about Image."""
|
||||
|
||||
def __init__(self, image_name: str, *args, **kwargs) -> None:
|
||||
"""Initialize RegistryData object.
|
||||
|
||||
Args:
|
||||
image_name: Name of Image.
|
||||
|
||||
Keyword Args:
|
||||
client (APIClient): Configured connection to a Podman service.
|
||||
collection (Manager): Manager of this category of resource,
|
||||
named `collection` for compatibility
|
||||
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.image_name = image_name
|
||||
|
||||
self.attrs = kwargs.get("attrs")
|
||||
if self.attrs is None:
|
||||
self.attrs = self.manager.get(image_name).attrs
|
||||
|
||||
def pull(self, platform: Optional[str] = None) -> Image:
|
||||
"""Returns Image pulled by identifier.
|
||||
|
||||
Args:
|
||||
platform: Platform for which to pull Image. Default: None (all platforms.)
|
||||
"""
|
||||
repository = api.parse_repository(self.image_name)
|
||||
return self.manager.pull(repository, tag=self.id, platform=platform)
|
||||
|
||||
def has_platform(self, platform: Union[str, Mapping[str, Any]]) -> bool:
|
||||
"""Returns True if platform is available for Image.
|
||||
|
||||
Podman API does not support "variant" therefore it is ignored.
|
||||
|
||||
Args:
|
||||
platform: Name as os[/arch[/variant]] or Mapping[str,Any]
|
||||
|
||||
Returns:
|
||||
True if platform is available
|
||||
|
||||
Raises:
|
||||
InvalidArgument: when platform value is not valid
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
invalid_platform = InvalidArgument(f"'{platform}' is not a valid platform descriptor.")
|
||||
|
||||
if platform is None:
|
||||
platform = {}
|
||||
|
||||
if isinstance(platform, dict):
|
||||
if not {"os", "architecture"} <= platform.keys():
|
||||
version = self.client.version()
|
||||
platform["os"] = platform.get("os", version["Os"])
|
||||
platform["architecture"] = platform.get("architecture", version["Arch"])
|
||||
elif isinstance(platform, str):
|
||||
elements = platform.split("/")
|
||||
if 1 < len(elements) > 3:
|
||||
raise invalid_platform
|
||||
|
||||
platform = {"os": elements[0]}
|
||||
if len(elements) > 2:
|
||||
platform["variant"] = elements[2]
|
||||
if len(elements) > 1:
|
||||
platform["architecture"] = elements[1]
|
||||
else:
|
||||
raise invalid_platform
|
||||
|
||||
return (
|
||||
# Variant not carried in libpod attrs
|
||||
platform["os"] == self.attrs["Os"]
|
||||
and platform["architecture"] == self.attrs["Architecture"]
|
||||
)
|
||||
139
venv/lib/python3.11/site-packages/podman/domain/secrets.py
Normal file
139
venv/lib/python3.11/site-packages/podman/domain/secrets.py
Normal file
@ -0,0 +1,139 @@
|
||||
"""Model and Manager for Secrets resources."""
|
||||
from contextlib import suppress
|
||||
from typing import Any, List, Mapping, Optional, Union
|
||||
|
||||
from podman.api import APIClient
|
||||
from podman.domain.manager import Manager, PodmanResource
|
||||
|
||||
|
||||
class Secret(PodmanResource):
|
||||
"""Details and configuration for a secret registered with the Podman service."""
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}: {self.name}>"
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
return self.attrs.get("ID")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""str: name of the secret."""
|
||||
with suppress(KeyError):
|
||||
return self.attrs['Spec']['Name']
|
||||
return ""
|
||||
|
||||
def remove(
|
||||
self,
|
||||
all: Optional[bool] = None, # pylint: disable=redefined-builtin
|
||||
):
|
||||
"""Delete secret.
|
||||
|
||||
Args:
|
||||
all: When True, delete all secrets.
|
||||
|
||||
Raises:
|
||||
NotFound: when Secret does not exist
|
||||
APIError: when error returned by service
|
||||
"""
|
||||
self.manager.remove(self.id, all=all)
|
||||
|
||||
|
||||
class SecretsManager(Manager):
|
||||
"""Specialized Manager for Secret resources."""
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Type[Secret]: prepare_model() will create Secret classes."""
|
||||
return Secret
|
||||
|
||||
def __init__(self, client: APIClient):
|
||||
"""Initialize SecretsManager object.
|
||||
|
||||
Args:
|
||||
client: Connection to Podman service.
|
||||
"""
|
||||
super().__init__(client)
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
response = self.client.get(f"/secrets/{key}/json")
|
||||
return response.ok
|
||||
|
||||
# pylint is flagging 'secret_id' here vs. 'key' parameter in super.get()
|
||||
def get(self, secret_id: str) -> Secret: # pylint: disable=arguments-differ,arguments-renamed
|
||||
"""Return information for Secret by name or id.
|
||||
|
||||
Args:
|
||||
secret_id: Secret name or id.
|
||||
|
||||
Raises:
|
||||
NotFound: when Secret does not exist
|
||||
APIError: when error returned by service
|
||||
"""
|
||||
response = self.client.get(f"/secrets/{secret_id}/json")
|
||||
response.raise_for_status()
|
||||
return self.prepare_model(attrs=(response.json()))
|
||||
|
||||
def list(self, **kwargs) -> List[Secret]:
|
||||
"""Report on Secrets.
|
||||
|
||||
Keyword Args:
|
||||
filters (Dict[str, Any]): Ignored.
|
||||
|
||||
Raises:
|
||||
APIError: when error returned by service
|
||||
"""
|
||||
response = self.client.get("/secrets/json")
|
||||
response.raise_for_status()
|
||||
return [self.prepare_model(attrs=item) for item in response.json()]
|
||||
|
||||
def create(
|
||||
self,
|
||||
name: str,
|
||||
data: bytes,
|
||||
labels: Optional[Mapping[str, Any]] = None, # pylint: disable=unused-argument
|
||||
driver: Optional[str] = None,
|
||||
) -> Secret:
|
||||
"""Create a Secret.
|
||||
|
||||
Args:
|
||||
name: User-defined name of the secret.
|
||||
data: Secret to be registered with Podman service.
|
||||
labels: Ignored.
|
||||
driver: Secret driver.
|
||||
|
||||
Raises:
|
||||
APIError: when service returns an error
|
||||
"""
|
||||
params = {
|
||||
"name": name,
|
||||
"driver": driver,
|
||||
}
|
||||
response = self.client.post("/secrets/create", params=params, data=data)
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.json()
|
||||
return self.get(body["ID"])
|
||||
|
||||
def remove(
|
||||
self,
|
||||
secret_id: Union[Secret, str],
|
||||
all: Optional[bool] = None, # pylint: disable=redefined-builtin
|
||||
):
|
||||
"""Delete secret.
|
||||
|
||||
Podman only
|
||||
|
||||
Args:
|
||||
secret_id: Identifier of Secret to delete.
|
||||
all: When True, delete all secrets.
|
||||
|
||||
Raises:
|
||||
NotFound: when Secret does not exist
|
||||
APIError: when an error returned by service
|
||||
"""
|
||||
if isinstance(secret_id, Secret):
|
||||
secret_id = secret_id.id
|
||||
|
||||
response = self.client.delete(f"/secrets/{secret_id}", params={"all": all})
|
||||
response.raise_for_status()
|
||||
92
venv/lib/python3.11/site-packages/podman/domain/system.py
Normal file
92
venv/lib/python3.11/site-packages/podman/domain/system.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""SystemManager to provide system level information from Podman service."""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from podman.api.client import APIClient
|
||||
from podman import api
|
||||
|
||||
logger = logging.getLogger("podman.system")
|
||||
|
||||
|
||||
class SystemManager:
|
||||
"""SystemManager to provide system level information from Podman service."""
|
||||
|
||||
def __init__(self, client: APIClient) -> None:
|
||||
"""Initialize SystemManager object.
|
||||
|
||||
Args:
|
||||
client: Connection to Podman service.
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
def df(self) -> Dict[str, Any]: # pylint: disable=invalid-name
|
||||
"""Disk usage by Podman resources.
|
||||
|
||||
Returns:
|
||||
dict: Keyed by resource categories and their data usage.
|
||||
"""
|
||||
response = self.client.get("/system/df")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def info(self, *_, **__) -> Dict[str, Any]:
|
||||
"""Returns information on Podman service."""
|
||||
response = self.client.get("/info")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def login(
|
||||
self,
|
||||
username: str,
|
||||
password: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
registry: Optional[str] = None,
|
||||
reauth: Optional[bool] = False, # pylint: disable=unused-argument
|
||||
dockercfg_path: Optional[str] = None, # pylint: disable=unused-argument
|
||||
) -> Dict[str, Any]:
|
||||
"""Log into Podman service.
|
||||
|
||||
Args:
|
||||
username: Registry username
|
||||
password: Registry plaintext password
|
||||
email: Registry account email address
|
||||
registry: URL for registry access. For example,
|
||||
reauth: Ignored: If True, refresh existing authentication. Default: False
|
||||
dockercfg_path: Ignored: Path to custom configuration file.
|
||||
https://quay.io/v2
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email,
|
||||
"serveraddress": registry,
|
||||
}
|
||||
payload = api.prepare_body(payload)
|
||||
response = self.client.post(
|
||||
path="/auth",
|
||||
headers={"Content-type": "application/json"},
|
||||
data=payload,
|
||||
compatible=True,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def ping(self) -> bool:
|
||||
"""Returns True if service responded with OK."""
|
||||
response = self.client.head("/_ping")
|
||||
return response.ok
|
||||
|
||||
def version(self, **kwargs) -> Dict[str, Any]:
|
||||
"""Get version information from service.
|
||||
|
||||
Keyword Args:
|
||||
api_version (bool): When True include API version
|
||||
"""
|
||||
response = self.client.get("/version")
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.json()
|
||||
if not kwargs.get("api_version", True):
|
||||
del body["APIVersion"]
|
||||
return body
|
||||
157
venv/lib/python3.11/site-packages/podman/domain/volumes.py
Normal file
157
venv/lib/python3.11/site-packages/podman/domain/volumes.py
Normal file
@ -0,0 +1,157 @@
|
||||
"""Model and Manager for Volume resources."""
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import requests
|
||||
|
||||
from podman import api
|
||||
from podman.api import Literal
|
||||
from podman.domain.manager import Manager, PodmanResource
|
||||
from podman.errors import APIError
|
||||
|
||||
logger = logging.getLogger("podman.volumes")
|
||||
|
||||
|
||||
class Volume(PodmanResource):
|
||||
"""Details and configuration for an image managed by the Podman service."""
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""str: Returns the name of the volume."""
|
||||
return self.attrs.get("Name")
|
||||
|
||||
def remove(self, force: Optional[bool] = None) -> None:
|
||||
"""Delete this volume.
|
||||
|
||||
Args:
|
||||
force: When true, force deletion of in-use volume
|
||||
|
||||
Raises:
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
self.manager.remove(self.name, force=force)
|
||||
|
||||
|
||||
class VolumesManager(Manager):
|
||||
"""Specialized Manager for Volume resources."""
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Type[Volume]: prepare_model() will create Volume classes."""
|
||||
return Volume
|
||||
|
||||
def create(self, name: Optional[str] = None, **kwargs) -> Volume:
|
||||
"""Create a Volume.
|
||||
|
||||
Args:
|
||||
name: Name given to new volume
|
||||
|
||||
Keyword Args:
|
||||
driver (str): Volume driver to use
|
||||
driver_opts (Dict[str, str]): Options to use with driver
|
||||
labels (Dict[str, str]): Labels to apply to volume
|
||||
|
||||
Raises:
|
||||
APIError: when service reports error
|
||||
"""
|
||||
data = {
|
||||
"Driver": kwargs.get("driver"),
|
||||
"Labels": kwargs.get("labels"),
|
||||
"Name": name,
|
||||
"Options": kwargs.get("driver_opts"),
|
||||
}
|
||||
response = self.client.post(
|
||||
"/volumes/create",
|
||||
data=api.prepare_body(data),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return self.prepare_model(attrs=(response.json()))
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
response = self.client.get(f"/volumes/{key}/exists")
|
||||
return response.ok
|
||||
|
||||
# pylint is flagging 'volume_id' here vs. 'key' parameter in super.get()
|
||||
def get(self, volume_id: str) -> Volume: # pylint: disable=arguments-differ,arguments-renamed
|
||||
"""Returns and volume by name or id.
|
||||
|
||||
Args:
|
||||
volume_id: Volume id or name for which to search
|
||||
|
||||
Raises:
|
||||
NotFound: when volume could not be found
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
response = self.client.get(f"/volumes/{volume_id}/json")
|
||||
response.raise_for_status()
|
||||
return self.prepare_model(attrs=response.json())
|
||||
|
||||
def list(self, *_, **kwargs) -> List[Volume]:
|
||||
"""Report on volumes.
|
||||
|
||||
Keyword Args:
|
||||
filters (Dict[str, str]): criteria to filter Volume list
|
||||
|
||||
- driver (str): filter volumes by their driver
|
||||
- label (Dict[str, str]): filter by label and/or value
|
||||
- name (str): filter by volume's name
|
||||
"""
|
||||
filters = api.prepare_filters(kwargs.get("filters"))
|
||||
response = self.client.get("/volumes/json", params={"filters": filters})
|
||||
|
||||
if response.status_code == requests.codes.not_found:
|
||||
return []
|
||||
response.raise_for_status()
|
||||
|
||||
return [self.prepare_model(i) for i in response.json()]
|
||||
|
||||
def prune(
|
||||
self, filters: Optional[Dict[str, str]] = None # pylint: disable=unused-argument
|
||||
) -> Dict[Literal["VolumesDeleted", "SpaceReclaimed"], Any]:
|
||||
"""Delete unused volumes.
|
||||
|
||||
Args:
|
||||
filters: Criteria for selecting volumes to delete. Ignored.
|
||||
|
||||
Raises:
|
||||
APIError: when service reports error
|
||||
"""
|
||||
response = self.client.post("/volumes/prune")
|
||||
data = response.json()
|
||||
response.raise_for_status()
|
||||
|
||||
volumes: List[str] = []
|
||||
space_reclaimed = 0
|
||||
for item in data:
|
||||
if "Err" in item:
|
||||
raise APIError(
|
||||
item["Err"],
|
||||
response=response,
|
||||
explanation=f"""Failed to prune volume '{item.get("Id")}'""",
|
||||
)
|
||||
volumes.append(item.get("Id"))
|
||||
space_reclaimed += item["Size"]
|
||||
|
||||
return {"VolumesDeleted": volumes, "SpaceReclaimed": space_reclaimed}
|
||||
|
||||
def remove(self, name: Union[Volume, str], force: Optional[bool] = None) -> None:
|
||||
"""Delete a volume.
|
||||
|
||||
Podman only.
|
||||
|
||||
Args:
|
||||
name: Identifier for Volume to be deleted.
|
||||
force: When true, force deletion of in-use volume
|
||||
|
||||
Raises:
|
||||
APIError: when service reports an error
|
||||
"""
|
||||
if isinstance(name, Volume):
|
||||
name = name.name
|
||||
response = self.client.delete(f"/volumes/{name}", params={"force": force})
|
||||
response.raise_for_status()
|
||||
112
venv/lib/python3.11/site-packages/podman/errors/__init__.py
Normal file
112
venv/lib/python3.11/site-packages/podman/errors/__init__.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""Podman API errors Package.
|
||||
|
||||
Import exceptions from 'importlib' are used to differentiate between APIConnection
|
||||
and PodmanClient errors. Therefore, installing both APIConnection and PodmanClient
|
||||
is not supported. PodmanClient related errors take precedence over APIConnection ones.
|
||||
|
||||
ApiConnection and associated classes have been deprecated.
|
||||
"""
|
||||
import warnings
|
||||
from http.client import HTTPException
|
||||
|
||||
# isort: unique-list
|
||||
__all__ = [
|
||||
'APIError',
|
||||
'BuildError',
|
||||
'ContainerError',
|
||||
'DockerException',
|
||||
'ImageNotFound',
|
||||
'InvalidArgument',
|
||||
'NotFound',
|
||||
'NotFoundError',
|
||||
'PodmanError',
|
||||
]
|
||||
|
||||
try:
|
||||
from .exceptions import (
|
||||
APIError,
|
||||
BuildError,
|
||||
ContainerError,
|
||||
DockerException,
|
||||
InvalidArgument,
|
||||
NotFound,
|
||||
PodmanError,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(HTTPException):
|
||||
"""HTTP request returned a http.HTTPStatus.NOT_FOUND.
|
||||
|
||||
Deprecated.
|
||||
"""
|
||||
|
||||
def __init__(self, message, response=None):
|
||||
super().__init__(message)
|
||||
self.response = response
|
||||
warnings.warn("APIConnection() and supporting classes.", PendingDeprecationWarning)
|
||||
|
||||
|
||||
# If found, use new ImageNotFound otherwise old class
|
||||
try:
|
||||
from .exceptions import ImageNotFound
|
||||
except ImportError:
|
||||
|
||||
class ImageNotFound(NotFoundError):
|
||||
"""HTTP request returned a http.HTTPStatus.NOT_FOUND.
|
||||
|
||||
Specialized for Image not found. Deprecated.
|
||||
"""
|
||||
|
||||
|
||||
class NetworkNotFound(NotFoundError):
|
||||
"""Network request returned a http.HTTPStatus.NOT_FOUND.
|
||||
|
||||
Deprecated.
|
||||
"""
|
||||
|
||||
|
||||
class ContainerNotFound(NotFoundError):
|
||||
"""HTTP request returned a http.HTTPStatus.NOT_FOUND.
|
||||
|
||||
Specialized for Container not found. Deprecated.
|
||||
"""
|
||||
|
||||
|
||||
class PodNotFound(NotFoundError):
|
||||
"""HTTP request returned a http.HTTPStatus.NOT_FOUND.
|
||||
|
||||
Specialized for Pod not found. Deprecated.
|
||||
"""
|
||||
|
||||
|
||||
class ManifestNotFound(NotFoundError):
|
||||
"""HTTP request returned a http.HTTPStatus.NOT_FOUND.
|
||||
|
||||
Specialized for Manifest not found. Deprecated.
|
||||
"""
|
||||
|
||||
|
||||
class RequestError(HTTPException):
|
||||
"""Podman service reported issue with the request.
|
||||
|
||||
Deprecated.
|
||||
"""
|
||||
|
||||
def __init__(self, message, response=None):
|
||||
super().__init__(message)
|
||||
self.response = response
|
||||
warnings.warn("APIConnection() and supporting classes.", PendingDeprecationWarning)
|
||||
|
||||
|
||||
class InternalServerError(HTTPException):
|
||||
"""Podman service reported an internal error.
|
||||
|
||||
Deprecated.
|
||||
"""
|
||||
|
||||
def __init__(self, message, response=None):
|
||||
super().__init__(message)
|
||||
self.response = response
|
||||
warnings.warn("APIConnection() and supporting classes.", PendingDeprecationWarning)
|
||||
Binary file not shown.
Binary file not shown.
144
venv/lib/python3.11/site-packages/podman/errors/exceptions.py
Normal file
144
venv/lib/python3.11/site-packages/podman/errors/exceptions.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""Podman API Errors."""
|
||||
import typing
|
||||
from typing import Iterable, List, Optional, Union
|
||||
|
||||
from requests import Response
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
# Break circular import
|
||||
if typing.TYPE_CHECKING:
|
||||
from podman.domain.containers import Container
|
||||
from podman.api.client import APIResponse
|
||||
|
||||
|
||||
class APIError(HTTPError):
|
||||
"""Wraps HTTP errors for processing by the API and clients."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
response: Union[Response, "APIResponse", None] = None,
|
||||
explanation: Optional[str] = None,
|
||||
):
|
||||
"""Initialize APIError.
|
||||
|
||||
Args:
|
||||
message: Message from service. Default: response.text, may be enhanced or wrapped by
|
||||
bindings
|
||||
response: HTTP Response from service.
|
||||
explanation: An enhanced or wrapped version of message with additional context.
|
||||
"""
|
||||
super().__init__(message, response=response)
|
||||
self.explanation = explanation
|
||||
|
||||
def __str__(self):
|
||||
msg = super().__str__()
|
||||
|
||||
if self.response is not None:
|
||||
msg = self.response.reason
|
||||
|
||||
if self.is_client_error():
|
||||
msg = f"{self.status_code} Client Error: {msg}"
|
||||
|
||||
elif self.is_server_error():
|
||||
msg = f"{self.status_code} Server Error: {msg}"
|
||||
|
||||
if self.explanation:
|
||||
msg = f"{msg} ({self.explanation})"
|
||||
|
||||
return msg
|
||||
|
||||
@property
|
||||
def status_code(self):
|
||||
"""Optional[int]: HTTP status code from response."""
|
||||
if self.response is not None:
|
||||
return self.response.status_code
|
||||
return None
|
||||
|
||||
def is_error(self) -> bool:
|
||||
"""Returns True when HTTP operation resulted in an error."""
|
||||
return self.is_client_error() or self.is_server_error()
|
||||
|
||||
def is_client_error(self) -> bool:
|
||||
"""Returns True when request is incorrect."""
|
||||
return 400 <= (self.status_code or 0) < 500
|
||||
|
||||
def is_server_error(self) -> bool:
|
||||
"""Returns True when error occurred in service."""
|
||||
return 500 <= (self.status_code or 0) < 600
|
||||
|
||||
|
||||
class NotFound(APIError):
|
||||
"""Resource not found on Podman service.
|
||||
|
||||
Named for compatibility.
|
||||
"""
|
||||
|
||||
|
||||
class ImageNotFound(APIError):
|
||||
"""Image not found on Podman service."""
|
||||
|
||||
|
||||
class DockerException(Exception):
|
||||
"""Base class for exception hierarchy.
|
||||
|
||||
Provided for compatibility.
|
||||
"""
|
||||
|
||||
|
||||
class PodmanError(DockerException):
|
||||
"""Base class for PodmanPy exceptions."""
|
||||
|
||||
|
||||
class BuildError(PodmanError):
|
||||
"""Error occurred during build operation."""
|
||||
|
||||
def __init__(self, reason: str, build_log: Iterable[str]) -> None:
|
||||
"""Initialize BuildError.
|
||||
|
||||
Args:
|
||||
reason: describes the error
|
||||
build_log: build log output
|
||||
"""
|
||||
super().__init__(reason)
|
||||
self.msg = reason
|
||||
self.build_log = build_log
|
||||
|
||||
|
||||
class ContainerError(PodmanError):
|
||||
"""Represents a container that has exited with a non-zero exit code."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
container: "Container",
|
||||
exit_status: int,
|
||||
command: Union[str, List[str]],
|
||||
image: str,
|
||||
stderr: Optional[Iterable[str]] = None,
|
||||
):
|
||||
"""Initialize ContainerError.
|
||||
|
||||
Args:
|
||||
container: Container that reported error.
|
||||
exit_status: Non-zero status code from Container exit.
|
||||
command: Command passed to container when created.
|
||||
image: Name of image that was used to create container.
|
||||
stderr: Errors reported by Container.
|
||||
"""
|
||||
err = f": {stderr}" if stderr is not None else ""
|
||||
msg = (
|
||||
f"Command '{command}' in image '{image}' returned non-zero exit "
|
||||
f"status {exit_status}{err}"
|
||||
)
|
||||
|
||||
super().__init__(msg)
|
||||
|
||||
self.container = container
|
||||
self.exit_status: int = exit_status
|
||||
self.command = command
|
||||
self.image = image
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
class InvalidArgument(PodmanError):
|
||||
"""Parameter to method/function was not valid."""
|
||||
28
venv/lib/python3.11/site-packages/podman/tlsconfig.py
Normal file
28
venv/lib/python3.11/site-packages/podman/tlsconfig.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""Holds TLS configuration."""
|
||||
|
||||
|
||||
class TLSConfig:
|
||||
"""TLS configuration.
|
||||
|
||||
Provided for compatibility, currently ignored.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize TLSConfig.
|
||||
|
||||
Keywords may be delegated to the SSH client configuration.
|
||||
|
||||
Keyword Args:
|
||||
client_cert (tuple of str): Path to client cert, path to client key.
|
||||
ca_cert (str): Path to CA cert file.
|
||||
verify (bool or str): This can be False, or a path to a CA cert file.
|
||||
ssl_version (int): Ignored.
|
||||
assert_hostname (bool): Verify the hostname of the server.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def configure_client(client) -> None:
|
||||
"""Add TLS configuration to the client."""
|
||||
# TODO Somehow work this into SSHAdapter(), if/when someone complains.
|
||||
4
venv/lib/python3.11/site-packages/podman/version.py
Normal file
4
venv/lib/python3.11/site-packages/podman/version.py
Normal file
@ -0,0 +1,4 @@
|
||||
"""Version of PodmanPy."""
|
||||
|
||||
__version__ = "4.3.0"
|
||||
__compatible_version__ = "1.40"
|
||||
Reference in New Issue
Block a user