diff --git a/compose/network.py b/compose/network.py index 1a080c40c..e0fdb3e27 100644 --- a/compose/network.py +++ b/compose/network.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import logging +import re from collections import OrderedDict from docker.errors import NotFound @@ -10,9 +11,11 @@ from docker.types import IPAMPool from docker.utils import version_gte from docker.utils import version_lt +from . import __version__ from .config import ConfigurationError from .const import LABEL_NETWORK from .const import LABEL_PROJECT +from .const import LABEL_VERSION log = logging.getLogger(__name__) @@ -39,6 +42,7 @@ class Network(object): self.enable_ipv6 = enable_ipv6 self.labels = labels self.custom_name = custom_name + self.legacy = False def ensure(self): if self.external: @@ -68,6 +72,14 @@ class Network(object): data = self.inspect() check_remote_network_config(data, self) except NotFound: + try: + data = self.inspect(legacy=True) + self.legacy = True + check_remote_network_config(data, self) + return + except NotFound: + pass + driver_name = 'the default driver' if self.driver: driver_name = 'driver "{}"'.format(self.driver) @@ -94,18 +106,37 @@ class Network(object): log.info("Network %s is external, skipping", self.full_name) return - log.info("Removing network {}".format(self.full_name)) - self.client.remove_network(self.full_name) + log.info("Removing network {}".format(self.true_name)) + try: + self.client.remove_network(self.full_name) + except NotFound: + self.client.remove_network(self.legacy_full_name) - def inspect(self): + def inspect(self, legacy=False): + if legacy: + return self.client.inspect_network(self.legacy_full_name) return self.client.inspect_network(self.full_name) + @property + def legacy_full_name(self): + if self.custom_name: + return self.name + return '{0}_{1}'.format( + re.sub(r'[_-]', '', self.project), self.name + ) + @property def full_name(self): if self.custom_name: return self.name return '{0}_{1}'.format(self.project, self.name) + @property + def true_name(self): + if self.legacy: + return self.legacy_full_name + return self.full_name + @property def _labels(self): if version_lt(self.client._version, '1.23'): @@ -114,6 +145,7 @@ class Network(object): labels.update({ LABEL_PROJECT: self.project, LABEL_NETWORK: self.name, + LABEL_VERSION: __version__, }) return labels @@ -150,49 +182,49 @@ def check_remote_ipam_config(remote, local): remote_ipam = remote.get('IPAM') ipam_dict = create_ipam_config_from_dict(local.ipam) if local.ipam.get('driver') and local.ipam.get('driver') != remote_ipam.get('Driver'): - raise NetworkConfigChangedError(local.full_name, 'IPAM driver') + raise NetworkConfigChangedError(local.true_name, 'IPAM driver') if len(ipam_dict['Config']) != 0: if len(ipam_dict['Config']) != len(remote_ipam['Config']): - raise NetworkConfigChangedError(local.full_name, 'IPAM configs') + raise NetworkConfigChangedError(local.true_name, 'IPAM configs') remote_configs = sorted(remote_ipam['Config'], key='Subnet') local_configs = sorted(ipam_dict['Config'], key='Subnet') while local_configs: lc = local_configs.pop() rc = remote_configs.pop() if lc.get('Subnet') != rc.get('Subnet'): - raise NetworkConfigChangedError(local.full_name, 'IPAM config subnet') + raise NetworkConfigChangedError(local.true_name, 'IPAM config subnet') if lc.get('Gateway') is not None and lc.get('Gateway') != rc.get('Gateway'): - raise NetworkConfigChangedError(local.full_name, 'IPAM config gateway') + raise NetworkConfigChangedError(local.true_name, 'IPAM config gateway') if lc.get('IPRange') != rc.get('IPRange'): - raise NetworkConfigChangedError(local.full_name, 'IPAM config ip_range') + raise NetworkConfigChangedError(local.true_name, 'IPAM config ip_range') if sorted(lc.get('AuxiliaryAddresses')) != sorted(rc.get('AuxiliaryAddresses')): - raise NetworkConfigChangedError(local.full_name, 'IPAM config aux_addresses') + raise NetworkConfigChangedError(local.true_name, 'IPAM config aux_addresses') remote_opts = remote_ipam.get('Options') or {} local_opts = local.ipam.get('options') or {} for k in set.union(set(remote_opts.keys()), set(local_opts.keys())): if remote_opts.get(k) != local_opts.get(k): - raise NetworkConfigChangedError(local.full_name, 'IPAM option "{}"'.format(k)) + raise NetworkConfigChangedError(local.true_name, 'IPAM option "{}"'.format(k)) def check_remote_network_config(remote, local): if local.driver and remote.get('Driver') != local.driver: - raise NetworkConfigChangedError(local.full_name, 'driver') + raise NetworkConfigChangedError(local.true_name, 'driver') local_opts = local.driver_opts or {} remote_opts = remote.get('Options') or {} for k in set.union(set(remote_opts.keys()), set(local_opts.keys())): if k in OPTS_EXCEPTIONS: continue if remote_opts.get(k) != local_opts.get(k): - raise NetworkConfigChangedError(local.full_name, 'option "{}"'.format(k)) + raise NetworkConfigChangedError(local.true_name, 'option "{}"'.format(k)) if local.ipam is not None: check_remote_ipam_config(remote, local) if local.internal is not None and local.internal != remote.get('Internal', False): - raise NetworkConfigChangedError(local.full_name, 'internal') + raise NetworkConfigChangedError(local.true_name, 'internal') if local.enable_ipv6 is not None and local.enable_ipv6 != remote.get('EnableIPv6', False): - raise NetworkConfigChangedError(local.full_name, 'enable_ipv6') + raise NetworkConfigChangedError(local.true_name, 'enable_ipv6') local_labels = local.labels or {} remote_labels = remote.get('Labels', {}) @@ -202,7 +234,7 @@ def check_remote_network_config(remote, local): if remote_labels.get(k) != local_labels.get(k): log.warn( 'Network {}: label "{}" has changed. It may need to be' - ' recreated.'.format(local.full_name, k) + ' recreated.'.format(local.true_name, k) ) @@ -257,7 +289,7 @@ class ProjectNetworks(object): try: network.remove() except NotFound: - log.warn("Network %s not found.", network.full_name) + log.warn("Network %s not found.", network.true_name) def initialize(self): if not self.use_networking: @@ -286,7 +318,7 @@ def get_networks(service_dict, network_definitions): for name, netdef in get_network_defs_for_service(service_dict).items(): network = network_definitions.get(name) if network: - networks[network.full_name] = netdef + networks[network.true_name] = netdef else: raise ConfigurationError( 'Service "{}" uses an undefined network "{}"' diff --git a/compose/service.py b/compose/service.py index bb9e26baa..0a866161c 100644 --- a/compose/service.py +++ b/compose/service.py @@ -51,6 +51,7 @@ from .progress_stream import StreamOutputError from .utils import json_hash from .utils import parse_bytes from .utils import parse_seconds_float +from .version import ComposeVersion log = logging.getLogger(__name__) @@ -192,11 +193,25 @@ class Service(object): def containers(self, stopped=False, one_off=False, filters={}): filters.update({'label': self.labels(one_off=one_off)}) - return list(filter(None, [ + result = list(filter(None, [ Container.from_ps(self.client, container) for container in self.client.containers( all=stopped, - filters=filters)])) + filters=filters)]) + ) + if result: + return result + + filters.update({'label': self.labels(one_off=one_off, legacy=True)}) + return list( + filter( + self.has_legacy_proj_name, 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 @@ -380,6 +395,10 @@ class Service(object): has_diverged = False for c in containers: + if self.has_legacy_proj_name(c): + log.debug('%s has diverged: Legacy project name' % c.name) + has_diverged = True + continue container_config_hash = c.labels.get(LABEL_CONFIG_HASH, None) if container_config_hash != config_hash: log.debug( @@ -1053,11 +1072,12 @@ class Service(object): def can_be_built(self): return 'build' in self.options - def labels(self, one_off=False): + def labels(self, one_off=False, legacy=False): + proj_name = self.project if not legacy else re.sub(r'[_-]', '', self.project) return [ - '{0}={1}'.format(LABEL_PROJECT, self.project), + '{0}={1}'.format(LABEL_PROJECT, proj_name), '{0}={1}'.format(LABEL_SERVICE, self.name), - '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False") + '{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"), ] @property @@ -1214,6 +1234,12 @@ class Service(object): return result + def has_legacy_proj_name(self, ctnr): + return ( + ComposeVersion(ctnr.labels.get(LABEL_VERSION)) < ComposeVersion('1.21.0') and + ctnr.project != self.project + ) + def short_id_alias_exists(container, network): aliases = container.get( diff --git a/compose/volume.py b/compose/volume.py index 6bf184045..6cad1e0de 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -2,15 +2,19 @@ from __future__ import absolute_import from __future__ import unicode_literals import logging +import re from docker.errors import NotFound from docker.utils import version_lt +from . import __version__ from .config import ConfigurationError from .config.types import VolumeSpec from .const import LABEL_PROJECT +from .const import LABEL_VERSION from .const import LABEL_VOLUME + log = logging.getLogger(__name__) @@ -25,6 +29,7 @@ class Volume(object): self.external = external self.labels = labels self.custom_name = custom_name + self.legacy = False def create(self): return self.client.create_volume( @@ -36,15 +41,26 @@ class Volume(object): log.info("Volume %s is external, skipping", self.full_name) return log.info("Removing volume %s", self.full_name) - return self.client.remove_volume(self.full_name) + try: + return self.client.remove_volume(self.full_name) + except NotFound: + self.client.remove_volume(self.legacy_full_name) - def inspect(self): + def inspect(self, legacy=False): + if legacy: + return self.client.inspect_volume(self.legacy_full_name) return self.client.inspect_volume(self.full_name) def exists(self): try: self.inspect() except NotFound: + try: + self.inspect(legacy=True) + self.legacy = True + return True + except NotFound: + pass return False return True @@ -54,6 +70,20 @@ class Volume(object): return self.name return '{0}_{1}'.format(self.project, self.name) + @property + def legacy_full_name(self): + if self.custom_name: + return self.name + return '{0}_{1}'.format( + re.sub(r'[_-]', '', self.project), self.name + ) + + @property + def true_name(self): + if self.legacy: + return self.legacy_full_name + return self.full_name + @property def _labels(self): if version_lt(self.client._version, '1.23'): @@ -62,6 +92,7 @@ class Volume(object): labels.update({ LABEL_PROJECT: self.project, LABEL_VOLUME: self.name, + LABEL_VERSION: __version__, }) return labels @@ -94,7 +125,7 @@ class ProjectVolumes(object): try: volume.remove() except NotFound: - log.warn("Volume %s not found.", volume.full_name) + log.warn("Volume %s not found.", volume.true_name) def initialize(self): try: @@ -136,9 +167,9 @@ class ProjectVolumes(object): if isinstance(volume_spec, VolumeSpec): volume = self.volumes[volume_spec.external] - return volume_spec._replace(external=volume.full_name) + return volume_spec._replace(external=volume.true_name) else: - volume_spec.source = self.volumes[volume_spec.source].full_name + volume_spec.source = self.volumes[volume_spec.source].true_name return volume_spec @@ -152,7 +183,7 @@ class VolumeConfigChangedError(ConfigurationError): 'first:\n$ docker volume rm {full_name}'.format( vol_name=local.name, property_name=property_name, local_value=local_value, remote_value=remote_value, - full_name=local.full_name + full_name=local.true_name ) )