Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions compose/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import print_function
from __future__ import unicode_literals

import json
import logging
import re
import signal
Expand Down Expand Up @@ -132,6 +133,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 @@ -244,6 +246,30 @@ 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):
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):
"""
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 @@ -6,6 +6,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
42 changes: 41 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,44 @@ 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 {
'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,
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 @@ -88,3 +88,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
31 changes: 31 additions & 0 deletions tests/acceptance/cli_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import absolute_import
from __future__ import unicode_literals

import datetime
import json
import os
import shlex
import signal
Expand Down Expand Up @@ -855,6 +857,35 @@ 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['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):
config_path = os.path.abspath('tests/fixtures/env-file/docker-compose.yml')
self.dispatch(['-f', config_path, 'up', '-d'], None)
Expand Down
98 changes: 98 additions & 0 deletions tests/unit/project_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import absolute_import
from __future__ import unicode_literals

import datetime

import docker

from .. import mock
Expand Down Expand Up @@ -197,6 +199,102 @@ 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':
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):
project = Project.from_config('test', Config(None, [
{
Expand Down