mirror of https://github.com/docker/compose.git
Merge pull request #3601 from aanand/warn-on-missing-digests
Warn on missing digests, don't push/pull by default
This commit is contained in:
commit
352cdf0a80
|
@ -40,6 +40,22 @@ SUPPORTED_KEYS = {
|
||||||
VERSION = '0.1'
|
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):
|
def serialize_bundle(config, image_digests):
|
||||||
if config.networks:
|
if config.networks:
|
||||||
log.warn("Unsupported top level key 'networks' - ignoring")
|
log.warn("Unsupported top level key 'networks' - ignoring")
|
||||||
|
@ -54,21 +70,36 @@ def serialize_bundle(config, image_digests):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_image_digests(project):
|
def get_image_digests(project, allow_fetch=False):
|
||||||
return {
|
digests = {}
|
||||||
service.name: get_image_digest(service)
|
needs_push = set()
|
||||||
for service in project.services
|
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:
|
if 'image' not in service.options:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
"Service '{s.name}' doesn't define an image tag. An image name is "
|
"Service '{s.name}' doesn't define an image tag. An image name is "
|
||||||
"required to generate a proper image digest for the bundle. Specify "
|
"required to generate a proper image digest for the bundle. Specify "
|
||||||
"an image repo and tag with the 'image' option.".format(s=service))
|
"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
|
# Compose file already uses a digest, no lookup required
|
||||||
if separator == '@':
|
if separator == '@':
|
||||||
return service.options['image']
|
return service.options['image']
|
||||||
|
@ -87,13 +118,17 @@ def get_image_digest(service):
|
||||||
# digests
|
# digests
|
||||||
return image['RepoDigests'][0]
|
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:
|
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()
|
digest = service.pull()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
@ -108,12 +143,15 @@ def get_image_digest(service):
|
||||||
if not digest:
|
if not digest:
|
||||||
raise ValueError("Failed to get digest for %s" % service.name)
|
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)
|
identifier = '{repo}@{digest}'.format(repo=repo, digest=digest)
|
||||||
|
|
||||||
# Pull by digest so that image['RepoDigests'] is populated for next time
|
# Pull by digest so that image['RepoDigests'] is populated for next time
|
||||||
# and we don't have to pull/push again
|
# and we don't have to pull/push again
|
||||||
service.client.pull(identifier)
|
service.client.pull(identifier)
|
||||||
|
|
||||||
|
log.info("Stored digest for {}".format(service.image_name))
|
||||||
|
|
||||||
return identifier
|
return identifier
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ from . import errors
|
||||||
from . import signals
|
from . import signals
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..bundle import get_image_digests
|
from ..bundle import get_image_digests
|
||||||
|
from ..bundle import MissingDigests
|
||||||
from ..bundle import serialize_bundle
|
from ..bundle import serialize_bundle
|
||||||
from ..config import ConfigurationError
|
from ..config import ConfigurationError
|
||||||
from ..config import parse_environment
|
from ..config import parse_environment
|
||||||
|
@ -218,12 +219,17 @@ class TopLevelCommand(object):
|
||||||
"""
|
"""
|
||||||
Generate a Docker bundle from the Compose file.
|
Generate a Docker bundle from the Compose file.
|
||||||
|
|
||||||
Local images will be pushed to a Docker registry, and remote images
|
Images must have digests stored, which requires interaction with a
|
||||||
will be pulled to fetch an image digest.
|
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]
|
Usage: bundle [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
--fetch-digests Automatically fetch image digests if missing
|
||||||
|
|
||||||
-o, --output PATH Path to write the bundle file to.
|
-o, --output PATH Path to write the bundle file to.
|
||||||
Defaults to "<project name>.dsb".
|
Defaults to "<project name>.dsb".
|
||||||
"""
|
"""
|
||||||
|
@ -235,7 +241,26 @@ class TopLevelCommand(object):
|
||||||
output = "{}.dsb".format(self.project.name)
|
output = "{}.dsb".format(self.project.name)
|
||||||
|
|
||||||
with errors.handle_connection_errors(self.project.client):
|
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:
|
with open(output, 'w') as f:
|
||||||
f.write(serialize_bundle(compose_config, image_digests))
|
f.write(serialize_bundle(compose_config, image_digests))
|
||||||
|
|
Loading…
Reference in New Issue