Repository: docker/docker-py

Issue:
Container creation does not support passing the platform
While this is not documented in the API docs (https://docs.docker.com/engine/api/v1.41/#operation/ContainerCreate), the Go client uses that: https://github.com/docker/cli/blob/master/vendor/github.com/docker/docker/client/container_create.go#L42

It would be great if Docker SDK for Python could support the `platform` parameter for container creation as well.


Discussion:
This issue is about the low-level API, not the high-level API. The high-level API supports that parameter, but uses it differently than the `docker` CLI tool: the high-level API only uses it for pulling images, while the `docker` CLI tool passes it on for the `create` command to the daemon as well.
I would also like this feature

Resolving patch:
diff --git a/docker/api/container.py b/docker/api/container.py
--- 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
--- 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
--- 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',


Test patch:
diff --git a/tests/unit/api_container_test.py b/tests/unit/api_container_test.py
--- 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
--- 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(