Skip to content

Commit ce6538b

Browse files
committed
fix: podman.domain.containers.run pulls image if not found
1 parent c9ca28d commit ce6538b

File tree

3 files changed

+113
-24
lines changed

3 files changed

+113
-24
lines changed

podman/domain/containers_create.py

Lines changed: 13 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

@@ -361,7 +361,6 @@ def create(
361361
A Container object.
362362
363363
Raises:
364-
ImageNotFound: when Image not found by Podman service
365364
APIError: when Podman service reports an error
366365
"""
367366
if isinstance(image, Image):
@@ -379,7 +378,18 @@ def create(
379378
headers={"content-type": "application/json"},
380379
data=payload,
381380
)
382-
response.raise_for_status(not_found=ImageNotFound)
381+
if response.status_code == requests.codes.not_found:
382+
self.podman_client.images.pull(
383+
image,
384+
auth_config=kwargs.get("auth_config"),
385+
platform=kwargs.get("platform"),
386+
policy=kwargs.get("policy", "missing"),
387+
)
388+
response = self.client.post(
389+
"/containers/create",
390+
headers={"content-type": "application/json"},
391+
data=payload,
392+
)
383393

384394
container_id = response.json()["Id"]
385395

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,24 +62,14 @@ 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):
6968
image = image.id
7069
if isinstance(command, str):
7170
command = [command]
7271

73-
try:
74-
container = self.create(image=image, command=command, **kwargs)
75-
except ImageNotFound:
76-
self.podman_client.images.pull(
77-
image,
78-
auth_config=kwargs.get("auth_config"),
79-
platform=kwargs.get("platform"),
80-
policy=kwargs.get("policy", "missing"),
81-
)
82-
container = self.create(image=image, command=command, **kwargs)
72+
container = self.create(image=image, command=command, **kwargs)
8373

8474
container.start()
8575
container.reload()

podman/tests/unit/test_containersmanager.py

Lines changed: 98 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,45 @@ 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+
],
362+
)
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+
auth_config=None,
372+
platform=None,
373+
policy="missing",
349374
)
350-
with self.assertRaises(ImageNotFound):
351-
self.client.containers.create("fedora", "/usr/bin/ls", cpu_count=9999)
375+
self.assertIsInstance(actual, Container)
376+
self.assertEqual(actual.id, FIRST_CONTAINER['Id'])
377+
# 2 POSTs for create
378+
# 1 GET for container json
379+
self.assertEqual(mock.call_count, 3)
352380

353381
@requests_mock.Mocker()
354382
def test_create_parse_host_port(self, mock):
@@ -644,6 +672,67 @@ def test_run(self, mock):
644672
self.assertEqual(next(actual), b"This is a unittest - line 1")
645673
self.assertEqual(next(actual), b"This is a unittest - line 2")
646674

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

648737
if __name__ == "__main__":
649738
unittest.main()

0 commit comments

Comments
 (0)