Compare commits

...

41 Commits
v5.5.0 ... main

Author SHA1 Message Date
openshift-merge-bot[bot] 0fd4e55fd5
Merge pull request #581 from inknos/5.6.0
Bump release to 5.6.0
2025-09-05 09:38:28 +00:00
Nicola Sella a3a8b7ca23
Bump release to 5.6.0
Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-09-04 15:30:35 +02:00
openshift-merge-bot[bot] 85bc9560a1
Merge pull request #580 from containers/inknos-update-owners
Add Honn1 to OWNERS
2025-09-04 13:01:48 +00:00
openshift-merge-bot[bot] 77fea64100
Merge pull request #569 from inknos/update-ruff-0-12-8
Update Ruff to 0.12.8
2025-09-04 12:30:34 +00:00
Nicola Sella 24379a4cf5
Add Honn1 to OWNERS
Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-09-04 14:26:27 +02:00
Nicola Sella c3d53a443e
Fix and enable a bunch of ruff checks
Fix E501: line too long
Fix E741: ambiguous var name
Fix F401: module imported but unused
Fix F541: f-string is missing placeholders
Fix F821: undefined name
Fix F841: local variable assigned but never used

Enable B: bugbear
Enable UP: pyupgrade

Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-09-04 12:22:27 +02:00
Nicola Sella 3a8c5f9fb6
Lint pep-naming N80
This linting found the tearDown function missing and mistakenly named
tearUp. This commits renames the function to tearDown. We might expect
some related issues due to this fix

Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-09-04 12:18:20 +02:00
Nicola Sella c97384262b
Update Ruff to 0.12.8
Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-09-04 12:18:19 +02:00
openshift-merge-bot[bot] 3007fdba71
Merge pull request #579 from containers/renovate/actions-setup-python-6.x
[skip-ci] Update actions/setup-python action to v6
2025-09-04 10:07:44 +00:00
renovate[bot] d34c718e26
[skip-ci] Update actions/setup-python action to v6
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 09:16:18 +00:00
openshift-merge-bot[bot] 5d4ff56c0c
Merge pull request #540 from inknos/list-sparse
Implement sparse keyword for containers.list()
2025-09-04 09:15:55 +00:00
Nicola Sella 010949a925 Implement sparse keyword for containers.list()
Defaults to True for Libpod calls and False for Docker Compat calls

This ensures:
1. Docker API compatibility
2. No breaking changes with Libpod

It also provides:
1. Possibility to inspect containers on demand for list calls
2. Safer behavior if container hangs
3. Fewer expensive calls to the API by default

Note: Requests need to pass compat explicitely to reload containers. A
unit test has been added.

Fixes: https://github.com/containers/podman-py/issues/459
Fixes: https://github.com/containers/podman-py/issues/446

Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-09-03 17:47:02 +02:00
openshift-merge-bot[bot] 1368c96bae
Merge pull request #575 from ricardobranco777/timezone_aware
tests: Fix deprecation warning for utcfromtimestamp()
2025-09-03 15:45:04 +00:00
Ricardo Branco f38fe91d46
tests: Fix deprecation warning for utcfromtimestamp()
Fix DeprecationWarning for datetime.datetime.utcfromtimestamp()

It suggests to use timezone-aware objects to represent datetimes in UTC
with datetime.UTC but datetime.timezone.utc is backwards compatible.

Signed-off-by: Ricardo Branco <rbranco@suse.de>
2025-09-03 17:18:21 +02:00
openshift-merge-bot[bot] c4c86486cc
Merge pull request #566 from containers/renovate/major-github-artifact-actions
[skip-ci] Update actions/download-artifact action to v5
2025-09-03 14:52:59 +00:00
renovate[bot] f92036c156
[skip-ci] Update actions/download-artifact action to v5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 14:27:30 +00:00
openshift-merge-bot[bot] ca381f217a
Merge pull request #568 from containers/renovate/actions-checkout-5.x
[skip-ci] Update actions/checkout action to v5
2025-09-03 14:27:10 +00:00
renovate[bot] f044c7e10c
[skip-ci] Update actions/checkout action to v5
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-03 14:04:44 +00:00
openshift-merge-bot[bot] 058de18c20
Merge pull request #578 from inknos/issue-571
Skip tests conditionally based on version
2025-09-03 14:04:17 +00:00
Nicola Sella 203eea8d5d
Skip tests conditionally based on version
The sorting based on tuples will not take into account sorting
versions like x.y.z-dev or x.y.z-rc1, which will be considered
as x.y.z.

Fixes: https://github.com/containers/podman-py/issues/571

Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-09-02 17:19:45 +02:00
openshift-merge-bot[bot] 9b99805024
Merge pull request #567 from inknos/test-os-release
Run tests conditionally based on os_release
2025-08-20 13:25:14 +00:00
Nicola Sella 365534ebfe
Run tests conditionally based on OS_RELEASE
Add fallback function freedesktop_os_release() for systems that run
python versions that don't implement the `platform` function
(python < 3.10)

Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-08-13 16:27:17 +02:00
Daniel J Walsh 3c624222bc
Merge pull request #565 from inknos/add-pull-policy
Implement policy option for image.pull
2025-08-11 16:24:45 -04:00
Nicola Sella 2f4b14f8ee
Implement policy option for images.pull
Also, pass options from containers.run to images.pull:
    - policy is passed with default to "missing" to mirror podman-run
      --pull-policy behavior
    - auth_config is passed to images.pull

Fixes: https://github.com/containers/podman-py/issues/564

Signed-off-by: Nicola Sella <nsella@redhat.com>
2025-08-07 16:03:37 +02:00
openshift-merge-bot[bot] 7e834d3cbe
Merge pull request #563 from Luap99/cirrus-rm
remove cirrus files
2025-07-01 08:07:34 +00:00
Paul Holzinger 56ebf6c1ea
remove cirrus files
Cirrus was disabled already and is not is use however the cirrus specfic
scripts were left around so remove them as well.

