Skip to content

Commit 1f77bb6

Browse files
Merge pull request #269 from JonathonReinhart/get_image_entrypoint-fix
Fix get_image_entrypoint() exception on Docker CE 28.2
2 parents 429ec47 + c100744 commit 1f77bb6

File tree

3 files changed

+246
-21
lines changed

3 files changed

+246
-21
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased]
6+
### Fixed
7+
- Fixed regression with Docker Enginer 28.2 (#269)
8+
69
### Removed
710
- Dropped support for Python 3.7 (#242)
811

scuba/dockerutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def get_images() -> Sequence[str]:
113113
def _get_image_config(image: str, key: str) -> Optional[Sequence[str]]:
114114
info = docker_inspect_or_pull(image)
115115
try:
116-
result = info["Config"][key]
116+
result = info["Config"].get(key)
117117
except KeyError as ke:
118118
raise DockerError(f"Failed to inspect image: JSON result missing key {ke}")
119119

tests/test_dockerutil.py

Lines changed: 242 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
from pathlib import Path
22
import pytest
33
import subprocess
4-
from typing import Sequence
4+
from typing import Optional, Sequence
55
from unittest import mock
66

77
from .const import ALT_DOCKER_IMAGE, DOCKER_IMAGE
88

99
import scuba.dockerutil as uut
1010

1111

12+
def _mock_subprocess_run( # type: ignore[no-untyped-def]
13+
stdout: str,
14+
returncode: int = 0,
15+
expected_args: Optional[Sequence[str]] = None,
16+
): # -> mock._patch[mock.MagicMock]:
17+
def mocked_run(args, **kwargs): # type: ignore[no-untyped-def]
18+
assert expected_args is None or args == expected_args
19+
mock_obj = mock.MagicMock()
20+
mock_obj.returncode = returncode
21+
mock_obj.stdout = stdout
22+
return mock_obj
23+
24+
return mock.patch("subprocess.run", side_effect=mocked_run)
25+
26+
27+
# -----------------------------------------------------------------------------
28+
# get_image_command()
29+
30+
1231
def test_get_image_command_success() -> None:
1332
"""get_image_command works"""
1433
assert uut.get_image_command(DOCKER_IMAGE)
@@ -33,14 +52,30 @@ def mocked_run(args, real_run=subprocess.run, **kw): # type: ignore[no-untyped-
3352
uut.get_image_command("n/a")
3453

3554

36-
def _test_get_images(stdout: str, returncode: int = 0) -> Sequence[str]:
37-
def mocked_run(*args, **kwargs): # type: ignore[no-untyped-def]
38-
mock_obj = mock.MagicMock()
39-
mock_obj.returncode = returncode
40-
mock_obj.stdout = stdout
41-
return mock_obj
55+
def test__get_image_command__pulls_image_if_missing() -> None:
56+
"""get_image_command pulls an image if missing"""
57+
image = ALT_DOCKER_IMAGE
4258

43-
with mock.patch("subprocess.run", side_effect=mocked_run) as run_mock:
59+
# First remove the image
60+
subprocess.call(["docker", "rmi", image])
61+
62+
# Now try to get the image's Command
63+
result = uut.get_image_command(image)
64+
65+
# Should return a non-empty string
66+
assert result
67+
68+
69+
# -----------------------------------------------------------------------------
70+
# get_images()
71+
72+
73+
def _test_get_images(stdout: str, returncode: int = 0) -> Sequence[str]:
74+
run_mock = _mock_subprocess_run(
75+
stdout=stdout,
76+
returncode=returncode,
77+
)
78+
with run_mock:
4479
return uut.get_images()
4580

4681

@@ -79,18 +114,8 @@ def test_get_images__failure() -> None:
79114
_test_get_images("This is a pre-canned error", 1)
80115

81116

82-
def test__get_image_command__pulls_image_if_missing() -> None:
83-
"""get_image_command pulls an image if missing"""
84-
image = ALT_DOCKER_IMAGE
85-
86-
# First remove the image
87-
subprocess.call(["docker", "rmi", image])
88-
89-
# Now try to get the image's Command
90-
result = uut.get_image_command(image)
91-
92-
# Should return a non-empty string
93-
assert result
117+
# -----------------------------------------------------------------------------
118+
# get_image_entrypoint()
94119

95120

96121
def test_get_image_entrypoint() -> None:
@@ -105,6 +130,203 @@ def test_get_image_entrypoint__none() -> None:
105130
assert result is None
106131

107132

133+
_DOCKER_INSPECT_OUTPUT_NO_ENTRYPOINT_OLD = """
134+
[
135+
{
136+
"Id": "sha256:78138d4a1048e7c080a636858484eb7170ba15e93251a4f69fc42e8c4ad288b6",
137+
"RepoTags": [
138+
"scuba/hello:latest"
139+
],
140+
"RepoDigests": [],
141+
"Parent": "",
142+
"Comment": "buildkit.dockerfile.v0",
143+
"Created": "2025-06-08T02:01:44.709546887-04:00",
144+
"DockerVersion": "",
145+
"Author": "",
146+
"Config": {
147+
"ArgsEscaped": true,
148+
"Hostname": "",
149+
"Domainname": "",
150+
"User": "",
151+
"AttachStdin": false,
152+
"AttachStdout": false,
153+
"AttachStderr": false,
154+
"Tty": false,
155+
"OpenStdin": false,
156+
"StdinOnce": false,
157+
"Env": [
158+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
159+
],
160+
"Cmd": [
161+
"/hello.sh"
162+
],
163+
"Image": "",
164+
"Volumes": null,
165+
"WorkingDir": "/",
166+
"Entrypoint": null,
167+
"OnBuild": null,
168+
"Labels": null
169+
},
170+
"Architecture": "amd64",
171+
"Os": "linux",
172+
"Size": 8309167,
173+
"GraphDriver": {
174+
"Data": {
175+
"LowerDir": "/var/lib/docker/overlay2/i0vanzaum61dxwhffnfupowvx/diff:/var/lib/docker/overlay2/c8f9936203f4f7f53e95eacc1c1931c5a02f21f734a42d992785b938a00419ec/diff",
176+
"MergedDir": "/var/lib/docker/overlay2/afv1jwfgnud934iv6kc4fdzsq/merged",
177+
"UpperDir": "/var/lib/docker/overlay2/afv1jwfgnud934iv6kc4fdzsq/diff",
178+
"WorkDir": "/var/lib/docker/overlay2/afv1jwfgnud934iv6kc4fdzsq/work"
179+
},
180+
"Name": "overlay2"
181+
},
182+
"RootFS": {
183+
"Type": "layers",
184+
"Layers": [
185+
"sha256:fd2758d7a50e2b78d275ee7d1c218489f2439084449d895fa17eede6c61ab2c4",
186+
"sha256:309ded99abb0f87ed73f4f364375a3f9765c1e04494910b07b969c0ebb09fb31",
187+
"sha256:05b16255096123e00ba39436de05901b62d646a48d8ed514a28d843bb23393b6"
188+
]
189+
},
190+
"Metadata": {
191+
"LastTagTime": "2025-06-08T02:01:44.76502078-04:00"
192+
}
193+
}
194+
]
195+
"""
196+
197+
198+
def test_get_image_entrypoint_mocked_no_entrypoint_old() -> None:
199+
run_mock = _mock_subprocess_run(
200+
stdout=_DOCKER_INSPECT_OUTPUT_NO_ENTRYPOINT_OLD,
201+
)
202+
with run_mock:
203+
result = uut.get_image_entrypoint(DOCKER_IMAGE)
204+
assert result is None
205+
206+
207+
_DOCKER_INSPECT_OUTPUT_NO_ENTRYPOINT_NEW = """
208+
[
209+
{
210+
"Id": "sha256:78138d4a1048e7c080a636858484eb7170ba15e93251a4f69fc42e8c4ad288b6",
211+
"RepoTags": [
212+
"scuba/hello:latest"
213+
],
214+
"RepoDigests": [],
215+
"Parent": "",
216+
"Comment": "buildkit.dockerfile.v0",
217+
"Created": "2025-06-08T02:01:44.709546887-04:00",
218+
"DockerVersion": "",
219+
"Author": "",
220+
"Config": {
221+
"Env": [
222+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
223+
],
224+
"Cmd": [
225+
"/hello.sh"
226+
],
227+
"WorkingDir": "/",
228+
"ArgsEscaped": true
229+
},
230+
"Architecture": "amd64",
231+
"Os": "linux",
232+
"Size": 8309167,
233+
"GraphDriver": {
234+
"Data": {
235+
"LowerDir": "/var/lib/docker/overlay2/i0vanzaum61dxwhffnfupowvx/diff:/var/lib/docker/overlay2/c8f9936203f4f7f53e95eacc1c1931c5a02f21f734a42d992785b938a00419ec/diff",
236+
"MergedDir": "/var/lib/docker/overlay2/afv1jwfgnud934iv6kc4fdzsq/merged",
237+
"UpperDir": "/var/lib/docker/overlay2/afv1jwfgnud934iv6kc4fdzsq/diff",
238+
"WorkDir": "/var/lib/docker/overlay2/afv1jwfgnud934iv6kc4fdzsq/work"
239+
},
240+
"Name": "overlay2"
241+
},
242+
"RootFS": {
243+
"Type": "layers",
244+
"Layers": [
245+
"sha256:fd2758d7a50e2b78d275ee7d1c218489f2439084449d895fa17eede6c61ab2c4",
246+
"sha256:309ded99abb0f87ed73f4f364375a3f9765c1e04494910b07b969c0ebb09fb31",
247+
"sha256:05b16255096123e00ba39436de05901b62d646a48d8ed514a28d843bb23393b6"
248+
]
249+
},
250+
"Metadata": {
251+
"LastTagTime": "2025-06-08T02:01:44.76502078-04:00"
252+
}
253+
}
254+
]
255+
"""
256+
257+
258+
def test_get_image_entrypoint_mocked_no_entrypoint_new() -> None:
259+
run_mock = _mock_subprocess_run(
260+
stdout=_DOCKER_INSPECT_OUTPUT_NO_ENTRYPOINT_NEW,
261+
)
262+
with run_mock:
263+
result = uut.get_image_entrypoint(DOCKER_IMAGE)
264+
assert result is None
265+
266+
267+
_DOCKER_INSPECT_OUTPUT_ENTRYPOINT = """
268+
[
269+
{
270+
"Id": "sha256:d8c0eab119d7bd0c449e62023bb045a4996dc39078da9843b2483605a15a7bb8",
271+
"RepoTags": [
272+
"scuba/entrypoint-test:latest"
273+
],
274+
"RepoDigests": [],
275+
"Parent": "",
276+
"Comment": "buildkit.dockerfile.v0",
277+
"Created": "2025-06-08T02:01:43.869621065-04:00",
278+
"DockerVersion": "",
279+
"Author": "",
280+
"Config": {
281+
"Env": [
282+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
283+
],
284+
"Entrypoint": [
285+
"/entrypoint.sh"
286+
],
287+
"WorkingDir": "/"
288+
},
289+
"Architecture": "amd64",
290+
"Os": "linux",
291+
"Size": 8309289,
292+
"GraphDriver": {
293+
"Data": {
294+
"LowerDir": "/var/lib/docker/overlay2/s8yfiyigocp8jmafjlwrnvbjh/diff:/var/lib/docker/overlay2/c8f9936203f4f7f53e95eacc1c1931c5a02f21f734a42d992785b938a00419ec/diff",
295+
"MergedDir": "/var/lib/docker/overlay2/bko2v72yngijjzs77qw42im5c/merged",
296+
"UpperDir": "/var/lib/docker/overlay2/bko2v72yngijjzs77qw42im5c/diff",
297+
"WorkDir": "/var/lib/docker/overlay2/bko2v72yngijjzs77qw42im5c/work"
298+
},
299+
"Name": "overlay2"
300+
},
301+
"RootFS": {
302+
"Type": "layers",
303+
"Layers": [
304+
"sha256:fd2758d7a50e2b78d275ee7d1c218489f2439084449d895fa17eede6c61ab2c4",
305+
"sha256:b9d8112de4c9dbe254f0f9264f5bbd2b4af2ede492a0a4795b7cf899257bea60",
306+
"sha256:89c4ad1aa7abed7e63c84d12cf81771472b64e1a6fbe9169a6c0e22e0f0aceb7"
307+
]
308+
},
309+
"Metadata": {
310+
"LastTagTime": "2025-06-08T02:01:43.92217129-04:00"
311+
}
312+
}
313+
]
314+
"""
315+
316+
317+
def test_get_image_entrypoint_mocked() -> None:
318+
run_mock = _mock_subprocess_run(
319+
stdout=_DOCKER_INSPECT_OUTPUT_ENTRYPOINT,
320+
)
321+
with run_mock:
322+
result = uut.get_image_entrypoint(DOCKER_IMAGE)
323+
assert result == ["/entrypoint.sh"]
324+
325+
326+
# -----------------------------------------------------------------------------
327+
# make_vol_opt()
328+
329+
108330
def test_make_vol_opt_no_opts() -> None:
109331
assert (
110332
uut.make_vol_opt(Path("/hostdir"), Path("/contdir"))

0 commit comments

Comments
 (0)