Skip to content

Commit

Permalink
Add docker-compose event
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Nephin <dnephin@docker.com>
  • Loading branch information
dnephin committed Jan 7, 2016
1 parent ed87d1f commit 20b6486
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 6 deletions.
23 changes: 23 additions & 0 deletions compose/cli/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import print_function
from __future__ import unicode_literals

import json
import logging
import re
import signal
Expand Down Expand Up @@ -131,6 +132,7 @@ class TopLevelCommand(DocoptCommand):
build Build or rebuild services
config Validate and view the compose file
create Create services
events Receive real time events from containers
help Get help on a command
kill Kill containers
logs View output from containers
Expand Down Expand Up @@ -243,6 +245,27 @@ def create(self, project, options):
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):
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):
"""
Get help on a command.
Expand Down
1 change: 1 addition & 0 deletions compose/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
38 changes: 37 additions & 1 deletion compose/project.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import absolute_import
from __future__ import unicode_literals

import datetime
import logging
from functools import reduce

Expand All @@ -11,6 +12,7 @@
from .config import ConfigurationError
from .config.sort_services 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
Expand All @@ -20,6 +22,7 @@
from .service import Net
from .service import Service
from .service import ServiceNet
from .utils import microseconds_from_time_nano
from .volume import Volume


Expand Down Expand Up @@ -267,7 +270,40 @@ def create(self, service_names=None, strategy=ConvergenceStrategy.changed, do_bu
plans = self._get_convergence_plans(services, strategy)

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 {
'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:
# 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,
service_names=None,
Expand Down
4 changes: 4 additions & 0 deletions compose/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,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)
34 changes: 34 additions & 0 deletions docs/reference/events.md
Original file line number Diff line number Diff line change
@@ -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",
}
```
11 changes: 6 additions & 5 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

* [build](build.md)
* [events](events.md)
* [help](help.md)
* [kill](kill.md)
* [ps](ps.md)
* [restart](restart.md)
* [run](run.md)
* [start](start.md)
* [up](up.md)
* [logs](logs.md)
* [port](port.md)
* [ps](ps.md)
* [pull](pull.md)
* [restart](restart.md)
* [rm](rm.md)
* [run](run.md)
* [scale](scale.md)
* [start](start.md)
* [stop](stop.md)
* [up](up.md)

## Where to go next

Expand Down
11 changes: 11 additions & 0 deletions tests/acceptance/cli_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import absolute_import

import json
import os
import shlex
import signal
Expand Down Expand Up @@ -854,6 +855,16 @@ def get_port(number, index=None):
self.assertEqual(get_port(3000, index=2), containers[1].get_local_port(3000))
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['event'] for e in lines] == ['create', 'start', 'create', 'start']

def test_env_file_relative_to_compose_file(self):
config_path = os.path.abspath('tests/fixtures/env-file/docker-compose.yml')
self.dispatch(['-f', config_path, 'up', '-d'], None)
Expand Down
79 changes: 79 additions & 0 deletions tests/unit/project_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import unicode_literals

import datetime

import docker

from .. import mock
Expand Down Expand Up @@ -196,6 +198,83 @@ def test_use_volumes_from_service_container(self):
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 dt_with_microseconds(dt, us):
return datetime.datetime.fromtimestamp(dt).replace(microsecond=us)

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': dt_with_microseconds(1420092061, 2),
},
{
'service': 'web',
'event': 'attach',
'container': 'abcde',
'image': 'example/image',
'time': dt_with_microseconds(1420092061, 3),
},
{
'service': 'db',
'event': 'create',
'container': 'ababa',
'image': 'example/db',
'time': dt_with_microseconds(1420092061, 4),
},
]

def test_net_unset(self):
project = Project.from_config('test', Config(None, [
{
Expand Down

0 comments on commit 20b6486

Please sign in to comment.