From 1a7e01c39abcf90ad70dbf0d20e9206e1bc151b1 Mon Sep 17 00:00:00 2001 From: King Chung Huang Date: Sat, 14 Jan 2017 10:54:29 -0700 Subject: [PATCH 1/4] Add image digest arguments to config serialization Add arguments for image digests in the config.serialize module to optionally pin images to digests, like bundles. Signed-off-by: King Chung Huang --- compose/config/serialize.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 1de1f14fb..5b36124d0 100644 --- a/compose/config/serialize.py +++ b/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'] From 1da3ac4715589cc59a0811becab04e363b1de9d3 Mon Sep 17 00:00:00 2001 From: King Chung Huang Date: Sat, 14 Jan 2017 10:56:45 -0700 Subject: [PATCH 2/4] Add --resolve-image-digests argument to config command Add a --resolve-image-digests argument to the config command that pins images to a specific image digest, just like the bundle command. This can be used to pin images in compose files being used to deploy stacks in Docker 1.13. Signed-off-by: King Chung Huang --- compose/cli/main.py | 53 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 63a0036b4..2d763a922 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -313,13 +313,56 @@ 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) + + with errors.handle_connection_errors(self.project.client): + try: + image_digests = get_image_digests( + self.project, + allow_push=False + ) + 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)) if options['--quiet']: return @@ -332,7 +375,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): """ From 0464476f0857e527b19b68f5046ab742ebe3b138 Mon Sep 17 00:00:00 2001 From: King Chung Huang Date: Sat, 14 Jan 2017 11:24:05 -0700 Subject: [PATCH 3/4] Add unit test for image digests in config Add two unit tests to validate that the denormalize_service_dict function still works without passing a third argument for image_digest, and correctly uses an image digest if one is provided. Signed-off-by: King Chung Huang --- tests/unit/config/config_test.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index c86485d7b..49da2b473 100644 --- a/tests/unit/config/config_test.py +++ b/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', From 962ba5b9379c46e54d3f8a31d3e61930b90ccbfa Mon Sep 17 00:00:00 2001 From: King Chung Huang Date: Sun, 12 Feb 2017 11:18:11 -0700 Subject: [PATCH 4/4] Extract image tag to digest code into a function Extract the code in bundle() and config() that translates image tags into digests into a function named image_digests_for_project(). Signed-off-by: King Chung Huang --- compose/cli/main.py | 115 ++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 74 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 2d763a922..84cae9f53 100644 --- a/compose/cli/main.py +++ b/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)) @@ -326,43 +290,7 @@ class TopLevelCommand(object): if options['--resolve-image-digests']: self.project = project_from_options('.', config_options) - - with errors.handle_connection_errors(self.project.client): - try: - image_digests = get_image_digests( - self.project, - allow_push=False - ) - 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)) + image_digests = image_digests_for_project(self.project) if options['--quiet']: return @@ -1077,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: