From 33cc601176addaedf44d8f3bafd576f38bd6312e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 14 Jun 2016 16:03:56 -0700 Subject: [PATCH] Warn on missing digests, don't push/pull by default Add a --fetch-digests flag to automatically push/pull Signed-off-by: Aanand Prasad --- compose/bundle.py | 64 ++++++++++++++++++++++++++++++++++++--------- compose/cli/main.py | 31 +++++++++++++++++++--- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/compose/bundle.py b/compose/bundle.py index e93c5bd9c..965d65c87 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -40,6 +40,22 @@ SUPPORTED_KEYS = { VERSION = '0.1' +class NeedsPush(Exception): + def __init__(self, image_name): + self.image_name = image_name + + +class NeedsPull(Exception): + def __init__(self, image_name): + self.image_name = image_name + + +class MissingDigests(Exception): + def __init__(self, needs_push, needs_pull): + self.needs_push = needs_push + self.needs_pull = needs_pull + + def serialize_bundle(config, image_digests): if config.networks: log.warn("Unsupported top level key 'networks' - ignoring") @@ -54,21 +70,36 @@ def serialize_bundle(config, image_digests): ) -def get_image_digests(project): - return { - service.name: get_image_digest(service) - for service in project.services - } +def get_image_digests(project, allow_fetch=False): + digests = {} + needs_push = set() + needs_pull = set() + + for service in project.services: + try: + digests[service.name] = get_image_digest( + service, + allow_fetch=allow_fetch, + ) + except NeedsPush as e: + needs_push.add(e.image_name) + except NeedsPull as e: + needs_pull.add(e.image_name) + + if needs_push or needs_pull: + raise MissingDigests(needs_push, needs_pull) + + return digests -def get_image_digest(service): +def get_image_digest(service, allow_fetch=False): if 'image' not in service.options: raise UserError( "Service '{s.name}' doesn't define an image tag. An image name is " "required to generate a proper image digest for the bundle. Specify " "an image repo and tag with the 'image' option.".format(s=service)) - repo, tag, separator = parse_repository_tag(service.options['image']) + separator = parse_repository_tag(service.options['image'])[2] # Compose file already uses a digest, no lookup required if separator == '@': return service.options['image'] @@ -87,13 +118,17 @@ def get_image_digest(service): # digests return image['RepoDigests'][0] + if not allow_fetch: + if 'build' in service.options: + raise NeedsPush(service.image_name) + else: + raise NeedsPull(service.image_name) + + return fetch_image_digest(service) + + +def fetch_image_digest(service): if 'build' not in service.options: - log.warn( - "Compose needs to pull the image for '{s.name}' in order to create " - "a bundle. This may result in a more recent image being used. " - "It is recommended that you use an image tagged with a " - "specific version to minimize the potential " - "differences.".format(s=service)) digest = service.pull() else: try: @@ -108,12 +143,15 @@ def get_image_digest(service): if not digest: raise ValueError("Failed to get digest for %s" % service.name) + repo = parse_repository_tag(service.options['image'])[0] identifier = '{repo}@{digest}'.format(repo=repo, digest=digest) # Pull by digest so that image['RepoDigests'] is populated for next time # and we don't have to pull/push again service.client.pull(identifier) + log.info("Stored digest for {}".format(service.image_name)) + return identifier diff --git a/compose/cli/main.py b/compose/cli/main.py index 3e4404630..25ee90507 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -15,6 +15,7 @@ from . import errors from . import signals from .. import __version__ from ..bundle import get_image_digests +from ..bundle import MissingDigests from ..bundle import serialize_bundle from ..config import ConfigurationError from ..config import parse_environment @@ -218,12 +219,17 @@ class TopLevelCommand(object): """ Generate a Docker bundle from the Compose file. - Local images will be pushed to a Docker registry, and remote images - will be pulled to fetch an image digest. + Images must have digests stored, which requires interaction with a + Docker registry. If digests aren't stored for all images, you can pass + `--fetch-digests` to automatically fetch them. Images for services + with a `build` key will be pushed. Images for services without a + `build` key will be pulled. Usage: bundle [options] Options: + --fetch-digests Automatically fetch image digests if missing + -o, --output PATH Path to write the bundle file to. Defaults to ".dsb". """ @@ -235,7 +241,26 @@ class TopLevelCommand(object): output = "{}.dsb".format(self.project.name) with errors.handle_connection_errors(self.project.client): - image_digests = get_image_digests(self.project) + try: + image_digests = get_image_digests( + self.project, + allow_fetch=options['--fetch-digests'], + ) + 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: + paras += ["The following images need to be pushed:", list_images(e.needs_push)] + + if e.needs_pull: + paras += ["The following images need to be pulled:", list_images(e.needs_pull)] + + paras.append("If this is OK, run `docker-compose bundle --fetch-digests`.") + + raise UserError("\n\n".join(paras)) with open(output, 'w') as f: f.write(serialize_bundle(compose_config, image_digests))