mirror of https://github.com/docker/compose.git
Use improved API fields for project events when possible
Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
parent
14e7a11b3c
commit
8b293d486e
|
@ -236,7 +236,8 @@ def watch_events(thread_map, event_stream, presenters, thread_args):
|
||||||
thread_map[event['id']] = build_thread(
|
thread_map[event['id']] = build_thread(
|
||||||
event['container'],
|
event['container'],
|
||||||
next(presenters),
|
next(presenters),
|
||||||
*thread_args)
|
*thread_args
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def consume_queue(queue, cascade_stop):
|
def consume_queue(queue, cascade_stop):
|
||||||
|
|
|
@ -7,7 +7,6 @@ from .version import ComposeVersion
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
HTTP_TIMEOUT = 60
|
HTTP_TIMEOUT = 60
|
||||||
IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', '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'
|
||||||
|
|
|
@ -10,13 +10,13 @@ from functools import reduce
|
||||||
import enum
|
import enum
|
||||||
import six
|
import six
|
||||||
from docker.errors import APIError
|
from docker.errors import APIError
|
||||||
|
from docker.utils import version_lt
|
||||||
|
|
||||||
from . import parallel
|
from . import parallel
|
||||||
from .config import ConfigurationError
|
from .config import ConfigurationError
|
||||||
from .config.config import V1
|
from .config.config import V1
|
||||||
from .config.sort_services import get_container_name_from_network_mode
|
from .config.sort_services import get_container_name_from_network_mode
|
||||||
from .config.sort_services import get_service_name_from_network_mode
|
from .config.sort_services import get_service_name_from_network_mode
|
||||||
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
|
||||||
|
@ -402,11 +402,13 @@ class Project(object):
|
||||||
detached=True,
|
detached=True,
|
||||||
start=False)
|
start=False)
|
||||||
|
|
||||||
def events(self, service_names=None):
|
def _legacy_event_processor(self, service_names):
|
||||||
|
# Only for v1 files or when Compose is forced to use an older API version
|
||||||
def build_container_event(event, container):
|
def build_container_event(event, container):
|
||||||
time = datetime.datetime.fromtimestamp(event['time'])
|
time = datetime.datetime.fromtimestamp(event['time'])
|
||||||
time = time.replace(
|
time = time.replace(
|
||||||
microsecond=microseconds_from_time_nano(event['timeNano']))
|
microsecond=microseconds_from_time_nano(event['timeNano'])
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
'time': time,
|
'time': time,
|
||||||
'type': 'container',
|
'type': 'container',
|
||||||
|
@ -425,17 +427,15 @@ class Project(object):
|
||||||
filters={'label': self.labels()},
|
filters={'label': self.labels()},
|
||||||
decode=True
|
decode=True
|
||||||
):
|
):
|
||||||
# The first part of this condition is a guard against some events
|
# This is a guard against some events broadcasted by swarm that
|
||||||
# broadcasted by swarm that don't have a status field.
|
# don't have a status field.
|
||||||
# See https://github.com/docker/compose/issues/3316
|
# See https://github.com/docker/compose/issues/3316
|
||||||
if 'status' not in event or event['status'] in IMAGE_EVENTS:
|
if 'status' not in event:
|
||||||
# We don't receive any image events because labels aren't applied
|
|
||||||
# to images
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# TODO: get labels from the API v1.22 , see github issue 2618
|
|
||||||
try:
|
try:
|
||||||
# this can fail if the container has been removed
|
# this can fail if the container has been removed or if the event
|
||||||
|
# refers to an image
|
||||||
container = Container.from_id(self.client, event['id'])
|
container = Container.from_id(self.client, event['id'])
|
||||||
except APIError:
|
except APIError:
|
||||||
continue
|
continue
|
||||||
|
@ -443,6 +443,56 @@ class Project(object):
|
||||||
continue
|
continue
|
||||||
yield build_container_event(event, container)
|
yield build_container_event(event, container)
|
||||||
|
|
||||||
|
def events(self, service_names=None):
|
||||||
|
if version_lt(self.client.api_version, '1.22'):
|
||||||
|
# New, better event API was introduced in 1.22.
|
||||||
|
return self._legacy_event_processor(service_names)
|
||||||
|
|
||||||
|
def build_container_event(event):
|
||||||
|
container_attrs = event['Actor']['Attributes']
|
||||||
|
time = datetime.datetime.fromtimestamp(event['time'])
|
||||||
|
time = time.replace(
|
||||||
|
microsecond=microseconds_from_time_nano(event['timeNano'])
|
||||||
|
)
|
||||||
|
|
||||||
|
container = None
|
||||||
|
try:
|
||||||
|
container = Container.from_id(self.client, event['id'])
|
||||||
|
except APIError:
|
||||||
|
# Container may have been removed (e.g. if this is a destroy event)
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
'time': time,
|
||||||
|
'type': 'container',
|
||||||
|
'action': event['status'],
|
||||||
|
'id': event['Actor']['ID'],
|
||||||
|
'service': container_attrs.get(LABEL_SERVICE),
|
||||||
|
'attributes': dict([
|
||||||
|
(k, v) for k, v in container_attrs.items()
|
||||||
|
if not k.startswith('com.docker.compose.')
|
||||||
|
]),
|
||||||
|
'container': container,
|
||||||
|
}
|
||||||
|
|
||||||
|
def yield_loop(service_names):
|
||||||
|
for event in self.client.events(
|
||||||
|
filters={'label': self.labels()},
|
||||||
|
decode=True
|
||||||
|
):
|
||||||
|
# TODO: support other event types
|
||||||
|
if event.get('Type') != 'container':
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if event['Actor']['Attributes'][LABEL_SERVICE] not in service_names:
|
||||||
|
continue
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
yield build_container_event(event)
|
||||||
|
|
||||||
|
return yield_loop(set(service_names) if service_names else self.service_names)
|
||||||
|
|
||||||
def up(self,
|
def up(self,
|
||||||
service_names=None,
|
service_names=None,
|
||||||
start_deps=True,
|
start_deps=True,
|
||||||
|
|
|
@ -254,9 +254,10 @@ class ProjectTest(unittest.TestCase):
|
||||||
[container_ids[0] + ':rw']
|
[container_ids[0] + ':rw']
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_events(self):
|
def test_events_legacy(self):
|
||||||
services = [Service(name='web'), Service(name='db')]
|
services = [Service(name='web'), Service(name='db')]
|
||||||
project = Project('test', services, self.mock_client)
|
project = Project('test', services, self.mock_client)
|
||||||
|
self.mock_client.api_version = '1.21'
|
||||||
self.mock_client.events.return_value = iter([
|
self.mock_client.events.return_value = iter([
|
||||||
{
|
{
|
||||||
'status': 'create',
|
'status': 'create',
|
||||||
|
@ -362,6 +363,175 @@ class ProjectTest(unittest.TestCase):
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_events(self):
|
||||||
|
services = [Service(name='web'), Service(name='db')]
|
||||||
|
project = Project('test', services, self.mock_client)
|
||||||
|
self.mock_client.api_version = '1.35'
|
||||||
|
self.mock_client.events.return_value = iter([
|
||||||
|
{
|
||||||
|
'status': 'create',
|
||||||
|
'from': 'example/image',
|
||||||
|
'Type': 'container',
|
||||||
|
'Actor': {
|
||||||
|
'ID': 'abcde',
|
||||||
|
'Attributes': {
|
||||||
|
'com.docker.compose.project': 'test',
|
||||||
|
'com.docker.compose.service': 'web',
|
||||||
|
'image': 'example/image',
|
||||||
|
'name': 'test_web_1',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'id': 'abcde',
|
||||||
|
'time': 1420092061,
|
||||||
|
'timeNano': 14200920610000002000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'status': 'attach',
|
||||||
|
'from': 'example/image',
|
||||||
|
'Type': 'container',
|
||||||
|
'Actor': {
|
||||||
|
'ID': 'abcde',
|
||||||
|
'Attributes': {
|
||||||
|
'com.docker.compose.project': 'test',
|
||||||
|
'com.docker.compose.service': 'web',
|
||||||
|
'image': 'example/image',
|
||||||
|
'name': 'test_web_1',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'id': 'abcde',
|
||||||
|
'time': 1420092061,
|
||||||
|
'timeNano': 14200920610000003000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'status': 'create',
|
||||||
|
'from': 'example/other',
|
||||||
|
'Type': 'container',
|
||||||
|
'Actor': {
|
||||||
|
'ID': 'bdbdbd',
|
||||||
|
'Attributes': {
|
||||||
|
'image': 'example/other',
|
||||||
|
'name': 'shrewd_einstein',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'id': 'bdbdbd',
|
||||||
|
'time': 1420092061,
|
||||||
|
'timeNano': 14200920610000005000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'status': 'create',
|
||||||
|
'from': 'example/db',
|
||||||
|
'Type': 'container',
|
||||||
|
'Actor': {
|
||||||
|
'ID': 'ababa',
|
||||||
|
'Attributes': {
|
||||||
|
'com.docker.compose.project': 'test',
|
||||||
|
'com.docker.compose.service': 'db',
|
||||||
|
'image': 'example/db',
|
||||||
|
'name': 'test_db_1',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'id': 'ababa',
|
||||||
|
'time': 1420092061,
|
||||||
|
'timeNano': 14200920610000004000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'status': 'destroy',
|
||||||
|
'from': 'example/db',
|
||||||
|
'Type': 'container',
|
||||||
|
'Actor': {
|
||||||
|
'ID': 'eeeee',
|
||||||
|
'Attributes': {
|
||||||
|
'com.docker.compose.project': 'test',
|
||||||
|
'com.docker.compose.service': 'db',
|
||||||
|
'image': 'example/db',
|
||||||
|
'name': 'test_db_1',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'id': 'eeeee',
|
||||||
|
'time': 1420092061,
|
||||||
|
'timeNano': 14200920610000004000,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
def dt_with_microseconds(dt, us):
|
||||||
|
return datetime.datetime.fromtimestamp(dt).replace(microsecond=us)
|
||||||
|
|
||||||
|
def get_container(cid):
|
||||||
|
if cid == 'eeeee':
|
||||||
|
raise NotFound(None, None, "oops")
|
||||||
|
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': 'test_web_1',
|
||||||
|
'image': 'example/image',
|
||||||
|
},
|
||||||
|
'time': dt_with_microseconds(1420092061, 2),
|
||||||
|
'container': Container(None, get_container('abcde')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'container',
|
||||||
|
'service': 'web',
|
||||||
|
'action': 'attach',
|
||||||
|
'id': 'abcde',
|
||||||
|
'attributes': {
|
||||||
|
'name': 'test_web_1',
|
||||||
|
'image': 'example/image',
|
||||||
|
},
|
||||||
|
'time': dt_with_microseconds(1420092061, 3),
|
||||||
|
'container': Container(None, get_container('abcde')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'container',
|
||||||
|
'service': 'db',
|
||||||
|
'action': 'create',
|
||||||
|
'id': 'ababa',
|
||||||
|
'attributes': {
|
||||||
|
'name': 'test_db_1',
|
||||||
|
'image': 'example/db',
|
||||||
|
},
|
||||||
|
'time': dt_with_microseconds(1420092061, 4),
|
||||||
|
'container': Container(None, get_container('ababa')),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'container',
|
||||||
|
'service': 'db',
|
||||||
|
'action': 'destroy',
|
||||||
|
'id': 'eeeee',
|
||||||
|
'attributes': {
|
||||||
|
'name': 'test_db_1',
|
||||||
|
'image': 'example/db',
|
||||||
|
},
|
||||||
|
'time': dt_with_microseconds(1420092061, 4),
|
||||||
|
'container': None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
def test_net_unset(self):
|
def test_net_unset(self):
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='test',
|
name='test',
|
||||||
|
|
Loading…
Reference in New Issue