diff --git a/docker/api/image.py b/docker/api/image.py index 3ebca32e5..5f05d8877 100644 --- a/docker/api/image.py +++ b/docker/api/image.py @@ -245,6 +245,27 @@ def inspect_image(self, image): self._get(self._url("/images/{0}/json", image)), True ) + @utils.minimum_version('1.30') + @utils.check_resource('image') + def inspect_distribution(self, image): + """ + Get image digest and platform information by contacting the registry. + + Args: + image (str): The image name to inspect + + Returns: + (dict): A dict containing distribution data + + Raises: + :py:class:`docker.errors.APIError` + If the server returns an error. + """ + + return self._result( + self._get(self._url("/distribution/{0}/json", image)), True + ) + def load_image(self, data, quiet=None): """ Load an image that was previously saved using diff --git a/docker/models/images.py b/docker/models/images.py index bb24eb5c7..d4893bb6a 100644 --- a/docker/models/images.py +++ b/docker/models/images.py @@ -5,7 +5,7 @@ from ..api import APIClient from ..constants import DEFAULT_DATA_CHUNK_SIZE -from ..errors import BuildError, ImageLoadError +from ..errors import BuildError, ImageLoadError, InvalidArgument from ..utils import parse_repository_tag from ..utils.json_stream import json_stream from .resource import Collection, Model @@ -105,6 +105,81 @@ def tag(self, repository, tag=None, **kwargs): return self.client.api.tag(self.id, repository, tag=tag, **kwargs) +class RegistryData(Model): + """ + Image metadata stored on the registry, including available platforms. + """ + def __init__(self, image_name, *args, **kwargs): + super(RegistryData, self).__init__(*args, **kwargs) + self.image_name = image_name + + @property + def id(self): + """ + The ID of the object. + """ + return self.attrs['Descriptor']['digest'] + + @property + def short_id(self): + """ + The ID of the image truncated to 10 characters, plus the ``sha256:`` + prefix. + """ + return self.id[:17] + + def pull(self, platform=None): + """ + Pull the image digest. + + Args: + platform (str): The platform to pull the image for. + Default: ``None`` + + Returns: + (:py:class:`Image`): A reference to the pulled image. + """ + repository, _ = parse_repository_tag(self.image_name) + return self.collection.pull(repository, tag=self.id, platform=platform) + + def has_platform(self, platform): + """ + Check whether the given platform identifier is available for this + digest. + + Args: + platform (str or dict): A string using the ``os[/arch[/variant]]`` + format, or a platform dictionary. + + Returns: + (bool): ``True`` if the platform is recognized as available, + ``False`` otherwise. + + Raises: + :py:class:`docker.errors.InvalidArgument` + If the platform argument is not a valid descriptor. + """ + if platform and not isinstance(platform, dict): + parts = platform.split('/') + if len(parts) > 3 or len(parts) < 1: + raise InvalidArgument( + '"{0}" is not a valid platform descriptor'.format(platform) + ) + platform = {'os': parts[0]} + if len(parts) > 2: + platform['variant'] = parts[2] + if len(parts) > 1: + platform['architecture'] = parts[1] + return normalize_platform( + platform, self.client.version() + ) in self.attrs['Platforms'] + + def reload(self): + self.attrs = self.client.api.inspect_distribution(self.image_name) + + reload.__doc__ = Model.reload.__doc__ + + class ImageCollection(Collection): model = Image @@ -219,6 +294,26 @@ def get(self, name): """ return self.prepare_model(self.client.api.inspect_image(name)) + def get_registry_data(self, name): + """ + Gets the registry data for an image. + + Args: + name (str): The name of the image. + + Returns: + (:py:class:`RegistryData`): The data object. + Raises: + :py:class:`docker.errors.APIError` + If the server returns an error. + """ + return RegistryData( + image_name=name, + attrs=self.client.api.inspect_distribution(name), + client=self.client, + collection=self, + ) + def list(self, name=None, all=False, filters=None): """ List images on the server. @@ -336,3 +431,13 @@ def search(self, *args, **kwargs): def prune(self, filters=None): return self.client.api.prune_images(filters=filters) prune.__doc__ = APIClient.prune_images.__doc__ + + +def normalize_platform(platform, engine_info): + if platform is None: + platform = {} + if 'os' not in platform: + platform['os'] = engine_info['Os'] + if 'architecture' not in platform: + platform['architecture'] = engine_info['Arch'] + return platform diff --git a/docs/images.rst b/docs/images.rst index 12b0fd184..4d425e95a 100644 --- a/docs/images.rst +++ b/docs/images.rst @@ -12,6 +12,7 @@ Methods available on ``client.images``: .. automethod:: build .. automethod:: get + .. automethod:: get_registry_data .. automethod:: list(**kwargs) .. automethod:: load .. automethod:: prune @@ -41,3 +42,21 @@ Image objects .. automethod:: reload .. automethod:: save .. automethod:: tag + +RegistryData objects +-------------------- + +.. autoclass:: RegistryData() + + .. py:attribute:: attrs + + The raw representation of this object from the server. + + .. autoattribute:: id + .. autoattribute:: short_id + + + + .. automethod:: has_platform + .. automethod:: pull + .. automethod:: reload diff --git a/tests/integration/api_image_test.py b/tests/integration/api_image_test.py index ab638c9e4..050e7f339 100644 --- a/tests/integration/api_image_test.py +++ b/tests/integration/api_image_test.py @@ -357,3 +357,12 @@ def test_get_image_load_image(self): success = True break assert success is True + + +@requires_api_version('1.30') +class InspectDistributionTest(BaseAPIIntegrationTest): + def test_inspect_distribution(self): + data = self.client.inspect_distribution('busybox:latest') + assert data is not None + assert 'Platforms' in data + assert {'os': 'linux', 'architecture': 'amd64'} in data['Platforms']