Compare commits

...

3 Commits
main ... v4.0.1

Author SHA1 Message Date
Charlie Doern b500bcc7c3 bump to v4.0.1
bumping version numbers to v4.0.1, tag to come soon

Signed-off-by: Charlie Doern <cdoern@redhat.com>
2022-10-06 08:24:57 -04:00
Charlie Doern aaedfb82df backports for v4.0.0
add support in 4.0 for healthcheck on failure actions

Signed-off-by: Charlie Doern <cdoern@redhat.com>
2022-10-06 08:24:57 -04:00
Jhon Honce ff0a8a9711 Update CI to fail on Podman 4.0 errors
CI has been testing against containers/podman/main to report issues.
With the release of Podman 4.0, any errors are now failures.

* Makefile target "test" and "lint" updated to use tox covering
  python 3.6, 3.8, 3.9 and 3.10.
* Added black formatting check to Makefile target "lint"
* Source code changes made to satisfy "tox -e black-format" and
  "tox -e pylint"

Signed-off-by: Jhon Honce <jhonce@redhat.com>
2022-10-06 08:24:57 -04:00
36 changed files with 361 additions and 589 deletions

View File

@ -1,127 +0,0 @@
---
env:
DEST_BRANCH: "main"
GOPATH: "/var/tmp/go"
GOBIN: "${GOPATH}/bin"
GOCACHE: "${GOPATH}/cache"
GOSRC: "${GOPATH}/src/github.com/containers/podman"
CIRRUS_WORKING_DIR: "${GOPATH}/src/github.com/containers/podman-py"
SCRIPT_BASE: "./contrib/cirrus"
CIRRUS_SHELL: "/bin/bash"
HOME: "/root" # not set by default
####
#### Cache-image names to test with (double-quotes around names are critical)
####
FEDORA_NAME: "fedora-35"
PRIOR_FEDORA_NAME: "fedora-34"
UBUNTU_NAME: "ubuntu-2110"
# Google-cloud VM Images
IMAGE_SUFFIX: "c4764556961513472"
FEDORA_CACHE_IMAGE_NAME: "fedora-podman-py-${IMAGE_SUFFIX}"
gcp_credentials: ENCRYPTED[0c639039cdd3a9a93fac7746ea1bf366d432e5ff3303bf293e64a7ff38dee85fd445f71625fa5626dc438be2b8efe939]
# Default VM to use unless set or modified by task
gce_instance:
image_project: "libpod-218412"
zone: "us-central1-c" # Required by Cirrus for the time being
cpu: 2
memory: "4Gb"
disk: 200 # Required for performance reasons
image_name: "${FEDORA_CACHE_IMAGE_NAME}"
gating_task:
name: "Gating test"
alias: gating
# Only run this on PRs, never during post-merge testing. This is also required
# for proper setting of EPOCH_TEST_COMMIT value, required by validation tools.
only_if: $CIRRUS_PR != ""
timeout_in: 20m
env:
PATH: ${PATH}:${GOPATH}/bin
script:
- make
- make lint
test_task:
name: "Test on $FEDORA_NAME"
alias: test
depends_on:
- gating
script:
- ${SCRIPT_BASE}/enable_ssh.sh
- ${SCRIPT_BASE}/enable_podman.sh
- ${SCRIPT_BASE}/test.sh
latest_task:
name: "Test Podman main on $FEDORA_NAME"
alias: latest
allow_failures: true
depends_on:
- gating
env:
PATH: ${PATH}:${GOPATH}/bin
script:
- ${SCRIPT_BASE}/enable_ssh.sh
- ${SCRIPT_BASE}/build_podman.sh
- ${SCRIPT_BASE}/enable_podman.sh
- ${SCRIPT_BASE}/test.sh
# This task is critical. It updates the "last-used by" timestamp stored
# in metadata for all VM images. This mechanism functions in tandem with
# an out-of-band pruning operation to remove disused VM images.
meta_task:
alias: meta
name: "VM img. keepalive"
container: &smallcontainer
image: "quay.io/libpod/imgts:$IMAGE_SUFFIX"
cpu: 1
memory: 1
env:
IMGNAMES: ${FEDORA_CACHE_IMAGE_NAME}
BUILDID: "${CIRRUS_BUILD_ID}"
REPOREF: "${CIRRUS_REPO_NAME}"
GCPJSON: ENCRYPTED[e8a53772eff6e86bf6b99107b6e6ee3216e2ca00c36252ae3bd8cb29d9b903ffb2e1a1322ea810ca251b04f833b8f8d9]
GCPNAME: ENCRYPTED[fb878daf188d35c2ed356dc777267d99b59863ff3abf0c41199d562fca50ba0668fdb0d87e109c9eaa2a635d2825feed]
GCPPROJECT: "libpod-218412"
clone_script: &noop mkdir -p $CIRRUS_WORKING_DIR
script: /usr/local/bin/entrypoint.sh
# Status aggregator for all tests. This task simply ensures a defined
# set of tasks all passed, and allows confirming that based on the status
# of this task.
success_task:
name: "Total Success"
alias: success
# N/B: ALL tasks must be listed here, minus their '_task' suffix.
depends_on:
- meta
- gating
- test
- latest
container:
image: quay.io/libpod/alpine:latest
cpu: 1
memory: 1
env:
CIRRUS_SHELL: "/bin/sh"
clone_script: *noop
script: *noop

View File

@ -11,7 +11,9 @@ ignore=CVS,docs
# Add files or directories matching the regex patterns to the blacklist. The # Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths. # regex matches against base names, not paths.
ignore-patterns=test_.* # ignore-patterns=test_.*
ignore-paths=^podman/tests/.*$
# Python code to execute, usually for sys.path manipulation such as # Python code to execute, usually for sys.path manipulation such as
# pygtk.require(). # pygtk.require().

View File

@ -8,25 +8,23 @@ DESTDIR ?=
EPOCH_TEST_COMMIT ?= $(shell git merge-base $${DEST_BRANCH:-main} HEAD) EPOCH_TEST_COMMIT ?= $(shell git merge-base $${DEST_BRANCH:-main} HEAD)
HEAD ?= HEAD HEAD ?= HEAD
export PODMAN_VERSION ?= "3.2.0" export PODMAN_VERSION ?= "4.0.1"
.PHONY: podman .PHONY: podman
podman: podman:
rm dist/* || : rm dist/* || :
python -m pip install --user -r requirements.txt $(PYTHON) -m pip install --user -r requirements.txt
PODMAN_VERSION=$(PODMAN_VERSION) \ PODMAN_VERSION=$(PODMAN_VERSION) \
$(PYTHON) setup.py sdist bdist bdist_wheel $(PYTHON) setup.py sdist bdist bdist_wheel
.PHONY: lint .PHONY: lint
lint: lint: tox
$(PYTHON) -m pylint podman || exit $$(($$? % 4)); $(PYTHON) -m tox -e black,pylint
.PHONY: tests .PHONY: tests
tests: tests: tox
python -m pip install --user -r test-requirements.txt # see tox.ini for environment variable settings
DEBUG=1 coverage run -m unittest discover -s podman/tests $(PYTHON) -m tox -e pylint,coverage,py36,py38,py39,py310
coverage report -m --skip-covered --fail-under=80 --omit=./podman/tests/* --omit=.tox/* \
--omit=/usr/lib/* --omit=*/lib/python*
.PHONY: unittest .PHONY: unittest
unittest: unittest:
@ -38,6 +36,12 @@ integration:
coverage run -m unittest discover -s podman/tests/integration coverage run -m unittest discover -s podman/tests/integration
coverage report -m --skip-covered --fail-under=80 --omit=./podman/tests/* --omit=.tox/* --omit=/usr/lib/* coverage report -m --skip-covered --fail-under=80 --omit=./podman/tests/* --omit=.tox/* --omit=/usr/lib/*
.PHONY: tox
tox:
-dnf install -y python3 python3.6 python3.8 python3.9
# ensure tox is available. It will take care of other testing requirements
$(PYTHON) -m pip install --user tox
.PHONY: test-release .PHONY: test-release
test-release: SOURCE = $(shell find dist -regex '.*/podman-[0-9][0-9\.]*.tar.gz' -print) test-release: SOURCE = $(shell find dist -regex '.*/podman-[0-9][0-9\.]*.tar.gz' -print)
test-release: test-release:

View File

@ -2,13 +2,9 @@
set -xeo pipefail set -xeo pipefail
mkdir -p "$GOPATH/src/github.com/containers/" systemctl stop podman.socket || :
cd "$GOPATH/src/github.com/containers/"
systemctl stop podman.socket ||:
dnf erase podman -y dnf erase podman -y
git clone https://github.com/containers/podman.git dnf copr enable rhcontainerbot/podman-next -y
dnf install podman -y
cd podman
make binaries
make install PREFIX=/usr

View File

@ -2,6 +2,4 @@
set -eo pipefail set -eo pipefail
make tests make tests

View File

