Merge pull request #2392 from dnephin/docker_compose_events

docker-compose events
This commit is contained in:
Aanand Prasad 2016-01-12 11:45:27 +00:00
commit 063a25ae7d
8 changed files with 241 additions and 6 deletions

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import json
import logging
import re
import signal
@ -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
@ -244,6 +246,30 @@ class TopLevelCommand(DocoptCommand):
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.

View File

@ -6,6 +6,7 @@ import sys
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'

View File

@ -1,6 +1,7 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
import logging
from functools import reduce
@ -11,6 +12,7 @@ from . import parallel
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
@ -20,6 +22,7 @@ from .service import ConvergenceStrategy
from .service import Net
from .service import Service
from .service import ServiceNet
from .utils import microseconds_from_time_nano
from .volume import Volume
@ -267,7 +270,44 @@ class Project(object):
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,

View File

@ -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
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.
* [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

View File

@ -1,6 +1,8 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
import json
import os
import shlex
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(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)

View File

@ -1,6 +1,8 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import datetime
import docker
from .. import mock
@ -197,6 +199,102 @@ class ProjectTest(unittest.TestCase):
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, [
{