瀏覽代碼

Merge pull request #2392 from dnephin/docker_compose_events

docker-compose events
Aanand Prasad 9 年之前
父節點
當前提交
063a25ae7d
共有 8 個文件被更改,包括 241 次插入6 次删除
  1. 26 0
      compose/cli/main.py
  2. 1 0
      compose/const.py
  3. 41 1
      compose/project.py
  4. 4 0
      compose/utils.py
  5. 34 0
      docs/reference/events.md
  6. 6 5
      docs/reference/index.md
  7. 31 0
      tests/acceptance/cli_test.py
  8. 98 0
      tests/unit/project_test.py

+ 26 - 0
compose/cli/main.py

@@ -2,6 +2,7 @@ from __future__ import absolute_import
 from __future__ import print_function
 from __future__ import print_function
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+import json
 import logging
 import logging
 import re
 import re
 import signal
 import signal
@@ -132,6 +133,7 @@ class TopLevelCommand(DocoptCommand):
       build              Build or rebuild services
       build              Build or rebuild services
       config             Validate and view the compose file
       config             Validate and view the compose file
       create             Create services
       create             Create services
+      events             Receive real time events from containers
       help               Get help on a command
       help               Get help on a command
       kill               Kill containers
       kill               Kill containers
       logs               View output from containers
       logs               View output from containers
@@ -244,6 +246,30 @@ class TopLevelCommand(DocoptCommand):
             do_build=not options['--no-build']
             do_build=not options['--no-build']
         )
         )
 
 
+    def events(self, project, options):
+        """
+        Receive real time events from containers.
+
+        Usage: events [options] [SERVICE...]
+
+        Options:
+            --json      Output events as a stream of json objects
+        """
+        def format_event(event):
+            attributes = ["%s=%s" % item for item in event['attributes'].items()]
+            return ("{time} {type} {action} {id} ({attrs})").format(
+                attrs=", ".join(sorted(attributes)),
+                **event)
+
+        def json_format_event(event):
+            event['time'] = event['time'].isoformat()
+            return json.dumps(event)
+
+        for event in project.events():
+            formatter = json_format_event if options['--json'] else format_event
+            print(formatter(event))
+            sys.stdout.flush()
+
     def help(self, project, options):
     def help(self, project, options):
         """
         """
         Get help on a command.
         Get help on a command.

+ 1 - 0
compose/const.py

@@ -6,6 +6,7 @@ import sys
 
 
 DEFAULT_TIMEOUT = 10
 DEFAULT_TIMEOUT = 10
 HTTP_TIMEOUT = int(os.environ.get('COMPOSE_HTTP_TIMEOUT', os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)))
 HTTP_TIMEOUT = int(os.environ.get('COMPOSE_HTTP_TIMEOUT', os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)))
+IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag']
 IS_WINDOWS_PLATFORM = (sys.platform == "win32")
 IS_WINDOWS_PLATFORM = (sys.platform == "win32")
 LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
 LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
 LABEL_ONE_OFF = 'com.docker.compose.oneoff'
 LABEL_ONE_OFF = 'com.docker.compose.oneoff'

+ 41 - 1
compose/project.py

@@ -1,6 +1,7 @@
 from __future__ import absolute_import
 from __future__ import absolute_import
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+import datetime
 import logging
 import logging
 from functools import reduce
 from functools import reduce
 
 
@@ -11,6 +12,7 @@ from . import parallel
 from .config import ConfigurationError
 from .config import ConfigurationError
 from .config.sort_services import get_service_name_from_net
 from .config.sort_services import get_service_name_from_net
 from .const import DEFAULT_TIMEOUT
 from .const import DEFAULT_TIMEOUT
+from .const import IMAGE_EVENTS
 from .const import LABEL_ONE_OFF
 from .const import LABEL_ONE_OFF
 from .const import LABEL_PROJECT
 from .const import LABEL_PROJECT
 from .const import LABEL_SERVICE
 from .const import LABEL_SERVICE
@@ -20,6 +22,7 @@ from .service import ConvergenceStrategy
 from .service import Net
 from .service import Net
 from .service import Service
 from .service import Service
 from .service import ServiceNet
 from .service import ServiceNet
+from .utils import microseconds_from_time_nano
 from .volume import Volume
 from .volume import Volume
 
 
 
 
@@ -267,7 +270,44 @@ class Project(object):
         plans = self._get_convergence_plans(services, strategy)
         plans = self._get_convergence_plans(services, strategy)
 
 
         for service in services:
         for service in services:
