Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docker/api/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's expected to be a JSON-encoded at the API level. But at the SDK level, it would probably be better if it was an instance of a nicer "Platform" class/struct.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever the case shouldn’t it match the pull? There shouldn’t be a string in one and class in another.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, these have different format. Pull accepts a plain platform string like linux/amd64, but this one is a full JSON representing an OCI platform.

But you're right, that it's inconsistent and we probably should fix it at some point.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vvoland can you take a look at this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an instance of the new Platform


Returns:
(generator or str): The output from the server.
Expand Down Expand Up @@ -488,6 +490,16 @@ 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'
)
# Handle both Platform instances and dict inputs
if isinstance(platform, dict):
platform = Platform(**platform)
params['platform'] = platform

response = self._post_json(
u, None, headers=headers, stream=stream, params=params
)
Expand Down
1 change: 1 addition & 0 deletions docker/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .containers import ContainerConfig, DeviceRequest, HostConfig, LogConfig, Ulimit
from .daemon import CancellableStream
from .healthcheck import Healthcheck
from .image import Platform
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
from .services import (
ConfigReference,
Expand Down
34 changes: 34 additions & 0 deletions docker/types/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from .base import DictType


class Platform(DictType):
def __init__(self, **kwargs):
architecture = kwargs.get('architecture')
os = kwargs.get('os')

if architecture is None or os is None:
raise ValueError("Both 'architecture' and 'os' must be provided")

super().__init__({
'architecture': architecture,
'os': os,
'os_version': kwargs.get('os_version'),
'os_features': kwargs.get('os_features'),
'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
50 changes: 50 additions & 0 deletions tests/unit/api_image_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -271,6 +272,55 @@ def test_push_image_with_auth(self):
timeout=DEFAULT_TIMEOUT_SECONDS
)

@requires_api_version('1.46')
def test_push_image_with_platform(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to be picky, but your function can take either Platform OR dict, but you are just testing one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issue, good point. I'll see towards that.

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
)

@requires_api_version('1.46')
def test_push_image_with_platform_dict(self):
platform_dict = {'os': 'linux', 'architecture': 'arm', 'variant': 'v7'}
with mock.patch('docker.auth.resolve_authconfig',
fake_resolve_authconfig):
self.client.push(
fake_api.FAKE_IMAGE_NAME,
platform=platform_dict
)

# When passed as dict, it should be converted to Platform instance
from docker.types.image import Platform
expected_platform = Platform(**platform_dict)

fake_request.assert_called_with(
'POST',
f"{url_prefix}images/test_image/push",
params={
'tag': None,
'platform': expected_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):
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/fake_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from docker import constants
from docker.types.image import Platform

from . import fake_stat

Expand All @@ -22,6 +23,8 @@
FAKE_CONFIG_ID = 'sekvs771242jfdjnvfuds8232'
FAKE_CONFIG_NAME = 'super_config'

FAKE_PLATFORM = Platform(os='linux', architecture='arm', variant='v5')

# Each method is prefixed with HTTP method (get, post...)
# for clarity and readability

Expand Down