mirror of
https://github.com/docker/compose.git
synced 2025-07-25 22:54:54 +02:00
Add docker-compose event
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
f38c29f37b
commit
d1d3969661
@ -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.
|
||||||
|
@ -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'
|
||||||
|
@ -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,
|
||||||
|
@ -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
34
docs/reference/events.md
Normal 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",
|
||||||
|
}
|
||||||
|
```
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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, [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user