|
|
@@ -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
|
|
|
|
|
|
|