| from __future__ import absolute_import |
| from __future__ import unicode_literals |
| |
| import logging |
| 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 LogConfig |
| from docker.utils.ports import build_port_bindings |
| from docker.utils.ports import split_port |
| |
| from . import __version__ |
| from . import progress_stream |
| from .config import DOCKER_CONFIG_KEYS |
| from .config import merge_environment |
| from .config.types import VolumeSpec |
| from .const import DEFAULT_TIMEOUT |
| 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 .errors import OperationFailedError |
| from .parallel import parallel_execute |
| from .parallel import parallel_start |
| from .progress_stream import stream_output |
| from .progress_stream import StreamOutputError |
| from .utils import json_hash |
| |
| |
| log = logging.getLogger(__name__) |
| |
| |
| DOCKER_START_KEYS = [ |
| 'cap_add', |
| 'cap_drop', |
| 'cgroup_parent', |
| 'cpu_quota', |
| 'devices', |
| 'dns', |
| 'dns_search', |
| 'env_file', |
| 'extra_hosts', |
| 'ipc', |
| 'read_only', |
| 'log_driver', |
| 'log_opt', |
| 'mem_limit', |
| 'memswap_limit', |
| 'pid', |
| 'privileged', |
| 'restart', |
| 'security_opt', |
| 'shm_size', |
| 'volumes_from', |
| ] |
| |
| |
| class BuildError(Exception): |
| def __init__(self, service, reason): |
| self.service = service |
| self.reason = reason |
| |
| |
| class NeedsBuildError(Exception): |
| def __init__(self, service): |
| self.service = service |
| |
| |
| class NoSuchImageError(Exception): |
| pass |
| |
| |
| ServiceName = namedtuple('ServiceName', 'project service number') |
| |
| |
| ConvergencePlan = namedtuple('ConvergencePlan', 'action containers') |
| |
| |
| @enum.unique |
| 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 |
| |
| |
| @enum.unique |
| class ImageType(enum.Enum): |
| """Enumeration for the types of images known to compose.""" |
| none = 0 |
| local = 1 |
| all = 2 |
| |
| |
| @enum.unique |
| class BuildAction(enum.Enum): |
| """Enumeration for the possible build actions.""" |
| none = 0 |
| force = 1 |
| skip = 2 |
| |
| |
| class Service(object): |
| def __init__( |
| self, |
| name, |
| client=None, |
| project='default', |
| use_networking=False, |
| links=None, |
| volumes_from=None, |
| network_mode=None, |
| networks=None, |
| **options |
| ): |
| 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.network_mode = network_mode or NetworkMode(None) |
| self.networks = networks or {} |
| self.options = options |
| |
| def __repr__(self): |
| return '<Service: {}>'.format(self.name) |
| |
| def containers(self, stopped=False, one_off=False, filters={}): |
| filters.update({'label': self.labels(one_off=one_off)}) |
| |
| return list(filter(None, [ |
| Container.from_ps(self.client, container) |
| for container in self.client.containers( |
| all=stopped, |
| filters=filters)])) |
| |
| def get_container(self, number=1): |
| """Return a :class:`compose.container.Container` for this service. The |
| container must be active, and match `number`. |
| """ |
| labels = self.labels() + ['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)] |
| for container in self.client.containers(filters={'label': labels}): |
| return Container.from_ps(self.client, container) |
| |
| raise ValueError("No container found for %s_%s" % (self.name, number)) |
| |
| def start(self, **options): |
| containers = self.containers(stopped=True) |
| for c in containers: |
| self.start_container_if_stopped(c, **options) |
| return containers |
| |
| def scale(self, desired_num, timeout=DEFAULT_TIMEOUT): |
| """ |
| Adjusts the number of containers to the specified number and ensures |
| they are running. |
| |
| - creates containers until there are at least `desired_num` |
| - stops containers until there are at most `desired_num` running |
| - starts containers until there are at least `desired_num` running |
| - removes all stopped containers |
| """ |
| if self.custom_container_name and desired_num > 1: |
| log.warn('The "%s" service is using the custom container name "%s". ' |
| 'Docker requires each container to have a unique name. ' |
| 'Remove the custom name to scale the service.' |
| % (self.name, self.custom_container_name)) |
| |
| if self.specifies_host_port() and desired_num > 1: |
| log.warn('The "%s" service specifies a port on the host. If multiple containers ' |
| 'for this service are created on a single host, the port will clash.' |
| % self.name) |
| |
| def create_and_start(service, number): |
| container = service.create_container(number=number, quiet=True) |
| service.start_container(container) |
| return container |
| |
| def stop_and_remove(container): |
| container.stop(timeout=timeout) |
| container.remove() |
| |
| running_containers = self.containers(stopped=False) |
| num_running = len(running_containers) |
| |
| if desired_num == num_running: |
| # do nothing as we already have the desired number |
| log.info('Desired container number already achieved') |
| return |
| |
| if desired_num > num_running: |
| # we need to start/create until we have desired_num |
| all_containers = self.containers(stopped=True) |
| |
| if num_running != len(all_containers): |
| # we have some stopped containers, let's start them up again |
| stopped_containers = sorted( |
| (c for c in all_containers if not c.is_running), |
| key=attrgetter('number')) |
| |
| num_stopped = len(stopped_containers) |
| |
| if num_stopped + num_running > desired_num: |
| num_to_start = desired_num - num_running |
| containers_to_start = stopped_containers[:num_to_start] |
| else: |
| containers_to_start = stopped_containers |
| |
| parallel_start(containers_to_start, {}) |
| |
| num_running += len(containers_to_start) |
| |
| num_to_create = desired_num - num_running |
| next_number = self._next_container_number() |
| container_numbers = [ |
| number for number in range( |
| next_number, next_number + num_to_create |
| ) |
| ] |
| |
| parallel_execute( |
| container_numbers, |
| lambda n: create_and_start(service=self, number=n), |
| lambda n: self.get_container_name(n), |
| "Creating and starting" |
| ) |
| |
| if desired_num < num_running: |
| num_to_stop = num_running - desired_num |
| |
| sorted_running_containers = sorted( |
| running_containers, |
| key=attrgetter('number')) |
| |
| parallel_execute( |
| sorted_running_containers[-num_to_stop:], |
| stop_and_remove, |
| lambda c: c.name, |
| "Stopping and removing", |
| ) |
| |
| def create_container(self, |
| one_off=False, |
| previous_container=None, |
| number=None, |
| quiet=False, |
| **override_options): |
| """ |
| Create a container for this service. If the image doesn't exist, attempt to pull |
| it. |
| """ |
| # This is only necessary for `scale` and `volumes_from` |
| # auto-creating containers to satisfy the dependency. |
| self.ensure_image_exists() |
| |
| container_options = self._get_container_create_options( |
| override_options, |
| number or self._next_container_number(one_off=one_off), |
| one_off=one_off, |
| previous_container=previous_container, |
| ) |
| |
| if 'name' in container_options and not quiet: |
| log.info("Creating %s" % container_options['name']) |
| |
| try: |
| return Container.create(self.client, **container_options) |
| except APIError as ex: |
| raise OperationFailedError("Cannot create container for service %s: %s" % |
| (self.name, ex.explanation)) |
| |
| def ensure_image_exists(self, do_build=BuildAction.none): |
| if self.can_be_built() and do_build == BuildAction.force: |
| self.build() |
| return |
| |
| try: |
| self.image() |
| return |
| except NoSuchImageError: |
| pass |
| |
| if not self.can_be_built(): |
| self.pull() |
| return |
| |
| if do_build == BuildAction.skip: |
| raise NeedsBuildError(self) |
| |
| self.build() |
| log.warn( |
| "Image for service {} was built because it did not already exist. To " |
| "rebuild this image you must use `docker-compose build` or " |
| "`docker-compose up --build`.".format(self.name)) |
| |
| def image(self): |
| try: |
| return self.client.inspect_image(self.image_name) |
| except APIError as e: |
| if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation): |
| raise NoSuchImageError("Image '{}' not found".format(self.image_name)) |
| else: |
| raise |
| |
| @property |
| def image_name(self): |
| return self.options.get('image', '{s.project}_{s.name}'.format(s=self)) |
| |
| def convergence_plan(self, strategy=ConvergenceStrategy.changed): |
| containers = self.containers(stopped=True) |
| |
| if not containers: |
| return ConvergencePlan('create', []) |
| |
| if strategy is ConvergenceStrategy.never: |
| return ConvergencePlan('start', 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] |
| |
| if stopped: |
| return ConvergencePlan('start', stopped) |
| |
| return ConvergencePlan('noop', containers) |
| |
| def _containers_have_diverged(self, containers): |
| config_hash = None |
| |
| try: |
| config_hash = self.config_hash |
| except NoSuchImageError as e: |
| log.debug( |
| 'Service %s has diverged: %s', |
| self.name, six.text_type(e), |
| ) |
| return True |
| |
| has_diverged = False |
| |
| for c in containers: |
| container_config_hash = c.labels.get(LABEL_CONFIG_HASH, None) |
| if container_config_hash != config_hash: |
| log.debug( |
| '%s has diverged: %s != %s', |
| c.name, container_config_hash, config_hash, |
| ) |
| has_diverged = True |
| |
| return has_diverged |
| |
| def execute_convergence_plan(self, |
| plan, |
| timeout=DEFAULT_TIMEOUT, |
| detached=False, |
| start=True): |
| (action, containers) = plan |
| should_attach_logs = not detached |
| |
| if action == 'create': |
| container = self.create_container() |
| |
| if should_attach_logs: |
| container.attach_log_stream() |
| |
| if start: |
| self.start_container(container) |
| |
| return [container] |
| |
| elif action == 'recreate': |
| return [ |
| self.recreate_container( |
| container, |
| timeout=timeout, |
| attach_logs=should_attach_logs, |
| start_new_container=start |
| ) |
| for container in containers |
| ] |
| |
| elif action == 'start': |
| if start: |
| for container in containers: |
| self.start_container_if_stopped(container, attach_logs=should_attach_logs) |
| |
| return containers |
| |
| elif action == 'noop': |
| for c in containers: |
| log.info("%s is up-to-date" % c.name) |
| |
| return containers |
| |
| else: |
| raise Exception("Invalid action: {}".format(action)) |
| |
| def recreate_container( |
| self, |
| container, |
| timeout=DEFAULT_TIMEOUT, |
| attach_logs=False, |
| start_new_container=True): |
| """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) |
| |
| container.stop(timeout=timeout) |
| container.rename_to_tmp_name() |
| new_container = self.create_container( |
| previous_container=container, |
| number=container.labels.get(LABEL_CONTAINER_NUMBER), |
| quiet=True, |
| ) |
| if attach_logs: |
| new_container.attach_log_stream() |
| if start_new_container: |
| self.start_container(new_container) |
| container.remove() |
| return new_container |
| |
| def start_container_if_stopped(self, container, attach_logs=False, quiet=False): |
| if not container.is_running: |
| if not quiet: |
| log.info("Starting %s" % container.name) |
| if attach_logs: |
| container.attach_log_stream() |
| return self.start_container(container) |
| |
| def start_container(self, container): |
| self.connect_container_to_networks(container) |
| try: |
| container.start() |
| except APIError as ex: |
| raise OperationFailedError("Cannot start service %s: %s" % (self.name, ex.explanation)) |
| return container |
| |
| def connect_container_to_networks(self, container): |
| connected_networks = container.get('NetworkSettings.Networks') |
| |
| for network, netdefs in self.networks.items(): |
| if network in connected_networks: |
| if short_id_alias_exists(container, network): |
| continue |
| |
| self.client.disconnect_container_from_network( |
| container.id, |
| network) |
| |
| self.client.connect_container_to_network( |
| container.id, network, |
| aliases=self._get_aliases(netdefs, container), |
| ipv4_address=netdefs.get('ipv4_address', None), |
| ipv6_address=netdefs.get('ipv6_address', None), |
| links=self._get_links(False)) |
| |
| def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT): |
| for c in self.duplicate_containers(): |
| log.info('Removing %s' % c.name) |
| c.stop(timeout=timeout) |
| c.remove() |
| |
| def duplicate_containers(self): |
| containers = sorted( |
| self.containers(stopped=True), |
| key=lambda c: c.get('Created'), |
| ) |
| |
| numbers = set() |
| |
| for c in containers: |
| if c.number in numbers: |
| yield c |
| else: |
| numbers.add(c.number) |
| |
| @property |
| def config_hash(self): |
| return json_hash(self.config_dict()) |
| |
| def config_dict(self): |
| return { |
| 'options': self.options, |
| 'image_id': self.image()['Id'], |
| 'links': self.get_link_names(), |
| 'net': self.network_mode.id, |
| 'networks': self.networks, |
| 'volumes_from': [ |
| (v.source.name, v.mode) |
| for v in self.volumes_from if isinstance(v.source, Service) |
| ], |
| } |
| |
| def get_dependency_names(self): |
| net_name = self.network_mode.service_name |
| return (self.get_linked_service_names() + |
| self.get_volumes_from_names() + |
| ([net_name] if net_name else []) + |
| self.options.get('depends_on', [])) |
| |
| def get_linked_service_names(self): |
| return [service.name for (service, _) in self.links] |
| |
| def get_link_names(self): |
| return [(service.name, alias) for service, alias in self.links] |
| |
| def get_volumes_from_names(self): |
| return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)] |
| |
| # TODO: this would benefit from github.com/docker/docker/pull/14699 |
| # to remove the need to inspect every container |
| def _next_container_number(self, one_off=False): |
| containers = filter(None, [ |
| Container.from_ps(self.client, container) |
| for container in self.client.containers( |
| all=True, |
| filters={'label': self.labels(one_off=one_off)}) |
| ]) |
| numbers = [c.number for c in containers] |
| return 1 if not numbers else max(numbers) + 1 |
| |
| def _get_aliases(self, network, container=None): |
| if container and container.labels.get(LABEL_ONE_OFF) == "True": |
| return [] |
| |
| return list( |
| {self.name} | |
| ({container.short_id} if container else set()) | |
| set(network.get('aliases', ())) |
| ) |
| |
| def build_default_networking_config(self): |
| if not self.networks: |
| return {} |
| |
| network = self.networks[self.network_mode.id] |
| endpoint = { |
| 'Aliases': self._get_aliases(network), |
| 'IPAMConfig': {}, |
| } |
| |
| if network.get('ipv4_address'): |
| endpoint['IPAMConfig']['IPv4Address'] = network.get('ipv4_address') |
| if network.get('ipv6_address'): |
| endpoint['IPAMConfig']['IPv6Address'] = network.get('ipv6_address') |
| |
| return {"EndpointsConfig": {self.network_mode.id: endpoint}} |
| |
| def _get_links(self, link_to_self): |
| links = {} |
| |
| for service, link_name in self.links: |
| for container in service.containers(): |
| links[link_name or service.name] = container.name |
| links[container.name] = container.name |
| links[container.name_without_project] = container.name |
| |
| if link_to_self: |
| for container in self.containers(): |
| links[self.name] = container.name |
| links[container.name] = container.name |
| links[container.name_without_project] = container.name |
| |
| for external_link in self.options.get('external_links') or []: |
| if ':' not in external_link: |
| link_name = external_link |
| else: |
| external_link, link_name = external_link.split(':') |
| links[link_name] = external_link |
| |
| return [ |
| (alias, container_name) |
| for (container_name, alias) in links.items() |
| ] |
| |
| def _get_volumes_from(self): |
| return [build_volume_from(spec) for spec in self.volumes_from] |
| |
| def _get_container_create_options( |
| self, |
| override_options, |
| number, |
| one_off=False, |
| previous_container=None): |
| add_config_hash = (not one_off and not override_options) |
| |
| container_options = dict( |
| (k, self.options[k]) |
| for k in DOCKER_CONFIG_KEYS if k in self.options) |
| container_options.update(override_options) |
| |
| if not container_options.get('name'): |
| container_options['name'] = self.get_container_name(number, one_off) |
| |
| container_options.setdefault('detach', True) |
| |
| # If a qualified hostname was given, split it into an |
| # unqualified hostname and a domainname unless domainname |
| # was also given explicitly. This matches the behavior of |
| # the official Docker CLI in that scenario. |
| if ('hostname' in container_options and |
| 'domainname' not in container_options and |
| '.' in container_options['hostname']): |
| parts = container_options['hostname'].partition('.') |
| container_options['hostname'] = parts[0] |
| container_options['domainname'] = parts[2] |
| |
| if 'ports' in container_options or 'expose' in self.options: |
| container_options['ports'] = build_container_ports( |
| container_options, |
| self.options) |
| |
| container_options['environment'] = merge_environment( |
| self.options.get('environment'), |
| override_options.get('environment')) |
| |
| binds, affinity = merge_volume_bindings( |
| container_options.get('volumes') or [], |
| previous_container) |
| override_options['binds'] = binds |
| container_options['environment'].update(affinity) |
| |
| if 'volumes' in container_options: |
| container_options['volumes'] = dict( |
| (v.internal, {}) for v in container_options['volumes']) |
| |
| container_options['image'] = self.image_name |
| |
| container_options['labels'] = build_container_labels( |
| container_options.get('labels', {}), |
| self.labels(one_off=one_off), |
| number, |
| self.config_hash if add_config_hash else None) |
| |
| # Delete options which are only used when starting |
| for key in DOCKER_START_KEYS: |
| container_options.pop(key, None) |
| |
| container_options['host_config'] = self._get_container_host_config( |
| override_options, |
| one_off=one_off) |
| |
| networking_config = self.build_default_networking_config() |
| if networking_config: |
| container_options['networking_config'] = networking_config |
| |
| container_options['environment'] = format_environment( |
| container_options['environment']) |
| return container_options |
| |
| def _get_container_host_config(self, override_options, one_off=False): |
| options = dict(self.options, **override_options) |
| |
| logging_dict = options.get('logging', None) |
| log_config = get_log_config(logging_dict) |
| |
| return self.client.create_host_config( |
| links=self._get_links(link_to_self=one_off), |
| port_bindings=build_port_bindings(options.get('ports') or []), |
| binds=options.get('binds'), |
| volumes_from=self._get_volumes_from(), |
| privileged=options.get('privileged', False), |
| network_mode=self.network_mode.mode, |
| devices=options.get('devices'), |
| dns=options.get('dns'), |
| dns_search=options.get('dns_search'), |
| restart_policy=options.get('restart'), |
| cap_add=options.get('cap_add'), |
| cap_drop=options.get('cap_drop'), |
| mem_limit=options.get('mem_limit'), |
| memswap_limit=options.get('memswap_limit'), |
| ulimits=build_ulimits(options.get('ulimits')), |
| log_config=log_config, |
| extra_hosts=options.get('extra_hosts'), |
| read_only=options.get('read_only'), |
| pid_mode=options.get('pid'), |
| security_opt=options.get('security_opt'), |
| ipc_mode=options.get('ipc'), |
| cgroup_parent=options.get('cgroup_parent'), |
| cpu_quota=options.get('cpu_quota'), |
| shm_size=options.get('shm_size'), |
| tmpfs=options.get('tmpfs'), |
| ) |
| |
| def build(self, no_cache=False, pull=False, force_rm=False): |
| log.info('Building %s' % self.name) |
| |
| build_opts = self.options.get('build', {}) |
| path = build_opts.get('context') |
| # 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, |
| forcerm=force_rm, |
| pull=pull, |
| nocache=no_cache, |
| dockerfile=build_opts.get('dockerfile', None), |
| buildargs=build_opts.get('args', None), |
| ) |
| |
| try: |
| all_events = stream_output(build_output, sys.stdout) |
| except StreamOutputError as 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 |
| # complain about it |
| self.client.close() |
| |
| image_id = None |
| |
| for event in all_events: |
| if 'stream' in event: |
| match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', '')) |
| if match: |
| image_id = match.group(1) |
| |
| if image_id is None: |
| raise BuildError(self, event if all_events else 'Unknown') |
| |
| return image_id |
| |
| def can_be_built(self): |
| return 'build' in self.options |
| |
| def labels(self, one_off=False): |
| return [ |
| '{0}={1}'.format(LABEL_PROJECT, self.project), |
| '{0}={1}'.format(LABEL_SERVICE, self.name), |
| '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False") |
| ] |
| |
| @property |
| def custom_container_name(self): |
| return self.options.get('container_name') |
| |
| def get_container_name(self, number, one_off=False): |
| if self.custom_container_name and not one_off: |
| return self.custom_container_name |
| |
| return build_container_name(self.project, self.name, number, one_off) |
| |
| def remove_image(self, image_type): |
| if not image_type or image_type == ImageType.none: |
| return False |
| if image_type == ImageType.local and self.options.get('image'): |
| return False |
| |
| log.info("Removing image %s", self.image_name) |
| try: |
| self.client.remove_image(self.image_name) |
| return True |
| except APIError as e: |
| log.error("Failed to remove image for service %s: %s", self.name, e) |
| return False |
| |
| def specifies_host_port(self): |
| def has_host_port(binding): |
| _, external_bindings = split_port(binding) |
| |
| # there are no external bindings |
| if external_bindings is None: |
| return False |
| |
| # we only need to check the first binding from the range |
| external_binding = external_bindings[0] |
| |
| # non-tuple binding means there is a host port specified |
| if not isinstance(external_binding, tuple): |
| return True |
| |
| # extract actual host port from tuple of (host_ip, host_port) |
| _, host_port = external_binding |
| if host_port is not None: |
| return True |
| |
| return False |
| |
| return any(has_host_port(binding) for binding in self.options.get('ports', [])) |
| |
| def pull(self, ignore_pull_failures=False): |
| if 'image' not in self.options: |
| return |
| |
| repo, tag, separator = parse_repository_tag(self.options['image']) |
| tag = tag or 'latest' |
| log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag)) |
| output = self.client.pull(repo, tag=tag, stream=True) |
| |
| try: |
| return progress_stream.get_digest_from_pull( |
| stream_output(output, sys.stdout)) |
| except StreamOutputError as e: |
| if not ignore_pull_failures: |
| raise |
| else: |
| log.error(six.text_type(e)) |
| |
| def push(self, ignore_push_failures=False): |
| if 'image' not in self.options or 'build' not in self.options: |
| return |
| |
| repo, tag, separator = parse_repository_tag(self.options['image']) |
| tag = tag or 'latest' |
| log.info('Pushing %s (%s%s%s)...' % (self.name, repo, separator, tag)) |
| output = self.client.push(repo, tag=tag, stream=True) |
| |
| try: |
| return progress_stream.get_digest_from_push( |
| stream_output(output, sys.stdout)) |
| except StreamOutputError as e: |
| if not ignore_push_failures: |
| raise |
| else: |
| log.error(six.text_type(e)) |
| |
| |
| def short_id_alias_exists(container, network): |
| aliases = container.get( |
| 'NetworkSettings.Networks.{net}.Aliases'.format(net=network)) or () |
| return container.short_id in aliases |
| |
| |
| class NetworkMode(object): |
| """A `standard` network mode (ex: host, bridge)""" |
| |
| service_name = None |
| |
| def __init__(self, network_mode): |
| self.network_mode = network_mode |
| |
| @property |
| def id(self): |
| return self.network_mode |
| |
| mode = id |
| |
| |
| class ContainerNetworkMode(object): |
| """A network mode that uses a container's network stack.""" |
| |
| service_name = None |
| |
| def __init__(self, container): |
| self.container = container |
| |
| @property |
| def id(self): |
| return self.container.id |
| |
| @property |
| def mode(self): |
| return 'container:' + self.container.id |
| |
| |
| class ServiceNetworkMode(object): |
| """A network mode that uses a service's network stack.""" |
| |
| def __init__(self, service): |
| self.service = service |
| |
| @property |
| def id(self): |
| return self.service.name |
| |
| service_name = id |
| |
| @property |
| def mode(self): |
| containers = self.service.containers() |
| if containers: |
| return 'container:' + containers[0].id |
| |
| log.warn("Service %s is trying to use reuse the network stack " |
| "of another service that is not running." % (self.id)) |
| return None |
| |
| |
| # Names |
| |
| |
| def build_container_name(project, service, number, one_off=False): |
| bits = [project, service] |
| if one_off: |
| bits.append('run') |
| return '_'.join(bits + [str(number)]) |
| |
| |
| # Images |
| |
| def parse_repository_tag(repo_path): |
| """Splits image identification into base image path, tag/digest |
| and it's separator. |
| |
| 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 |
| |
| |
| def merge_volume_bindings(volumes, previous_container): |
| """Return a list of volume bindings for a container. Container data volumes |
| are replaced by those from the previous container. |
| """ |
| affinity = {} |
| |
| volume_bindings = dict( |
| build_volume_binding(volume) |
| for volume in volumes |
| if volume.external) |
| |
| if previous_container: |
| old_volumes = get_container_data_volumes(previous_container, volumes) |
| warn_on_masked_volume(volumes, old_volumes, previous_container.service) |
| volume_bindings.update( |
| build_volume_binding(volume) for volume in old_volumes) |
| |
| if old_volumes: |
| affinity = {'affinity:container': '=' + previous_container.id} |
| |
| return list(volume_bindings.values()), affinity |
| |
| |
| def get_container_data_volumes(container, volumes_option): |
| """Find the container data volumes that are in `volumes_option`, and return |
| a mapping of volume bindings for those volumes. |
| """ |
| volumes = [] |
| volumes_option = volumes_option or [] |
| |
| container_mounts = dict( |
| (mount['Destination'], mount) |
| for mount in container.get('Mounts') or {} |
| ) |
| |
| image_volumes = [ |
| VolumeSpec.parse(volume) |
| for volume in |
| container.image_config['ContainerConfig'].get('Volumes') or {} |
| ] |
| |
| for volume in set(volumes_option + image_volumes): |
| # No need to preserve host volumes |
| if volume.external: |
| continue |
| |
| mount = container_mounts.get(volume.internal) |
| |
| # New volume, doesn't exist in the old container |
| if not mount: |
| continue |
| |
| # Volume was previously a host volume, now it's a container volume |
| if not mount.get('Name'): |
| continue |
| |
| # Copy existing volume from old container |
| volume = volume._replace(external=mount['Name']) |
| volumes.append(volume) |
| |
| return volumes |
| |
| |
| def warn_on_masked_volume(volumes_option, container_volumes, service): |
| container_volumes = dict( |
| (volume.internal, volume.external) |
| for volume in container_volumes) |
| |
| for volume in volumes_option: |
| if ( |
| volume.external and |
| volume.internal in container_volumes and |
| container_volumes.get(volume.internal) != volume.external |
| ): |
| log.warn(( |
| "Service \"{service}\" is using volume \"{volume}\" from the " |
| "previous container. Host mapping \"{host_path}\" has no effect. " |
| "Remove the existing containers (with `docker-compose rm {service}`) " |
| "to use the host volume mapping." |
| ).format( |
| service=service, |
| volume=volume.internal, |
| host_path=volume.external)) |
| |
| |
| def build_volume_binding(volume_spec): |
| return volume_spec.internal, volume_spec.repr() |
| |
| |
| 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) |
| |
| 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) |
| |
| |
| # Labels |
| |
| |
| def build_container_labels(label_options, service_labels, number, config_hash): |
| labels = dict(label_options or {}) |
| labels.update(label.split('=', 1) for label in service_labels) |
| labels[LABEL_CONTAINER_NUMBER] = str(number) |
| labels[LABEL_VERSION] = __version__ |
| |
| if config_hash: |
| log.debug("Added config hash: %s" % config_hash) |
| labels[LABEL_CONFIG_HASH] = config_hash |
| |
| return labels |
| |
| |
| # Ulimits |
| |
| |
| def build_ulimits(ulimit_config): |
| if not ulimit_config: |
| return None |
| ulimits = [] |
| for limit_name, soft_hard_values in six.iteritems(ulimit_config): |
| if isinstance(soft_hard_values, six.integer_types): |
| ulimits.append({'name': limit_name, 'soft': soft_hard_values, 'hard': soft_hard_values}) |
| elif isinstance(soft_hard_values, dict): |
| ulimit_dict = {'name': limit_name} |
| ulimit_dict.update(soft_hard_values) |
| ulimits.append(ulimit_dict) |
| |
| return ulimits |
| |
| |
| def get_log_config(logging_dict): |
| log_driver = logging_dict.get('driver', "") if logging_dict else "" |
| log_options = logging_dict.get('options', None) if logging_dict else None |
| return LogConfig( |
| type=log_driver, |
| config=log_options |
| ) |
| |
| |
| # TODO: remove once fix is available in docker-py |
| def format_environment(environment): |
| def format_env(key, value): |
| if value is None: |
| return key |
| return '{key}={value}'.format(key=key, value=value) |
| return [format_env(*item) for item in environment.items()] |
| |
| # Ports |
| |
| |
| def build_container_ports(container_options, options): |
| ports = [] |
| all_ports = container_options.get('ports', []) + options.get('expose', []) |
| 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) |
| return ports |