浏览代码

Allow to specify image by digest. Fixes #1670

Signed-off-by: Karol Duleba <[email protected]>
Karol Duleba 10 年之前
父节点
当前提交
56f03bc20a
共有 5 个文件被更改,包括 60 次插入16 次删除
  1. 26 9
      compose/service.py
  2. 2 1
      docs/yml.md
  3. 6 0
      tests/fixtures/simple-composefile/digest.yml
  4. 6 0
      tests/integration/cli_test.py
  5. 20 6
      tests/unit/service_test.py

+ 26 - 9
compose/service.py

@@ -757,9 +757,9 @@ class Service(object):
         if 'image' not in self.options:
             return
 
-        repo, tag = parse_repository_tag(self.options['image'])
+        repo, tag, separator = parse_repository_tag(self.options['image'])
         tag = tag or 'latest'
-        log.info('Pulling %s (%s:%s)...' % (self.name, repo, tag))
+        log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag))
         output = self.client.pull(
             repo,
             tag=tag,
@@ -780,14 +780,31 @@ def build_container_name(project, service, number, one_off=False):
 
 # Images
 
+def parse_repository_tag(repo_path):
+    """Splits image identification into base image path, tag/digest
+    and it's separator.
 
-def parse_repository_tag(s):
-    if ":" not in s:
-        return s, ""
-    repo, tag = s.rsplit(":", 1)
-    if "/" in tag:
-        return s, ""
-    return repo, tag
+    Example:
+
+    >>> parse_repository_tag('user/repo@sha256:digest')
+    ('user/repo', 'sha256:digest', '@')
+    >>> parse_repository_tag('user/repo:v1')
+    ('user/repo', 'v1', ':')
+    """
+    tag_separator = ":"
+    digest_separator = "@"
+
+    if digest_separator in repo_path:
+        repo, tag = repo_path.rsplit(digest_separator, 1)
+        return repo, tag, digest_separator
+
+    repo, tag = repo_path, ""
+    if tag_separator in repo_path:
+        repo, tag = repo_path.rsplit(tag_separator, 1)
+        if "/" in tag:
+            repo, tag = repo_path, ""
+
+    return repo, tag, tag_separator
 
 
 # Volumes

+ 2 - 1
docs/yml.md

@@ -25,12 +25,13 @@ Values for configuration options can contain environment variables, e.g.
 
 ### image
 
-Tag or partial image ID. Can be local or remote - Compose will attempt to
+Tag, partial image ID or digest. Can be local or remote - Compose will attempt to
 pull if it doesn't exist locally.
 
     image: ubuntu
     image: orchardup/postgresql
     image: a4bc65fd
+    image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d
 
 ### build
 

+ 6 - 0
tests/fixtures/simple-composefile/digest.yml

@@ -0,0 +1,6 @@
+simple:
+  image: busybox:latest
+  command: top
+digest:
+  image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d
+  command: top

+ 6 - 0
tests/integration/cli_test.py

@@ -88,6 +88,12 @@ class CLITestCase(DockerClientTestCase):
         mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
         mock_logging.info.assert_any_call('Pulling another (busybox:latest)...')
 
+    @patch('compose.service.log')
+    def test_pull_with_digest(self, mock_logging):
+        self.command.dispatch(['-f', 'digest.yml', 'pull'], None)
+        mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...')
+        mock_logging.info.assert_any_call('Pulling digest (busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d)...')
+
     @patch('sys.stdout', new_callable=StringIO)
     def test_build_no_cache(self, mock_stdout):
         self.command.base_dir = 'tests/fixtures/simple-dockerfile'

+ 20 - 6
tests/unit/service_test.py

@@ -192,6 +192,16 @@ class ServiceTest(unittest.TestCase):
             tag='latest',
             stream=True)
 
+    @mock.patch('compose.service.log', autospec=True)
+    def test_pull_image_digest(self, mock_log):
+        service = Service('foo', client=self.mock_client, image='someimage@sha256:1234')
+        service.pull()
+        self.mock_client.pull.assert_called_once_with(
+            'someimage',
+            tag='sha256:1234',
+            stream=True)
+        mock_log.info.assert_called_once_with('Pulling foo (someimage@sha256:1234)...')
+
     @mock.patch('compose.service.Container', autospec=True)
     def test_recreate_container(self, _):
         mock_container = mock.create_autospec(Container)
@@ -217,12 +227,16 @@ class ServiceTest(unittest.TestCase):
         mock_container.stop.assert_called_once_with(timeout=1)
 
     def test_parse_repository_tag(self):
-        self.assertEqual(parse_repository_tag("root"), ("root", ""))
-        self.assertEqual(parse_repository_tag("root:tag"), ("root", "tag"))
-        self.assertEqual(parse_repository_tag("user/repo"), ("user/repo", ""))
-        self.assertEqual(parse_repository_tag("user/repo:tag"), ("user/repo", "tag"))
-        self.assertEqual(parse_repository_tag("url:5000/repo"), ("url:5000/repo", ""))
-        self.assertEqual(parse_repository_tag("url:5000/repo:tag"), ("url:5000/repo", "tag"))
+        self.assertEqual(parse_repository_tag("root"), ("root", "", ":"))
+        self.assertEqual(parse_repository_tag("root:tag"), ("root", "tag", ":"))
+        self.assertEqual(parse_repository_tag("user/repo"), ("user/repo", "", ":"))
+        self.assertEqual(parse_repository_tag("user/repo:tag"), ("user/repo", "tag", ":"))
+        self.assertEqual(parse_repository_tag("url:5000/repo"), ("url:5000/repo", "", ":"))
+        self.assertEqual(parse_repository_tag("url:5000/repo:tag"), ("url:5000/repo", "tag", ":"))
+
+        self.assertEqual(parse_repository_tag("root@sha256:digest"), ("root", "sha256:digest", "@"))
+        self.assertEqual(parse_repository_tag("user/repo@sha256:digest"), ("user/repo", "sha256:digest", "@"))
+        self.assertEqual(parse_repository_tag("url:5000/repo@sha256:digest"), ("url:5000/repo", "sha256:digest", "@"))
 
     @mock.patch('compose.service.Container', autospec=True)
     def test_create_container_latest_is_used_when_no_tag_specified(self, mock_container):