Browse Source

Merge pull request #6454 from rumpl/digest-distribution

Resolve digests without pulling image
Chris Crone 6 years ago
parent
commit
718346f103
3 changed files with 46 additions and 12 deletions
  1. 29 12
      compose/bundle.py
  2. 6 0
      compose/service.py
  3. 11 0
      tests/unit/bundle_test.py

+ 29 - 12
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()

+ 6 - 0
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)

+ 11 - 0
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'))