mirror of https://github.com/docker/compose.git
Merge pull request #2392 from dnephin/docker_compose_events
docker-compose events
This commit is contained in:
commit
063a25ae7d
|
@ -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.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
```
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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, [
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue