From 0c20fc5d914367816387dbf149077835117daa14 Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Fri, 11 Jan 2019 10:04:57 +0100 Subject: [PATCH] Resolve digests without pulling image If there is no image locally `docker-compose --resolve-image-digests` will try and get the digest from the repository. Fixes https://github.com/docker/compose/issues/5818 Signed-off-by: Djordje Lukic --- compose/bundle.py | 41 +++++++++++++++++++++++++++------------ compose/service.py | 6 ++++++ tests/unit/bundle_test.py | 11 +++++++++++ 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/compose/bundle.py b/compose/bundle.py index 937a3708a..efc455b72 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -95,19 +95,10 @@ def get_image_digest(service, allow_push=False): if separator == '@': return service.options['image'] - try: - image = service.image() - except NoSuchImageError: - action = 'build' if 'build' in service.options else 'pull' - raise UserError( - "Image not found for service '{service}'. " - "You might need to run `docker-compose {action} {service}`." - .format(service=service.name, action=action)) + digest = get_digest(service) - if image['RepoDigests']: - # TODO: pick a digest based on the image tag if there are multiple - # digests - return image['RepoDigests'][0] + if digest: + return digest if 'build' not in service.options: raise NeedsPull(service.image_name, service.name) @@ -118,6 +109,32 @@ def get_image_digest(service, allow_push=False): return push_image(service) +def get_digest(service): + digest = None + try: + image = service.image() + # TODO: pick a digest based on the image tag if there are multiple + # digests + if image['RepoDigests']: + digest = image['RepoDigests'][0] + except NoSuchImageError: + try: + # Fetch the image digest from the registry + distribution = service.get_image_registry_data() + + if distribution['Descriptor']['digest']: + digest = '{image_name}@{digest}'.format( + image_name=service.image_name, + digest=distribution['Descriptor']['digest'] + ) + except NoSuchImageError: + raise UserError( + "Digest not found for service '{service}'. " + "Repository does not exist or may require 'docker login'" + .format(service=service.name)) + return digest + + def push_image(service): try: digest = service.push() diff --git a/compose/service.py b/compose/service.py index 3c5e356a3..59f05f8dc 100644 --- a/compose/service.py +++ b/compose/service.py @@ -363,6 +363,12 @@ class Service(object): "rebuild this image you must use `docker-compose build` or " "`docker-compose up --build`.".format(self.name)) + def get_image_registry_data(self): + try: + return self.client.inspect_distribution(self.image_name) + except APIError: + raise NoSuchImageError("Image '{}' not found".format(self.image_name)) + def image(self): try: return self.client.inspect_image(self.image_name) diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py index 88f75405a..065cdd00b 100644 --- a/tests/unit/bundle_test.py +++ b/tests/unit/bundle_test.py @@ -10,6 +10,7 @@ from compose import service from compose.cli.errors import UserError from compose.config.config import Config from compose.const import COMPOSEFILE_V2_0 as V2_0 +from compose.service import NoSuchImageError @pytest.fixture @@ -35,6 +36,16 @@ def test_get_image_digest_image_uses_digest(mock_service): assert not mock_service.image.called +def test_get_image_digest_from_repository(mock_service): + mock_service.options['image'] = 'abcd' + mock_service.image_name = 'abcd' + mock_service.image.side_effect = NoSuchImageError(None) + mock_service.get_image_registry_data.return_value = {'Descriptor': {'digest': 'digest'}} + + digest = bundle.get_image_digest(mock_service) + assert digest == 'abcd@digest' + + def test_get_image_digest_no_image(mock_service): with pytest.raises(UserError) as exc: bundle.get_image_digest(service.Service(name='theservice'))