Skip to content

Commit

Permalink
Warn on missing digests, don't push/pull by default
Browse files Browse the repository at this point in the history
Add a --fetch-digests flag to automatically push/pull

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
  • Loading branch information
aanand committed Jul 6, 2016
1 parent 44e82ed commit 33cc601
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 16 deletions.
64 changes: 51 additions & 13 deletions compose/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@
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")
Expand All @@ -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']
Expand All @@ -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:
Expand All @@ -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


Expand Down
31 changes: 28 additions & 3 deletions compose/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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
Expand Down Expand Up @@ -218,12 +219,17 @@ def bundle(self, config_options, options):
"""
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 "<project name>.dsb".
"""
Expand All @@ -235,7 +241,26 @@ def bundle(self, config_options, options):
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))
Expand Down

0 comments on commit 33cc601

Please sign in to comment.