diff --git a/compose/cli/main.py b/compose/cli/main.py index 338566c72..b393d43e0 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -619,11 +619,24 @@ class TopLevelCommand(object): """ List containers. - Usage: ps [options] [SERVICE...] + Usage: ps [options] [--filter KEY=VAL] [SERVICE...] Options: - -q Only display IDs + -q Only display IDs + --services Display services + --filter KEY=VAL Filter services by a property """ + if options['-q'] and options['--services']: + raise UserError('-q and --services cannot be combined') + + if options['--services']: + filt = build_filter(options.get('--filter')) + services = self.project.services + if filt: + services = filter_services(filt, services, self.project) + print('\n'.join(service.name for service in services)) + return + containers = sorted( self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only), @@ -1352,3 +1365,49 @@ def build_exec_command(options, container_id, command): args += [container_id] args += command return args + + +def has_container_with_state(containers, state): + states = { + 'running': lambda c: c.is_running, + 'stopped': lambda c: not c.is_running, + 'paused': lambda c: c.is_paused, + 'restarting': lambda c: c.is_restarting, + } + for container in containers: + if state not in states: + raise UserError("Invalid state: %s" % state) + if states[state](container): + return True + + +def filter_services(filt, services, project): + def should_include(service): + for f in filt: + if f == 'status': + state = filt[f] + containers = project.containers([service.name], stopped=True) + if not has_container_with_state(containers, state): + return False + elif f == 'source': + source = filt[f] + if source == 'image' or source == 'build': + if source not in service.options: + return False + else: + raise UserError("Invalid value for source filter: %s" % source) + else: + raise UserError("Invalid filter: %s" % f) + return True + + return filter(should_include, services) + + +def build_filter(arg): + filt = {} + if arg is not None: + if '=' not in arg: + raise UserError("Arguments to --filter should be in form KEY=VAL") + key, val = arg.split('=', 1) + filt[key] = val + return filt diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index f3e14b02f..2c7194fcb 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -64,48 +64,32 @@ __docker_compose_services_all() { COMPREPLY=( $(compgen -W "$(___docker_compose_all_services_in_compose_file)" -- "$cur") ) } -# All services that have an entry with the given key in their compose_file section -___docker_compose_services_with_key() { - # flatten sections under "services" to one line, then filter lines containing the key and return section name - __docker_compose_q config \ - | sed -n -e '/^services:/,/^[^ ]/p' \ - | sed -n 's/^ //p' \ - | awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \ - | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' -} - # All services that are defined by a Dockerfile reference __docker_compose_services_from_build() { - COMPREPLY=( $(compgen -W "$(___docker_compose_services_with_key build)" -- "$cur") ) + COMPREPLY=( $(compgen -W "$(__docker_compose_q ps --services --filter "source=build")" -- "$cur") ) } # All services that are defined by an image __docker_compose_services_from_image() { - COMPREPLY=( $(compgen -W "$(___docker_compose_services_with_key image)" -- "$cur") ) -} - -# The services for which containers have been created, optionally filtered -# by a boolean expression passed in as argument. -__docker_compose_services_with() { - local containers names - containers="$(__docker_compose_q ps -q)" - names=$(docker 2>/dev/null inspect -f "{{if ${1:-true}}}{{range \$k, \$v := .Config.Labels}}{{if eq \$k \"com.docker.compose.service\"}}{{\$v}}{{end}}{{end}}{{end}}" $containers) - COMPREPLY=( $(compgen -W "$names" -- "$cur") ) + COMPREPLY=( $(compgen -W "$(__docker_compose_q ps --services --filter "source=image")" -- "$cur") ) } # The services for which at least one paused container exists __docker_compose_services_paused() { - __docker_compose_services_with '.State.Paused' + names=$(__docker_compose_q ps --services --filter "status=paused") + COMPREPLY=( $(compgen -W "$names" -- "$cur") ) } # The services for which at least one running container exists __docker_compose_services_running() { - __docker_compose_services_with '.State.Running' + names=$(__docker_compose_q ps --services --filter "status=running") + COMPREPLY=( $(compgen -W "$names" -- "$cur") ) } # The services for which at least one stopped container exists __docker_compose_services_stopped() { - __docker_compose_services_with 'not .State.Running' + names=$(__docker_compose_q ps --services --filter "status=stopped") + COMPREPLY=( $(compgen -W "$names" -- "$cur") ) } diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index f801704d7..cb583b343 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -491,6 +491,34 @@ class CLITestCase(DockerClientTestCase): assert 'multiplecomposefiles_another_1' not in result.stdout assert 'multiplecomposefiles_yetanother_1' in result.stdout + def test_ps_services_filter_option(self): + self.base_dir = 'tests/fixtures/ps-services-filter' + image = self.dispatch(['ps', '--services', '--filter', 'source=image']) + build = self.dispatch(['ps', '--services', '--filter', 'source=build']) + all_services = self.dispatch(['ps', '--services']) + + assert 'with_build' in all_services.stdout + assert 'with_image' in all_services.stdout + assert 'with_build' in build.stdout + assert 'with_build' not in image.stdout + assert 'with_image' in image.stdout + assert 'with_image' not in build.stdout + + def test_ps_services_filter_status(self): + self.base_dir = 'tests/fixtures/ps-services-filter' + self.dispatch(['up', '-d']) + self.dispatch(['pause', 'with_image']) + paused = self.dispatch(['ps', '--services', '--filter', 'status=paused']) + stopped = self.dispatch(['ps', '--services', '--filter', 'status=stopped']) + running = self.dispatch(['ps', '--services', '--filter', 'status=running']) + + assert 'with_build' not in stopped.stdout + assert 'with_image' not in stopped.stdout + assert 'with_build' not in paused.stdout + assert 'with_image' in paused.stdout + assert 'with_build' in running.stdout + assert 'with_image' in running.stdout + def test_pull(self): result = self.dispatch(['pull']) assert sorted(result.stderr.split('\n'))[1:] == [ diff --git a/tests/fixtures/ps-services-filter/docker-compose.yml b/tests/fixtures/ps-services-filter/docker-compose.yml new file mode 100644 index 000000000..3d8609373 --- /dev/null +++ b/tests/fixtures/ps-services-filter/docker-compose.yml @@ -0,0 +1,6 @@ +with_image: + image: busybox:latest + command: top +with_build: + build: ../build-ctx/ + command: top