From b531c90515c25d4b3af86cbd89e363a54eff504b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 13 Nov 2015 16:23:24 -0500 Subject: [PATCH] Add docker-compose event Signed-off-by: Daniel Nephin --- compose/cli/main.py | 16 +++++++- compose/const.py | 1 + compose/project.py | 31 ++++++++++++++++ compose/utils.py | 4 ++ tests/unit/project_test.py | 76 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 126 insertions(+), 2 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 45cc1803dd3..57c56f848a7 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -1,6 +1,7 @@ from __future__ import print_function from __future__ import unicode_literals +import json import logging import re import signal @@ -53,7 +54,7 @@ def main(): command = TopLevelCommand() command.sys_dispatch() except KeyboardInterrupt: - log.error("\nAborting.") + log.warn("Aborting.") sys.exit(1) except (UserError, NoSuchService, ConfigurationError, legacy.LegacyError) as e: log.error(e.msg) @@ -199,8 +200,19 @@ def events(self, project, options): Usage: events [options] [SERVICE...] Options: - --json + --json Output events as a stream of json objects """ + def format_event(event): + return ("{time}: service={service} event={event} " + "container={container} image={image}").format(**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)) def help(self, project, options): """ diff --git a/compose/const.py b/compose/const.py index 1b6894189e2..78713e34983 100644 --- a/compose/const.py +++ b/compose/const.py @@ -3,6 +3,7 @@ DEFAULT_TIMEOUT = 10 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") LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' LABEL_ONE_OFF = 'com.docker.compose.oneoff' diff --git a/compose/project.py b/compose/project.py index 41af8626151..718c65e779b 100644 --- a/compose/project.py +++ b/compose/project.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import datetime import logging from functools import reduce @@ -10,6 +11,7 @@ from .config import ConfigurationError from .config import get_service_name_from_net from .const import DEFAULT_TIMEOUT +from .const import IMAGE_EVENTS from .const import LABEL_ONE_OFF from .const import LABEL_PROJECT from .const import LABEL_SERVICE @@ -22,6 +24,7 @@ from .service import Service from .service import ServiceNet from .service import VolumeFromSpec +from .utils import microseconds_from_time_nano from .utils import parallel_execute @@ -285,6 +288,34 @@ def build(self, service_names=None, no_cache=False, pull=False, force_rm=False): else: log.info('%s uses an image, skipping' % service.name) + 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 { + 'service': container.service, + 'event': event['status'], + 'container': container.id, + 'image': event['from'], + 'time': time, + } + + service_names = set(self.service_names) + for event in self.client.events( + filters={'label': self.labels()}, + decode=True + ): + if event['status'] in IMAGE_EVENTS: + # TODO + continue + + # TODO: cache this + 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, service_names=None, start_deps=True, diff --git a/compose/utils.py b/compose/utils.py index 2c6c4584d29..bc259f5516f 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -180,3 +180,7 @@ def json_hash(obj): h = hashlib.sha256() h.update(dump.encode('utf8')) return h.hexdigest() + + +def microseconds_from_time_nano(time_nano): + return int(time_nano % 1000000000 / 1000) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index b38f5c783c8..4c095d5fffa 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import datetime + import docker from .. import mock @@ -215,6 +217,80 @@ def test_use_volumes_from_service_container(self, mock_return): ], None) self.assertEqual(project.get_service('test')._get_volumes_from(), [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 get_container(cid): + if cid == 'abcde': + labels = {LABEL_SERVICE: 'web'} + elif cid == 'ababa': + labels = {LABEL_SERVICE: 'db'} + else: + labels = {} + return {'Id': cid, 'Config': {'Labels': labels}} + + 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 == [ + { + 'service': 'web', + 'event': 'create', + 'container': 'abcde', + 'image': 'example/image', + 'time': datetime.datetime(2015, 1, 1, 1, 1, 1, 2), + }, + { + 'service': 'web', + 'event': 'attach', + 'container': 'abcde', + 'image': 'example/image', + 'time': datetime.datetime(2015, 1, 1, 1, 1, 1, 3), + }, + { + 'service': 'db', + 'event': 'create', + 'container': 'ababa', + 'image': 'example/db', + 'time': datetime.datetime(2015, 1, 1, 1, 1, 1, 4), + }, + ] + def test_net_unset(self): project = Project.from_dicts('test', [ {