Skip to content

Commit 41e646d

Browse files
tollsimyinknos
authored andcommitted
fix: podman.domain.containers.run pulls image if not found
Signed-off-by: Simone Tollardo <simone.tollardo@redhat.com>
1 parent 52a3036 commit 41e646d

3 files changed

Lines changed: 154 additions & 24 deletions

File tree

‎podman/domain/containers_create.py‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from contextlib import suppress
88
from typing import Any, Union
99
from collections.abc import MutableMapping
10+
import requests
1011

1112
from podman import api
1213
from podman.domain.containers import Container
1314
from podman.domain.images import Image
1415
from podman.domain.pods import Pod
1516
from podman.domain.secrets import Secret
16-
from podman.errors import ImageNotFound
1717

1818
logger = logging.getLogger("podman.containers")
1919

@@ -36,6 +36,12 @@ def create(
3636
command: Command to run in the container.
3737
3838
Keyword Args:
39+
all_tags (bool): Pull all tags from repository. Default: False.
40+
Only used if the method needs to pull the requested image.
41+
auth_config (Mapping[str, str]): Override the credentials that are found in the
42+
config for this request. auth_config should contain the username and password
43+
keys to be valid.
44+
Only used if the method needs to pull the requested image.
3945
auto_remove (bool): Enable auto-removal of the container on daemon side when the
4046
container's process exits.
4147
blkio_weight_device (dict[str, Any]): Block IO weight (relative device weight)
@@ -55,6 +61,9 @@ def create(
5561
cpuset_cpus (str): CPUs in which to allow execution (0-3, 0,1).
5662
cpuset_mems (str): Memory nodes (MEMs) in which to allow execution (0-3, 0,1).
5763
Only effective on NUMA systems.
64+
decode (bool): Decode the JSON data from the server into dicts.
65+
Only applies with ``stream=True``. Default: False.
66+
Only used if the method needs to pull the requested image.
5867
detach (bool): Run container in the background and return a Container object.
5968
device_cgroup_rules (list[str]): A list of cgroup rules to apply to the container.
6069
device_read_bps: Limit read rate (bytes per second) from a device in the form of:
@@ -210,6 +219,8 @@ def create(
210219
pids_limit (int): Tune a container's pids limit. Set -1 for unlimited.
211220
platform (str): Platform in the format os[/arch[/variant]]. Only used if the method
212221
needs to pull the requested image.
222+
policy (str): Pull policy. "missing" (default for create), "always", "never", "newer".
223+
Only used if the method needs to pull the requested image.
213224
ports (
214225
dict[
215226
Union[int, str],
@@ -271,7 +282,12 @@ def create(
271282
272283
}
273284
privileged (bool): Give extended privileges to this container.
285+
progress_bar (bool): Display a progress bar with the image pull progress (uses
286+
the compat endpoint). Default: False.
287+
Only used if the method needs to pull the requested image.
274288
publish_all_ports (bool): Publish all ports to the host.
289+
pull_stream (bool): When True, the pull progress will be published as received.
290+
Default: False. Only used if the method needs to pull the requested image.
275291
read_only (bool): Mount the container's root filesystem as read only.
276292
read_write_tmpfs (bool): Mount temporary file systems as read write,
277293
in case of read_only options set to True. Default: False
@@ -340,6 +356,8 @@ def create(
340356
341357
For example: {'/mnt/vol2': '', '/mnt/vol1': 'size=3G,uid=1000'}
342358
359+
tls_verify (bool): Require TLS verification. Default: True.
360+
Only used if the method needs to pull the requested image.
343361
tty (bool): Allocate a pseudo-TTY.
344362
ulimits (list[Ulimit]): Ulimits to set inside the container.
345363
use_config_proxy (bool): If True, and if the docker client configuration
@@ -391,7 +409,6 @@ def create(
391409
A Container object.
392410
393411
Raises:
394-
ImageNotFound: when Image not found by Podman service
395412
APIError: when Podman service reports an error
396413
"""
397414
if isinstance(image, Image):
@@ -409,7 +426,24 @@ def create(
409426
headers={"content-type": "application/json"},
410427
data=payload,
411428
)
412-
response.raise_for_status(not_found=ImageNotFound)
429+
if response.status_code == requests.codes.not_found:
430+
self.podman_client.images.pull(
431+
image,
432+
all_tags=kwargs.get("all_tags", False),
433+
auth_config=kwargs.get("auth_config"),
434+
decode=kwargs.get("decode", False),
435+
platform=kwargs.get("platform"),
436+
policy=kwargs.get("policy", "missing"),
437+
progress_bar=kwargs.get("progress_bar", False),
438+
stream=kwargs.get("pull_stream", False),
439+
tls_verify=kwargs.get("tls_verify", True),
440+
)
441+
response = self.client.post(
442+
"/containers/create",
443+
headers={"content-type": "application/json"},
444+
data=payload,
445+
)
446+
response.raise_for_status()
413447

414448
container_id = response.json()["Id"]
415449

@@ -479,6 +513,13 @@ def _render_payload(kwargs: MutableMapping[str, Any]) -> dict[str, Any]:
479513
"stdout", # used by caller
480514
"stream", # used by caller
481515
"detach", # used by caller
516+
"all_tags", # used by caller
517+
"auth_config", # used by caller
518+
"decode", # used by caller
519+
"policy", # used by caller
520+
"progress_bar", # used by caller
521+
"pull_stream", # used by caller
522+
"tls_verify", # used by caller
482523
"volume_driver",
483524
):
484525
with suppress(KeyError):

‎podman/domain/containers_run.py‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from podman.domain.containers import Container
1010
from podman.domain.images import Image
11-
from podman.errors import ContainerError, ImageNotFound
11+
from podman.errors import ContainerError
1212

1313
logger = logging.getLogger("podman.containers")
1414

@@ -62,7 +62,6 @@ def run(
6262
6363
Raises:
6464
ContainerError: when Container exists with a non-zero code
65-
ImageNotFound: when Image not found by Podman service
6665
APIError: when Podman service reports an error
6766
"""
6867
if isinstance(image, Image):
@@ -72,16 +71,7 @@ def run(
7271
if isinstance(command, str):
7372
command = [command]
7473

75-
try:
76-
container = self.create(image=image_id, command=command, **kwargs) # type: ignore[attr-defined]
77-
except ImageNotFound:
78-
self.podman_client.images.pull( # type: ignore[attr-defined]
79-
image_id,
80-
auth_config=kwargs.get("auth_config"),
81-
platform=kwargs.get("platform"),
82-
policy=kwargs.get("policy", "missing"),
83-
)
84-
container = self.create(image=image_id, command=command, **kwargs) # type: ignore[attr-defined]
74+
container = self.create(image=image_id, command=command, **kwargs) # type: ignore[attr-defined]
8575

8676
container.start()
8777
container.reload()

‎podman/tests/unit/test_containersmanager.py‎

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from podman.domain.containers import Container
1717
from podman.domain.containers_create import CreateMixin
1818
from podman.domain.containers_manager import ContainersManager
19-
from podman.errors import ImageNotFound, NotFound
19+
from podman.errors import NotFound
2020

2121
FIRST_CONTAINER = {
2222
"Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
@@ -338,17 +338,50 @@ def test_create(self, mock):
338338

339339
@requests_mock.Mocker()
340340
def test_create_404(self, mock):
341+
# mock the first POST to return 404,
342+
# then the second POST (after pulling the image) to return 201
341343
mock.post(
342344
tests.LIBPOD_URL + "/containers/create",
343-
status_code=404,
344-
json={
345-
"cause": "Image not found",
346-
"message": "Image not found",
347-
"response": 404,
348-
},
345+
[
346+
{
347+
"status_code": 404,
348+
"json": {
349+
"cause": "Image not found",
350+
"message": "Image not found",
351+
"response": 404,
352+
},
353+
},
354+
{
355+
"status_code": 201,
356+
"json": {
357+
"Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
358+
"Warnings": [],
359+
},
360+
},
361+
],
349362
)
350-
with self.assertRaises(ImageNotFound):
351-
self.client.containers.create("fedora", "/usr/bin/ls", cpu_count=9999)
363+
self.client.images.pull = MagicMock()
364+
mock.get(
365+
tests.LIBPOD_URL + f"/containers/{FIRST_CONTAINER['Id']}/json",
366+
json=FIRST_CONTAINER,
367+
)
368+
actual = self.client.containers.create("fedora", "/usr/bin/ls", cpu_count=9999)
369+
self.client.images.pull.assert_called_once_with(
370+
"fedora",
371+
all_tags=False,
372+
auth_config=None,
373+
decode=False,
374+
platform=None,
375+
policy="missing",
376+
progress_bar=False,
377+
stream=False,
378+
tls_verify=True,
379+
)
380+
self.assertIsInstance(actual, Container)
381+
self.assertEqual(actual.id, FIRST_CONTAINER['Id'])
382+
# 2 POSTs for create
383+
# 1 GET for container json
384+
self.assertEqual(mock.call_count, 3)
352385

353386
@requests_mock.Mocker()
354387
def test_create_parse_host_port(self, mock):
@@ -644,6 +677,72 @@ def test_run(self, mock):
644677
self.assertEqual(next(actual), b"This is a unittest - line 1")
645678
self.assertEqual(next(actual), b"This is a unittest - line 2")
646679

680+
@requests_mock.Mocker()
681+
def test_run_404(self, mock):
682+
# mock the first POST to return 404,
683+
# then the second POST (after pulling the image) to return 201
684+
mock.post(
685+
tests.LIBPOD_URL + "/containers/create",
686+
[
687+
{
688+
"status_code": 404,
689+
"json": {
690+
"cause": "Image not found",
691+
"message": "Image not found",
692+
"response": 404,
693+
},
694+
},
695+
{
696+
"status_code": 201,
697+
"json": {
698+
"Id": "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd",
699+
"Warnings": [],
700+
},
701+
},
702+
],
703+
)
704+
self.client.images.pull = MagicMock()
705+
mock.post(
706+
tests.LIBPOD_URL
707+
+ "/containers/87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd/start",
708+
status_code=204,
709+
)
710+
mock.get(
711+
tests.LIBPOD_URL + f"/containers/{FIRST_CONTAINER['Id']}/json",
712+
json=FIRST_CONTAINER,
713+
)
714+
715+
mock_logs = (
716+
b"This is a unittest - line 1",
717+
b"This is a unittest - line 2",
718+
)
719+
720+
with patch.multiple(Container, logs=DEFAULT, wait=DEFAULT, autospec=True) as mock_container:
721+
mock_container["wait"].return_value = 0
722+
mock_container["logs"].return_value = iter(mock_logs)
723+
724+
actual = self.client.containers.run("fedora", "/usr/bin/ls")
725+
self.client.images.pull.assert_called_once_with(
726+
"fedora",
727+
auth_config=None,
728+
platform=None,
729+
policy="missing",
730+
progress_bar=False,
731+
tls_verify=True,
732+
decode=False,
733+
all_tags=False,
734+
stream=False,
735+
)
736+
self.assertIsInstance(actual, bytes)
737+
self.assertEqual(actual, b"This is a unittest - line 1This is a unittest - line 2")
738+
# 2 POSTs for create
739+
# 1 POST for start
740+
# 1 GET for container json
741+
# 1 GET for reload
742+
for r in mock.request_history:
743+
print(r)
744+
self.assertEqual(mock.call_count, 5)
745+
647746
@requests_mock.Mocker()
648747
def test_create_all_healthcheck_parameters(self, mock):
649748
"""Test that all healthcheck parameters are correctly passed to the API."""

0 commit comments

Comments
 (0)