|
|
@@ -1,31 +1,39 @@
|
|
|
-from __future__ import unicode_literals
|
|
|
from __future__ import absolute_import
|
|
|
-from collections import namedtuple
|
|
|
+from __future__ import unicode_literals
|
|
|
+
|
|
|
import logging
|
|
|
-import re
|
|
|
import os
|
|
|
+import re
|
|
|
import sys
|
|
|
+from collections import namedtuple
|
|
|
from operator import attrgetter
|
|
|
|
|
|
+import enum
|
|
|
import six
|
|
|
from docker.errors import APIError
|
|
|
-from docker.utils import create_host_config, LogConfig
|
|
|
+from docker.utils import LogConfig
|
|
|
+from docker.utils.ports import build_port_bindings
|
|
|
+from docker.utils.ports import split_port
|
|
|
|
|
|
from . import __version__
|
|
|
-from .config import DOCKER_CONFIG_KEYS, merge_environment
|
|
|
-from .const import (
|
|
|
- DEFAULT_TIMEOUT,
|
|
|
- LABEL_CONTAINER_NUMBER,
|
|
|
- LABEL_ONE_OFF,
|
|
|
- LABEL_PROJECT,
|
|
|
- LABEL_SERVICE,
|
|
|
- LABEL_VERSION,
|
|
|
- LABEL_CONFIG_HASH,
|
|
|
-)
|
|
|
+from .config import DOCKER_CONFIG_KEYS
|
|
|
+from .config import merge_environment
|
|
|
+from .config.validation import VALID_NAME_CHARS
|
|
|
+from .const import DEFAULT_TIMEOUT
|
|
|
+from .const import IS_WINDOWS_PLATFORM
|
|
|
+from .const import LABEL_CONFIG_HASH
|
|
|
+from .const import LABEL_CONTAINER_NUMBER
|
|
|
+from .const import LABEL_ONE_OFF
|
|
|
+from .const import LABEL_PROJECT
|
|
|
+from .const import LABEL_SERVICE
|
|
|
+from .const import LABEL_VERSION
|
|
|
from .container import Container
|
|
|
from .legacy import check_for_legacy_containers
|
|
|
-from .progress_stream import stream_output, StreamOutputError
|
|
|
-from .utils import json_hash, parallel_execute
|
|
|
+from .progress_stream import stream_output
|
|
|
+from .progress_stream import StreamOutputError
|
|
|
+from .utils import json_hash
|
|
|
+from .utils import parallel_execute
|
|
|
+
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@@ -33,11 +41,13 @@ log = logging.getLogger(__name__)
|
|
|
DOCKER_START_KEYS = [
|
|
|
'cap_add',
|
|
|
'cap_drop',
|
|
|
+ 'cgroup_parent',
|
|
|
'devices',
|
|
|
'dns',
|
|
|
'dns_search',
|
|
|
'env_file',
|
|
|
'extra_hosts',
|
|
|
+ 'ipc',
|
|
|
'read_only',
|
|
|
'net',
|
|
|
'log_driver',
|
|
|
@@ -51,8 +61,6 @@ DOCKER_START_KEYS = [
|
|
|
'security_opt',
|
|
|
]
|
|
|
|
|
|
-VALID_NAME_CHARS = '[a-zA-Z0-9\._\-]'
|
|
|
-
|
|
|
|
|
|
class BuildError(Exception):
|
|
|
def __init__(self, service, reason):
|
|
|
@@ -76,46 +84,61 @@ class NoSuchImageError(Exception):
|
|
|
VolumeSpec = namedtuple('VolumeSpec', 'external internal mode')
|
|
|
|
|
|
|
|
|
+VolumeFromSpec = namedtuple('VolumeFromSpec', 'source mode')
|
|
|
+
|
|
|
+
|
|
|
ServiceName = namedtuple('ServiceName', 'project service number')
|
|
|
|
|
|
|
|
|
ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
|
|
|
|
|
|
|
|
|
[email protected]
|
|
|
+class ConvergenceStrategy(enum.Enum):
|
|
|
+ """Enumeration for all possible convergence strategies. Values refer to
|
|
|
+ when containers should be recreated.
|
|
|
+ """
|
|
|
+ changed = 1
|
|
|
+ always = 2
|
|
|
+ never = 3
|
|
|
+
|
|
|
+ @property
|
|
|
+ def allows_recreate(self):
|
|
|
+ return self is not type(self).never
|
|
|
+
|
|
|
+
|
|
|
class Service(object):
|
|
|
def __init__(
|
|
|
self,
|
|
|
name,
|
|
|
client=None,
|
|
|
project='default',
|
|
|
+ use_networking=False,
|
|
|
links=None,
|
|
|
volumes_from=None,
|
|
|
net=None,
|
|
|
**options
|
|
|
):
|
|
|
- if not re.match('^%s+$' % VALID_NAME_CHARS, name):
|
|
|
- raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS))
|
|
|
if not re.match('^%s+$' % VALID_NAME_CHARS, project):
|
|
|
raise ConfigError('Invalid project name "%s" - only %s are allowed' % (project, VALID_NAME_CHARS))
|
|
|
- if 'image' in options and 'build' in options:
|
|
|
- raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name)
|
|
|
- if 'image' not in options and 'build' not in options:
|
|
|
- raise ConfigError('Service %s has neither an image nor a build path specified. Exactly one must be provided.' % name)
|
|
|
|
|
|
self.name = name
|
|
|
self.client = client
|
|
|
self.project = project
|
|
|
+ self.use_networking = use_networking
|
|
|
self.links = links or []
|
|
|
self.volumes_from = volumes_from or []
|
|
|
self.net = net or Net(None)
|
|
|
self.options = options
|
|
|
|
|
|
- def containers(self, stopped=False, one_off=False):
|
|
|
- containers = filter(None, [
|
|
|
+ def containers(self, stopped=False, one_off=False, filters={}):
|
|
|
+ filters.update({'label': self.labels(one_off=one_off)})
|
|
|
+
|
|
|
+ containers = list(filter(None, [
|
|
|
Container.from_ps(self.client, container)
|
|
|
for container in self.client.containers(
|
|
|
all=stopped,
|
|
|
- filters={'label': self.labels(one_off=one_off)})])
|
|
|
+ filters=filters)]))
|
|
|
|
|
|
if not containers:
|
|
|
check_for_legacy_containers(
|
|
|
@@ -143,17 +166,27 @@ class Service(object):
|
|
|
# TODO: remove these functions, project takes care of starting/stopping,
|
|
|
def stop(self, **options):
|
|
|
for c in self.containers():
|
|
|
- log.info("Stopping %s..." % c.name)
|
|
|
+ log.info("Stopping %s" % c.name)
|
|
|
c.stop(**options)
|
|
|
|
|
|
+ def pause(self, **options):
|
|
|
+ for c in self.containers(filters={'status': 'running'}):
|
|
|
+ log.info("Pausing %s" % c.name)
|
|
|
+ c.pause(**options)
|
|
|
+
|
|
|
+ def unpause(self, **options):
|
|
|
+ for c in self.containers(filters={'status': 'paused'}):
|
|
|
+ log.info("Unpausing %s" % c.name)
|
|
|
+ c.unpause()
|
|
|
+
|
|
|
def kill(self, **options):
|
|
|
for c in self.containers():
|
|
|
- log.info("Killing %s..." % c.name)
|
|
|
+ log.info("Killing %s" % c.name)
|
|
|
c.kill(**options)
|
|
|
|
|
|
def restart(self, **options):
|
|
|
for c in self.containers():
|
|
|
- log.info("Restarting %s..." % c.name)
|
|
|
+ log.info("Restarting %s" % c.name)
|
|
|
c.restart(**options)
|
|
|
|
|
|
# end TODO
|
|
|
@@ -279,7 +312,7 @@ class Service(object):
|
|
|
)
|
|
|
|
|
|
if 'name' in container_options and not quiet:
|
|
|
- log.info("Creating %s..." % container_options['name'])
|
|
|
+ log.info("Creating %s" % container_options['name'])
|
|
|
|
|
|
return Container.create(self.client, **container_options)
|
|
|
|
|
|
@@ -316,22 +349,19 @@ class Service(object):
|
|
|
else:
|
|
|
return self.options['image']
|
|
|
|
|
|
- def convergence_plan(self,
|
|
|
- allow_recreate=True,
|
|
|
- force_recreate=False):
|
|
|
-
|
|
|
- if force_recreate and not allow_recreate:
|
|
|
- raise ValueError("force_recreate and allow_recreate are in conflict")
|
|
|
-
|
|
|
+ def convergence_plan(self, strategy=ConvergenceStrategy.changed):
|
|
|
containers = self.containers(stopped=True)
|
|
|
|
|
|
if not containers:
|
|
|
return ConvergencePlan('create', [])
|
|
|
|
|
|
- if not allow_recreate:
|
|
|
+ if strategy is ConvergenceStrategy.never:
|
|
|
return ConvergencePlan('start', containers)
|
|
|
|
|
|
- if force_recreate or self._containers_have_diverged(containers):
|
|
|
+ if (
|
|
|
+ strategy is ConvergenceStrategy.always or
|
|
|
+ self._containers_have_diverged(containers)
|
|
|
+ ):
|
|
|
return ConvergencePlan('recreate', containers)
|
|
|
|
|
|
stopped = [c for c in containers if not c.is_running]
|
|
|
@@ -345,7 +375,7 @@ class Service(object):
|
|
|
config_hash = None
|
|
|
|
|
|
try:
|
|
|
- config_hash = self.config_hash()
|
|
|
+ config_hash = self.config_hash
|
|
|
except NoSuchImageError as e:
|
|
|
log.debug(
|
|
|
'Service %s has diverged: %s',
|
|
|
@@ -369,13 +399,17 @@ class Service(object):
|
|
|
def execute_convergence_plan(self,
|
|
|
plan,
|
|
|
do_build=True,
|
|
|
- timeout=DEFAULT_TIMEOUT):
|
|
|
+ timeout=DEFAULT_TIMEOUT,
|
|
|
+ detached=False):
|
|
|
(action, containers) = plan
|
|
|
+ should_attach_logs = not detached
|
|
|
|
|
|
if action == 'create':
|
|
|
- container = self.create_container(
|
|
|
- do_build=do_build,
|
|
|
- )
|
|
|
+ container = self.create_container(do_build=do_build)
|
|
|
+
|
|
|
+ if should_attach_logs:
|
|
|
+ container.attach_log_stream()
|
|
|
+
|
|
|
self.start_container(container)
|
|
|
|
|
|
return [container]
|
|
|
@@ -383,15 +417,16 @@ class Service(object):
|
|
|
elif action == 'recreate':
|
|
|
return [
|
|
|
self.recreate_container(
|
|
|
- c,
|
|
|
- timeout=timeout
|
|
|
+ container,
|
|
|
+ timeout=timeout,
|
|
|
+ attach_logs=should_attach_logs
|
|
|
)
|
|
|
- for c in containers
|
|
|
+ for container in containers
|
|
|
]
|
|
|
|
|
|
elif action == 'start':
|
|
|
- for c in containers:
|
|
|
- self.start_container_if_stopped(c)
|
|
|
+ for container in containers:
|
|
|
+ self.start_container_if_stopped(container, attach_logs=should_attach_logs)
|
|
|
|
|
|
return containers
|
|
|
|
|
|
@@ -406,44 +441,37 @@ class Service(object):
|
|
|
|
|
|
def recreate_container(self,
|
|
|
container,
|
|
|
- timeout=DEFAULT_TIMEOUT):
|
|
|
+ timeout=DEFAULT_TIMEOUT,
|
|
|
+ attach_logs=False):
|
|
|
"""Recreate a container.
|
|
|
|
|
|
The original container is renamed to a temporary name so that data
|
|
|
volumes can be copied to the new container, before the original
|
|
|
container is removed.
|
|
|
"""
|
|
|
- log.info("Recreating %s..." % container.name)
|
|
|
- try:
|
|
|
- container.stop(timeout=timeout)
|
|
|
- except APIError as e:
|
|
|
- if (e.response.status_code == 500
|
|
|
- and e.explanation
|
|
|
- and 'no such process' in str(e.explanation)):
|
|
|
- pass
|
|
|
- else:
|
|
|
- raise
|
|
|
-
|
|
|
- # Use a hopefully unique container name by prepending the short id
|
|
|
- self.client.rename(
|
|
|
- container.id,
|
|
|
- '%s_%s' % (container.short_id, container.name))
|
|
|
+ log.info("Recreating %s" % container.name)
|
|
|
|
|
|
+ container.stop(timeout=timeout)
|
|
|
+ container.rename_to_tmp_name()
|
|
|
new_container = self.create_container(
|
|
|
do_build=False,
|
|
|
previous_container=container,
|
|
|
number=container.labels.get(LABEL_CONTAINER_NUMBER),
|
|
|
quiet=True,
|
|
|
)
|
|
|
+ if attach_logs:
|
|
|
+ new_container.attach_log_stream()
|
|
|
self.start_container(new_container)
|
|
|
container.remove()
|
|
|
return new_container
|
|
|
|
|
|
- def start_container_if_stopped(self, container):
|
|
|
+ def start_container_if_stopped(self, container, attach_logs=False):
|
|
|
if container.is_running:
|
|
|
return container
|
|
|
else:
|
|
|
- log.info("Starting %s..." % container.name)
|
|
|
+ log.info("Starting %s" % container.name)
|
|
|
+ if attach_logs:
|
|
|
+ container.attach_log_stream()
|
|
|
return self.start_container(container)
|
|
|
|
|
|
def start_container(self, container):
|
|
|
@@ -452,7 +480,7 @@ class Service(object):
|
|
|
|
|
|
def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):
|
|
|
for c in self.duplicate_containers():
|
|
|
- log.info('Removing %s...' % c.name)
|
|
|
+ log.info('Removing %s' % c.name)
|
|
|
c.stop(timeout=timeout)
|
|
|
c.remove()
|
|
|
|
|
|
@@ -470,6 +498,7 @@ class Service(object):
|
|
|
else:
|
|
|
numbers.add(c.number)
|
|
|
|
|
|
+ @property
|
|
|
def config_hash(self):
|
|
|
return json_hash(self.config_dict())
|
|
|
|
|
|
@@ -495,7 +524,7 @@ class Service(object):
|
|
|
return [(service.name, alias) for service, alias in self.links]
|
|
|
|
|
|
def get_volumes_from_names(self):
|
|
|
- return [s.name for s in self.volumes_from if isinstance(s, Service)]
|
|
|
+ return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)]
|
|
|
|
|
|
def get_container_name(self, number, one_off=False):
|
|
|
# TODO: Implement issue #652 here
|
|
|
@@ -514,6 +543,9 @@ class Service(object):
|
|
|
return 1 if not numbers else max(numbers) + 1
|
|
|
|
|
|
def _get_links(self, link_to_self):
|
|
|
+ if self.use_networking:
|
|
|
+ return []
|
|
|
+
|
|
|
links = []
|
|
|
for service, link_name in self.links:
|
|
|
for container in service.containers():
|
|
|
@@ -535,16 +567,9 @@ class Service(object):
|
|
|
|
|
|
def _get_volumes_from(self):
|
|
|
volumes_from = []
|
|
|
- for volume_source in self.volumes_from:
|
|
|
- if isinstance(volume_source, Service):
|
|
|
- containers = volume_source.containers(stopped=True)
|
|
|
- if not containers:
|
|
|
- volumes_from.append(volume_source.create_container().id)
|
|
|
- else:
|
|
|
- volumes_from.extend(map(attrgetter('id'), containers))
|
|
|
-
|
|
|
- elif isinstance(volume_source, Container):
|
|
|
- volumes_from.append(volume_source.id)
|
|
|
+ for volume_from_spec in self.volumes_from:
|
|
|
+ volumes = build_volume_from(volume_from_spec)
|
|
|
+ volumes_from.extend(volumes)
|
|
|
|
|
|
return volumes_from
|
|
|
|
|
|
@@ -563,7 +588,7 @@ class Service(object):
|
|
|
|
|
|
if self.custom_container_name() and not one_off:
|
|
|
container_options['name'] = self.custom_container_name()
|
|
|
- else:
|
|
|
+ elif not container_options.get('name'):
|
|
|
container_options['name'] = self.get_container_name(number, one_off)
|
|
|
|
|
|
if 'detach' not in container_options:
|
|
|
@@ -580,16 +605,19 @@ class Service(object):
|
|
|
container_options['hostname'] = parts[0]
|
|
|
container_options['domainname'] = parts[2]
|
|
|
|
|
|
+ if 'hostname' not in container_options and self.use_networking:
|
|
|
+ container_options['hostname'] = self.name
|
|
|
+
|
|
|
if 'ports' in container_options or 'expose' in self.options:
|
|
|
ports = []
|
|
|
all_ports = container_options.get('ports', []) + self.options.get('expose', [])
|
|
|
- for port in all_ports:
|
|
|
- port = str(port)
|
|
|
- if ':' in port:
|
|
|
- port = port.split(':')[-1]
|
|
|
- if '/' in port:
|
|
|
- port = tuple(port.split('/'))
|
|
|
- ports.append(port)
|
|
|
+ for port_range in all_ports:
|
|
|
+ internal_range, _ = split_port(port_range)
|
|
|
+ for port in internal_range:
|
|
|
+ port = str(port)
|
|
|
+ if '/' in port:
|
|
|
+ port = tuple(port.split('/'))
|
|
|
+ ports.append(port)
|
|
|
container_options['ports'] = ports
|
|
|
|
|
|
override_options['binds'] = merge_volume_bindings(
|
|
|
@@ -614,7 +642,7 @@ class Service(object):
|
|
|
container_options.get('labels', {}),
|
|
|
self.labels(one_off=one_off),
|
|
|
number,
|
|
|
- self.config_hash() if add_config_hash else None)
|
|
|
+ self.config_hash if add_config_hash else None)
|
|
|
|
|
|
# Delete options which are only used when starting
|
|
|
for key in DOCKER_START_KEYS:
|
|
|
@@ -634,7 +662,7 @@ class Service(object):
|
|
|
cap_add = options.get('cap_add', None)
|
|
|
cap_drop = options.get('cap_drop', None)
|
|
|
log_config = LogConfig(
|
|
|
- type=options.get('log_driver', 'json-file'),
|
|
|
+ type=options.get('log_driver', ""),
|
|
|
config=options.get('log_opt', None)
|
|
|
)
|
|
|
pid = options.get('pid', None)
|
|
|
@@ -654,8 +682,9 @@ class Service(object):
|
|
|
read_only = options.get('read_only', None)
|
|
|
|
|
|
devices = options.get('devices', None)
|
|
|
+ cgroup_parent = options.get('cgroup_parent', None)
|
|
|
|
|
|
- return create_host_config(
|
|
|
+ return self.client.create_host_config(
|
|
|
links=self._get_links(link_to_self=one_off),
|
|
|
port_bindings=port_bindings,
|
|
|
binds=options.get('binds'),
|
|
|
@@ -674,20 +703,26 @@ class Service(object):
|
|
|
extra_hosts=extra_hosts,
|
|
|
read_only=read_only,
|
|
|
pid_mode=pid,
|
|
|
- security_opt=security_opt
|
|
|
+ security_opt=security_opt,
|
|
|
+ ipc_mode=options.get('ipc'),
|
|
|
+ cgroup_parent=cgroup_parent
|
|
|
)
|
|
|
|
|
|
- def build(self, no_cache=False):
|
|
|
- log.info('Building %s...' % self.name)
|
|
|
+ def build(self, no_cache=False, pull=False):
|
|
|
+ log.info('Building %s' % self.name)
|
|
|
|
|
|
- path = six.binary_type(self.options['build'])
|
|
|
+ path = self.options['build']
|
|
|
+ # python2 os.path() doesn't support unicode, so we need to encode it to
|
|
|
+ # a byte string
|
|
|
+ if not six.PY3:
|
|
|
+ path = path.encode('utf8')
|
|
|
|
|
|
build_output = self.client.build(
|
|
|
path=path,
|
|
|
tag=self.image_name,
|
|
|
stream=True,
|
|
|
rm=True,
|
|
|
- pull=False,
|
|
|
+ pull=pull,
|
|
|
nocache=no_cache,
|
|
|
dockerfile=self.options.get('dockerfile', None),
|
|
|
)
|
|
|
@@ -695,7 +730,7 @@ class Service(object):
|
|
|
try:
|
|
|
all_events = stream_output(build_output, sys.stdout)
|
|
|
except StreamOutputError as e:
|
|
|
- raise BuildError(self, unicode(e))
|
|
|
+ raise BuildError(self, six.text_type(e))
|
|
|
|
|
|
# Ensure the HTTP connection is not reused for another
|
|
|
# streaming command, as the Docker daemon can sometimes
|
|
|
@@ -741,19 +776,26 @@ class Service(object):
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
- def pull(self):
|
|
|
+ def pull(self, ignore_pull_failures=False):
|
|
|
if 'image' not in self.options:
|
|
|
return
|
|
|
|
|
|
- repo, tag = parse_repository_tag(self.options['image'])
|
|
|
+ repo, tag, separator = parse_repository_tag(self.options['image'])
|
|
|
tag = tag or 'latest'
|
|
|
- log.info('Pulling %s (%s:%s)...' % (self.name, repo, tag))
|
|
|
+ log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag))
|
|
|
output = self.client.pull(
|
|
|
repo,
|
|
|
tag=tag,
|
|
|
stream=True,
|
|
|
)
|
|
|
- stream_output(output, sys.stdout)
|
|
|
+
|
|
|
+ try:
|
|
|
+ stream_output(output, sys.stdout)
|
|
|
+ except StreamOutputError as e:
|
|
|
+ if not ignore_pull_failures:
|
|
|
+ raise
|
|
|
+ else:
|
|
|
+ log.error(six.text_type(e))
|
|
|
|
|
|
|
|
|
class Net(object):
|
|
|
@@ -806,7 +848,7 @@ class ServiceNet(object):
|
|
|
if containers:
|
|
|
return 'container:' + containers[0].id
|
|
|
|
|
|
- log.warn("Warning: Service %s is trying to use reuse the network stack "
|
|
|
+ log.warn("Service %s is trying to use reuse the network stack "
|
|
|
"of another service that is not running." % (self.id))
|
|
|
return None
|
|
|
|
|
|
@@ -823,14 +865,31 @@ def build_container_name(project, service, number, one_off=False):
|
|
|
|
|
|
# Images
|
|
|
|
|
|
+def parse_repository_tag(repo_path):
|
|
|
+ """Splits image identification into base image path, tag/digest
|
|
|
+ and it's separator.
|
|
|
|
|
|
-def parse_repository_tag(s):
|
|
|
- if ":" not in s:
|
|
|
- return s, ""
|
|
|
- repo, tag = s.rsplit(":", 1)
|
|
|
- if "/" in tag:
|
|
|
- return s, ""
|
|
|
- return repo, tag
|
|
|
+ Example:
|
|
|
+
|
|
|
+ >>> parse_repository_tag('user/repo@sha256:digest')
|
|
|
+ ('user/repo', 'sha256:digest', '@')
|
|
|
+ >>> parse_repository_tag('user/repo:v1')
|
|
|
+ ('user/repo', 'v1', ':')
|
|
|
+ """
|
|
|
+ tag_separator = ":"
|
|
|
+ digest_separator = "@"
|
|
|
+
|
|
|
+ if digest_separator in repo_path:
|
|
|
+ repo, tag = repo_path.rsplit(digest_separator, 1)
|
|
|
+ return repo, tag, digest_separator
|
|
|
+
|
|
|
+ repo, tag = repo_path, ""
|
|
|
+ if tag_separator in repo_path:
|
|
|
+ repo, tag = repo_path.rsplit(tag_separator, 1)
|
|
|
+ if "/" in tag:
|
|
|
+ repo, tag = repo_path, ""
|
|
|
+
|
|
|
+ return repo, tag, tag_separator
|
|
|
|
|
|
|
|
|
# Volumes
|
|
|
@@ -849,7 +908,7 @@ def merge_volume_bindings(volumes_option, previous_container):
|
|
|
volume_bindings.update(
|
|
|
get_container_data_volumes(previous_container, volumes_option))
|
|
|
|
|
|
- return volume_bindings.values()
|
|
|
+ return list(volume_bindings.values())
|
|
|
|
|
|
|
|
|
def get_container_data_volumes(container, volumes_option):
|
|
|
@@ -862,7 +921,7 @@ def get_container_data_volumes(container, volumes_option):
|
|
|
container_volumes = container.get('Volumes') or {}
|
|
|
image_volumes = container.image_config['ContainerConfig'].get('Volumes') or {}
|
|
|
|
|
|
- for volume in set(volumes_option + image_volumes.keys()):
|
|
|
+ for volume in set(volumes_option + list(image_volumes)):
|
|
|
volume = parse_volume_spec(volume)
|
|
|
# No need to preserve host volumes
|
|
|
if volume.external:
|
|
|
@@ -884,53 +943,85 @@ def build_volume_binding(volume_spec):
|
|
|
return volume_spec.internal, "{}:{}:{}".format(*volume_spec)
|
|
|
|
|
|
|
|
|
+def normalize_paths_for_engine(external_path, internal_path):
|
|
|
+ """Windows paths, c:\my\path\shiny, need to be changed to be compatible with
|
|
|
+ the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
|
|
|
+ """
|
|
|
+ if not IS_WINDOWS_PLATFORM:
|
|
|
+ return external_path, internal_path
|
|
|
+
|
|
|
+ if external_path:
|
|
|
+ drive, tail = os.path.splitdrive(external_path)
|
|
|
+
|
|
|
+ if drive:
|
|
|
+ external_path = '/' + drive.lower().rstrip(':') + tail
|
|
|
+
|
|
|
+ external_path = external_path.replace('\\', '/')
|
|
|
+
|
|
|
+ return external_path, internal_path.replace('\\', '/')
|
|
|
+
|
|
|
+
|
|
|
def parse_volume_spec(volume_config):
|
|
|
- parts = volume_config.split(':')
|
|
|
+ """
|
|
|
+ Parse a volume_config path and split it into external:internal[:mode]
|
|
|
+ parts to be returned as a valid VolumeSpec.
|
|
|
+ """
|
|
|
+ if IS_WINDOWS_PLATFORM:
|
|
|
+ # relative paths in windows expand to include the drive, eg C:\
|
|
|
+ # so we join the first 2 parts back together to count as one
|
|
|
+ drive, tail = os.path.splitdrive(volume_config)
|
|
|
+ parts = tail.split(":")
|
|
|
+
|
|
|
+ if drive:
|
|
|
+ parts[0] = drive + parts[0]
|
|
|
+ else:
|
|
|
+ parts = volume_config.split(':')
|
|
|
+
|
|
|
if len(parts) > 3:
|
|
|
raise ConfigError("Volume %s has incorrect format, should be "
|
|
|
"external:internal[:mode]" % volume_config)
|
|
|
|
|
|
if len(parts) == 1:
|
|
|
- external = None
|
|
|
- internal = os.path.normpath(parts[0])
|
|
|
+ external, internal = normalize_paths_for_engine(None, os.path.normpath(parts[0]))
|
|
|
else:
|
|
|
- external = os.path.normpath(parts[0])
|
|
|
- internal = os.path.normpath(parts[1])
|
|
|
+ external, internal = normalize_paths_for_engine(os.path.normpath(parts[0]), os.path.normpath(parts[1]))
|
|
|
|
|
|
- mode = parts[2] if len(parts) == 3 else 'rw'
|
|
|
+ mode = 'rw'
|
|
|
+ if len(parts) == 3:
|
|
|
+ mode = parts[2]
|
|
|
|
|
|
return VolumeSpec(external, internal, mode)
|
|
|
|
|
|
|
|
|
-# Ports
|
|
|
-
|
|
|
+def build_volume_from(volume_from_spec):
|
|
|
+ """
|
|
|
+ volume_from can be either a service or a container. We want to return the
|
|
|
+ container.id and format it into a string complete with the mode.
|
|
|
+ """
|
|
|
+ if isinstance(volume_from_spec.source, Service):
|
|
|
+ containers = volume_from_spec.source.containers(stopped=True)
|
|
|
+ if not containers:
|
|
|
+ return ["{}:{}".format(volume_from_spec.source.create_container().id, volume_from_spec.mode)]
|
|
|
|
|
|
-def build_port_bindings(ports):
|
|
|
- port_bindings = {}
|
|
|
- for port in ports:
|
|
|
- internal_port, external = split_port(port)
|
|
|
- if internal_port in port_bindings:
|
|
|
- port_bindings[internal_port].append(external)
|
|
|
- else:
|
|
|
- port_bindings[internal_port] = [external]
|
|
|
- return port_bindings
|
|
|
+ container = containers[0]
|
|
|
+ return ["{}:{}".format(container.id, volume_from_spec.mode)]
|
|
|
+ elif isinstance(volume_from_spec.source, Container):
|
|
|
+ return ["{}:{}".format(volume_from_spec.source.id, volume_from_spec.mode)]
|
|
|
|
|
|
|
|
|
-def split_port(port):
|
|
|
- parts = str(port).split(':')
|
|
|
- if not 1 <= len(parts) <= 3:
|
|
|
- raise ConfigError('Invalid port "%s", should be '
|
|
|
- '[[remote_ip:]remote_port:]port[/protocol]' % port)
|
|
|
+def parse_volume_from_spec(volume_from_config):
|
|
|
+ parts = volume_from_config.split(':')
|
|
|
+ if len(parts) > 2:
|
|
|
+ raise ConfigError("Volume %s has incorrect format, should be "
|
|
|
+ "external:internal[:mode]" % volume_from_config)
|
|
|
|
|
|
if len(parts) == 1:
|
|
|
- internal_port, = parts
|
|
|
- return internal_port, None
|
|
|
- if len(parts) == 2:
|
|
|
- external_port, internal_port = parts
|
|
|
- return internal_port, external_port
|
|
|
+ source = parts[0]
|
|
|
+ mode = 'rw'
|
|
|
+ else:
|
|
|
+ source, mode = parts
|
|
|
|
|
|
- external_ip, external_port, internal_port = parts
|
|
|
- return internal_port, (external_ip, external_port or None)
|
|
|
+ return VolumeFromSpec(source, mode)
|
|
|
|
|
|
|
|
|
# Labels
|