mirror of https://github.com/docker/compose.git
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:
commit
9d2c6f156b
|
@ -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:
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue