Merge pull request #4333 from ucalgary/4332-config-image-digests

Add --resolve-image-digests option to docker-compose config command
This commit is contained in:
Joffrey F 2017-03-18 15:41:43 -07:00 committed by GitHub
commit 9d2c6f156b
3 changed files with 82 additions and 47 deletions

View File

@ -263,43 +263,7 @@ class TopLevelCommand(object):
if not output: if not output:
output = "{}.dab".format(self.project.name) output = "{}.dab".format(self.project.name)
with errors.handle_connection_errors(self.project.client): image_digests = image_digests_for_project(self.project, options['--push-images'])
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))
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))
@ -313,13 +277,20 @@ class TopLevelCommand(object):
Usage: config [options] Usage: config [options]
Options: Options:
-q, --quiet Only validate the configuration, don't print --resolve-image-digests Pin image tags to digests.
anything. -q, --quiet Only validate the configuration, don't print
--services Print the service names, one per line. anything.
--volumes Print the volume names, one per line. --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) 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)
image_digests = image_digests_for_project(self.project)
if options['--quiet']: if options['--quiet']:
return return
@ -332,7 +303,7 @@ class TopLevelCommand(object):
print('\n'.join(volume for volume in compose_config.volumes)) print('\n'.join(volume for volume in compose_config.volumes))
return return
print(serialize_config(compose_config)) print(serialize_config(compose_config, image_digests))
def create(self, options): def create(self, options):
""" """
@ -1034,6 +1005,45 @@ def timeout_from_opts(options):
return None if timeout is None else int(timeout) 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): def exitval_from_opts(options, project):
exit_value_from = options.get('--exit-code-from') exit_value_from = options.get('--exit-code-from')
if exit_value_from: if exit_value_from:

View File

@ -26,10 +26,13 @@ yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
yaml.SafeDumper.add_representer(types.ServicePort, 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} result = {'version': V2_1 if config.version == V1 else config.version}
denormalized_services = [ 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 for service_dict in config.services
] ]
result['services'] = { result['services'] = {
@ -51,9 +54,9 @@ def denormalize_config(config):
return result return result
def serialize_config(config): def serialize_config(config, image_digests=None):
return yaml.safe_dump( return yaml.safe_dump(
denormalize_config(config), denormalize_config(config, image_digests),
default_flow_style=False, default_flow_style=False,
indent=2, indent=2,
width=80) width=80)
@ -78,9 +81,12 @@ def serialize_ns_time_value(value):
return '{0}{1}'.format(*result) 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() service_dict = service_dict.copy()
if image_digest:
service_dict['image'] = image_digest
if 'restart' in service_dict: if 'restart' in service_dict:
service_dict['restart'] = types.serialize_restart_spec( service_dict['restart'] = types.serialize_restart_spec(
service_dict['restart'] service_dict['restart']

View File

@ -3654,6 +3654,25 @@ class SerializeTest(unittest.TestCase):
assert denormalized_service['healthcheck']['interval'] == '100s' assert denormalized_service['healthcheck']['interval'] == '100s'
assert denormalized_service['healthcheck']['timeout'] == '30s' 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): def test_serialize_secrets(self):
service_dict = { service_dict = {
'image': 'example/web', 'image': 'example/web',