Add docker-compose event

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2015-08-31 14:31:20 -04:00 committed by Daniel Nephin
parent f38c29f37b
commit d1d3969661
8 changed files with 195 additions and 6 deletions

View File

@ -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,27 @@ 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):
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): def help(self, project, options):
""" """
Get help on a command. Get help on a command.

View File

@ -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'

View File

@ -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,40 @@ 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 {
'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, def up(self,
service_names=None, service_names=None,

View File

@ -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
docs/reference/events.md Normal file
View File

@ -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",
}
```

View File

@ -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

View File

@ -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 json
import os import os
import shlex import shlex
import signal import signal
@ -855,6 +856,16 @@ 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['event'] for e in lines] == ['create', 'start', 'create', 'start']
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)

View File

@ -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,83 @@ 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':
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): def test_net_unset(self):
project = Project.from_config('test', Config(None, [ project = Project.from_config('test', Config(None, [
{ {