Compare commits
41 Commits
Author | SHA1 | Date |
---|---|---|
|
0fd4e55fd5 | |
|
a3a8b7ca23 | |
|
85bc9560a1 | |
|
77fea64100 | |
|
24379a4cf5 | |
|
c3d53a443e | |
|
3a8c5f9fb6 | |
|
c97384262b | |
|
3007fdba71 | |
|
d34c718e26 | |
|
5d4ff56c0c | |
|
010949a925 | |
|
1368c96bae | |
|
f38fe91d46 | |
|
c4c86486cc | |
|
f92036c156 | |
|
ca381f217a | |
|
f044c7e10c | |
|
058de18c20 | |
|
203eea8d5d | |
|
9b99805024 | |
|
365534ebfe | |
|
3c624222bc | |
|
2f4b14f8ee | |
|
7e834d3cbe | |
|
56ebf6c1ea | |
|
7e88fed72c | |
|
371ecb8ae6 | |
|
fd8bfcdadd | |
|
e472ae020d | |
|
a6ca81cec2 | |
|
fac45dd5ba | |
|
c150b07f29 | |
|
bbbe75813f | |
|
61f7725152 | |
|
f917e0a36c | |
|
527971f55e | |
|
ee13b44943 | |
|
068e23330f | |
|
23a0845b5e | |
|
4f843ad11c |
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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
1
OWNERS
|
@ -12,6 +12,7 @@ approvers:
|
|||
reviewers:
|
||||
- ashley-cui
|
||||
- baude
|
||||
- Honny1
|
||||
- rhatdan
|
||||
- TomSweeneyRedHat
|
||||
- Edward5hen
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
make tests
|
|
@ -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
|
|
@ -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__
|
||||
|
||||
|
|
|
@ -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()")
|
||||
|
|
|
@ -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()),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
|
@ -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"""
|
||||
|
|
|
@ -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 \"\"")
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
|
@ -1,4 +1,4 @@
|
|||
"""Version of PodmanPy."""
|
||||
|
||||
__version__ = "5.5.0"
|
||||
__version__ = "5.6.0"
|
||||
__compatible_version__ = "1.40"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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 [
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue