mirror of https://github.com/docker/compose.git
Merge pull request #6410 from docker/2618-new-events-api
Use improved API fields for project events when possible
This commit is contained in:
commit
6b3855335e
|
@ -236,7 +236,8 @@ def watch_events(thread_map, event_stream, presenters, thread_args):
|
|||
thread_map[event['id']] = build_thread(
|
||||
event['container'],
|
||||
next(presenters),
|
||||
*thread_args)
|
||||
*thread_args
|
||||
)
|
||||
|
||||
|
||||
def consume_queue(queue, cascade_stop):
|
||||
|
|
|
@ -7,7 +7,6 @@ from .version import ComposeVersion
|
|||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
HTTP_TIMEOUT = 60
|
||||
IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag']
|
||||
IS_WINDOWS_PLATFORM = (sys.platform == "win32")
|
||||
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
|
||||
LABEL_ONE_OFF = 'com.docker.compose.oneoff'
|
||||
|
|
|
@ -10,13 +10,13 @@ from functools import reduce
|
|||
import enum
|
||||
import six
|
||||
from docker.errors import APIError
|
||||
from docker.utils import version_lt
|
||||
|
||||
from . import parallel
|
||||
from .config import ConfigurationError
|
||||
from .config.config import V1
|
||||
from .config.sort_services import get_container_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_PROJECT
|
||||
from .const import LABEL_SERVICE
|
||||
|
@ -402,11 +402,13 @@ class Project(object):
|
|||
detached=True,
|
||||
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):
|
||||
time = datetime.datetime.fromtimestamp(event['time'])
|
||||
time = time.replace(
|
||||
microsecond=microseconds_from_time_nano(event['timeNano']))
|
||||
microsecond=microseconds_from_time_nano(event['timeNano'])
|
||||
)
|
||||
return {
|
||||
'time': time,
|
||||
'type': 'container',
|
||||
|
@ -425,17 +427,15 @@ class Project(object):
|
|||
filters={'label': self.labels()},
|
||||
decode=True
|
||||
):
|
||||
# The first part of this condition is a guard against some events
|
||||
# broadcasted by swarm that don't have a status field.
|
||||
# This is a guard against some events broadcasted by swarm that
|
||||
# don't have a status field.
|
||||
# See https://github.com/docker/compose/issues/3316
|
||||
if 'status' not in event or event['status'] in IMAGE_EVENTS:
|
||||
# We don't receive any image events because labels aren't applied
|
||||
# to images
|
||||
if 'status' not in event:
|
||||
continue
|
||||
|
||||
# TODO: get labels from the API v1.22 , see github issue 2618
|
||||
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'])
|
||||
except APIError:
|
||||
continue
|
||||
|
@ -443,6 +443,56 @@ class Project(object):
|
|||
continue
|
||||
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,
|
||||
service_names=None,
|
||||
start_deps=True,
|
||||
|
|
|
@ -254,9 +254,10 @@ class ProjectTest(unittest.TestCase):
|
|||
[container_ids[0] + ':rw']
|
||||
)
|
||||
|
||||
def test_events(self):
|
||||
def test_events_legacy(self):
|
||||
services = [Service(name='web'), Service(name='db')]
|
||||
project = Project('test', services, self.mock_client)
|
||||
self.mock_client.api_version = '1.21'
|
||||
self.mock_client.events.return_value = iter([
|
||||
{
|
||||
'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):
|
||||
project = Project.from_config(
|
||||
name='test',
|
||||
|
|
Loading…
Reference in New Issue