diff --git a/docker/api/image.py b/docker/api/image.py index 85109473b..eab0a771c 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -3,6 +3,7 @@ from .. import auth, errors, utils from ..constants import DEFAULT_DATA_CHUNK_SIZE +from ..types.image import Platform log = logging.getLogger(__name__) @@ -434,7 +435,7 @@ def pull(self, repository, tag=None, stream=False, auth_config=None, return self._result(response) def push(self, repository, tag=None, stream=False, auth_config=None, - decode=False): + decode=False, platform=None): """ Push an image or a repository to the registry. Similar to the ``docker push`` command. @@ -448,6 +449,7 @@ def push(self, repository, tag=None, stream=False, auth_config=None, ``username`` and ``password`` keys to be valid. decode (bool): Decode the JSON data from the server into dicts. Only applies with ``stream=True`` + platform (str): JSON-encoded OCI platform to select the platform-variant to push. If not provided, all available variants will attempt to be pushed. Returns: (generator or str): The output from the server. @@ -488,6 +490,13 @@ def push(self, repository, tag=None, stream=False, auth_config=None, 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.46'): + raise errors.InvalidVersion( + 'platform was only introduced in API version 1.46' + ) + params['platform'] = Platform + response = self._post_json( u, None, headers=headers, stream=stream, params=params ) diff --git a/docker/types/image.py b/docker/types/image.py new file mode 100644 index 000000000..2533c0ec8 --- /dev/null +++ b/docker/types/image.py @@ -0,0 +1,35 @@ +from .base import DictType + + +class Platform(DictType): + def __init__(self, **kwargs): + architecture = kwargs.get('architecture', kwargs.get('Architecture')) + os = kwargs.get('os', kwargs.get('OS')) + + if architecture is None and os is None: + raise ValueError("At least one of 'architecture' or 'os' must be provided") + + + super().__init__({ + 'Architecture': architecture, + 'OS': os, + 'OSVersion': kwargs.get('os_version', kwargs.get('OSVersion')), + 'OSFeatures': kwargs.get('os_features', kwargs.get('OSFeatures')), + 'Variant': kwargs.get('variant', kwargs.get('Variant')) + }) + + @property + def architecture(self): + return self['Architecture'] + + @property + def os(self): + return self['OS'] + + @architecture.setter + def architecture(self, value): + self['Architecture'] = value + + @os.setter + def os(self, value): + self['OS'] = value diff --git a/tests/unit/api_image_test.py b/tests/unit/api_image_test.py index 148109d37..2e567293f 100644 --- a/tests/unit/api_image_test.py +++ b/tests/unit/api_image_test.py @@ -5,6 +5,7 @@ import docker from docker import auth +from ..helpers import requires_api_version from . import fake_api from .api_test import ( DEFAULT_TIMEOUT_SECONDS, @@ -271,6 +272,28 @@ def test_push_image_with_auth(self): timeout=DEFAULT_TIMEOUT_SECONDS ) + @requires_api_version('1.46') + def test_push_image_with_platform(self): + with mock.patch('docker.auth.resolve_authconfig', + fake_resolve_authconfig): + self.client.push( + fake_api.FAKE_IMAGE_NAME, + platform=fake_api.FAKE_PLATFORM + ) + + fake_request.assert_called_with( + 'POST', + f"{url_prefix}images/test_image/push", + params={ + 'tag': None, + 'platform': fake_api.FAKE_PLATFORM + }, + data='{}', + headers={'Content-Type': 'application/json'}, + stream=False, + timeout=DEFAULT_TIMEOUT_SECONDS + ) + def test_push_image_stream(self): with mock.patch('docker.auth.resolve_authconfig', fake_resolve_authconfig): diff --git a/tests/unit/fake_api.py b/tests/unit/fake_api.py index 03e53cc64..fd9936709 100644 --- a/tests/unit/fake_api.py +++ b/tests/unit/fake_api.py @@ -21,6 +21,7 @@ FAKE_SECRET_NAME = 'super_secret' FAKE_CONFIG_ID = 'sekvs771242jfdjnvfuds8232' FAKE_CONFIG_NAME = 'super_config' +FAKE_PLATFORM = "{'os': 'linux','architecture': 'arm','variant': 'v5'}" # Each method is prefixed with HTTP method (get, post...) # for clarity and readability