@ -84,7 +84,7 @@ def create_tar(
return None return None
# Workaround https://bugs.python.org/issue32713. Fixed in Python 3.7 # Workaround https://bugs.python.org/issue32713. Fixed in Python 3.7
if info.mtime < 0 or info.mtime > 8 ** 11 - 1: if info.mtime < 0 or info.mtime > 8**11 - 1:
info.mtime = int(info.mtime) info.mtime = int(info.mtime)
# do not leak client information to service # do not leak client information to service
@ -97,9 +97,8 @@ def create_tar(
return info return info
if name is None: if name is None:
name = tempfile.NamedTemporaryFile( # pylint: disable=consider-using-with
prefix="podman_context", suffix=".tar" name = tempfile.NamedTemporaryFile(prefix="podman_context", suffix=".tar")
) # pylint: disable=consider-using-with
else: else:
name = pathlib.Path(name) name = pathlib.Path(name)

View File

@ -884,7 +884,6 @@ elif _geqv_defined:
return collections.deque(*args, **kwds) return collections.deque(*args, **kwds)
return _generic_new(collections.deque, cls, *args, **kwds) return _generic_new(collections.deque, cls, *args, **kwds)
else: else:
class Deque( class Deque(
@ -912,7 +911,6 @@ elif hasattr(contextlib, 'AbstractContextManager'):
): ):
__slots__ = () __slots__ = ()
else: else:
class ContextManager(typing.Generic[T_co]): class ContextManager(typing.Generic[T_co]):
@ -994,7 +992,6 @@ elif _geqv_defined:
return collections.defaultdict(*args, **kwds) return collections.defaultdict(*args, **kwds)
return _generic_new(collections.defaultdict, cls, *args, **kwds) return _generic_new(collections.defaultdict, cls, *args, **kwds)
else: else:
class DefaultDict( class DefaultDict(
@ -1032,7 +1029,6 @@ elif _geqv_defined:
return collections.OrderedDict(*args, **kwds) return collections.OrderedDict(*args, **kwds)
return _generic_new(collections.OrderedDict, cls, *args, **kwds) return _generic_new(collections.OrderedDict, cls, *args, **kwds)
else: else:
class OrderedDict( class OrderedDict(
@ -1073,7 +1069,6 @@ elif (3, 5, 0) <= sys.version_info[:3] <= (3, 5, 1):
return collections.Counter(*args, **kwds) return collections.Counter(*args, **kwds)
return _generic_new(collections.Counter, cls, *args, **kwds) return _generic_new(collections.Counter, cls, *args, **kwds)
elif _geqv_defined: elif _geqv_defined:
class Counter( class Counter(
@ -1090,7 +1085,6 @@ elif _geqv_defined:
return collections.Counter(*args, **kwds) return collections.Counter(*args, **kwds)
return _generic_new(collections.Counter, cls, *args, **kwds) return _generic_new(collections.Counter, cls, *args, **kwds)
else: else:
class Counter( class Counter(
@ -1353,9 +1347,7 @@ elif HAVE_PROTOCOLS and not PEP_560:
bases = tuple(b for b in bases if b is not Generic) bases = tuple(b for b in bases if b is not Generic)
namespace.update({'__origin__': origin, '__extra__': extra}) namespace.update({'__origin__': origin, '__extra__': extra})
self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, _root=True) self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, _root=True)
super(GenericMeta, self).__setattr__( super(GenericMeta, self).__setattr__('_gorg', self if not origin else _gorg(origin))
'_gorg', self if not origin else _gorg(origin)
)
self.__parameters__ = tvars self.__parameters__ = tvars
self.__args__ = ( self.__args__ = (
tuple( tuple(
@ -1479,9 +1471,7 @@ elif HAVE_PROTOCOLS and not PEP_560:
if not isinstance(params, tuple): if not isinstance(params, tuple):
params = (params,) params = (params,)
if not params and _gorg(self) is not Tuple: if not params and _gorg(self) is not Tuple:
raise TypeError( raise TypeError("Parameter list to %s[...] cannot be empty" % self.__qualname__)
"Parameter list to %s[...] cannot be empty" % self.__qualname__
)
msg = "Parameters to generic types must be types." msg = "Parameters to generic types must be types."
params = tuple(_type_check(p, msg) for p in params) params = tuple(_type_check(p, msg) for p in params)
if self in (Generic, Protocol): if self in (Generic, Protocol):
@ -2108,7 +2098,6 @@ elif PEP_560:
return hint return hint
return {k: _strip_annotations(t) for k, t in hint.items()} return {k: _strip_annotations(t) for k, t in hint.items()}
elif HAVE_ANNOTATED: elif HAVE_ANNOTATED:
def _is_dunder(name): def _is_dunder(name):
@ -2344,7 +2333,6 @@ elif sys.version_info[:2] >= (3, 9):
""" """
raise TypeError("{} is not subscriptable".format(self)) raise TypeError("{} is not subscriptable".format(self))
elif sys.version_info[:2] >= (3, 7): elif sys.version_info[:2] >= (3, 7):
class _TypeAliasForm(typing._SpecialForm, _root=True): class _TypeAliasForm(typing._SpecialForm, _root=True):
@ -2672,7 +2660,6 @@ elif sys.version_info[:2] >= (3, 9):
""" """
return _concatenate_getitem(self, parameters) return _concatenate_getitem(self, parameters)
elif sys.version_info[:2] >= (3, 7): elif sys.version_info[:2] >= (3, 7):
class _ConcatenateForm(typing._SpecialForm, _root=True): class _ConcatenateForm(typing._SpecialForm, _root=True):
@ -2821,7 +2808,6 @@ elif sys.version_info[:2] >= (3, 9):
item = typing._type_check(parameters, '{} accepts only single type.'.format(self)) item = typing._type_check(parameters, '{} accepts only single type.'.format(self))
return _GenericAlias(self, (item,)) return _GenericAlias(self, (item,))
elif sys.version_info[:2] >= (3, 7): elif sys.version_info[:2] >= (3, 7):
class _TypeGuardForm(typing._SpecialForm, _root=True): class _TypeGuardForm(typing._SpecialForm, _root=True):

View File

@ -74,6 +74,7 @@ class CreateMixin: # pylint: disable=too-few-public-methods
process will run as. process will run as.
healthcheck (Dict[str,Any]): Specify a test to perform to check that the healthcheck (Dict[str,Any]): Specify a test to perform to check that the
container is healthy. container is healthy.
health_check_on_failure_action (int): Specify an action if a healthcheck fails.
hostname (str): Optional hostname for the container. hostname (str): Optional hostname for the container.
init (bool): Run an init inside the container that forwards signals and reaps processes init (bool): Run an init inside the container that forwards signals and reaps processes
init_path (str): Path to the docker-init binary init_path (str): Path to the docker-init binary
@ -310,8 +311,7 @@ class CreateMixin: # pylint: disable=too-few-public-methods
if search: if search:
return int(search.group(1)) * (1024 ** mapping[search.group(2)]) return int(search.group(1)) * (1024 ** mapping[search.group(2)])
raise TypeError( raise TypeError(
f"Passed string size {size} should be in format\\d+[bBkKmMgG] (e.g." f"Passed string size {size} should be in format\\d+[bBkKmMgG] (e.g. '100m')"
" '100m')"
) from bad_size ) from bad_size
else: else:
raise TypeError( raise TypeError(
@ -341,6 +341,7 @@ class CreateMixin: # pylint: disable=too-few-public-methods
"expose": {}, "expose": {},
"groups": pop("group_add"), "groups": pop("group_add"),
"healthconfig": pop("healthcheck"), "healthconfig": pop("healthcheck"),
"health_check_on_failure_action": pop("health_check_on_failure_action"),
"hostadd": [], "hostadd": [],
"hostname": pop("hostname"), "hostname": pop("hostname"),
"httpproxy": pop("use_config_proxy"), "httpproxy": pop("use_config_proxy"),
@ -415,9 +416,7 @@ class CreateMixin: # pylint: disable=too-few-public-methods
if "Config" in args["log_config"]: if "Config" in args["log_config"]:
params["log_configuration"]["path"] = args["log_config"]["Config"].get("path") params["log_configuration"]["path"] = args["log_config"]["Config"].get("path")
params["log_configuration"]["size"] = args["log_config"]["Config"].get("size") params["log_configuration"]["size"] = args["log_config"]["Config"].get("size")
params["log_configuration"]["options"] = args["log_config"]["Config"].get( params["log_configuration"]["options"] = args["log_config"]["Config"].get("options")
"options"
)
args.pop("log_config") args.pop("log_config")
for item in args.pop("mounts", []): for item in args.pop("mounts", []):

View File

@ -25,10 +25,7 @@ class ContainersManager(RunMixin, CreateMixin, Manager):
response = self.client.get(f"/containers/{key}/exists") response = self.client.get(f"/containers/{key}/exists")
return response.ok return response.ok
# pylint is flagging 'container_id' here vs. 'key' parameter in super.get() def get(self, key: str) -> Container:
def get(
self, container_id: str
) -> Container: # pylint: disable=arguments-differ,arguments-renamed
"""Get container by name or id. """Get container by name or id.
Args: Args:
@ -38,7 +35,7 @@ class ContainersManager(RunMixin, CreateMixin, Manager):
NotFound: when Container does not exist NotFound: when Container does not exist
APIError: when an error return by service APIError: when an error return by service
""" """
container_id = urllib.parse.quote_plus(container_id) container_id = urllib.parse.quote_plus(key)
response = self.client.get(f"/containers/{container_id}/json") response = self.client.get(f"/containers/{container_id}/json")
response.raise_for_status() response.raise_for_status()
return self.prepare_model(attrs=response.json()) return self.prepare_model(attrs=response.json())

View File

@ -27,6 +27,7 @@ class ImagesManager(BuildMixin, Manager):
return Image return Image
def exists(self, key: str) -> bool: def exists(self, key: str) -> bool:
"""Return true when image exists."""
key = urllib.parse.quote_plus(key) key = urllib.parse.quote_plus(key)
response = self.client.get(f"/images/{key}/exists") response = self.client.get(f"/images/{key}/exists")
return response.ok return response.ok

View File

@ -2,7 +2,7 @@
import logging import logging
import urllib.parse import urllib.parse
from contextlib import suppress from contextlib import suppress
from typing import List, Optional, Union from typing import Any, Dict, List, Optional, Union
from podman import api from podman import api
from podman.domain.images import Image from podman.domain.images import Image
@ -17,20 +17,18 @@ class Manifest(PodmanResource):
@property @property
def id(self): def id(self):
"""str: Returns the identifier of the manifest.""" """str: Returns the identifier of the manifest list."""
with suppress(KeyError, TypeError, IndexError): with suppress(KeyError, TypeError, IndexError):
return self.attrs["manifests"][0]["digest"] digest = self.attrs["manifests"][0]["digest"]
if digest.startswith("sha256:"):
return digest[7:]
return digest
return self.name return self.name
@property @property
def name(self): def name(self):
"""str: Returns the identifier of the manifest.""" """str: Returns the human-formatted identifier of the manifest list."""
try: return self.attrs.get("names")
if len(self.names[0]) == 0:
raise ValueError("Manifest attribute 'names' is empty.")
return self.names[0]
except (TypeError, IndexError) as e:
raise ValueError("Manifest attribute 'names' is missing.") from e
@property @property
def quoted_name(self): def quoted_name(self):
@ -40,7 +38,7 @@ class Manifest(PodmanResource):
@property @property
def names(self): def names(self):
"""List[str]: Returns the identifier of the manifest.""" """List[str]: Returns the identifier of the manifest."""
return self.attrs.get("names") return self.name
@property @property
def media_type(self): def media_type(self):
@ -71,7 +69,7 @@ class Manifest(PodmanResource):
ImageNotFound: when Image(s) could not be found ImageNotFound: when Image(s) could not be found
APIError: when service reports an error APIError: when service reports an error
""" """
params = { data = {
"all": kwargs.get("all"), "all": kwargs.get("all"),
"annotation": kwargs.get("annotation"), "annotation": kwargs.get("annotation"),
"arch": kwargs.get("arch"), "arch": kwargs.get("arch"),
@ -80,14 +78,15 @@ class Manifest(PodmanResource):
"os": kwargs.get("os"), "os": kwargs.get("os"),
"os_version": kwargs.get("os_version"), "os_version": kwargs.get("os_version"),
"variant": kwargs.get("variant"), "variant": kwargs.get("variant"),
"operation": "update",
} }
for item in images: for item in images:
if isinstance(item, Image): if isinstance(item, Image):
item = item.attrs["RepoTags"][0] item = item.attrs["RepoTags"][0]
params["images"].append(item) data["images"].append(item)
data = api.prepare_body(params) data = api.prepare_body(data)
response = self.client.post(f"/manifests/{self.quoted_name}/add", data=data) response = self.client.put(f"/manifests/{self.quoted_name}", data=data)
response.raise_for_status(not_found=ImageNotFound) response.raise_for_status(not_found=ImageNotFound)
return self.reload() return self.reload()
@ -127,7 +126,10 @@ class Manifest(PodmanResource):
if "@" in digest: if "@" in digest:
digest = digest.split("@", maxsplit=2)[1] digest = digest.split("@", maxsplit=2)[1]
response = self.client.delete(f"/manifests/{self.quoted_name}", params={"digest": digest}) 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) response.raise_for_status(not_found=ImageNotFound)
return self.reload() return self.reload()
@ -147,14 +149,14 @@ class ManifestsManager(Manager):
def create( def create(
self, self,
names: List[str], name: str,
images: Optional[List[Union[Image, str]]] = None, images: Optional[List[Union[Image, str]]] = None,
all: Optional[bool] = None, # pylint: disable=redefined-builtin all: Optional[bool] = None, # pylint: disable=redefined-builtin
) -> Manifest: ) -> Manifest:
"""Create a Manifest. """Create a Manifest.
Args: Args:
names: Identifiers to be added to the manifest. There must be at least one. name: Name of manifest list.
images: Images or Image identifiers to be included in the manifest. images: Images or Image identifiers to be included in the manifest.
all: When True, add all contents from images given. all: When True, add all contents from images given.
@ -162,26 +164,24 @@ class ManifestsManager(Manager):
ValueError: when no names are provided ValueError: when no names are provided
NotFoundImage: when a given image does not exist NotFoundImage: when a given image does not exist
""" """
if names is None or len(names) == 0: params: Dict[str, Any] = {}
raise ValueError("At least one manifest name is required.")
params = {"name": names}
if images is not None: if images is not None:
params["image"] = [] params["images"] = []
for item in images: for item in images:
if isinstance(item, Image): if isinstance(item, Image):
item = item.attrs["RepoTags"][0] item = item.attrs["RepoTags"][0]
params["image"].append(item) params["images"].append(item)
if all is not None: if all is not None:
params["all"] = all params["all"] = all
response = self.client.post("/manifests/create", params=params) name_quoted = urllib.parse.quote_plus(name)
response = self.client.post(f"/manifests/{name_quoted}", params=params)
response.raise_for_status(not_found=ImageNotFound) response.raise_for_status(not_found=ImageNotFound)
body = response.json() body = response.json()
manifest = self.get(body["Id"]) manifest = self.get(body["Id"])
manifest.attrs["names"] = names manifest.attrs["names"] = name
if manifest.attrs["manifests"] is None: if manifest.attrs["manifests"] is None:
manifest.attrs["manifests"] = [] manifest.attrs["manifests"] = []
@ -198,9 +198,6 @@ class ManifestsManager(Manager):
To have Manifest conform with other PodmanResource's, we use the key that To have Manifest conform with other PodmanResource's, we use the key that
retrieved the Manifest be its name. retrieved the Manifest be its name.
See https://issues.redhat.com/browse/RUN-1217 for details on refactoring Podman service
manifests API.
Args: Args:
key: Manifest name for which to search key: Manifest name for which to search
@ -213,10 +210,23 @@ class ManifestsManager(Manager):
response.raise_for_status() response.raise_for_status()
body = response.json() body = response.json()
body["names"] = [key] if "names" not in body:
body["names"] = key
return self.prepare_model(attrs=body) return self.prepare_model(attrs=body)
def list(self, **kwargs) -> List[Manifest]: def list(self, **kwargs) -> List[Manifest]:
"""Not Implemented.""" """Not Implemented."""
raise NotImplementedError("Podman service currently does not support listing manifests.") 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

View File

@ -1,8 +1,10 @@
"""Model and Manager for Network resources. """Model for Network resources.
By default, most methods in this module uses the Podman compatible API rather than the Example:
libpod API as the results are so different. To use the libpod API add the keyword argument
compatible=False to any method call. 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 hashlib
import json import json
@ -42,11 +44,12 @@ class Network(PodmanResource):
with suppress(KeyError): with suppress(KeyError):
container_manager = ContainersManager(client=self.client) container_manager = ContainersManager(client=self.client)
return [container_manager.get(ident) for ident in self.attrs["Containers"].keys()] return [container_manager.get(ident) for ident in self.attrs["Containers"].keys()]
return {} return []
@property @property
def name(self): def name(self):
"""str: Returns the name of the network.""" """str: Returns the name of the network."""
if "Name" in self.attrs: if "Name" in self.attrs:
return self.attrs["Name"] return self.attrs["Name"]
@ -68,7 +71,6 @@ class Network(PodmanResource):
Keyword Args: Keyword Args:
aliases (List[str]): Aliases to add for this endpoint aliases (List[str]): Aliases to add for this endpoint
compatible (bool): Should compatible API be used. Default: True
driver_opt (Dict[str, Any]): Options to provide to network driver driver_opt (Dict[str, Any]): Options to provide to network driver
ipv4_address (str): IPv4 address for given Container on this network ipv4_address (str): IPv4 address for given Container on this network
ipv6_address (str): IPv6 address for given Container on this network ipv6_address (str): IPv6 address for given Container on this network
@ -78,8 +80,6 @@ class Network(PodmanResource):
Raises: Raises:
APIError: when Podman service reports an error APIError: when Podman service reports an error
""" """
compatible = kwargs.get("compatible", True)
if isinstance(container, Container): if isinstance(container, Container):
container = container.id container = container.id
@ -110,7 +110,6 @@ class Network(PodmanResource):
f"/networks/{self.name}/connect", f"/networks/{self.name}/connect",
data=json.dumps(data), data=json.dumps(data),
headers={"Content-type": "application/json"}, headers={"Content-type": "application/json"},
compatible=compatible,
) )
response.raise_for_status() response.raise_for_status()
@ -126,15 +125,11 @@ class Network(PodmanResource):
Raises: Raises:
APIError: when Podman service reports an error APIError: when Podman service reports an error
""" """
compatible = kwargs.get("compatible", True)
if isinstance(container, Container): if isinstance(container, Container):
container = container.id container = container.id
data = {"Container": container, "Force": kwargs.get("force")} data = {"Container": container, "Force": kwargs.get("force")}
response = self.client.post( response = self.client.post(f"/networks/{self.name}/disconnect", data=json.dumps(data))
f"/networks/{self.name}/disconnect", data=json.dumps(data), compatible=compatible
)
response.raise_for_status() response.raise_for_status()
def remove(self, force: Optional[bool] = None, **kwargs) -> None: def remove(self, force: Optional[bool] = None, **kwargs) -> None:
@ -143,9 +138,6 @@ class Network(PodmanResource):
Args: Args:
force: Remove network and any associated containers force: Remove network and any associated containers
Keyword Args:
compatible (bool): Should compatible API be used. Default: True
Raises: Raises:
APIError: when Podman service reports an error APIError: when Podman service reports an error
""" """

View File

@ -1,16 +1,21 @@
"""PodmanResource manager subclassed for Networks. """PodmanResource manager subclassed for Network resources.
By default, most methods in this module uses the Podman compatible API rather than the Classes and methods for manipulating network resources via Podman API service.
libpod API as the results are so different. To use the libpod API add the keyword argument
compatible=False to any method call. 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 ipaddress
import logging import logging
import sys
from contextlib import suppress from contextlib import suppress
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import podman.api.http_utils
from podman import api from podman import api
from podman.api import http_utils
from podman.domain.manager import Manager from podman.domain.manager import Manager
from podman.domain.networks import Network from podman.domain.networks import Network
from podman.errors import APIError from podman.errors import APIError
@ -27,7 +32,7 @@ class NetworksManager(Manager):
return Network return Network
def create(self, name: str, **kwargs) -> Network: def create(self, name: str, **kwargs) -> Network:
"""Create a Network. """Create a Network resource.
Args: Args:
name: Name of network to be created name: Name of network to be created
@ -35,14 +40,13 @@ class NetworksManager(Manager):
Keyword Args: Keyword Args:
attachable (bool): Ignored, always False. attachable (bool): Ignored, always False.
check_duplicate (bool): Ignored, always False. check_duplicate (bool): Ignored, always False.
disabled_dns (bool): When True, do not provision DNS for this network. dns_enabled (bool): When True, do not provision DNS for this network.
driver (str): Which network driver to use when creating network. driver (str): Which network driver to use when creating network.
enable_ipv6 (bool): Enable IPv6 on the network. enable_ipv6 (bool): Enable IPv6 on the network.
ingress (bool): Ignored, always False. ingress (bool): Ignored, always False.
internal (bool): Restrict external access to the network. internal (bool): Restrict external access to the network.
ipam (IPAMConfig): Optional custom IP scheme for the network. ipam (IPAMConfig): Optional custom IP scheme for the network.
labels (Dict[str, str]): Map of labels to set on the network. labels (Dict[str, str]): Map of labels to set on the network.
macvlan (str):
options (Dict[str, Any]): Driver options. options (Dict[str, Any]): Driver options.
scope (str): Ignored, always "local". scope (str): Ignored, always "local".
@ -50,85 +54,66 @@ class NetworksManager(Manager):
APIError: when Podman service reports an error APIError: when Podman service reports an error
""" """
data = { data = {
"DisabledDNS": kwargs.get("disabled_dns"), "name": name,
"Driver": kwargs.get("driver"), "driver": kwargs.get("driver"),
"Internal": kwargs.get("internal"), "dns_enabled": kwargs.get("dns_enabled"),
"IPv6": kwargs.get("enable_ipv6"), "subnets": kwargs.get("subnets"),
"Labels": kwargs.get("labels"), "ipv6_enabled": kwargs.get("enable_ipv6"),
"MacVLAN": kwargs.get("macvlan"), "internal": kwargs.get("internal"),
"Options": kwargs.get("options"), "labels": kwargs.get("labels"),
"options": kwargs.get("options"),
} }
with suppress(KeyError): with suppress(KeyError):
ipam = kwargs["ipam"] self._prepare_ipam(data, kwargs["ipam"])
if len(ipam["Config"]) > 0:
if len(ipam["Config"]) > 1:
raise ValueError("Podman service only supports one IPAM config.")
ip_config = ipam["Config"][0]
data["Gateway"] = ip_config.get("Gateway")
if "IPRange" in ip_config:
iprange = ipaddress.ip_network(ip_config["IPRange"])
iprange, mask = api.prepare_cidr(iprange)
data["Range"] = {
"IP": iprange,
"Mask": mask,
}
if "Subnet" in ip_config:
subnet = ipaddress.ip_network(ip_config["Subnet"])
subnet, mask = api.prepare_cidr(subnet)
data["Subnet"] = {
"IP": subnet,
"Mask": mask,
}
response = self.client.post( response = self.client.post(
"/networks/create", "/networks/create",
params={"name": name}, data=http_utils.prepare_body(data),
data=podman.api.http_utils.prepare_body(data),
headers={"Content-Type": "application/json"}, headers={"Content-Type": "application/json"},
) )
response.raise_for_status() response.raise_for_status()
sys.stderr.write(str(response.json()))
return self.prepare_model(attrs=response.json())
return self.get(name, **kwargs) 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: def exists(self, key: str) -> bool:
response = self.client.get(f"/networks/{key}/exists") response = self.client.get(f"/networks/{key}/exists")
return response.ok return response.ok
# pylint is flagging 'network_id' here vs. 'key' parameter in super.get() def get(self, key: str) -> Network:
def get(self, network_id: str, *_, **kwargs) -> Network: # pylint: disable=arguments-differ
"""Return information for the network_id. """Return information for the network_id.
Args: Args:
network_id: Network name or id. key: Network name or id.
Keyword Args:
compatible (bool): Should compatible API be used. Default: True
Raises: Raises:
NotFound: when Network does not exist NotFound: when Network does not exist
APIError: when error returned by service APIError: when error returned by service
Note:
The compatible API is used, this allows the server to provide dynamic fields.
id is the most important example.
""" """
compatible = kwargs.get("compatible", True) response = self.client.get(f"/networks/{key}")
path = f"/networks/{network_id}" + ("" if compatible else "/json")
response = self.client.get(path, compatible=compatible)
response.raise_for_status() response.raise_for_status()
body = response.json() return self.prepare_model(attrs=response.json())
if not compatible:
body = body[0]
return self.prepare_model(attrs=body)
def list(self, **kwargs) -> List[Network]: def list(self, **kwargs) -> List[Network]:
"""Report on networks. """Report on networks.
@ -162,23 +147,19 @@ class NetworksManager(Manager):
Raises: Raises:
APIError: when error returned by service APIError: when error returned by service
""" """
compatible = kwargs.get("compatible", True)
filters = kwargs.get("filters", {}) filters = kwargs.get("filters", {})
filters["name"] = kwargs.get("names") filters["name"] = kwargs.get("names")
filters["id"] = kwargs.get("ids") filters["id"] = kwargs.get("ids")
filters = api.prepare_filters(filters) filters = api.prepare_filters(filters)
params = {"filters": filters} params = {"filters": filters}
path = f"/networks{'' if compatible else '/json'}" response = self.client.get("/networks/json", params=params)
response = self.client.get(path, params=params, compatible=compatible)
response.raise_for_status() response.raise_for_status()
return [self.prepare_model(i) for i in response.json()] return [self.prepare_model(i) for i in response.json()]
def prune( def prune(
self, filters: Optional[Dict[str, Any]] = None, **kwargs self, filters: Optional[Dict[str, Any]] = None
) -> Dict[api.Literal["NetworksDeleted", "SpaceReclaimed"], Any]: ) -> Dict[api.Literal["NetworksDeleted", "SpaceReclaimed"], Any]:
"""Delete unused Networks. """Delete unused Networks.
@ -187,25 +168,14 @@ class NetworksManager(Manager):
Args: Args:
filters: Criteria for selecting volumes to delete. Ignored. filters: Criteria for selecting volumes to delete. Ignored.
Keyword Args:
compatible (bool): Should compatible API be used. Default: True
Raises: Raises:
APIError: when service reports error APIError: when service reports error
""" """
compatible = kwargs.get("compatible", True) response = self.client.post("/networks/prune", filters=api.prepare_filters(filters))
response = self.client.post(
"/networks/prune", filters=api.prepare_filters(filters), compatible=compatible
)
response.raise_for_status() response.raise_for_status()
body = response.json()
if compatible:
return body
deleted: List[str] = [] deleted: List[str] = []
for item in body: for item in response.json():
if item["Error"] is not None: if item["Error"] is not None:
raise APIError( raise APIError(
item["Error"], item["Error"],
@ -216,27 +186,18 @@ class NetworksManager(Manager):
return {"NetworksDeleted": deleted, "SpaceReclaimed": 0} return {"NetworksDeleted": deleted, "SpaceReclaimed": 0}
def remove(self, name: [Network, str], force: Optional[bool] = None, **kwargs) -> None: def remove(self, name: [Network, str], force: Optional[bool] = None) -> None:
"""Remove this network. """Remove Network resource.
Args: Args:
name: Identifier of Network to delete. name: Identifier of Network to delete.
force: Remove network and any associated containers force: Remove network and any associated containers
Keyword Args:
compatible (bool): Should compatible API be used. Default: True
Raises: Raises:
APIError: when Podman service reports an error APIError: when Podman service reports an error
Notes:
Podman only.
""" """
if isinstance(name, Network): if isinstance(name, Network):
name = name.name name = name.name
compatible = kwargs.get("compatible", True) response = self.client.delete(f"/networks/{name}", params={"force": force})
response = self.client.delete(
f"/networks/{name}", params={"force": force}, compatible=compatible
)
response.raise_for_status() response.raise_for_status()

View File

@ -1,6 +1,6 @@
"""Model and Manager for Pod resources.""" """Model and Manager for Pod resources."""
import logging import logging
from typing import Any, Dict, Tuple, Union, Optional from typing import Any, Dict, Optional, Tuple, Union
from podman.domain.manager import PodmanResource from podman.domain.manager import PodmanResource
@ -104,6 +104,8 @@ class Pod(PodmanResource):
response = self.client.get(f"/pods/{self.id}/top", params=params) response = self.client.get(f"/pods/{self.id}/top", params=params)
response.raise_for_status() response.raise_for_status()
if len(response.text) == 0:
return {"Processes": [], "Titles": []}
return response.json() return response.json()
def unpause(self) -> None: def unpause(self) -> None:

View File

@ -94,9 +94,7 @@ class PodsManager(Manager):
Raises: Raises:
APIError: when service reports error APIError: when service reports error
""" """
response = self.client.post( response = self.client.post("/pods/prune", params={"filters": api.prepare_filters(filters)})
"/pods/prune", params={"filters": api.prepare_filters(filters)}
)
response.raise_for_status() response.raise_for_status()
deleted: List[str] = [] deleted: List[str] = []

View File

@ -3,5 +3,5 @@
# Do not auto-update these from version.py, # Do not auto-update these from version.py,
# as test code should be changed to reflect changes in Podman API versions # as test code should be changed to reflect changes in Podman API versions
BASE_SOCK = "unix:///run/api.sock" BASE_SOCK = "unix:///run/api.sock"
LIBPOD_URL = "http://%2Frun%2Fapi.sock/v3.2.1/libpod" LIBPOD_URL = "http://%2Frun%2Fapi.sock/v4.0.0/libpod"
COMPATIBLE_URL = "http://%2Frun%2Fapi.sock/v1.40" COMPATIBLE_URL = "http://%2Frun%2Fapi.sock/v1.40"

View File

@ -37,24 +37,22 @@ class IntegrationTest(fixtures.TestWithFixtures):
@classmethod @classmethod
def setUpClass(cls) -> None: def setUpClass(cls) -> None:
super(fixtures.TestWithFixtures, cls).setUpClass()
command = os.environ.get("PODMAN_BINARY", "podman") command = os.environ.get("PODMAN_BINARY", "podman")
if shutil.which(command) is None: if shutil.which(command) is None:
raise AssertionError(f"'{command}' not found.") raise AssertionError(f"'{command}' not found.")
IntegrationTest.podman = command IntegrationTest.podman = command
# For testing, lock in logging configuration # This log_level is for our python code
if "DEBUG" in os.environ: log_level = os.environ.get("PODMAN_LOG_LEVEL", "INFO")
logging.basicConfig(level=logging.DEBUG) log_level = logging.getLevelName(log_level)
else: logging.basicConfig(level=log_level)
logging.basicConfig(level=logging.INFO)
def setUp(self): def setUp(self):
super().setUp() super().setUp()
# This is the log_level to pass to podman service self.log_level = os.environ.get("PODMAN_LOG_LEVEL", "INFO")
self.log_level = logging.WARNING
if "DEBUG" in os.environ:
self.log_level = logging.DEBUG
self.test_dir = self.useFixture(fixtures.TempDir()).path self.test_dir = self.useFixture(fixtures.TempDir()).path
self.socket_file = os.path.join(self.test_dir, uuid.uuid4().hex) self.socket_file = os.path.join(self.test_dir, uuid.uuid4().hex)

View File

@ -67,9 +67,13 @@ class ContainersIntegrationTest(base.IntegrationTest):
test['expected_value'], test['expected_value'],
) )
def test_container_kernel_memory(self): def test_container_healtchecks(self):
"""Test passing kernel memory""" """Test passing various healthcheck options"""
self._test_memory_limit('kernel_memory', 'KernelMemory') parameters = {}
parameters['healthcheck'] = {'Test': ['CMD-SHELL curl http://localhost || exit']}
parameters['health_check_on_failure_action'] = 1
container = self.client.containers.create(self.alpine_image, **parameters)
self.containers.append(container)
def test_container_mem_limit(self): def test_container_mem_limit(self):
"""Test passing memory limit""" """Test passing memory limit"""

View File

@ -105,9 +105,7 @@ class ContainersIntegrationTest(base.IntegrationTest):
self.assertIsInstance(logs_iter, Iterator) self.assertIsInstance(logs_iter, Iterator)
logs = list(logs_iter) logs = list(logs_iter)
self.assertIn(random_string.encode("utf-8"), logs) self.assertIn((random_string + "\n").encode("utf-8"), logs)
# podman 4.0 API support...
# self.assertIn((random_string + "\n").encode("utf-8"), logs)
with self.subTest("Delete Container"): with self.subTest("Delete Container"):
container.remove() container.remove()

View File

@ -25,23 +25,25 @@ class ManifestsIntegrationTest(base.IntegrationTest):
self.client.images.remove(self.alpine_image, force=True) self.client.images.remove(self.alpine_image, force=True)
with suppress(ImageNotFound): with suppress(ImageNotFound):
self.client.images.remove("quay.io/unittest/alpine:latest", force=True) self.client.images.remove("localhost/unittest/alpine", force=True)
def test_manifest_crud(self): def test_manifest_crud(self):
"""Test Manifest CRUD.""" """Test Manifest CRUD."""
self.assertFalse( self.assertFalse(
self.client.manifests.exists("quay.io/unittest/alpine:latest"), self.client.manifests.exists("localhost/unittest/alpine"),
"Image store is corrupt from previous run", "Image store is corrupt from previous run",
) )
with self.subTest("Create"): with self.subTest("Create"):
manifest = self.client.manifests.create(["quay.io/unittest/alpine:latest"]) manifest = self.client.manifests.create(
self.assertEqual(len(manifest.attrs["manifests"]), 0) "localhost/unittest/alpine", ["quay.io/libpod/alpine:latest"]
self.assertTrue(self.client.manifests.exists(manifest.id)) )
self.assertEqual(len(manifest.attrs["manifests"]), 1, manifest.attrs)
self.assertTrue(self.client.manifests.exists(manifest.names), manifest.id)
with self.assertRaises(APIError): with self.assertRaises(APIError):
self.client.manifests.create(["123456!@#$%^"]) self.client.manifests.create("123456!@#$%^")
with self.subTest("Add"): with self.subTest("Add"):
manifest.add([self.alpine_image]) manifest.add([self.alpine_image])
@ -54,12 +56,14 @@ class ManifestsIntegrationTest(base.IntegrationTest):
) )
with self.subTest("Inspect"): with self.subTest("Inspect"):
actual = self.client.manifests.get("quay.io/unittest/alpine:latest") actual = self.client.manifests.get("quay.io/libpod/alpine:latest")
self.assertEqual(actual.id, manifest.id) self.assertEqual(actual.id, manifest.id)
actual = self.client.manifests.get(manifest.name) actual = self.client.manifests.get(manifest.name)
self.assertEqual(actual.id, manifest.id) self.assertEqual(actual.id, manifest.id)
self.assertEqual(actual.version, 2)
with self.subTest("Remove digest"): with self.subTest("Remove digest"):
manifest.remove(self.alpine_image.attrs["RepoDigests"][0]) manifest.remove(self.alpine_image.attrs["RepoDigests"][0])
self.assertEqual(len(manifest.attrs["manifests"]), 0) self.assertEqual(len(manifest.attrs["manifests"]), 0)
@ -67,7 +71,7 @@ class ManifestsIntegrationTest(base.IntegrationTest):
def test_create_409(self): def test_create_409(self):
"""Test that invalid Image names are caught and not corrupt storage.""" """Test that invalid Image names are caught and not corrupt storage."""
with self.assertRaises(APIError): with self.assertRaises(APIError):
self.client.manifests.create([self.invalid_manifest_name]) self.client.manifests.create(self.invalid_manifest_name)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -27,26 +27,29 @@ from podman.errors import NotFound
class NetworksIntegrationTest(base.IntegrationTest): class NetworksIntegrationTest(base.IntegrationTest):
"""networks call integration test""" """networks call integration test"""
pool = IPAMPool(subnet="172.16.0.0/16", iprange="172.16.0.0/24", gateway="172.16.0.1") pool = IPAMPool(subnet="10.11.13.0/24", iprange="10.11.13.0/26", gateway="10.11.13.1")
ipam = IPAMConfig(pool_configs=[pool]) ipam = IPAMConfig(pool_configs=[pool])
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.client = PodmanClient(base_url=self.socket_uri) self.client = PodmanClient(base_url=self.socket_uri)
self.addCleanup(self.client.close) self.addCleanup(self.client.close)
def tearDown(self):
with suppress(NotFound): with suppress(NotFound):
self.client.networks.get("integration_test").remove(force=True) self.client.networks.get("integration_test").remove(force=True)
super().tearDown()
def test_network_crud(self): def test_network_crud(self):
"""integration: networks create and remove calls""" """integration: networks create and remove calls"""
with self.subTest("Create Network"): with self.subTest("Create Network"):
network = self.client.networks.create( network = self.client.networks.create(
"integration_test", "integration_test",
disabled_dns=True, dns_enabled=False,
enable_ipv6=False,
ipam=NetworksIntegrationTest.ipam, ipam=NetworksIntegrationTest.ipam,
) )
self.assertEqual(network.name, "integration_test") self.assertEqual(network.name, "integration_test")
@ -68,7 +71,7 @@ class NetworksIntegrationTest(base.IntegrationTest):
with self.assertRaises(NotFound): with self.assertRaises(NotFound):
self.client.networks.get("integration_test") self.client.networks.get("integration_test")
@unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root') @unittest.skip("Skipping, libpod endpoint does not report container count")
def test_network_connect(self): def test_network_connect(self):
self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest")

View File

@ -18,13 +18,13 @@ class PodsIntegrationTest(base.IntegrationTest):
self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest") self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest")
self.pod_name = f"pod_{random.getrandbits(160):x}" self.pod_name = f"pod_{random.getrandbits(160):x}"
# TODO should this use podman binary instead?
for container in self.client.containers.list(): for container in self.client.containers.list():
container.remove(force=True) container.remove(force=True)
def tearDown(self): def tearDown(self):
if self.client.pods.exists(self.pod_name): if self.client.pods.exists(self.pod_name):
self.client.pods.remove(self.pod_name) self.client.pods.remove(self.pod_name)
super().tearDown()
def test_pod_crud(self): def test_pod_crud(self):
"""Test Pod CRUD.""" """Test Pod CRUD."""
@ -62,6 +62,9 @@ class PodsIntegrationTest(base.IntegrationTest):
with self.assertRaises(NotFound): with self.assertRaises(NotFound):
pod.reload() pod.reload()
def test_pod_crud_infra(self):
"""Test Pod CRUD with infra container."""
with self.subTest("Create with infra"): with self.subTest("Create with infra"):
pod = self.client.pods.create( pod = self.client.pods.create(
self.pod_name, self.pod_name,
@ -75,22 +78,7 @@ class PodsIntegrationTest(base.IntegrationTest):
actual = self.client.pods.get(pod.id) actual = self.client.pods.get(pod.id)
self.assertEqual(actual.name, pod.name) self.assertEqual(actual.name, pod.name)
self.assertIn("Containers", actual.attrs) self.assertIn("Containers", actual.attrs)
self.assertEqual(actual.attrs["State"], "Created")
with self.subTest("Stop/Start"):
actual.stop()
actual.start()
with self.subTest("Restart"):
actual.restart()
with self.subTest("Pause/Unpause"):
actual.pause()
actual.reload()
self.assertEqual(actual.attrs["State"], "Paused")
actual.unpause()
actual.reload()
self.assertEqual(actual.attrs["State"], "Running")
with self.subTest("Add container"): with self.subTest("Add container"):
container = self.client.containers.create(self.alpine_image, command=["ls"], pod=actual) container = self.client.containers.create(self.alpine_image, command=["ls"], pod=actual)
@ -99,12 +87,6 @@ class PodsIntegrationTest(base.IntegrationTest):
ids = {c["Id"] for c in actual.attrs["Containers"]} ids = {c["Id"] for c in actual.attrs["Containers"]}
self.assertIn(container.id, ids) self.assertIn(container.id, ids)
with self.subTest("Ps"):
procs = actual.top()
self.assertGreater(len(procs["Processes"]), 0)
self.assertGreater(len(procs["Titles"]), 0)
with self.subTest("List"): with self.subTest("List"):
pods = self.client.pods.list() pods = self.client.pods.list()
self.assertGreaterEqual(len(pods), 1) self.assertGreaterEqual(len(pods), 1)
@ -112,15 +94,64 @@ class PodsIntegrationTest(base.IntegrationTest):
ids = {p.id for p in pods} ids = {p.id for p in pods}
self.assertIn(actual.id, ids) self.assertIn(actual.id, ids)
with self.subTest("Stats"):
report = self.client.pods.stats(all=True)
self.assertGreaterEqual(len(report), 1)
with self.subTest("Delete"): with self.subTest("Delete"):
pod.remove(force=True) pod.remove(force=True)
with self.assertRaises(NotFound): with self.assertRaises(NotFound):
pod.reload() pod.reload()
def test_ps(self):
pod = self.client.pods.create(
self.pod_name,
labels={
"unittest": "true",
},
no_infra=True,
)
self.assertTrue(self.client.pods.exists(pod.id))
self.client.containers.create(
self.alpine_image, command=["top"], detach=True, tty=True, pod=pod
)
pod.start()
pod.reload()
with self.subTest("top"):
# this is the API top call not the
# top command running in the container
procs = pod.top()
self.assertGreater(len(procs["Processes"]), 0)
self.assertGreater(len(procs["Titles"]), 0)
with self.subTest("stats"):
report = self.client.pods.stats(all=True)
self.assertGreaterEqual(len(report), 1)
with self.subTest("Stop/Start"):
pod.stop()
pod.reload()
self.assertIn(pod.attrs["State"], ("Stopped", "Exited"))
pod.start()
pod.reload()
self.assertEqual(pod.attrs["State"], "Running")
with self.subTest("Restart"):
pod.stop()
pod.restart()
pod.reload()
self.assertEqual(pod.attrs["State"], "Running")
with self.subTest("Pause/Unpause"):
pod.pause()
pod.reload()
self.assertEqual(pod.attrs["State"], "Paused")
pod.unpause()
pod.reload()
self.assertEqual(pod.attrs["State"], "Running")
pod.stop()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -18,6 +18,7 @@ import os
import shutil import shutil
import subprocess import subprocess
import threading import threading
from contextlib import suppress
from typing import List, Optional from typing import List, Optional
import time import time
@ -36,7 +37,7 @@ class PodmanLauncher:
podman_path: Optional[str] = None, podman_path: Optional[str] = None,
timeout: int = 0, timeout: int = 0,
privileged: bool = False, privileged: bool = False,
log_level: int = logging.WARNING, log_level: str = "WARNING",
) -> None: ) -> None:
"""create a launcher and build podman command""" """create a launcher and build podman command"""
podman_exe: str = podman_path podman_exe: str = podman_path
@ -57,7 +58,10 @@ class PodmanLauncher:
self.cmd.append(podman_exe) self.cmd.append(podman_exe)
self.cmd.append(f"--log-level={logging.getLevelName(log_level).lower()}") logger.setLevel(logging.getLevelName(log_level))
# Map from python to go logging levels, FYI trace level breaks cirrus logging
self.cmd.append(f"--log-level={log_level.lower()}")
if os.environ.get("container") == "oci": if os.environ.get("container") == "oci":
self.cmd.append("--storage-driver=vfs") self.cmd.append("--storage-driver=vfs")
@ -121,4 +125,7 @@ class PodmanLauncher:
return_code = self.proc.wait() return_code = self.proc.wait()
self.proc = None self.proc = None
with suppress(FileNotFoundError):
os.remove(self.socket_file)
logger.info("Command return Code: %d refid=%s", return_code, self.reference_id) logger.info("Command return Code: %d refid=%s", return_code, self.reference_id)

View File

@ -47,9 +47,7 @@ class PodmanConfigTestCase(unittest.TestCase):
expected = urllib.parse.urlparse("ssh://qe@localhost:2222/run/podman/podman.sock") expected = urllib.parse.urlparse("ssh://qe@localhost:2222/run/podman/podman.sock")
self.assertEqual(config.active_service.url, expected) self.assertEqual(config.active_service.url, expected)
self.assertEqual( self.assertEqual(config.services["production"].identity, Path("/home/root/.ssh/id_rsa"))
config.services["production"].identity, Path("/home/root/.ssh/id_rsa")
)
PodmanConfigTestCase.opener.assert_called_with( PodmanConfigTestCase.opener.assert_called_with(
Path("/home/developer/containers.conf"), encoding='utf-8' Path("/home/developer/containers.conf"), encoding='utf-8'

View File

@ -242,9 +242,7 @@ class ContainersManagerTestCase(unittest.TestCase):
json=FIRST_CONTAINER, json=FIRST_CONTAINER,
) )
with patch.multiple( with patch.multiple(Container, logs=DEFAULT, wait=DEFAULT, autospec=True) as mock_container:
Container, logs=DEFAULT, wait=DEFAULT, autospec=True
) as mock_container:
mock_container["logs"].return_value = [] mock_container["logs"].return_value = []
mock_container["wait"].return_value = {"StatusCode": 0} mock_container["wait"].return_value = {"StatusCode": 0}
@ -277,9 +275,7 @@ class ContainersManagerTestCase(unittest.TestCase):
b"This is a unittest - line 2", b"This is a unittest - line 2",
) )
with patch.multiple( with patch.multiple(Container, logs=DEFAULT, wait=DEFAULT, autospec=True) as mock_container:
Container, logs=DEFAULT, wait=DEFAULT, autospec=True
) as mock_container:
mock_container["wait"].return_value = {"StatusCode": 0} mock_container["wait"].return_value = {"StatusCode": 0}
with self.subTest("Results not streamed"): with self.subTest("Results not streamed"):

View File

@ -1,7 +1,7 @@
import unittest import unittest
from podman import PodmanClient, tests from podman import PodmanClient, tests
from podman.domain.manifests import ManifestsManager, Manifest from podman.domain.manifests import Manifest, ManifestsManager
class ManifestTestCase(unittest.TestCase): class ManifestTestCase(unittest.TestCase):
@ -9,11 +9,7 @@ class ManifestTestCase(unittest.TestCase):
super().setUp() super().setUp()
self.client = PodmanClient(base_url=tests.BASE_SOCK) self.client = PodmanClient(base_url=tests.BASE_SOCK)
self.addCleanup(self.client.close)
def tearDown(self) -> None:
super().tearDown()
self.client.close()
def test_podmanclient(self): def test_podmanclient(self):
manager = self.client.manifests manager = self.client.manifests
@ -24,13 +20,8 @@ class ManifestTestCase(unittest.TestCase):
self.client.manifests.list() self.client.manifests.list()
def test_name(self): def test_name(self):
with self.assertRaises(ValueError): manifest = Manifest()
manifest = Manifest(attrs={"names": ""}) self.assertIsNone(manifest.name)
_ = manifest.name
with self.assertRaises(ValueError):
manifest = Manifest()
_ = manifest.name
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -28,29 +28,29 @@ FIRST_NETWORK = {
"Labels": {}, "Labels": {},
} }
FIRST_NETWORK_LIBPOD = [ FIRST_NETWORK_LIBPOD = {
{ "name": "podman",
"cniVersion": "0.4.0", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
"name": "podman", "driver": "bridge",
"plugins": [ "network_interface": "libpod_veth0",
{ "created": "2022-01-28T09:18:37.491308364-07:00",
"bridge": "cni-podman0", "subnets": [
"hairpinMode": True, {
"ipMasq": True, "subnet": "10.11.12.0/24",
"ipam": { "gateway": "10.11.12.1",
"ranges": [[{"gateway": "10.88.0.1", "subnet": "10.88.0.0/16"}]], "lease_range": {
"routes": [{"dst": "0.0.0.0/0"}], "start_ip": "10.11.12.1",
"type": "host-local", "end_ip": "10.11.12.63",
},
"isGateway": True,
"type": "bridge",
}, },
{"capabilities": {"portMappings": True}, "type": "portmap"}, }
{"type": "firewall"}, ],
{"type": "tuning"}, "ipv6_enabled": False,
], "internal": False,
} "dns_enabled": False,
] "labels": {},
"options": {},
"ipam_options": {},
}
class NetworkTestCase(unittest.TestCase): class NetworkTestCase(unittest.TestCase):
@ -58,11 +58,7 @@ class NetworkTestCase(unittest.TestCase):
super().setUp() super().setUp()
self.client = PodmanClient(base_url=tests.BASE_SOCK) self.client = PodmanClient(base_url=tests.BASE_SOCK)
self.addCleanup(self.client.close)
def tearDown(self) -> None:
super().tearDown()
self.client.close()
def test_id(self): def test_id(self):
expected = {"Id": "1cf06390-709d-4ffa-a054-c3083abe367c"} expected = {"Id": "1cf06390-709d-4ffa-a054-c3083abe367c"}
@ -84,7 +80,7 @@ class NetworkTestCase(unittest.TestCase):
@requests_mock.Mocker() @requests_mock.Mocker()
def test_remove(self, mock): def test_remove(self, mock):
adapter = mock.delete( adapter = mock.delete(
tests.COMPATIBLE_URL + "/networks/podman?force=True", tests.LIBPOD_URL + "/networks/podman?force=True",
status_code=204, status_code=204,
json={"Name": "podman", "Err": None}, json={"Name": "podman", "Err": None},
) )
@ -96,7 +92,7 @@ class NetworkTestCase(unittest.TestCase):
@requests_mock.Mocker() @requests_mock.Mocker()
def test_connect(self, mock): def test_connect(self, mock):
adapter = mock.post(tests.COMPATIBLE_URL + "/networks/podman/connect") adapter = mock.post(tests.LIBPOD_URL + "/networks/podman/connect")
net = Network(attrs=FIRST_NETWORK, client=self.client.api) net = Network(attrs=FIRST_NETWORK, client=self.client.api)
net.connect( net.connect(
@ -120,7 +116,7 @@ class NetworkTestCase(unittest.TestCase):
@requests_mock.Mocker() @requests_mock.Mocker()
def test_disconnect(self, mock): def test_disconnect(self, mock):
adapter = mock.post(tests.COMPATIBLE_URL + "/networks/podman/disconnect") adapter = mock.post(tests.LIBPOD_URL + "/networks/podman/disconnect")
net = Network(attrs=FIRST_NETWORK, client=self.client.api) net = Network(attrs=FIRST_NETWORK, client=self.client.api)
net.disconnect("podman_ctnr", force=True) net.disconnect("podman_ctnr", force=True)

View File

@ -3,7 +3,6 @@ import unittest
import requests_mock import requests_mock
from podman import PodmanClient, tests from podman import PodmanClient, tests
from podman.domain.ipam import IPAMConfig, IPAMPool
from podman.domain.networks import Network from podman.domain.networks import Network
from podman.domain.networks_manager import NetworksManager from podman.domain.networks_manager import NetworksManager
@ -51,53 +50,53 @@ SECOND_NETWORK = {
"Labels": {}, "Labels": {},
} }
FIRST_NETWORK_LIBPOD = [ FIRST_NETWORK_LIBPOD = {
{ "name": "podman",
"cniVersion": "0.4.0", "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
"name": "podman", "driver": "bridge",
"plugins": [ "network_interface": "libpod_veth0",
{ "created": "2022-01-28T09:18:37.491308364-07:00",
"bridge": "cni-podman0", "subnets": [
"hairpinMode": True, {
"ipMasq": True, "subnet": "10.11.12.0/24",
"ipam": { "gateway": "10.11.12.1",
"ranges": [[{"gateway": "10.88.0.1", "subnet": "10.88.0.0/16"}]], "lease_range": {
"routes": [{"dst": "0.0.0.0/0"}], "start_ip": "10.11.12.1",
"type": "host-local", "end_ip": "10.11.12.63",
},
"isGateway": True,
"type": "bridge",
}, },
{"capabilities": {"portMappings": True}, "type": "portmap"}, }
{"type": "firewall"}, ],
{"type": "tuning"}, "ipv6_enabled": False,
], "internal": False,
} "dns_enabled": False,
] "labels": {},
"options": {},
"ipam_options": {},
}
SECOND_NETWORK_LIBPOD = [ SECOND_NETWORK_LIBPOD = {
{ "name": "database",
"cniVersion": "0.4.0", "id": "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8",
"name": "database", "created": "2021-03-01T09:18:37.491308364-07:00",
"plugins": [ "driver": "bridge",
{ "network_interface": "libpod_veth1",
"bridge": "cni-podman0", "subnets": [
"hairpinMode": True, {
"ipMasq": True, "subnet": "10.11.12.0/24",
"ipam": { "gateway": "10.11.12.1",
"ranges": [[{"gateway": "10.88.0.1", "subnet": "10.88.0.0/16"}]], "lease_range": {
"routes": [{"dst": "0.0.0.0/0"}], "start_ip": "10.11.12.1",
"type": "host-local", "end_ip": "10.11.12.63",
},
"isGateway": True,
"type": "bridge",
}, },
{"capabilities": {"portMappings": True}, "type": "portmap"}, }
{"type": "firewall"}, ],
{"type": "tuning"}, "ipv6_enabled": False,
], "internal": False,
} "dns_enabled": False,
] "labels": {},
"options": {},
"ipam_options": {},
}
class NetworksManagerTestCase(unittest.TestCase): class NetworksManagerTestCase(unittest.TestCase):
@ -112,11 +111,7 @@ class NetworksManagerTestCase(unittest.TestCase):
super().setUp() super().setUp()
self.client = PodmanClient(base_url=tests.BASE_SOCK) self.client = PodmanClient(base_url=tests.BASE_SOCK)
self.addCleanup(self.client.close)
def tearDown(self) -> None:
super().tearDown()
self.client.close()
def test_podmanclient(self): def test_podmanclient(self):
manager = self.client.networks manager = self.client.networks
@ -124,10 +119,7 @@ class NetworksManagerTestCase(unittest.TestCase):
@requests_mock.Mocker() @requests_mock.Mocker()
def test_get(self, mock): def test_get(self, mock):
mock.get( mock.get(tests.LIBPOD_URL + "/networks/podman", json=FIRST_NETWORK)
tests.COMPATIBLE_URL + "/networks/podman",
json=FIRST_NETWORK,
)
actual = self.client.networks.get("podman") actual = self.client.networks.get("podman")
self.assertIsInstance(actual, Network) self.assertIsInstance(actual, Network)
@ -135,47 +127,14 @@ class NetworksManagerTestCase(unittest.TestCase):
actual.id, "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9" actual.id, "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9"
) )
@requests_mock.Mocker()
def test_get_libpod(self, mock):
mock.get(
tests.LIBPOD_URL + "/networks/podman/json",
json=FIRST_NETWORK_LIBPOD,
)
actual = self.client.networks.get("podman", compatible=False)
self.assertIsInstance(actual, Network)
self.assertEqual(actual.attrs["name"], "podman")
@requests_mock.Mocker()
def test_list(self, mock):
mock.get(
tests.COMPATIBLE_URL + "/networks",
json=[FIRST_NETWORK, SECOND_NETWORK],
)
actual = self.client.networks.list()
self.assertEqual(len(actual), 2)
self.assertIsInstance(actual[0], Network)
self.assertEqual(
actual[0].id, "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9"
)
self.assertEqual(actual[0].attrs["Name"], "podman")
self.assertIsInstance(actual[1], Network)
self.assertEqual(
actual[1].id, "3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8"
)
self.assertEqual(actual[1].name, "database")
@requests_mock.Mocker() @requests_mock.Mocker()
def test_list_libpod(self, mock): def test_list_libpod(self, mock):
mock.get( mock.get(
tests.LIBPOD_URL + "/networks/json", tests.LIBPOD_URL + "/networks/json",
json=FIRST_NETWORK_LIBPOD + SECOND_NETWORK_LIBPOD, json=[FIRST_NETWORK_LIBPOD, SECOND_NETWORK_LIBPOD],
) )
actual = self.client.networks.list(compatible=False) actual = self.client.networks.list()
self.assertEqual(len(actual), 2) self.assertEqual(len(actual), 2)
self.assertIsInstance(actual[0], Network) self.assertIsInstance(actual[0], Network)
@ -191,68 +150,33 @@ class NetworksManagerTestCase(unittest.TestCase):
self.assertEqual(actual[1].name, "database") self.assertEqual(actual[1].name, "database")
@requests_mock.Mocker() @requests_mock.Mocker()
def test_create(self, mock): def test_create_libpod(self, mock):
adapter = mock.post( adapter = mock.post(tests.LIBPOD_URL + "/networks/create", json=FIRST_NETWORK_LIBPOD)
tests.LIBPOD_URL + "/networks/create?name=podman",
json={
"Filename": "/home/developer/.config/cni/net.d/podman.conflist",
},
)
mock.get(
tests.COMPATIBLE_URL + "/networks/podman",
json=FIRST_NETWORK,
)
pool = IPAMPool(subnet="172.16.0.0/12", iprange="172.16.0.0/16", gateway="172.31.255.254") network = self.client.networks.create("podman", dns_enabled=True, enable_ipv6=True)
ipam = IPAMConfig(pool_configs=[pool])
network = self.client.networks.create(
"podman", disabled_dns=True, enable_ipv6=False, ipam=ipam
)
self.assertIsInstance(network, Network) self.assertIsInstance(network, Network)
self.assertEqual(adapter.call_count, 1) self.assertEqual(adapter.call_count, 1)
self.assertDictEqual( self.assertDictEqual(
adapter.last_request.json(), adapter.last_request.json(),
{ {
'DisabledDNS': True, "name": "podman",
'Gateway': '172.31.255.254', "ipv6_enabled": True,
'IPv6': False, "dns_enabled": True,
'Range': {'IP': '172.16.0.0', 'Mask': "//8AAA=="},
'Subnet': {'IP': '172.16.0.0', 'Mask': "//AAAA=="},
}, },
) )
self.assertEqual(network.name, "podman")
@requests_mock.Mocker() @requests_mock.Mocker()
def test_create_defaults(self, mock): def test_create_defaults(self, mock):
adapter = mock.post( adapter = mock.post(tests.LIBPOD_URL + "/networks/create", json=FIRST_NETWORK_LIBPOD)
tests.LIBPOD_URL + "/networks/create?name=podman",
json={
"Filename": "/home/developer/.config/cni/net.d/podman.conflist",
},
)
mock.get(
tests.COMPATIBLE_URL + "/networks/podman",
json=FIRST_NETWORK,
)
network = self.client.networks.create("podman") network = self.client.networks.create("podman")
self.assertEqual(adapter.call_count, 1) self.assertEqual(adapter.call_count, 1)
self.assertEqual(network.name, "podman") self.assertDictEqual(
self.assertEqual(len(adapter.last_request.json()), 0) adapter.last_request.json(),
{"name": "podman"},
@requests_mock.Mocker()
def test_prune(self, mock):
mock.post(
tests.COMPATIBLE_URL + "/networks/prune",
json={"NetworksDeleted": ["podman", "database"]},
) )
actual = self.client.networks.prune()
self.assertListEqual(actual["NetworksDeleted"], ["podman", "database"])
@requests_mock.Mocker() @requests_mock.Mocker()
def test_prune_libpod(self, mock): def test_prune_libpod(self, mock):
mock.post( mock.post(
@ -263,7 +187,7 @@ class NetworksManagerTestCase(unittest.TestCase):
], ],
) )
actual = self.client.networks.prune(compatible=False) actual = self.client.networks.prune()
self.assertListEqual(actual["NetworksDeleted"], ["podman", "database"]) self.assertListEqual(actual["NetworksDeleted"], ["podman", "database"])

View File

@ -38,11 +38,7 @@ class VolumesManagerTestCase(unittest.TestCase):
super().setUp() super().setUp()
self.client = PodmanClient(base_url=tests.BASE_SOCK) self.client = PodmanClient(base_url=tests.BASE_SOCK)
self.addCleanup(self.client.close)
def tearDown(self) -> None:
super().tearDown()
self.client.close()
def test_podmanclient(self): def test_podmanclient(self):
manager = self.client.volumes manager = self.client.volumes

View File

@ -1,4 +1,4 @@
"""Version of PodmanPy.""" """Version of PodmanPy."""
__version__ = "3.2.1" __version__ = "4.0.1"
__compatible_version__ = "1.40" __compatible_version__ = "1.40"

View File

@ -19,12 +19,20 @@ exclude = '''
profile = "black" profile = "black"
line_length = 100 line_length = 100
[build-system] [build-system]
# Any changes should be copied into requirements.txt, setup.cfg, and/or test-requirements.txt
requires = [ requires = [
"pyxdg>=0.26",
"requests>=2.24", "requests>=2.24",
"setuptools>=46.4",
"sphinx",
"toml>=0.10.2", "toml>=0.10.2",
"urllib3>=1.24.2", "urllib3>=1.24.2",
"pyxdg>=0.26",
"setuptools>=46.4",
"wheel", "wheel",
] ]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "DEBUG"
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
log_cli_date_format = "%Y-%m-%d %H:%M:%S"

View File

@ -1,7 +1,8 @@
# Any changes should be copied into pyproject.toml
pyxdg>=0.26
requests>=2.24 requests>=2.24
setuptools
sphinx
toml>=0.10.2 toml>=0.10.2
urllib3>=1.24.2 urllib3>=1.24.2
pyxdg>=0.26
sphinx
wheel wheel
setuptools

View File

@ -31,11 +31,12 @@ keywords = podman, libpod
include_package_data = True include_package_data = True
python_requires = >=3.6 python_requires = >=3.6
test_suite = test_suite =
# Any changes should be copied into pyproject.toml
install_requires = install_requires =
pyxdg>=0.26
requests>=2.24 requests>=2.24
toml>=0.10.2 toml>=0.10.2
urllib3>=1.24.2 urllib3>=1.24.2
pyxdg>=0.26
# typing_extensions are included for RHEL 8.5 # typing_extensions are included for RHEL 8.5
# typing_extensions;python_version<'3.8' # typing_extensions;python_version<'3.8'

View File

@ -1,18 +1,10 @@
import setuptools
import fnmatch import fnmatch
import setuptools
from setuptools import find_packages from setuptools import find_packages
from setuptools.command.build_py import build_py as build_py_orig from setuptools.command.build_py import build_py as build_py_orig
excluded = [ excluded = [
"podman/api_connection.py",
"podman/containers/*",
"podman/images/*",
"podman/manifests/*",
"podman/networks/*",
"podman/pods/*",
"podman/system/*",
"podman/system/*",
"podman/tests/*", "podman/tests/*",
] ]

View File

@ -1,7 +1,9 @@
# Any changes should be copied into pyproject.toml
-r requirements.txt -r requirements.txt
black black
coverage coverage
fixtures~=3.0.0 fixtures~=3.0.0
pytest
pylint pylint
pytest
requests-mock requests-mock
tox

View File

@ -1,6 +1,6 @@
[tox] [tox]
minversion = 3.2.0 minversion = 3.2.0
envlist = py36,py38,py39,py310,pylint,coverage envlist = pylint,coverage,py36,py38,py39,py310
ignore_basepython_conflict = true ignore_basepython_conflict = true
[testenv] [testenv]
@ -8,7 +8,11 @@ basepython = python3
usedevelop = True usedevelop = True
install_command = pip install {opts} {packages} install_command = pip install {opts} {packages}
deps = -r{toxinidir}/test-requirements.txt deps = -r{toxinidir}/test-requirements.txt
commands = pytest commands = pytest {posargs}
setenv =
PODMAN_LOG_LEVEL = {env:PODMAN_LOG_LEVEL:INFO}
PODMAN_BINARY = {env:PODMAN_BINARY:podman}
DEBUG = {env:DEBUG:0}
[testenv:venv] [testenv:venv]
commands = {posargs} commands = {posargs}