-            service.execute_convergence_plan(plans[service.name], do_build, detached=True, start=False)
+            service.execute_convergence_plan(
+                plans[service.name],
+                do_build,
+                detached=True,
+                start=False)
+
+    def events(self):
+        def build_container_event(event, container):
+            time = datetime.datetime.fromtimestamp(event['time'])
+            time = time.replace(
+                microsecond=microseconds_from_time_nano(event['timeNano']))
+            return {
+                'time': time,
+                'type': 'container',
+                'action': event['status'],
+                'id': container.id,
+                'service': container.service,
+                'attributes': {
+                    'name': container.name,
+                    'image': event['from'],
+                }
+            }
+
+        service_names = set(self.service_names)
+        for event in self.client.events(
+            filters={'label': self.labels()},
+            decode=True
+        ):
+            if event['status'] in IMAGE_EVENTS:
+                # We don't receive any image events because labels aren't applied
+                # to images
+                continue
+
+            # TODO: get labels from the API v1.22 , see github issue 2618
+            container = Container.from_id(self.client, event['id'])
+            if container.service not in service_names:
+                continue
+            yield build_container_event(event, container)
 
 
     def up(self,
     def up(self,
            service_names=None,
            service_names=None,

+ 4 - 0
compose/utils.py

@@ -88,3 +88,7 @@ def json_hash(obj):
     h = hashlib.sha256()
     h = hashlib.sha256()
     h.update(dump.encode('utf8'))
     h.update(dump.encode('utf8'))
     return h.hexdigest()
     return h.hexdigest()
+
+
+def microseconds_from_time_nano(time_nano):
+    return int(time_nano % 1000000000 / 1000)

+ 34 - 0
docs/reference/events.md

@@ -0,0 +1,34 @@
+<!--[metadata]>
++++
+title = "events"
+description = "Receive real time events from containers."
+keywords = ["fig, composition, compose, docker, orchestration, cli, events"]
+[menu.main]
+identifier="events.compose"
+parent = "smn_compose_cli"
++++
+<![end-metadata]-->
+
+# events
+
+```
+Usage: events [options] [SERVICE...]
+
+Options:
+    --json      Output events as a stream of json objects
+```
+
+Stream container events for every container in the project.
+
+With the `--json` flag, a json object will be printed one per line with the
+format:
+
+```
+{
+    "service": "web",
+    "event": "create",
+    "container": "213cf75fc39a",
+    "image": "alpine:edge",
+    "time": "2015-11-20T18:01:03.615550",
+}
+```

+ 6 - 5
docs/reference/index.md

@@ -14,19 +14,20 @@ parent = "smn_compose_ref"
 The following pages describe the usage information for the [docker-compose](docker-compose.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line.
 The following pages describe the usage information for the [docker-compose](docker-compose.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line.
 
 
 * [build](build.md)
 * [build](build.md)
+* [events](events.md)
 * [help](help.md)
 * [help](help.md)
 * [kill](kill.md)
 * [kill](kill.md)
-* [ps](ps.md)
-* [restart](restart.md)
-* [run](run.md)
-* [start](start.md)
-* [up](up.md)
 * [logs](logs.md)
 * [logs](logs.md)
 * [port](port.md)
 * [port](port.md)
+* [ps](ps.md)
 * [pull](pull.md)
 * [pull](pull.md)
+* [restart](restart.md)
 * [rm](rm.md)
 * [rm](rm.md)
+* [run](run.md)
 * [scale](scale.md)
 * [scale](scale.md)
+* [start](start.md)
 * [stop](stop.md)
 * [stop](stop.md)
+* [up](up.md)
 
 
 ## Where to go next
 ## Where to go next
 
 

+ 31 - 0
tests/acceptance/cli_test.py

@@ -1,6 +1,8 @@
 from __future__ import absolute_import
 from __future__ import absolute_import
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+import datetime
+import json
 import os
 import os
 import shlex
 import shlex
 import signal
 import signal
@@ -855,6 +857,35 @@ class CLITestCase(DockerClientTestCase):
         self.assertEqual(get_port(3000, index=2), containers[1].get_local_port(3000))
         self.assertEqual(get_port(3000, index=2), containers[1].get_local_port(3000))
         self.assertEqual(get_port(3002), "")
         self.assertEqual(get_port(3002), "")
 
 
+    def test_events_json(self):
+        events_proc = start_process(self.base_dir, ['events', '--json'])
+        self.dispatch(['up', '-d'])
+        wait_on_condition(ContainerCountCondition(self.project, 2))
+
+        os.kill(events_proc.pid, signal.SIGINT)
+        result = wait_on_process(events_proc, returncode=1)
+        lines = [json.loads(line) for line in result.stdout.rstrip().split('\n')]
+        assert [e['action'] for e in lines] == ['create', 'start', 'create', 'start']
+
+    def test_events_human_readable(self):
+        events_proc = start_process(self.base_dir, ['events'])
+        self.dispatch(['up', '-d', 'simple'])
+        wait_on_condition(ContainerCountCondition(self.project, 1))
+
+        os.kill(events_proc.pid, signal.SIGINT)
+        result = wait_on_process(events_proc, returncode=1)
+        lines = result.stdout.rstrip().split('\n')
+        assert len(lines) == 2
+
+        container, = self.project.containers()
+        expected_template = (
+            ' container {} {} (image=busybox:latest, '
+            'name=simplecomposefile_simple_1)')
+
+        assert expected_template.format('create', container.id) in lines[0]
+        assert expected_template.format('start', container.id) in lines[1]
+        assert lines[0].startswith(datetime.date.today().isoformat())
+
     def test_env_file_relative_to_compose_file(self):
     def test_env_file_relative_to_compose_file(self):
         config_path = os.path.abspath('tests/fixtures/env-file/docker-compose.yml')
         config_path = os.path.abspath('tests/fixtures/env-file/docker-compose.yml')
         self.dispatch(['-f', config_path, 'up', '-d'], None)
         self.dispatch(['-f', config_path, 'up', '-d'], None)

+ 98 - 0
tests/unit/project_test.py

@@ -1,6 +1,8 @@
 from __future__ import absolute_import
 from __future__ import absolute_import
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+import datetime
+
 import docker
 import docker
 
 
 from .. import mock
 from .. import mock
@@ -197,6 +199,102 @@ class ProjectTest(unittest.TestCase):
                 project.get_service('test')._get_volumes_from(),
                 project.get_service('test')._get_volumes_from(),
                 [container_ids[0] + ':rw'])
                 [container_ids[0] + ':rw'])
 
 
+    def test_events(self):
+        services = [Service(name='web'), Service(name='db')]
+        project = Project('test', services, self.mock_client)
+        self.mock_client.events.return_value = iter([
+            {
+                'status': 'create',
+                'from': 'example/image',
+                'id': 'abcde',
+                'time': 1420092061,
+                'timeNano': 14200920610000002000,
+            },
+            {
+                'status': 'attach',
+                'from': 'example/image',
+                'id': 'abcde',
+                'time': 1420092061,
+                'timeNano': 14200920610000003000,
+            },
+            {
+                'status': 'create',
+                'from': 'example/other',
+                'id': 'bdbdbd',
+                'time': 1420092061,
+                'timeNano': 14200920610000005000,
+            },
+            {
+                'status': 'create',
+                'from': 'example/db',
+                'id': 'ababa',
+                'time': 1420092061,
+                'timeNano': 14200920610000004000,
+            },
+        ])
+
+        def dt_with_microseconds(dt, us):
+            return datetime.datetime.fromtimestamp(dt).replace(microsecond=us)
+
+        def get_container(cid):
+            if cid == 'abcde':
+                name = 'web'
+                labels = {LABEL_SERVICE: name}
+            elif cid == 'ababa':
+                name = 'db'
+                labels = {LABEL_SERVICE: name}
+            else:
+                labels = {}
+                name = ''
+            return {
+                'Id': cid,
+                'Config': {'Labels': labels},
+                'Name': '/project_%s_1' % name,
+            }
+
+        self.mock_client.inspect_container.side_effect = get_container
+
+        events = project.events()
+
+        events_list = list(events)
+        # Assert the return value is a generator
+        assert not list(events)
+        assert events_list == [
+            {
+                'type': 'container',
+                'service': 'web',
+                'action': 'create',
+                'id': 'abcde',
+                'attributes': {
+                    'name': 'project_web_1',
+                    'image': 'example/image',
+                },
+                'time': dt_with_microseconds(1420092061, 2),
+            },
+            {
+                'type': 'container',
+                'service': 'web',
+                'action': 'attach',
+                'id': 'abcde',
+                'attributes': {
+                    'name': 'project_web_1',
+                    'image': 'example/image',
+                },
+                'time': dt_with_microseconds(1420092061, 3),
+            },
+            {
+                'type': 'container',
+                'service': 'db',
+                'action': 'create',
+                'id': 'ababa',
+                'attributes': {
+                    'name': 'project_db_1',
+                    'image': 'example/db',
+                },
+                'time': dt_with_microseconds(1420092061, 4),
+            },
+        ]
+
     def test_net_unset(self):
     def test_net_unset(self):
         project = Project.from_config('test', Config(None, [
         project = Project.from_config('test', Config(None, [
             {
             {