Pārlūkot izejas kodu

Merge pull request #4333 from ucalgary/4332-config-image-digests

Add --resolve-image-digests option to docker-compose config command
Joffrey F 8 gadi atpakaļ
vecāks
revīzija
9d2c6f156b
3 mainītis faili ar 82 papildinājumiem un 47 dzēšanām
  1. 52 42
      compose/cli/main.py
  2. 11 5
      compose/config/serialize.py
  3. 19 0
      tests/unit/config/config_test.py

+ 52 - 42
compose/cli/main.py

@@ -263,43 +263,7 @@ class TopLevelCommand(object):
         if not output:
             output = "{}.dab".format(self.project.name)
 
-        with errors.handle_connection_errors(self.project.client):
-            try:
-                image_digests = get_image_digests(
-                    self.project,
-                    allow_push=options['--push-images'],
-                )
-            except MissingDigests as e:
-                def list_images(images):
-                    return "\n".join("    {}".format(name) for name in sorted(images))
-
-                paras = ["Some images are missing digests."]
-
-                if e.needs_push:
-                    command_hint = (
-                        "Use `docker-compose push {}` to push them. "
-                        "You can do this automatically with `docker-compose bundle --push-images`."
-                        .format(" ".join(sorted(e.needs_push)))
-                    )
-                    paras += [
-                        "The following images can be pushed:",
-                        list_images(e.needs_push),
-                        command_hint,
-                    ]
-
-                if e.needs_pull:
-                    command_hint = (
-                        "Use `docker-compose pull {}` to pull them. "
-                        .format(" ".join(sorted(e.needs_pull)))
-                    )
-
-                    paras += [
-                        "The following images need to be pulled:",
-                        list_images(e.needs_pull),
-                        command_hint,
-                    ]
-
-                raise UserError("\n\n".join(paras))
+        image_digests = image_digests_for_project(self.project, options['--push-images'])
 
         with open(output, 'w') as f:
             f.write(serialize_bundle(compose_config, image_digests))
@@ -313,13 +277,20 @@ class TopLevelCommand(object):
         Usage: config [options]
 
         Options:
-            -q, --quiet     Only validate the configuration, don't print
-                            anything.
-            --services      Print the service names, one per line.
-            --volumes       Print the volume names, one per line.
+            --resolve-image-digests  Pin image tags to digests.
+            -q, --quiet              Only validate the configuration, don't print
+                                     anything.
+            --services               Print the service names, one per line.
+            --volumes                Print the volume names, one per line.
 
         """
+
         compose_config = get_config_from_options(self.project_dir, config_options)
+        image_digests = None
+
+        if options['--resolve-image-digests']:
+            self.project = project_from_options('.', config_options)
+            image_digests = image_digests_for_project(self.project)
 
         if options['--quiet']:
             return
@@ -332,7 +303,7 @@ class TopLevelCommand(object):
             print('\n'.join(volume for volume in compose_config.volumes))
             return
 
-        print(serialize_config(compose_config))
+        print(serialize_config(compose_config, image_digests))
 
     def create(self, options):
         """
@@ -1034,6 +1005,45 @@ def timeout_from_opts(options):
     return None if timeout is None else int(timeout)
 
 
+def image_digests_for_project(project, allow_push=False):
+    with errors.handle_connection_errors(project.client):
+        try:
+            return get_image_digests(
+                project,
+                allow_push=allow_push
+            )
+        except MissingDigests as e:
+            def list_images(images):
+                return "\n".join("    {}".format(name) for name in sorted(images))
+
+            paras = ["Some images are missing digests."]
+
+            if e.needs_push:
+                command_hint = (
+                    "Use `docker-compose push {}` to push them. "
+                    .format(" ".join(sorted(e.needs_push)))
+                )
+                paras += [
+                    "The following images can be pushed:",
+                    list_images(e.needs_push),
+                    command_hint,
+                ]
+
+            if e.needs_pull:
+                command_hint = (
+                    "Use `docker-compose pull {}` to pull them. "
+                    .format(" ".join(sorted(e.needs_pull)))
+                )
+
+                paras += [
+                    "The following images need to be pulled:",
+                    list_images(e.needs_pull),
+                    command_hint,
+                ]
+
+            raise UserError("\n\n".join(paras))
+
+
 def exitval_from_opts(options, project):
     exit_value_from = options.get('--exit-code-from')
     if exit_value_from:

+ 11 - 5
compose/config/serialize.py

@@ -26,10 +26,13 @@ yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
 yaml.SafeDumper.add_representer(types.ServicePort, serialize_dict_type)
 
 
-def denormalize_config(config):
+def denormalize_config(config, image_digests=None):
     result = {'version': V2_1 if config.version == V1 else config.version}
     denormalized_services = [
-        denormalize_service_dict(service_dict, config.version)
+        denormalize_service_dict(
+            service_dict,
+            config.version,
+            image_digests[service_dict['name']] if image_digests else None)
         for service_dict in config.services
     ]
     result['services'] = {
@@ -51,9 +54,9 @@ def denormalize_config(config):
     return result
 
 
-def serialize_config(config):
+def serialize_config(config, image_digests=None):
     return yaml.safe_dump(
-        denormalize_config(config),
+        denormalize_config(config, image_digests),
         default_flow_style=False,
         indent=2,
         width=80)
@@ -78,9 +81,12 @@ def serialize_ns_time_value(value):
     return '{0}{1}'.format(*result)
 
 
-def denormalize_service_dict(service_dict, version):
+def denormalize_service_dict(service_dict, version, image_digest=None):
     service_dict = service_dict.copy()
 
+    if image_digest:
+        service_dict['image'] = image_digest
+
     if 'restart' in service_dict:
         service_dict['restart'] = types.serialize_restart_spec(
             service_dict['restart']

+ 19 - 0
tests/unit/config/config_test.py

@@ -3654,6 +3654,25 @@ class SerializeTest(unittest.TestCase):
         assert denormalized_service['healthcheck']['interval'] == '100s'
         assert denormalized_service['healthcheck']['timeout'] == '30s'
 
+    def test_denormalize_image_has_digest(self):
+        service_dict = {
+            'image': 'busybox'
+        }
+        image_digest = 'busybox@sha256:abcde'
+
+        assert denormalize_service_dict(service_dict, V3_0, image_digest) == {
+            'image': 'busybox@sha256:abcde'
+        }
+
+    def test_denormalize_image_no_digest(self):
+        service_dict = {
+            'image': 'busybox'
+        }
+
+        assert denormalize_service_dict(service_dict, V3_0) == {
+            'image': 'busybox'
+        }
+
     def test_serialize_secrets(self):
         service_dict = {
             'image': 'example/web',