diff --git a/compose/cli/main.py b/compose/cli/main.py index a7aec945e..98fc4e451 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -47,6 +47,7 @@ from .formatter import Formatter from .log_printer import build_log_presenters from .log_printer import LogPrinter from .utils import get_version_info +from .utils import human_readable_file_size from .utils import yesno @@ -175,6 +176,7 @@ class TopLevelCommand(object): events Receive real time events from containers exec Execute a command in a running container help Get help on a command + images List images kill Kill containers logs View output from containers pause Pause services @@ -481,6 +483,45 @@ class TopLevelCommand(object): print(getdoc(subject)) + def images(self, options): + """ + List images used by the created containers. + Usage: images [options] [SERVICE...] + + Options: + -q Only display IDs + """ + containers = sorted( + self.project.containers(service_names=options['SERVICE'], stopped=True) + + self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only), + key=attrgetter('name')) + + if options['-q']: + for image in set(c.image for c in containers): + print(image.split(':')[1]) + else: + headers = [ + 'Container', + 'Repository', + 'Tag', + 'Image Id', + 'Size' + ] + rows = [] + for container in containers: + image_config = container.image_config + repo_tags = image_config['RepoTags'][0].split(':') + image_id = image_config['Id'].split(':')[1][:12] + size = human_readable_file_size(image_config['Size']) + rows.append([ + container.name, + repo_tags[0], + repo_tags[1], + image_id, + size + ]) + print(Formatter().table(headers, rows)) + def kill(self, options): """ Force stop service containers. diff --git a/compose/cli/utils.py b/compose/cli/utils.py index 580bd1b07..4d4fc4c18 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals +import math import os import platform import ssl @@ -135,3 +136,15 @@ def unquote_path(s): if s[0] == '"' and s[-1] == '"': return s[1:-1] return s + + +def human_readable_file_size(size): + suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', ] + order = int(math.log(size, 2) / 10) if size else 0 + if order >= len(suffixes): + order = len(suffixes) - 1 + + return '{0:.3g} {1}'.format( + size / float(1 << (order * 10)), + suffixes[order] + ) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index f4b9342f3..fa099eac4 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -220,6 +220,16 @@ _docker_compose_help() { COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) ) } +_docker_compose_images() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help -q" -- "$cur" ) ) + ;; + *) + __docker_compose_services_all + ;; + esac +} _docker_compose_kill() { case "$prev" in diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 115dc6439..e1c1d8394 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1986,3 +1986,18 @@ class CLITestCase(DockerClientTestCase): result = wait_on_process(proc, returncode=1) assert 'exitcodefrom_another_1 exited with code 1' in result.stdout + + def test_images(self): + self.project.get_service('simple').create_container() + result = self.dispatch(['images']) + assert 'busybox' in result.stdout + assert 'simplecomposefile_simple_1' in result.stdout + + def test_images_default_composefile(self): + self.base_dir = 'tests/fixtures/multiple-composefiles' + self.dispatch(['up', '-d']) + result = self.dispatch(['images']) + + assert 'busybox' in result.stdout + assert 'multiplecomposefiles_another_1' in result.stdout + assert 'multiplecomposefiles_simple_1' in result.stdout