Fixes: cec8a83ecb ("Remove Cirrus testing")

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
2025-06-27 17:42:59 +02:00
openshift-merge-bot[bot] 7e88fed72c
Merge pull request #562 from dklimpel/patch-1
fix: broken configuration for readthedocs
2025-06-26 16:02:28 +00:00
openshift-merge-bot[bot] 371ecb8ae6
Merge pull request #513 from Mr-Sunglasses/fix/#489
Fix/#489
2025-06-24 10:06:19 +00:00
Kanishk Pachauri fd8bfcdadd
Merge branch 'main' into fix/#489 2025-06-24 01:48:53 +05:30
Kanishk Pachauri e472ae020d
fix: remove unused variable
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-06-24 01:31:36 +05:30
Kanishk Pachauri a6ca81cec2
fix: failing test due to type error
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-06-24 01:27:17 +05:30
Kanishk Pachauri fac45dd5ba
chore: fix formatting errors
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-06-24 01:11:59 +05:30
Kanishk Pachauri c150b07f29
feat: Add exception for edge cases and add unit tests
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-06-24 01:10:50 +05:30
openshift-merge-bot[bot] bbbe75813f
Merge pull request #561 from containers/renovate/sigstore-gh-action-sigstore-python-3.x
[skip-ci] Update sigstore/gh-action-sigstore-python action to v3.0.1
2025-06-23 10:11:26 +00:00
Dirk Klimpel 61f7725152
fix: broken configuration for readthedocs
Signed-off-by: Dirk Klimpel <5740567+dklimpel@users.noreply.github.com>
2025-06-22 22:06:04 +02:00
renovate[bot] f917e0a36c
[skip-ci] Update sigstore/gh-action-sigstore-python action to v3.0.1
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 19:55:56 +00:00
Kanishk Pachauri 527971f55e
Merge branch 'main' into fix/#489 2025-06-21 00:21:14 +05:30
Kanishk Pachauri ee13b44943
chore: removed unuseful comments
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-02-18 00:19:10 +05:30
Kanishk Pachauri 068e23330f
fix: broken tests
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-02-18 00:19:10 +05:30
Kanishk Pachauri 23a0845b5e
test: Add tests for the enviroment variables
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-02-18 00:19:10 +05:30
Kanishk Pachauri 4f843ad11c
fix: Correctly handle environment variables in container creation
Signed-off-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
2025-02-18 00:19:10 +05:30
34 changed files with 684 additions and 193 deletions

View File

@ -9,8 +9,8 @@ jobs:
env:
SKIP: no-commit-to-branch
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: |
3.9

View File

