|
@@ -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
|
|
@@ -29,6 +29,7 @@ from .service import ContainerNetworkMode
|
|
|
from .service import ContainerPidMode
|
|
from .service import ContainerPidMode
|
|
|
from .service import ConvergenceStrategy
|
|
from .service import ConvergenceStrategy
|
|
|
from .service import NetworkMode
|
|
from .service import NetworkMode
|
|
|
|
|
+from .service import parse_repository_tag
|
|
|
from .service import PidMode
|
|
from .service import PidMode
|
|
|
from .service import Service
|
|
from .service import Service
|
|
|
from .service import ServiceNetworkMode
|
|
from .service import ServiceNetworkMode
|
|
@@ -279,6 +280,7 @@ class Project(object):
|
|
|
operator.attrgetter('name'),
|
|
operator.attrgetter('name'),
|
|
|
'Starting',
|
|
'Starting',
|
|
|
get_deps,
|
|
get_deps,
|
|
|
|
|
+ fail_check=lambda obj: not obj.containers(),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
return containers
|
|
return containers
|
|
@@ -401,11 +403,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',
|
|
@@ -424,17 +428,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
|
|
|
|
|
- # 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
|
|
# 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
|
|
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
|
|
@@ -442,6 +444,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,
|
|
@@ -592,8 +644,15 @@ class Project(object):
|
|
|
service.pull(ignore_pull_failures, silent=silent)
|
|
service.pull(ignore_pull_failures, silent=silent)
|
|
|
|
|
|
|
|
def push(self, service_names=None, ignore_push_failures=False):
|
|
def push(self, service_names=None, ignore_push_failures=False):
|
|
|
|
|
+ unique_images = set()
|
|
|
for service in self.get_services(service_names, include_deps=False):
|
|
for service in self.get_services(service_names, include_deps=False):
|
|
|
- service.push(ignore_push_failures)
|
|
|
|
|
|
|
+ # Considering <image> and <image:latest> as the same
|
|
|
|
|
+ repo, tag, sep = parse_repository_tag(service.image_name)
|
|
|
|
|
+ service_image_name = sep.join((repo, tag)) if tag else sep.join((repo, 'latest'))
|
|
|
|
|
+
|
|
|
|
|
+ if service_image_name not in unique_images:
|
|
|
|
|
+ service.push(ignore_push_failures)
|
|
|
|
|
+ unique_images.add(service_image_name)
|
|
|
|
|
|
|
|
def _labeled_containers(self, stopped=False, one_off=OneOffFilter.exclude):
|
|
def _labeled_containers(self, stopped=False, one_off=OneOffFilter.exclude):
|
|
|
ctnrs = list(filter(None, [
|
|
ctnrs = list(filter(None, [
|