Skip to content

Commit 1a4cacd

Browse files
felixfonteinmilas
andauthored
api: add platform to container create (#2927)
Add platform parameter for container creation/run Signed-off-by: Felix Fontein <[email protected]> Signed-off-by: Milas Bowman <[email protected]> Co-authored-by: Milas Bowman <[email protected]>
1 parent 26064dd commit 1a4cacd

File tree

5 files changed

+69
-8
lines changed

5 files changed

+69
-8
lines changed

docker/api/container.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def create_container(self, image, command=None, hostname=None, user=None,
223223
mac_address=None, labels=None, stop_signal=None,
224224
networking_config=None, healthcheck=None,
225225
stop_timeout=None, runtime=None,
226-
use_config_proxy=True):
226+
use_config_proxy=True, platform=None):
227227
"""
228228
Creates a container. Parameters are similar to those for the ``docker
229229
run`` command except it doesn't support the attach options (``-a``).
@@ -398,6 +398,7 @@ def create_container(self, image, command=None, hostname=None, user=None,
398398
configuration file (``~/.docker/config.json`` by default)
399399
contains a proxy configuration, the corresponding environment
400400
variables will be set in the container being created.
401+
platform (str): Platform in the format ``os[/arch[/variant]]``.
401402
402403
Returns:
403404
A dictionary with an image 'Id' key and a 'Warnings' key.
@@ -427,16 +428,22 @@ def create_container(self, image, command=None, hostname=None, user=None,
427428
stop_signal, networking_config, healthcheck,
428429
stop_timeout, runtime
429430
)
430-
return self.create_container_from_config(config, name)
431+
return self.create_container_from_config(config, name, platform)
431432

432433
def create_container_config(self, *args, **kwargs):
433434
return ContainerConfig(self._version, *args, **kwargs)
434435

435-
def create_container_from_config(self, config, name=None):
436+
def create_container_from_config(self, config, name=None, platform=None):
436437
u = self._url("/containers/create")
437438
params = {
438439
'name': name
439440
}
441+
if platform:
442+
if utils.version_lt(self._version, '1.41'):
443+
raise errors.InvalidVersion(
444+
'platform is not supported for API version < 1.41'
445+
)
446+
params['platform'] = platform
440447
res = self._post_json(u, data=config, params=params)
441448
return self._result(res, True)
442449

docker/errors.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import requests
22

3+
_image_not_found_explanation_fragments = frozenset(
4+
fragment.lower() for fragment in [
5+
'no such image',
6+
'not found: does not exist or no pull access',
7+
'repository does not exist',
8+
'was found but does not match the specified platform',
9+
]
10+
)
11+
312

413
class DockerException(Exception):
514
"""
@@ -21,10 +30,9 @@ def create_api_error_from_http_exception(e):
2130
explanation = (response.content or '').strip()
2231
cls = APIError
2332
if response.status_code == 404:
24-
if explanation and ('No such image' in str(explanation) or
25-
'not found: does not exist or no pull access'
26-
in str(explanation) or
27-
'repository does not exist' in str(explanation)):
33+
explanation_msg = (explanation or '').lower()
34+
if any(fragment in explanation_msg
35+
for fragment in _image_not_found_explanation_fragments):
2836
cls = ImageNotFound
2937
else:
3038
cls = NotFound

docker/models/containers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ def run(self, image, command=None, stdout=True, stderr=False,
801801
image = image.id
802802
stream = kwargs.pop('stream', False)
803803
detach = kwargs.pop('detach', False)
804-
platform = kwargs.pop('platform', None)
804+
platform = kwargs.get('platform', None)
805805

806806
if detach and remove:
807807
if version_gte(self.client.api._version, '1.25'):
@@ -985,6 +985,7 @@ def prune(self, filters=None):
985985
'mac_address',
986986
'name',
987987
'network_disabled',
988+
'platform',
988989
'stdin_open',
989990
'stop_signal',
990991
'tty',

tests/unit/api_container_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,22 @@ def test_create_named_container(self):
348348
assert args[1]['headers'] == {'Content-Type': 'application/json'}
349349
assert args[1]['params'] == {'name': 'marisa-kirisame'}
350350

351+
def test_create_container_with_platform(self):
352+
self.client.create_container('busybox', 'true',
353+
platform='linux')
354+
355+
args = fake_request.call_args
356+
assert args[0][1] == url_prefix + 'containers/create'
357+
assert json.loads(args[1]['data']) == json.loads('''
358+
{"Tty": false, "Image": "busybox", "Cmd": ["true"],
359+
"AttachStdin": false,
360+
"AttachStderr": true, "AttachStdout": true,
361+
"StdinOnce": false,
362+
"OpenStdin": false, "NetworkDisabled": false}
363+
''')
364+
assert args[1]['headers'] == {'Content-Type': 'application/json'}
365+
assert args[1]['params'] == {'name': None, 'platform': 'linux'}
366+
351367
def test_create_container_with_mem_limit_as_int(self):
352368
self.client.create_container(
353369
'busybox', 'true', host_config=self.client.create_host_config(

tests/unit/models_containers_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def test_create_container_args(self):
7777
oom_score_adj=5,
7878
pid_mode='host',
7979
pids_limit=500,
80+
platform='linux',
8081
ports={
8182
1111: 4567,
8283
2222: None
@@ -186,6 +187,7 @@ def test_create_container_args(self):
186187
name='somename',
187188
network_disabled=False,
188189
networking_config={'foo': None},
190+
platform='linux',
189191
ports=[('1111', 'tcp'), ('2222', 'tcp')],
190192
stdin_open=True,
191193
stop_signal=9,
@@ -314,6 +316,33 @@ def test_run_remove(self):
314316
'NetworkMode': 'default'}
315317
)
316318

319+
def test_run_platform(self):
320+
client = make_fake_client()
321+
322+
# raise exception on first call, then return normal value
323+
client.api.create_container.side_effect = [
324+
docker.errors.ImageNotFound(""),
325+
client.api.create_container.return_value
326+
]
327+
328+
client.containers.run(image='alpine', platform='linux/arm64')
329+
330+
client.api.pull.assert_called_with(
331+
'alpine',
332+
tag='latest',
333+
all_tags=False,
334+
stream=True,
335+
platform='linux/arm64',
336+
)
337+
338+
client.api.create_container.assert_called_with(
339+
detach=False,
340+
platform='linux/arm64',
341+
image='alpine',
342+
command=None,
343+
host_config={'NetworkMode': 'default'},
344+
)
345+
317346
def test_create(self):
318347
client = make_fake_client()
319348
container = client.containers.create(

0 commit comments

Comments
 (0)