@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.x"
@ -46,7 +46,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/
@ -68,12 +68,12 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.0
uses: sigstore/gh-action-sigstore-python@v3.0.1
with:
inputs: >-
./dist/*.tar.gz
@ -114,7 +114,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/

View File

@ -8,7 +8,7 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.8.1
rev: v0.12.8
hooks:
# Run the linter.
- id: ruff

View File

@ -21,10 +21,10 @@ build:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
method: pip
path: .
extra_requirements:
- docs
- method: pip
path: .
extra_requirements:
- docs
# Build documentation in the docs/ directory with Sphinx
sphinx:

View File

@ -8,7 +8,7 @@ DESTDIR ?=
EPOCH_TEST_COMMIT ?= $(shell git merge-base $${DEST_BRANCH:-main} HEAD)
HEAD ?= HEAD
export PODMAN_VERSION ?= "5.5.0"
export PODMAN_VERSION ?= "5.6.0"
.PHONY: podman
podman:

1
OWNERS
View File

@ -12,6 +12,7 @@ approvers:
reviewers:
- ashley-cui
- baude
- Honny1
- rhatdan
- TomSweeneyRedHat
- Edward5hen

View File

@ -1,9 +0,0 @@
#!/bin/bash
set -xeo pipefail
systemctl stop podman.socket || :
dnf remove podman -y
dnf copr enable rhcontainerbot/podman-next -y
dnf install podman -y

View File

@ -1,11 +0,0 @@
#!/bin/bash
set -eo pipefail
systemctl enable podman.socket podman.service
systemctl start podman.socket
systemctl status podman.socket ||:
# log which version of podman we just enabled
echo "Locate podman: $(type -P podman)"
podman --version

View File

@ -1,11 +0,0 @@
#!/bin/bash
set -eo pipefail
systemctl enable sshd
systemctl start sshd
systemctl status sshd ||:
ssh-keygen -t ecdsa -b 521 -f /root/.ssh/id_ecdsa -P ""
cp /root/.ssh/authorized_keys /root/.ssh/authorized_keys%
cat /root/.ssh/id_ecdsa.pub >>/root/.ssh/authorized_keys

View File

@ -1,5 +0,0 @@
#!/bin/bash
set -eo pipefail
make tests

View File

@ -1,61 +0,0 @@
#!/usr/bin/env bash
#
# For help and usage information, simply execute the script w/o any arguments.
#
# This script is intended to be run by Red Hat podman-py developers who need
# to debug problems specifically related to Cirrus-CI automated testing.
# It requires that you have been granted prior access to create VMs in
# google-cloud. For non-Red Hat contributors, VMs are available as-needed,
# with supervision upon request.
set -e
SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}")
SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH")
REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../")
# Help detect if we were called by get_ci_vm container
GET_CI_VM="${GET_CI_VM:-0}"
in_get_ci_vm() {
if ((GET_CI_VM==0)); then
echo "Error: $1 is not intended for use in this context"
exit 2
fi
}
# get_ci_vm APIv1 container entrypoint calls into this script
# to obtain required repo. specific configuration options.
if [[ "$1" == "--config" ]]; then
in_get_ci_vm "$1"
cat <<EOF
DESTDIR="/var/tmp/go/src/github.com/containers/podman-py"
UPSTREAM_REPO="https://github.com/containers/podman-py.git"
CI_ENVFILE="/etc/ci_environment"
GCLOUD_PROJECT="podman-py"
GCLOUD_IMGPROJECT="libpod-218412"
GCLOUD_CFG="podman-py"
GCLOUD_ZONE="${GCLOUD_ZONE:-us-central1-c}"
GCLOUD_CPUS="2"
GCLOUD_MEMORY="4Gb"
GCLOUD_DISK="200"
EOF
elif [[ "$1" == "--setup" ]]; then
in_get_ci_vm "$1"
echo "+ Setting up and Running make" > /dev/stderr
echo 'PATH=$PATH:$GOPATH/bin' > /etc/ci_environment
make
else
# Create and access VM for specified Cirrus-CI task
mkdir -p $HOME/.config/gcloud/ssh
podman run -it --rm \
--tz=local \
-e NAME="$USER" \
-e SRCDIR=/src \
-e GCLOUD_ZONE="$GCLOUD_ZONE" \
-e DEBUG="${DEBUG:-0}" \
-v $REPO_DIRPATH:/src:O \
-v $HOME/.config/gcloud:/root/.config/gcloud:z \
-v $HOME/.config/gcloud/ssh:/root/.ssh:z \
quay.io/libpod/get_ci_vm:latest "$@"
fi

View File

@ -18,7 +18,7 @@ from requests.adapters import HTTPAdapter
from podman.api.api_versions import VERSION, COMPATIBLE_VERSION
from podman.api.ssh import SSHAdapter
from podman.api.uds import UDSAdapter
from podman.errors import APIError, NotFound
from podman.errors import APIError, NotFound, PodmanError
from podman.tlsconfig import TLSConfig
from podman.version import __version__

View File

@ -4,7 +4,7 @@ import base64
import ipaddress
import json
import struct
from datetime import datetime
from datetime import datetime, timezone
from typing import Any, Optional, Union
from collections.abc import Iterator
@ -48,7 +48,9 @@ def prepare_timestamp(value: Union[datetime, int, None]) -> Optional[int]:
return value
if isinstance(value, datetime):
delta = value - datetime.utcfromtimestamp(0)
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
delta = value - datetime.fromtimestamp(0, timezone.utc)
return delta.seconds + delta.days * 24 * 3600
raise ValueError(f"Type '{type(value)}' is not supported by prepare_timestamp()")

View File

@ -24,7 +24,7 @@ def prepare_containerignore(anchor: str) -> list[str]:
with ignore.open(encoding='utf-8') as file:
return list(
filter(
lambda L: L and not L.startswith("#"),
lambda i: i and not i.startswith("#"),
(line.strip() for line in file.readlines()),
)
)

View File

@ -17,7 +17,7 @@ from podman.errors import ImageNotFound
logger = logging.getLogger("podman.containers")
NAMED_VOLUME_PATTERN = re.compile(r'[a-zA-Z0-9][a-zA-Z0-9_.-]*')
NAMED_VOLUME_PATTERN = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.-]*")
class CreateMixin: # pylint: disable=too-few-public-methods
@ -375,7 +375,9 @@ class CreateMixin: # pylint: disable=too-few-public-methods
payload = api.prepare_body(payload)
response = self.client.post(
"/containers/create", headers={"content-type": "application/json"}, data=payload
"/containers/create",
headers={"content-type": "application/json"},
data=payload,
)
response.raise_for_status(not_found=ImageNotFound)
@ -383,6 +385,48 @@ class CreateMixin: # pylint: disable=too-few-public-methods
return self.get(container_id)
@staticmethod
def _convert_env_list_to_dict(env_list):
"""Convert a list of environment variables to a dictionary.
Args:
env_list (List[str]): List of environment variables in the format ["KEY=value"]
Returns:
Dict[str, str]: Dictionary of environment variables
Raises:
ValueError: If any environment variable is not in the correct format
"""
if not isinstance(env_list, list):
raise TypeError(f"Expected list, got {type(env_list).__name__}")
env_dict = {}
for env_var in env_list:
if not isinstance(env_var, str):
raise TypeError(
f"Environment variable must be a string, "
f"got {type(env_var).__name__}: {repr(env_var)}"
)
# Handle empty strings
if not env_var.strip():
raise ValueError("Environment variable cannot be empty")
if "=" not in env_var:
raise ValueError(
f"Environment variable '{env_var}' is not in the correct format. "
"Expected format: 'KEY=value'"
)
key, value = env_var.split("=", 1) # Split on first '=' only
# Validate key is not empty
if not key.strip():
raise ValueError(f"Environment variable has empty key: '{env_var}'")
env_dict[key] = value
return env_dict
# pylint: disable=too-many-locals,too-many-statements,too-many-branches
@staticmethod
def _render_payload(kwargs: MutableMapping[str, Any]) -> dict[str, Any]:
@ -410,6 +454,23 @@ class CreateMixin: # pylint: disable=too-few-public-methods
with suppress(KeyError):
del args[key]
# Handle environment variables
environment = args.pop("environment", None)
if environment is not None:
if isinstance(environment, list):
try:
environment = CreateMixin._convert_env_list_to_dict(environment)
except ValueError as e:
raise ValueError(
"Failed to convert environment variables list to dictionary. "
f"Error: {str(e)}"
) from e
elif not isinstance(environment, dict):
raise TypeError(
"Environment variables must be provided as either a dictionary "
"or a list of strings in the format ['KEY=value']"
)
# These keywords are not supported for various reasons.
unsupported_keys = set(args.keys()).intersection(
(
@ -466,9 +527,9 @@ class CreateMixin: # pylint: disable=too-few-public-methods
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())
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(
@ -497,7 +558,7 @@ class CreateMixin: # pylint: disable=too-few-public-methods
"dns_search": pop("dns_search"),
"dns_server": pop("dns"),
"entrypoint": pop("entrypoint"),
"env": pop("environment"),
"env": environment,
"env_host": pop("env_host"), # TODO document, podman only
"expose": {},
"groups": pop("group_add"),
@ -607,7 +668,7 @@ class CreateMixin: # pylint: disable=too-few-public-methods
if _k in bool_options and v is True:
options.append(option_name)
elif _k in regular_options:
options.append(f'{option_name}={v}')
options.append(f"{option_name}={v}")
elif _k in simple_options:
options.append(v)
@ -709,12 +770,12 @@ class CreateMixin: # pylint: disable=too-few-public-methods
for item in args.pop("volumes", {}).items():
key, value = item
extended_mode = value.get('extended_mode', [])
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')
mode = value.get("mode")
if mode is not None:
if not isinstance(mode, str):
raise ValueError("'mode' value should be a str")
@ -729,10 +790,10 @@ class CreateMixin: # pylint: disable=too-few-public-methods
params["volumes"].append(volume)
else:
mount_point = {
"destination": value['bind'],
"destination": value["bind"],
"options": options,
"source": key,
"type": 'bind',
"type": "bind",
}
params["mounts"].append(mount_point)

View File

@ -2,8 +2,8 @@
import logging
import urllib
from typing import Any, Union
from collections.abc import Mapping
from typing import Any, Union
from podman import api
from podman.domain.containers import Container
@ -27,12 +27,15 @@ class ContainersManager(RunMixin, CreateMixin, Manager):
response = self.client.get(f"/containers/{key}/exists")
return response.ok
def get(self, key: str) -> Container:
def get(self, key: str, **kwargs) -> Container:
"""Get container by name or id.
Args:
key: Container name or id.
Keyword Args:
compatible (bool): Use Docker compatibility endpoint
Returns:
A `Container` object corresponding to `key`.
@ -40,8 +43,10 @@ class ContainersManager(RunMixin, CreateMixin, Manager):
NotFound: when Container does not exist
APIError: when an error return by service
"""
compatible = kwargs.get("compatible", False)
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", compatible=compatible)
response.raise_for_status()
return self.prepare_model(attrs=response.json())
@ -67,12 +72,26 @@ class ContainersManager(RunMixin, CreateMixin, Manager):
Give the container name or id.
- since (str): Only containers created after a particular container.
Give container name or id.
sparse: Ignored
sparse: If False, return basic container information without additional
inspection requests. This improves performance when listing many containers
but might provide less detail. You can call Container.reload() on individual
containers later to retrieve complete attributes. Default: True.
When Docker compatibility is enabled with `compatible=True`: Default: False.
ignore_removed: If True, ignore failures due to missing containers.
Raises:
APIError: when service returns an error
"""
compatible = kwargs.get("compatible", False)
# Set sparse default based on mode:
# Libpod behavior: default is sparse=True (faster, requires reload for full details)
# Docker behavior: default is sparse=False (full details immediately, compatible)
if "sparse" in kwargs:
sparse = kwargs["sparse"]
else:
sparse = not compatible # True for libpod, False for compat
params = {
"all": kwargs.get("all"),
"filters": kwargs.get("filters", {}),
@ -86,10 +105,21 @@ class ContainersManager(RunMixin, CreateMixin, Manager):
# 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 = self.client.get("/containers/json", params=params, compatible=compatible)
response.raise_for_status()
return [self.prepare_model(attrs=i) for i in response.json()]
containers: list[Container] = [self.prepare_model(attrs=i) for i in response.json()]
# If sparse is False, reload each container to get full details
if not sparse:
for container in containers:
try:
container.reload(compatible=compatible)
except APIError:
# Skip containers that might have been removed
pass
return containers
def prune(self, filters: Mapping[str, str] = None) -> dict[str, Any]:
"""Delete stopped containers.

View File

@ -44,7 +44,14 @@ class RunMixin: # pylint: disable=too-few-public-methods
side. Default: False.
Keyword Args:
- See the create() method for keyword arguments.
- These args are directly used to pull an image when the image is not found.
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]]
policy (str): Pull policy. "missing" (default), "always", "never", "newer"
- See the create() method for other keyword arguments.
Returns:
- When detach is True, return a Container
@ -66,7 +73,12 @@ class RunMixin: # pylint: disable=too-few-public-methods
try:
container = self.create(image=image, command=command, **kwargs)
except ImageNotFound:
self.podman_client.images.pull(image, platform=kwargs.get("platform"))
self.podman_client.images.pull(
image,
auth_config=kwargs.get("auth_config"),
platform=kwargs.get("platform"),
policy=kwargs.get("policy", "missing"),
)
container = self.create(image=image, command=command, **kwargs)
container.start()

View File

@ -85,7 +85,8 @@ class BuildMixin:
if kwargs.get("custom_context"):
if "fileobj" not in kwargs:
raise PodmanError(
"Custom context requires fileobj to be set to a binary file-like object containing a build-directory tarball."
"Custom context requires fileobj to be set to a binary file-like object "
"containing a build-directory tarball."
)
if "dockerfile" not in kwargs:
# TODO: Scan the tarball for either a Dockerfile or a Containerfile.
@ -93,7 +94,8 @@ class BuildMixin:
# and could require buffering/copying the tarball if `fileobj` is not seekable.
# As a workaround for now, don't support omitting the filename.
raise PodmanError(
"Custom context requires specifying the name of the Dockerfile (typically 'Dockerfile' or 'Containerfile')."
"Custom context requires specifying the name of the Dockerfile "
"(typically 'Dockerfile' or 'Containerfile')."
)
body = kwargs["fileobj"]
elif "fileobj" in kwargs:

View File

@ -337,6 +337,7 @@ class ImagesManager(BuildMixin, Manager):
decode (bool) Decode the JSON data from the server into dicts.
Only applies with ``stream=True``
platform (str) Platform in the format os[/arch[/variant]]
policy (str) - Pull policy. "always" (default), "missing", "never", "newer"
progress_bar (bool) - Display a progress bar with the image pull progress (uses
the compat endpoint). Default: False
tls_verify (bool) - Require TLS verification. Default: True.
@ -365,6 +366,7 @@ class ImagesManager(BuildMixin, Manager):
}
params = {
"policy": kwargs.get("policy", "always"),
"reference": repository,
"tlsVerify": kwargs.get("tls_verify", True),
"compatMode": kwargs.get("compatMode", True),

View File

@ -2,11 +2,14 @@
from abc import ABC, abstractmethod
from collections import abc
from typing import Any, Optional, TypeVar, Union
from typing import Any, Optional, TypeVar, Union, TYPE_CHECKING
from collections.abc import Mapping
from podman.api.client import APIClient
if TYPE_CHECKING:
from podman import PodmanClient
# Methods use this Type when a subclass of PodmanResource is expected.
PodmanResourceType: TypeVar = TypeVar("PodmanResourceType", bound="PodmanResource")
@ -67,9 +70,13 @@ class PodmanResource(ABC): # noqa: B024
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)
def reload(self, **kwargs) -> None:
"""Refresh this object's data from the service.
Keyword Args:
compatible (bool): Use Docker compatibility endpoint
"""
latest = self.manager.get(self.id, **kwargs)
self.attrs = latest.attrs

View File

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

View File

@ -1,10 +1,11 @@
import unittest
import pytest
import re
import os
import pytest
import podman.tests.integration.base as base
from podman import PodmanClient
from podman.tests.utils import PODMAN_VERSION
# @unittest.skipIf(os.geteuid() != 0, 'Skipping, not running as root')
@ -21,7 +22,7 @@ class ContainersIntegrationTest(base.IntegrationTest):
self.alpine_image = self.client.images.pull("quay.io/libpod/alpine", tag="latest")
self.containers = []
def tearUp(self):
def tearDown(self):
for container in self.containers:
container.remove(force=True)
@ -103,6 +104,44 @@ class ContainersIntegrationTest(base.IntegrationTest):
for hosts_entry in formatted_hosts:
self.assertIn(hosts_entry, logs)
def test_container_environment_variables(self):
"""Test environment variables passed to the container."""
with self.subTest("Check environment variables as dictionary"):
env_dict = {"MY_VAR": "123", "ANOTHER_VAR": "456"}
container = self.client.containers.create(
self.alpine_image, command=["env"], environment=env_dict
)
self.containers.append(container)
container_env = container.attrs.get('Config', {}).get('Env', [])
for key, value in env_dict.items():
self.assertIn(f"{key}={value}", container_env)
container.start()
container.wait()
logs = b"\n".join(container.logs()).decode()
for key, value in env_dict.items():
self.assertIn(f"{key}={value}", logs)
with self.subTest("Check environment variables as list"):
env_list = ["MY_VAR=123", "ANOTHER_VAR=456"]
container = self.client.containers.create(
self.alpine_image, command=["env"], environment=env_list
)
self.containers.append(container)
container_env = container.attrs.get('Config', {}).get('Env', [])
for env in env_list:
self.assertIn(env, container_env)
container.start()
container.wait()
logs = b"\n".join(container.logs()).decode()
for env in env_list:
self.assertIn(env, logs)
def _test_memory_limit(self, parameter_name, host_config_name, set_mem_limit=False):
"""Base for tests which checks memory limits"""
memory_limit_tests = [
@ -240,6 +279,11 @@ class ContainersIntegrationTest(base.IntegrationTest):
"""Test passing shared memory size"""
self._test_memory_limit('shm_size', 'ShmSize')
@pytest.mark.skipif(os.geteuid() != 0, reason='Skipping, not running as root')
@pytest.mark.skipif(
PODMAN_VERSION >= (5, 6, 0),
reason="Test against this feature in Podman 5.6.0 or greater https://github.com/containers/podman/pull/25942",
)
def test_container_mounts(self):
"""Test passing mounts"""
with self.subTest("Check bind mount"):
@ -312,9 +356,11 @@ class ContainersIntegrationTest(base.IntegrationTest):
self.assertEqual(container.attrs.get('State', dict()).get('ExitCode', 256), 0)
@pytest.mark.pnext
# repeat this test against this upstream change
# https://github.com/containers/podman/pull/25942
@pytest.mark.skipif(os.geteuid() != 0, reason='Skipping, not running as root')
@pytest.mark.skipif(
PODMAN_VERSION < (5, 6, 0),
reason="Test against this feature before Podman 5.6.0 https://github.com/containers/podman/pull/25942",
)
def test_container_mounts_without_rw_as_default(self):
"""Test passing mounts"""
with self.subTest("Check bind mount"):

View File

@ -164,11 +164,11 @@ class ImagesIntegrationTest(base.IntegrationTest):
# Rewind to the start of the generated file so we can read it
context.seek(0)
with self.assertRaises(PodmanError) as e:
with self.assertRaises(PodmanError):
# If requesting a custom context, must provide the context as `fileobj`
self.client.images.build(custom_context=True, path='invalid')
with self.assertRaises(PodmanError) as e:
with self.assertRaises(PodmanError):
# If requesting a custom context, currently must specify the dockerfile name
self.client.images.build(custom_context=True, fileobj=context)

View File

@ -8,12 +8,13 @@ except ImportError:
# Python < 3.10
from collections.abc import Iterator
from unittest.mock import DEFAULT, patch, MagicMock
from unittest.mock import DEFAULT, MagicMock, patch
import requests_mock
from podman import PodmanClient, tests
from podman.domain.containers import Container
from podman.domain.containers_create import CreateMixin
from podman.domain.containers_manager import ContainersManager
from podman.errors import ImageNotFound, NotFound
@ -64,7 +65,8 @@ class ContainersManagerTestCase(unittest.TestCase):
"87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
)
self.assertEqual(
actual.id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
actual.id,
"87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
)
@requests_mock.Mocker()
@ -104,10 +106,12 @@ class ContainersManagerTestCase(unittest.TestCase):
self.assertIsInstance(actual, list)
self.assertEqual(
actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
actual[0].id,
"87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
)
self.assertEqual(
actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03"
actual[1].id,
"6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03",
)
@requests_mock.Mocker()
@ -132,10 +136,12 @@ class ContainersManagerTestCase(unittest.TestCase):
self.assertIsInstance(actual, list)
self.assertEqual(
actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
actual[0].id,
"87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
)
self.assertEqual(
actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03"
actual[1].id,
"6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03",
)
@requests_mock.Mocker()
@ -147,6 +153,24 @@ class ContainersManagerTestCase(unittest.TestCase):
actual = self.client.containers.list()
self.assertIsInstance(actual, list)
self.assertEqual(
actual[0].id,
"87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
)
self.assertEqual(
actual[1].id,
"6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03",
)
@requests_mock.Mocker()
def test_list_sparse_libpod_default(self, mock):
mock.get(
tests.LIBPOD_URL + "/containers/json",
json=[FIRST_CONTAINER, SECOND_CONTAINER],
)
actual = self.client.containers.list()
self.assertIsInstance(actual, list)
self.assertEqual(
actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
)
@ -154,6 +178,118 @@ class ContainersManagerTestCase(unittest.TestCase):
actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03"
)
# Verify that no individual reload() calls were made for sparse=True (default)
# Should be only 1 request for the list endpoint
self.assertEqual(len(mock.request_history), 1)
# lower() needs to be enforced since the mocked url is transformed as lowercase and
# this avoids %2f != %2F errors. Same applies for other instances of assertEqual
self.assertEqual(mock.request_history[0].url, tests.LIBPOD_URL.lower() + "/containers/json")
@requests_mock.Mocker()
def test_list_sparse_libpod_false(self, mock):
mock.get(
tests.LIBPOD_URL + "/containers/json",
json=[FIRST_CONTAINER, SECOND_CONTAINER],
)
# Mock individual container detail endpoints for reload() calls
# that are done for sparse=False
mock.get(
tests.LIBPOD_URL + f"/containers/{FIRST_CONTAINER['Id']}/json",
json=FIRST_CONTAINER,
)
mock.get(
tests.LIBPOD_URL + f"/containers/{SECOND_CONTAINER['Id']}/json",
json=SECOND_CONTAINER,
)
actual = self.client.containers.list(sparse=False)
self.assertIsInstance(actual, list)
self.assertEqual(
actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
)
self.assertEqual(
actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03"
)
# Verify that individual reload() calls were made for sparse=False
# Should be 3 requests total: 1 for list + 2 for individual container details
self.assertEqual(len(mock.request_history), 3)
# Verify the list endpoint was called first
self.assertEqual(mock.request_history[0].url, tests.LIBPOD_URL.lower() + "/containers/json")
# Verify the individual container detail endpoints were called
individual_urls = {req.url for req in mock.request_history[1:]}
expected_urls = {
tests.LIBPOD_URL.lower() + f"/containers/{FIRST_CONTAINER['Id']}/json",
tests.LIBPOD_URL.lower() + f"/containers/{SECOND_CONTAINER['Id']}/json",
}
self.assertEqual(individual_urls, expected_urls)
@requests_mock.Mocker()
def test_list_sparse_compat_default(self, mock):
mock.get(
tests.COMPATIBLE_URL + "/containers/json",
json=[FIRST_CONTAINER, SECOND_CONTAINER],
)
# Mock individual container detail endpoints for reload() calls
# that are done for sparse=False
mock.get(
tests.COMPATIBLE_URL + f"/containers/{FIRST_CONTAINER['Id']}/json",
json=FIRST_CONTAINER,
)
mock.get(
tests.COMPATIBLE_URL + f"/containers/{SECOND_CONTAINER['Id']}/json",
json=SECOND_CONTAINER,
)
actual = self.client.containers.list(compatible=True)
self.assertIsInstance(actual, list)
self.assertEqual(
actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
)
self.assertEqual(
actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03"
)
# Verify that individual reload() calls were made for compat default (sparse=True)
# Should be 3 requests total: 1 for list + 2 for individual container details
self.assertEqual(len(mock.request_history), 3)
self.assertEqual(
mock.request_history[0].url, tests.COMPATIBLE_URL.lower() + "/containers/json"
)
# Verify the individual container detail endpoints were called
individual_urls = {req.url for req in mock.request_history[1:]}
expected_urls = {
tests.COMPATIBLE_URL.lower() + f"/containers/{FIRST_CONTAINER['Id']}/json",
tests.COMPATIBLE_URL.lower() + f"/containers/{SECOND_CONTAINER['Id']}/json",
}
self.assertEqual(individual_urls, expected_urls)
@requests_mock.Mocker()
def test_list_sparse_compat_true(self, mock):
mock.get(
tests.COMPATIBLE_URL + "/containers/json",
json=[FIRST_CONTAINER, SECOND_CONTAINER],
)
actual = self.client.containers.list(sparse=True, compatible=True)
self.assertIsInstance(actual, list)
self.assertEqual(
actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
)
self.assertEqual(
actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03"
)
# Verify that no individual reload() calls were made for sparse=True
# Should be only 1 request for the list endpoint
self.assertEqual(len(mock.request_history), 1)
self.assertEqual(
mock.request_history[0].url, tests.COMPATIBLE_URL.lower() + "/containers/json"
)
@requests_mock.Mocker()
def test_prune(self, mock):
mock.post(
@ -228,8 +364,8 @@ class ContainersManagerTestCase(unittest.TestCase):
json=FIRST_CONTAINER,
)
port_str = {'2233': 3333}
port_str_protocol = {'2244/tcp': 3344}
port_str = {"2233": 3333}
port_str_protocol = {"2244/tcp": 3344}
port_int = {2255: 3355}
ports = {**port_str, **port_str_protocol, **port_int}
self.client.containers.create("fedora", "/usr/bin/ls", ports=ports)
@ -237,23 +373,23 @@ class ContainersManagerTestCase(unittest.TestCase):
self.client.containers.client.post.assert_called()
expected_ports = [
{
'container_port': 2233,
'host_port': 3333,
'protocol': 'tcp',
"container_port": 2233,
"host_port": 3333,
"protocol": "tcp",
},
{
'container_port': 2244,
'host_port': 3344,
'protocol': 'tcp',
"container_port": 2244,
"host_port": 3344,
"protocol": "tcp",
},
{
'container_port': 2255,
'host_port': 3355,
'protocol': 'tcp',
"container_port": 2255,
"host_port": 3355,
"protocol": "tcp",
},
]
actual_ports = json.loads(self.client.containers.client.post.call_args[1]['data'])[
'portmappings'
actual_ports = json.loads(self.client.containers.client.post.call_args[1]["data"])[
"portmappings"
]
self.assertEqual(expected_ports, actual_ports)
@ -313,6 +449,127 @@ class ContainersManagerTestCase(unittest.TestCase):
with self.assertRaises(TypeError):
self.client.containers.create("fedora", "/usr/bin/ls", unknown_key=100.0)
@requests_mock.Mocker()
def test_create_convert_env_list_to_dict(self, mock):
env_list1 = ["FOO=foo", "BAR=bar"]
# Test valid list
converted_dict1 = {"FOO": "foo", "BAR": "bar"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list1), converted_dict1)
# Test empty string
env_list2 = ["FOO=foo", ""]
self.assertRaises(ValueError, CreateMixin._convert_env_list_to_dict, env_list2)
# Test non iterable
env_list3 = ["FOO=foo", None]
self.assertRaises(TypeError, CreateMixin._convert_env_list_to_dict, env_list3)
# Test iterable with non string element
env_list4 = ["FOO=foo", []]
self.assertRaises(TypeError, CreateMixin._convert_env_list_to_dict, env_list4)
# Test empty list
env_list5 = []
converted_dict5 = {}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list5), converted_dict5)
# Test single valid environment variable
env_list6 = ["SINGLE=value"]
converted_dict6 = {"SINGLE": "value"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list6), converted_dict6)
# Test environment variable with empty value
env_list7 = ["EMPTY="]
converted_dict7 = {"EMPTY": ""}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list7), converted_dict7)
# Test environment variable with multiple equals signs
env_list8 = ["URL=https://example.com/path?param=value"]
converted_dict8 = {"URL": "https://example.com/path?param=value"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list8), converted_dict8)
# Test environment variable with spaces in value
env_list9 = ["MESSAGE=Hello World", "PATH=/usr/local/bin:/usr/bin"]
converted_dict9 = {"MESSAGE": "Hello World", "PATH": "/usr/local/bin:/usr/bin"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list9), converted_dict9)
# Test environment variable with special characters
env_list10 = ["SPECIAL=!@#$%^&*()_+-=[]{}|;':\",./<>?"]
converted_dict10 = {"SPECIAL": "!@#$%^&*()_+-=[]{}|;':\",./<>?"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list10), converted_dict10)
# Test environment variable with numeric values
env_list11 = ["PORT=8080", "TIMEOUT=30"]
converted_dict11 = {"PORT": "8080", "TIMEOUT": "30"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list11), converted_dict11)
# Test environment variable with boolean-like values
env_list12 = ["DEBUG=true", "VERBOSE=false", "ENABLED=1", "DISABLED=0"]
converted_dict12 = {
"DEBUG": "true",
"VERBOSE": "false",
"ENABLED": "1",
"DISABLED": "0",
}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list12), converted_dict12)
# Test environment variable with whitespace in key (should preserve)
env_list13 = [" SPACED_KEY =value", "KEY= spaced_value "]
converted_dict13 = {" SPACED_KEY ": "value", "KEY": " spaced_value "}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list13), converted_dict13)
# Test missing equals sign
env_list14 = ["FOO=foo", "INVALID"]
self.assertRaises(ValueError, CreateMixin._convert_env_list_to_dict, env_list14)
# Test environment variable with only equals sign (empty key)
env_list15 = ["FOO=foo", "=value"]
self.assertRaises(ValueError, CreateMixin._convert_env_list_to_dict, env_list15)
# Test environment variable with only whitespace key
env_list16 = ["FOO=foo", " =value"]
self.assertRaises(ValueError, CreateMixin._convert_env_list_to_dict, env_list16)
# Test whitespace-only string
env_list17 = ["FOO=foo", " "]
self.assertRaises(ValueError, CreateMixin._convert_env_list_to_dict, env_list17)
# Test various non-string types in list
env_list18 = ["FOO=foo", 123]
self.assertRaises(TypeError, CreateMixin._convert_env_list_to_dict, env_list18)
env_list19 = ["FOO=foo", {"key": "value"}]
self.assertRaises(TypeError, CreateMixin._convert_env_list_to_dict, env_list19)
env_list20 = ["FOO=foo", True]
self.assertRaises(TypeError, CreateMixin._convert_env_list_to_dict, env_list20)
# Test duplicate keys (last one should win)
env_list21 = ["KEY=first", "KEY=second", "OTHER=value"]
converted_dict21 = {"KEY": "second", "OTHER": "value"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list21), converted_dict21)
# Test very long environment variable
long_value = "x" * 1000
env_list22 = [f"LONG_VAR={long_value}"]
converted_dict22 = {"LONG_VAR": long_value}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list22), converted_dict22)
# Test environment variable with newlines and tabs
env_list23 = ["MULTILINE=line1\nline2\ttabbed"]
converted_dict23 = {"MULTILINE": "line1\nline2\ttabbed"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list23), converted_dict23)
# Test environment variable with unicode characters
env_list24 = ["UNICODE=こんにちは", "EMOJI=🚀🌟"]
converted_dict24 = {"UNICODE": "こんにちは", "EMOJI": "🚀🌟"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list24), converted_dict24)
# Test case sensitivity
env_list25 = ["path=/usr/bin", "PATH=/usr/local/bin"]
converted_dict25 = {"path": "/usr/bin", "PATH": "/usr/local/bin"}
self.assertEqual(CreateMixin._convert_env_list_to_dict(env_list25), converted_dict25)
@requests_mock.Mocker()
def test_run_detached(self, mock):
mock.post(
@ -375,7 +632,7 @@ class ContainersManagerTestCase(unittest.TestCase):
actual = self.client.containers.run("fedora", "/usr/bin/ls")
self.assertIsInstance(actual, bytes)
self.assertEqual(actual, b'This is a unittest - line 1This is a unittest - line 2')
self.assertEqual(actual, b"This is a unittest - line 1This is a unittest - line 2")
# iter() cannot be reset so subtests used to create new instance
with self.subTest("Stream results"):
@ -388,5 +645,5 @@ class ContainersManagerTestCase(unittest.TestCase):
self.assertEqual(next(actual), b"This is a unittest - line 2")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,84 @@
import unittest
import requests_mock
from podman import PodmanClient, tests
CONTAINER = {
"Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
"Name": "quay.io/fedora:latest",
"Image": "eloquent_pare",
"State": {"Status": "running"},
}
class PodmanResourceTestCase(unittest.TestCase):
"""Test PodmanResource area of concern."""
def setUp(self) -> None:
super().setUp()
self.client = PodmanClient(base_url=tests.BASE_SOCK)
def tearDown(self) -> None:
super().tearDown()
self.client.close()
@requests_mock.Mocker()
def test_reload_with_compatible_options(self, mock):
"""Test that reload uses the correct endpoint."""
# Mock the get() call
mock.get(
f"{tests.LIBPOD_URL}/"
f"containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json",
json=CONTAINER,
)
# Mock the reload() call
mock.get(
f"{tests.LIBPOD_URL}/"
f"containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json",
json=CONTAINER,
)
# Mock the reload(compatible=False) call
mock.get(
f"{tests.LIBPOD_URL}/"
f"containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json",
json=CONTAINER,
)
# Mock the reload(compatible=True) call
mock.get(
f"{tests.COMPATIBLE_URL}/"
f"containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json",
json=CONTAINER,
)
container = self.client.containers.get(
"87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd"
)
container.reload()
container.reload(compatible=False)
container.reload(compatible=True)
self.assertEqual(len(mock.request_history), 4)
for i in range(3):
self.assertEqual(
mock.request_history[i].url,
tests.LIBPOD_URL.lower()
+ "/containers/"
+ "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json",
)
self.assertEqual(
mock.request_history[3].url,
tests.COMPATIBLE_URL.lower()
+ "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/json",
)
if __name__ == '__main__':
unittest.main()

View File

@ -517,7 +517,7 @@ class ImagesManagerTestCase(unittest.TestCase):
self.assertEqual(report[0]["name"], "quay.io/libpod/fedora")
@requests_mock.Mocker()
def test_search_listTags(self, mock):
def test_search_list_tags(self, mock):
mock.get(
tests.LIBPOD_URL + "/images/search?term=fedora&noTrunc=true&listTags=true",
json=[
@ -649,6 +649,27 @@ class ImagesManagerTestCase(unittest.TestCase):
images[1].id, "c4b16966ecd94ffa910eab4e630e24f259bf34a87e924cd4b1434f267b0e354e"
)
@requests_mock.Mocker()
def test_pull_policy(self, mock):
image_id = "sha256:326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab"
mock.post(
tests.LIBPOD_URL + "/images/pull?reference=quay.io%2ffedora%3Alatest&policy=missing",
json={
"error": "",
"id": image_id,
"images": [image_id],
"stream": "",
},
)
mock.get(
tests.LIBPOD_URL + "/images"
"/sha256%3A326dd9d7add24646a325e8eaa82125294027db2332e49c5828d96312c5d773ab/json",
json=FIRST_IMAGE,
)
image = self.client.images.pull("quay.io/fedora:latest", policy="missing")
self.assertEqual(image.id, image_id)
@requests_mock.Mocker()
def test_list_with_name_parameter(self, mock):
"""Test that name parameter is correctly converted to a reference filter"""

View File

@ -0,0 +1,44 @@
import unittest
from unittest.mock import patch, MagicMock
from podman.tests import utils
class TestPodmanVersion(unittest.TestCase):
@patch('podman.tests.utils.subprocess.Popen')
def test_podman_version(self, mock_popen):
mock_proc = MagicMock()
mock_proc.stdout.read.return_value = b'5.6.0'
mock_popen.return_value.__enter__.return_value = mock_proc
self.assertEqual(utils.podman_version(), (5, 6, 0))
@patch('podman.tests.utils.subprocess.Popen')
def test_podman_version_dev(self, mock_popen):
mock_proc = MagicMock()
mock_proc.stdout.read.return_value = b'5.6.0-dev'
mock_popen.return_value.__enter__.return_value = mock_proc
self.assertEqual(utils.podman_version(), (5, 6, 0))
@patch('podman.tests.utils.subprocess.Popen')
def test_podman_version_four_digits(self, mock_popen):
mock_proc = MagicMock()
mock_proc.stdout.read.return_value = b'5.6.0.1'
mock_popen.return_value.__enter__.return_value = mock_proc
self.assertEqual(utils.podman_version(), (5, 6, 0))
@patch('podman.tests.utils.subprocess.Popen')
def test_podman_version_release_candidate(self, mock_popen):
mock_proc = MagicMock()
mock_proc.stdout.read.return_value = b'5.6.0-rc1'
mock_popen.return_value.__enter__.return_value = mock_proc
self.assertEqual(utils.podman_version(), (5, 6, 0))
@patch('podman.tests.utils.subprocess.Popen')
def test_podman_version_none(self, mock_popen):
mock_proc = MagicMock()
mock_proc.stdout.read.return_value = b''
mock_popen.return_value.__enter__.return_value = mock_proc
with self.assertRaises(RuntimeError) as context:
utils.podman_version()
self.assertEqual(str(context.exception), "Unable to detect podman version. Got \"\"")

View File

@ -41,9 +41,7 @@ class VolumeTestCase(unittest.TestCase):
@requests_mock.Mocker()
def test_inspect(self, mock):
adapter = mock.get(
tests.LIBPOD_URL + "/volumes/dbase/json?tlsVerify=False", json=FIRST_VOLUME
)
mock.get(tests.LIBPOD_URL + "/volumes/dbase/json?tlsVerify=False", json=FIRST_VOLUME)
vol_manager = VolumesManager(self.client.api)
actual = vol_manager.prepare_model(attrs=FIRST_VOLUME)
self.assertEqual(actual.inspect(tls_verify=False)["Mountpoint"], "/var/database")

32
podman/tests/utils.py Normal file
View File

@ -0,0 +1,32 @@
import pathlib
import csv
import re
import subprocess
try:
from platform import freedesktop_os_release
except ImportError:
def freedesktop_os_release() -> dict[str, str]:
"""This is a fallback for platforms that don't have the freedesktop_os_release function.
Python < 3.10
"""
path = pathlib.Path("/etc/os-release")
with open(path) as f:
reader = csv.reader(f, delimiter="=")
return dict(reader)
def podman_version() -> tuple[int, ...]:
cmd = ["podman", "info", "--format", "{{.Version.Version}}"]
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
version = proc.stdout.read().decode("utf-8").strip()
match = re.match(r"(\d+\.\d+\.\d+)", version)
if not match:
raise RuntimeError(f"Unable to detect podman version. Got \"{version}\"")
version = match.group(1)
return tuple(int(x) for x in version.split("."))
OS_RELEASE = freedesktop_os_release()
PODMAN_VERSION = podman_version()

View File

@ -1,4 +1,4 @@
"""Version of PodmanPy."""
__version__ = "5.5.0"
__version__ = "5.6.0"
__compatible_version__ = "1.40"

View File

@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "podman"
# TODO: remove the line version = ... on podman-py > 5.4.0 releases
# dynamic = ["version"]
version = "5.5.0"
version = "5.6.0"
description = "Bindings for Podman RESTful API"
readme = "README.md"
license = {file = "LICENSE"}
@ -104,10 +104,10 @@ select = [
"E", # Pycodestyle Error
"W", # Pycodestyle Warning
"N", # PEP8 Naming
# TODO "UP", # Pyupgrade
"UP", # Pyupgrade
# TODO "ANN",
# TODO "S", # Bandit
# "B", # Bugbear
"B", # Bugbear
"A", # flake-8-builtins
"YTT", # flake-8-2020
"PLC", # Pylint Convention
@ -117,18 +117,7 @@ select = [
# Some checks should be enabled for code sanity disabled now
# to avoid changing too many lines
ignore = [
"F821", # TODO Undefined name
"F541", # TODO f-string is missing placeholders
"F401", # TODO Module imported but unused
"F841", # TODO Local variable is assigned to but never used
"E402", # TODO Module level import not at top of file
"E741", # TODO ambiguous variable name
"E722", # TODO do not use bare 'except'
"E501", # TODO line too long
"N818", # TODO Error Suffix in exception name
"N80", # TODO Invalid Name
"ANN10", # Missing type annotation
"PLW2901", # TODO Redefined Loop Name
]
[tool.ruff.lint.flake8-builtins]
builtins-ignorelist = ["copyright", "all"]

View File

@ -1,6 +1,6 @@
[metadata]
name = podman
version = 5.5.0
version = 5.6.0
author = Brent Baude, Jhon Honce, Urvashi Mohnani, Nicola Sella
author_email = jhonce@redhat.com
description = Bindings for Podman RESTful API

View File

@ -9,7 +9,7 @@ excluded = [
]
class build_py(build_py_orig):
class build_py(build_py_orig): # noqa: N801
def find_package_modules(self, package, package_dir):
modules = super().find_package_modules(package, package_dir)
return [

View File

@ -17,7 +17,7 @@ setenv =
commands = {posargs}
[testenv:lint]
deps = ruff==0.8.1
deps = ruff==0.12.8
allowlist_externals = ruff
commands = ruff check --diff
@ -29,7 +29,7 @@ commands =
coverage report -m --skip-covered --fail-under=80 --omit=podman/tests/* --omit=.tox/*
[testenv:format]
deps = ruff==0.8.1
deps = ruff==0.12.8
allowlist_externals = ruff
commands =
ruff format --diff