From f95b958429b38dab50929e013db3c636a12e1536 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 8 Jan 2018 18:11:29 -0800 Subject: [PATCH] Add support for experimental platform flag in build and pull Signed-off-by: Joffrey F --- docker/api/build.py | 10 +++++++++- docker/api/image.py | 13 +++++++++++-- docker/models/containers.py | 8 ++++++-- docker/models/images.py | 2 ++ tests/integration/api_build_test.py | 15 +++++++++++++++ tests/integration/api_image_test.py | 11 ++++++++++- tests/unit/models_containers_test.py | 2 +- 7 files changed, 54 insertions(+), 7 deletions(-) diff --git a/docker/api/build.py b/docker/api/build.py index 34456ab373..32238efed9 100644 --- a/docker/api/build.py +++ b/docker/api/build.py @@ -19,7 +19,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, forcerm=False, dockerfile=None, container_limits=None, decode=False, buildargs=None, gzip=False, shmsize=None, labels=None, cache_from=None, target=None, network_mode=None, - squash=None, extra_hosts=None): + squash=None, extra_hosts=None, platform=None): """ Similar to the ``docker build`` command. Either ``path`` or ``fileobj`` needs to be set. ``path`` can be a local path (to a directory @@ -103,6 +103,7 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, single layer. extra_hosts (dict): Extra hosts to add to /etc/hosts in building containers, as a mapping of hostname to IP address. + platform (str): Platform in the format ``os[/arch[/variant]]`` Returns: A generator for the build output. @@ -243,6 +244,13 @@ def build(self, path=None, tag=None, quiet=False, fileobj=None, extra_hosts = utils.format_extra_hosts(extra_hosts) params.update({'extrahosts': extra_hosts}) + if platform is not None: + if utils.version_lt(self._version, '1.32'): + raise errors.InvalidVersion( + 'platform was only introduced in API version 1.32' + ) + params['platform'] = platform + if context is not None: headers = {'Content-Type': 'application/tar'} if encoding: diff --git a/docker/api/image.py b/docker/api/image.py index 77553122d6..065fae3959 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -323,7 +323,8 @@ def prune_images(self, filters=None): return self._result(self._post(url, params=params), True) def pull(self, repository, tag=None, stream=False, - insecure_registry=False, auth_config=None, decode=False): + insecure_registry=False, auth_config=None, decode=False, + platform=None): """ Pulls an image. Similar to the ``docker pull`` command. @@ -336,6 +337,7 @@ def pull(self, repository, tag=None, stream=False, :py:meth:`~docker.api.daemon.DaemonApiMixin.login` has set for this request. ``auth_config`` should contain the ``username`` and ``password`` keys to be valid. + platform (str): Platform in the format ``os[/arch[/variant]]`` Returns: (generator or str): The output @@ -376,7 +378,7 @@ def pull(self, repository, tag=None, stream=False, } headers = {} - if utils.compare_version('1.5', self._version) >= 0: + if utils.version_gte(self._version, '1.5'): if auth_config is None: header = auth.get_config_header(self, registry) if header: @@ -385,6 +387,13 @@ def pull(self, repository, tag=None, stream=False, log.debug('Sending supplied auth config') headers['X-Registry-Auth'] = auth.encode_header(auth_config) + if platform is not None: + if utils.version_lt(self._version, '1.32'): + raise errors.InvalidVersion( + 'platform was only introduced in API version 1.32' + ) + params['platform'] = platform + response = self._post( self._url('/images/create'), params=params, headers=headers, stream=stream, timeout=None diff --git a/docker/models/containers.py b/docker/models/containers.py index 6ba308e492..5e2aa88a3d 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -579,6 +579,8 @@ def run(self, image, command=None, stdout=True, stderr=False, inside the container. pids_limit (int): Tune a container's pids limit. Set ``-1`` for unlimited. + platform (str): Platform in the format ``os[/arch[/variant]]``. + Only used if the method needs to pull the requested image. ports (dict): Ports to bind inside the container. The keys of the dictionary are the ports to bind inside the @@ -700,7 +702,9 @@ def run(self, image, command=None, stdout=True, stderr=False, if isinstance(image, Image): image = image.id stream = kwargs.pop('stream', False) - detach = kwargs.pop("detach", False) + detach = kwargs.pop('detach', False) + platform = kwargs.pop('platform', None) + if detach and remove: if version_gte(self.client.api._version, '1.25'): kwargs["auto_remove"] = True @@ -718,7 +722,7 @@ def run(self, image, command=None, stdout=True, stderr=False, container = self.create(image=image, command=command, detach=detach, **kwargs) except ImageNotFound: - self.client.images.pull(image) + self.client.images.pull(image, platform=platform) container = self.create(image=image, command=command, detach=detach, **kwargs) diff --git a/docker/models/images.py b/docker/models/images.py index 82ca54135e..891c565f66 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -157,6 +157,7 @@ def build(self, **kwargs): single layer. extra_hosts (dict): Extra hosts to add to /etc/hosts in building containers, as a mapping of hostname to IP address. + platform (str): Platform in the format ``os[/arch[/variant]]``. Returns: (:py:class:`Image`): The built image. @@ -265,6 +266,7 @@ def pull(self, name, tag=None, **kwargs): :py:meth:`~docker.client.DockerClient.login` has set for this request. ``auth_config`` should contain the ``username`` and ``password`` keys to be valid. + platform (str): Platform in the format ``os[/arch[/variant]]`` Returns: (:py:class:`Image`): The image that has been pulled. diff --git a/tests/integration/api_build_test.py b/tests/integration/api_build_test.py index 7cc32346ba..245214e1a2 100644 --- a/tests/integration/api_build_test.py +++ b/tests/integration/api_build_test.py @@ -377,3 +377,18 @@ def test_build_with_dockerfile_empty_lines(self): def test_build_gzip_custom_encoding(self): with self.assertRaises(errors.DockerException): self.client.build(path='.', gzip=True, encoding='text/html') + + @requires_api_version('1.32') + @requires_experimental(until=None) + def test_build_invalid_platform(self): + script = io.BytesIO('FROM busybox\n'.encode('ascii')) + + with pytest.raises(errors.APIError) as excinfo: + stream = self.client.build( + fileobj=script, stream=True, platform='foobar' + ) + for _ in stream: + pass + + assert excinfo.value.status_code == 400 + assert 'invalid platform' in excinfo.exconly() diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index 14fb77aa46..178c34e995 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -14,7 +14,7 @@ import docker -from ..helpers import requires_api_version +from ..helpers import requires_api_version, requires_experimental from .base import BaseAPIIntegrationTest, BUSYBOX @@ -67,6 +67,15 @@ def test_pull_streaming(self): img_info = self.client.inspect_image('hello-world') self.assertIn('Id', img_info) + @requires_api_version('1.32') + @requires_experimental(until=None) + def test_pull_invalid_platform(self): + with pytest.raises(docker.errors.APIError) as excinfo: + self.client.pull('hello-world', platform='foobar') + + assert excinfo.value.status_code == 500 + assert 'invalid platform' in excinfo.exconly() + class CommitTest(BaseAPIIntegrationTest): def test_commit(self): diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index a479e836e6..95295a91b7 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -225,7 +225,7 @@ def test_run_pull(self): container = client.containers.run('alpine', 'sleep 300', detach=True) assert container.id == FAKE_CONTAINER_ID - client.api.pull.assert_called_with('alpine', tag=None) + client.api.pull.assert_called_with('alpine', platform=None, tag=None) def test_run_with_error(self): client = make_fake_client()