From 1a4cacdfb63f0fbf2299962732c75484c24ad8b0 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 29 Jul 2022 19:57:30 +0200 Subject: [PATCH] api: add platform to container create (#2927) Add platform parameter for container creation/run Signed-off-by: Felix Fontein Signed-off-by: Milas Bowman Co-authored-by: Milas Bowman --- docker/api/container.py | 13 ++++++++++--- docker/errors.py | 16 +++++++++++---- docker/models/containers.py | 3 ++- tests/unit/api_container_test.py | 16 +++++++++++++++ tests/unit/models_containers_test.py | 29 ++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/docker/api/container.py b/docker/api/container.py index 17c09726b..f600be181 100644 --- a/docker/api/container.py +++ b/docker/api/container.py @@ -223,7 +223,7 @@ def create_container(self, image, command=None, hostname=None, user=None, mac_address=None, labels=None, stop_signal=None, networking_config=None, healthcheck=None, stop_timeout=None, runtime=None, - use_config_proxy=True): + use_config_proxy=True, platform=None): """ Creates a container. Parameters are similar to those for the ``docker 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, configuration file (``~/.docker/config.json`` by default) contains a proxy configuration, the corresponding environment variables will be set in the container being created. + platform (str): Platform in the format ``os[/arch[/variant]]``. Returns: 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, stop_signal, networking_config, healthcheck, stop_timeout, runtime ) - return self.create_container_from_config(config, name) + return self.create_container_from_config(config, name, platform) def create_container_config(self, *args, **kwargs): return ContainerConfig(self._version, *args, **kwargs) - def create_container_from_config(self, config, name=None): + def create_container_from_config(self, config, name=None, platform=None): u = self._url("/containers/create") params = { 'name': name } + if platform: + if utils.version_lt(self._version, '1.41'): + raise errors.InvalidVersion( + 'platform is not supported for API version < 1.41' + ) + params['platform'] = platform res = self._post_json(u, data=config, params=params) return self._result(res, True) diff --git a/docker/errors.py b/docker/errors.py index 7725295f5..8cf8670ba 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -1,5 +1,14 @@ import requests +_image_not_found_explanation_fragments = frozenset( + fragment.lower() for fragment in [ + 'no such image', + 'not found: does not exist or no pull access', + 'repository does not exist', + 'was found but does not match the specified platform', + ] +) + class DockerException(Exception): """ @@ -21,10 +30,9 @@ def create_api_error_from_http_exception(e): explanation = (response.content or '').strip() cls = APIError if response.status_code == 404: - if explanation and ('No such image' in str(explanation) or - 'not found: does not exist or no pull access' - in str(explanation) or - 'repository does not exist' in str(explanation)): + explanation_msg = (explanation or '').lower() + if any(fragment in explanation_msg + for fragment in _image_not_found_explanation_fragments): cls = ImageNotFound else: cls = NotFound diff --git a/docker/models/containers.py b/docker/models/containers.py index e34659cbe..7769ed091 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -801,7 +801,7 @@ def run(self, image, command=None, stdout=True, stderr=False, image = image.id stream = kwargs.pop('stream', False) detach = kwargs.pop('detach', False) - platform = kwargs.pop('platform', None) + platform = kwargs.get('platform', None) if detach and remove: if version_gte(self.client.api._version, '1.25'): @@ -985,6 +985,7 @@ def prune(self, filters=None): 'mac_address', 'name', 'network_disabled', + 'platform', 'stdin_open', 'stop_signal', 'tty', diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py index 703084168..3a2fbde88 100644 --- a/tests/unit/api_container_test.py +++ b/tests/unit/api_container_test.py @@ -348,6 +348,22 @@ def test_create_named_container(self): assert args[1]['headers'] == {'Content-Type': 'application/json'} assert args[1]['params'] == {'name': 'marisa-kirisame'} + def test_create_container_with_platform(self): + self.client.create_container('busybox', 'true', + platform='linux') + + args = fake_request.call_args + assert args[0][1] == url_prefix + 'containers/create' + assert json.loads(args[1]['data']) == json.loads(''' + {"Tty": false, "Image": "busybox", "Cmd": ["true"], + "AttachStdin": false, + "AttachStderr": true, "AttachStdout": true, + "StdinOnce": false, + "OpenStdin": false, "NetworkDisabled": false} + ''') + assert args[1]['headers'] == {'Content-Type': 'application/json'} + assert args[1]['params'] == {'name': None, 'platform': 'linux'} + def test_create_container_with_mem_limit_as_int(self): self.client.create_container( 'busybox', 'true', host_config=self.client.create_host_config( diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 785a84903..e4ee074d8 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -77,6 +77,7 @@ def test_create_container_args(self): oom_score_adj=5, pid_mode='host', pids_limit=500, + platform='linux', ports={ 1111: 4567, 2222: None @@ -186,6 +187,7 @@ def test_create_container_args(self): name='somename', network_disabled=False, networking_config={'foo': None}, + platform='linux', ports=[('1111', 'tcp'), ('2222', 'tcp')], stdin_open=True, stop_signal=9, @@ -314,6 +316,33 @@ def test_run_remove(self): 'NetworkMode': 'default'} ) + def test_run_platform(self): + client = make_fake_client() + + # raise exception on first call, then return normal value + client.api.create_container.side_effect = [ + docker.errors.ImageNotFound(""), + client.api.create_container.return_value + ] + + client.containers.run(image='alpine', platform='linux/arm64') + + client.api.pull.assert_called_with( + 'alpine', + tag='latest', + all_tags=False, + stream=True, + platform='linux/arm64', + ) + + client.api.create_container.assert_called_with( + detach=False, + platform='linux/arm64', + image='alpine', + command=None, + host_config={'NetworkMode': 'default'}, + ) + def test_create(self): client = make_fake_client() container = client.containers.create(