|
|
@@ -2,10 +2,12 @@ from __future__ import absolute_import
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
|
import itertools
|
|
|
+import json
|
|
|
import logging
|
|
|
import os
|
|
|
import re
|
|
|
import sys
|
|
|
+import tempfile
|
|
|
from collections import namedtuple
|
|
|
from collections import OrderedDict
|
|
|
from operator import attrgetter
|
|
|
@@ -59,8 +61,12 @@ from .utils import parse_seconds_float
|
|
|
from .utils import truncate_id
|
|
|
from .utils import unique_everseen
|
|
|
|
|
|
-log = logging.getLogger(__name__)
|
|
|
+if six.PY2:
|
|
|
+ import subprocess32 as subprocess
|
|
|
+else:
|
|
|
+ import subprocess
|
|
|
|
|
|
+log = logging.getLogger(__name__)
|
|
|
|
|
|
HOST_CONFIG_KEYS = [
|
|
|
'cap_add',
|
|
|
@@ -130,7 +136,6 @@ class NoSuchImageError(Exception):
|
|
|
|
|
|
ServiceName = namedtuple('ServiceName', 'project service number')
|
|
|
|
|
|
-
|
|
|
ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
|
|
|
|
|
|
|
|
|
@@ -166,20 +171,21 @@ class BuildAction(enum.Enum):
|
|
|
|
|
|
class Service(object):
|
|
|
def __init__(
|
|
|
- self,
|
|
|
- name,
|
|
|
- client=None,
|
|
|
- project='default',
|
|
|
- use_networking=False,
|
|
|
- links=None,
|
|
|
- volumes_from=None,
|
|
|
- network_mode=None,
|
|
|
- networks=None,
|
|
|
- secrets=None,
|
|
|
- scale=1,
|
|
|
- pid_mode=None,
|
|
|
- default_platform=None,
|
|
|
- **options
|
|
|
+ self,
|
|
|
+ name,
|
|
|
+ client=None,
|
|
|
+ project='default',
|
|
|
+ use_networking=False,
|
|
|
+ links=None,
|
|
|
+ volumes_from=None,
|
|
|
+ network_mode=None,
|
|
|
+ networks=None,
|
|
|
+ secrets=None,
|
|
|
+ scale=1,
|
|
|
+ pid_mode=None,
|
|
|
+ default_platform=None,
|
|
|
+ extra_labels=[],
|
|
|
+ **options
|
|
|
):
|
|
|
self.name = name
|
|
|
self.client = client
|
|
|
@@ -194,6 +200,7 @@ class Service(object):
|
|
|
self.scale_num = scale
|
|
|
self.default_platform = default_platform
|
|
|
self.options = options
|
|
|
+ self.extra_labels = extra_labels
|
|
|
|
|
|
def __repr__(self):
|
|
|
return '<Service: {}>'.format(self.name)
|
|
|
@@ -208,7 +215,7 @@ class Service(object):
|
|
|
for container in self.client.containers(
|
|
|
all=stopped,
|
|
|
filters=filters)])
|
|
|
- )
|
|
|
+ )
|
|
|
if result:
|
|
|
return result
|
|
|
|
|
|
@@ -338,9 +345,9 @@ class Service(object):
|
|
|
raise OperationFailedError("Cannot create container for service %s: %s" %
|
|
|
(self.name, ex.explanation))
|
|
|
|
|
|
- def ensure_image_exists(self, do_build=BuildAction.none, silent=False):
|
|
|
+ def ensure_image_exists(self, do_build=BuildAction.none, silent=False, cli=False):
|
|
|
if self.can_be_built() and do_build == BuildAction.force:
|
|
|
- self.build()
|
|
|
+ self.build(cli=cli)
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
@@ -356,7 +363,7 @@ class Service(object):
|
|
|
if do_build == BuildAction.skip:
|
|
|
raise NeedsBuildError(self)
|
|
|
|
|
|
- self.build()
|
|
|
+ self.build(cli=cli)
|
|
|
log.warning(
|
|
|
"Image for service {} was built because it did not already exist. To "
|
|
|
"rebuild this image you must use `docker-compose build` or "
|
|
|
@@ -397,8 +404,8 @@ class Service(object):
|
|
|
return ConvergencePlan('start', containers)
|
|
|
|
|
|
if (
|
|
|
- strategy is ConvergenceStrategy.always or
|
|
|
- self._containers_have_diverged(containers)
|
|
|
+ strategy is ConvergenceStrategy.always or
|
|
|
+ self._containers_have_diverged(containers)
|
|
|
):
|
|
|
return ConvergencePlan('recreate', containers)
|
|
|
|
|
|
@@ -475,6 +482,7 @@ class Service(object):
|
|
|
container, timeout=timeout, attach_logs=not detached,
|
|
|
start_new_container=start, renew_anonymous_volumes=renew_anonymous_volumes
|
|
|
)
|
|
|
+
|
|
|
containers, errors = parallel_execute(
|
|
|
containers,
|
|
|
recreate,
|
|
|
@@ -616,6 +624,8 @@ class Service(object):
|
|
|
try:
|
|
|
container.start()
|
|
|
except APIError as ex:
|
|
|
+ if "driver failed programming external connectivity" in ex.explanation:
|
|
|
+ log.warn("Host is already in use by another container")
|
|
|
raise OperationFailedError("Cannot start service %s: %s" % (self.name, ex.explanation))
|
|
|
return container
|
|
|
|
|
|
@@ -696,11 +706,11 @@ class Service(object):
|
|
|
net_name = self.network_mode.service_name
|
|
|
pid_namespace = self.pid_mode.service_name
|
|
|
return (
|
|
|
- self.get_linked_service_names() +
|
|
|
- self.get_volumes_from_names() +
|
|
|
- ([net_name] if net_name else []) +
|
|
|
- ([pid_namespace] if pid_namespace else []) +
|
|
|
- list(self.options.get('depends_on', {}).keys())
|
|
|
+ self.get_linked_service_names() +
|
|
|
+ self.get_volumes_from_names() +
|
|
|
+ ([net_name] if net_name else []) +
|
|
|
+ ([pid_namespace] if pid_namespace else []) +
|
|
|
+ list(self.options.get('depends_on', {}).keys())
|
|
|
)
|
|
|
|
|
|
def get_dependency_configs(self):
|
|
|
@@ -890,7 +900,7 @@ class Service(object):
|
|
|
|
|
|
container_options['labels'] = build_container_labels(
|
|
|
container_options.get('labels', {}),
|
|
|
- self.labels(one_off=one_off),
|
|
|
+ self.labels(one_off=one_off) + self.extra_labels,
|
|
|
number,
|
|
|
self.config_hash if add_config_hash else None,
|
|
|
slug
|
|
|
@@ -1049,7 +1059,7 @@ class Service(object):
|
|
|
return [build_spec(secret) for secret in self.secrets]
|
|
|
|
|
|
def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_args_override=None,
|
|
|
- gzip=False, rm=True, silent=False):
|
|
|
+ gzip=False, rm=True, silent=False, cli=False, progress=None):
|
|
|
output_stream = open(os.devnull, 'w')
|
|
|
if not silent:
|
|
|
output_stream = sys.stdout
|
|
|
@@ -1070,7 +1080,8 @@ class Service(object):
|
|
|
'Impossible to perform platform-targeted builds for API version < 1.35'
|
|
|
)
|
|
|
|
|
|
- build_output = self.client.build(
|
|
|
+ builder = self.client if not cli else _CLIBuilder(progress)
|
|
|
+ build_output = builder.build(
|
|
|
path=path,
|
|
|
tag=self.image_name,
|
|
|
rm=rm,
|
|
|
@@ -1542,9 +1553,9 @@ def warn_on_masked_volume(volumes_option, container_volumes, service):
|
|
|
|
|
|
for volume in volumes_option:
|
|
|
if (
|
|
|
- volume.external and
|
|
|
- volume.internal in container_volumes and
|
|
|
- container_volumes.get(volume.internal) != volume.external
|
|
|
+ volume.external and
|
|
|
+ volume.internal in container_volumes and
|
|
|
+ container_volumes.get(volume.internal) != volume.external
|
|
|
):
|
|
|
log.warning((
|
|
|
"Service \"{service}\" is using volume \"{volume}\" from the "
|
|
|
@@ -1591,6 +1602,7 @@ def build_mount(mount_spec):
|
|
|
read_only=mount_spec.read_only, consistency=mount_spec.consistency, **kwargs
|
|
|
)
|
|
|
|
|
|
+
|
|
|
# Labels
|
|
|
|
|
|
|
|
|
@@ -1645,6 +1657,7 @@ def format_environment(environment):
|
|
|
if isinstance(value, six.binary_type):
|
|
|
value = value.decode('utf-8')
|
|
|
return '{key}={value}'.format(key=key, value=value)
|
|
|
+
|
|
|
return [format_env(*item) for item in environment.items()]
|
|
|
|
|
|
|
|
|
@@ -1701,3 +1714,136 @@ def rewrite_build_path(path):
|
|
|
path = WINDOWS_LONGPATH_PREFIX + os.path.normpath(path)
|
|
|
|
|
|
return path
|
|
|
+
|
|
|
+
|
|
|
+class _CLIBuilder(object):
|
|
|
+ def __init__(self, progress):
|
|
|
+ self._progress = progress
|
|
|
+
|
|
|
+ def build(self, path, tag=None, quiet=False, fileobj=None,
|
|
|
+ nocache=False, rm=False, timeout=None,
|
|
|
+ custom_context=False, encoding=None, pull=False,
|
|
|
+ forcerm=False, dockerfile=None, container_limits=None,
|
|
|
+ decode=False, buildargs=None, gzip=False, shmsize=None,
|
|
|
+ labels=None, cache_from=None, target=None, network_mode=None,
|
|
|
+ squash=None, extra_hosts=None, platform=None, isolation=None,
|
|
|
+ use_config_proxy=True):
|
|
|
+ """
|
|
|
+ Args:
|
|
|
+ path (str): Path to the directory containing the Dockerfile
|
|
|
+ buildargs (dict): A dictionary of build arguments
|
|
|
+ cache_from (:py:class:`list`): A list of images used for build
|
|
|
+ cache resolution
|
|
|
+ container_limits (dict): A dictionary of limits applied to each
|
|
|
+ container created by the build process. Valid keys:
|
|
|
+ - memory (int): set memory limit for build
|
|
|
+ - memswap (int): Total memory (memory + swap), -1 to disable
|
|
|
+ swap
|
|
|
+ - cpushares (int): CPU shares (relative weight)
|
|
|
+ - cpusetcpus (str): CPUs in which to allow execution, e.g.,
|
|
|
+ ``"0-3"``, ``"0,1"``
|
|
|
+ custom_context (bool): Optional if using ``fileobj``
|
|
|
+ decode (bool): If set to ``True``, the returned stream will be
|
|
|
+ decoded into dicts on the fly. Default ``False``
|
|
|
+ dockerfile (str): path within the build context to the Dockerfile
|
|
|
+ encoding (str): The encoding for a stream. Set to ``gzip`` for
|
|
|
+ compressing
|
|
|
+ extra_hosts (dict): Extra hosts to add to /etc/hosts in building
|
|
|
+ containers, as a mapping of hostname to IP address.
|
|
|
+ fileobj: A file object to use as the Dockerfile. (Or a file-like
|
|
|
+ object)
|
|
|
+ forcerm (bool): Always remove intermediate containers, even after
|
|
|
+ unsuccessful builds
|
|
|
+ isolation (str): Isolation technology used during build.
|
|
|
+ Default: `None`.
|
|
|
+ labels (dict): A dictionary of labels to set on the image
|
|
|
+ network_mode (str): networking mode for the run commands during
|
|
|
+ build
|
|
|
+ nocache (bool): Don't use the cache when set to ``True``
|
|
|
+ platform (str): Platform in the format ``os[/arch[/variant]]``
|
|
|
+ pull (bool): Downloads any updates to the FROM image in Dockerfiles
|
|
|
+ quiet (bool): Whether to return the status
|
|
|
+ rm (bool): Remove intermediate containers. The ``docker build``
|
|
|
+ command now defaults to ``--rm=true``, but we have kept the old
|
|
|
+ default of `False` to preserve backward compatibility
|
|
|
+ shmsize (int): Size of `/dev/shm` in bytes. The size must be
|
|
|
+ greater than 0. If omitted the system uses 64MB
|
|
|
+ squash (bool): Squash the resulting images layers into a
|
|
|
+ single layer.
|
|
|
+ tag (str): A tag to add to the final image
|
|
|
+ target (str): Name of the build-stage to build in a multi-stage
|
|
|
+ Dockerfile
|
|
|
+ timeout (int): HTTP timeout
|
|
|
+ use_config_proxy (bool): If ``True``, and if the docker client
|
|
|
+ configuration file (``~/.docker/config.json`` by default)
|
|
|
+ contains a proxy configuration, the corresponding environment
|
|
|
+ variables will be set in the container being built.
|
|
|
+ Returns:
|
|
|
+ A generator for the build output.
|
|
|
+ """
|
|
|
+ if dockerfile:
|
|
|
+ dockerfile = os.path.join(path, dockerfile)
|
|
|
+ iidfile = tempfile.mktemp()
|
|
|
+
|
|
|
+ command_builder = _CommandBuilder()
|
|
|
+ command_builder.add_params("--build-arg", buildargs)
|
|
|
+ command_builder.add_list("--cache-from", cache_from)
|
|
|
+ command_builder.add_arg("--file", dockerfile)
|
|
|
+ command_builder.add_flag("--force-rm", forcerm)
|
|
|
+ command_builder.add_arg("--memory", container_limits.get("memory"))
|
|
|
+ command_builder.add_flag("--no-cache", nocache)
|
|
|
+ command_builder.add_arg("--progress", self._progress)
|
|
|
+ command_builder.add_flag("--pull", pull)
|
|
|
+ command_builder.add_arg("--tag", tag)
|
|
|
+ command_builder.add_arg("--target", target)
|
|
|
+ command_builder.add_arg("--iidfile", iidfile)
|
|
|
+ args = command_builder.build([path])
|
|
|
+
|
|
|
+ magic_word = "Successfully built "
|
|
|
+ appear = False
|
|
|
+ with subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True) as p:
|
|
|
+ while True:
|
|
|
+ line = p.stdout.readline()
|
|
|
+ if not line:
|
|
|
+ break
|
|
|
+ if line.startswith(magic_word):
|
|
|
+ appear = True
|
|
|
+ yield json.dumps({"stream": line})
|
|
|
+
|
|
|
+ with open(iidfile) as f:
|
|
|
+ line = f.readline()
|
|
|
+ image_id = line.split(":")[1].strip()
|
|
|
+ os.remove(iidfile)
|
|
|
+
|
|
|
+ # In case of `DOCKER_BUILDKIT=1`
|
|
|
+ # there is no success message already present in the output.
|
|
|
+ # Since that's the way `Service::build` gets the `image_id`
|
|
|
+ # it has to be added `manually`
|
|
|
+ if not appear:
|
|
|
+ yield json.dumps({"stream": "{}{}\n".format(magic_word, image_id)})
|
|
|
+
|
|
|
+
|
|
|
+class _CommandBuilder(object):
|
|
|
+ def __init__(self):
|
|
|
+ self._args = ["docker", "build"]
|
|
|
+
|
|
|
+ def add_arg(self, name, value):
|
|
|
+ if value:
|
|
|
+ self._args.extend([name, str(value)])
|
|
|
+
|
|
|
+ def add_flag(self, name, flag):
|
|
|
+ if flag:
|
|
|
+ self._args.extend([name])
|
|
|
+
|
|
|
+ def add_params(self, name, params):
|
|
|
+ if params:
|
|
|
+ for key, val in params.items():
|
|
|
+ self._args.extend([name, "{}={}".format(key, val)])
|
|
|
+
|
|
|
+ def add_list(self, name, values):
|
|
|
+ if values:
|
|
|
+ for val in values:
|
|
|
+ self._args.extend([name, val])
|
|
|
+
|
|
|
+ def build(self, args):
|
|
|
+ return self._args + args
|