浏览代码

Merge pull request #5384 from ilinum/1498-docker-compose-services

Implement --filter flag for docker-compose config --services and use it in bash completion
Joffrey F 8 年之前
父节点
当前提交
c4fda0834d

+ 61 - 2
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

+ 8 - 24
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") )
 }
 
 

+ 28 - 0
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:] == [

+ 6 - 0
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