Bläddra i källkod

Merge pull request #5896 from docker/5874-legacy-proj-name

Retrieve objects using legacy (< 1.21) project names
Joffrey F 7 år sedan
förälder
incheckning
faa532c315
5 ändrade filer med 131 tillägg och 36 borttagningar
  1. 52 21
      compose/network.py
  2. 31 5
      compose/service.py
  3. 43 10
      compose/volume.py
  4. 3 0
      tests/unit/network_test.py
  5. 2 0
      tests/unit/project_test.py

+ 52 - 21
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 = None
 
     def ensure(self):
         if self.external:
@@ -64,8 +68,9 @@ class Network(object):
                 )
             return
 
+        self._set_legacy_flag()
         try:
-            data = self.inspect()
+            data = self.inspect(legacy=self.legacy)
             check_remote_network_config(data, self)
         except NotFound:
             driver_name = 'the default driver'
@@ -73,8 +78,7 @@ class Network(object):
                 driver_name = 'driver "{}"'.format(self.driver)
 
             log.info(
-                'Creating network "{}" with {}'
-                .format(self.full_name, driver_name)
+                'Creating network "{}" with {}'.format(self.full_name, driver_name)
             )
 
             self.client.create_network(
@@ -91,21 +95,38 @@ class Network(object):
 
     def remove(self):
         if self.external:
-            log.info("Network %s is external, skipping", self.full_name)
+            log.info("Network %s is external, skipping", self.true_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))
+        self.client.remove_network(self.true_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):
+        self._set_legacy_flag()
+        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,9 +135,19 @@ class Network(object):
         labels.update({
             LABEL_PROJECT: self.project,
             LABEL_NETWORK: self.name,
+            LABEL_VERSION: __version__,
         })
         return labels
 
+    def _set_legacy_flag(self):
+        if self.legacy is not None:
+            return
+        try:
+            data = self.inspect(legacy=True)
+            self.legacy = data is not None
+        except NotFound:
+            self.legacy = False
+
 
 def create_ipam_config_from_dict(ipam_dict):
     if not ipam_dict:
@@ -150,49 +181,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 +233,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 +288,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 +317,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 "{}"'

+ 31 - 5
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(

+ 43 - 10
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 = None
 
     def create(self):
         return self.client.create_volume(
@@ -33,17 +38,20 @@ class Volume(object):
 
     def remove(self):
         if self.external:
-            log.info("Volume %s is external, skipping", self.full_name)
+            log.info("Volume %s is external, skipping", self.true_name)
             return
-        log.info("Removing volume %s", self.full_name)
-        return self.client.remove_volume(self.full_name)
+        log.info("Removing volume %s", self.true_name)
+        return self.client.remove_volume(self.true_name)
 
-    def inspect(self):
+    def inspect(self, legacy=None):
+        if legacy:
+            return self.client.inspect_volume(self.legacy_full_name)
         return self.client.inspect_volume(self.full_name)
 
     def exists(self):
+        self._set_legacy_flag()
         try:
-            self.inspect()
+            self.inspect(legacy=self.legacy)
         except NotFound:
             return False
         return True
@@ -54,6 +62,21 @@ 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):
+        self._set_legacy_flag()
+        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,9 +85,19 @@ class Volume(object):
         labels.update({
             LABEL_PROJECT: self.project,
             LABEL_VOLUME: self.name,
+            LABEL_VERSION: __version__,
         })
         return labels
 
+    def _set_legacy_flag(self):
+        if self.legacy is not None:
+            return
+        try:
+            data = self.inspect(legacy=True)
+            self.legacy = data is not None
+        except NotFound:
+            self.legacy = False
+
 
 class ProjectVolumes(object):
 
@@ -94,7 +127,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:
@@ -124,7 +157,7 @@ class ProjectVolumes(object):
                     )
                     volume.create()
                 else:
-                    check_remote_volume_config(volume.inspect(), volume)
+                    check_remote_volume_config(volume.inspect(legacy=volume.legacy), volume)
         except NotFound:
             raise ConfigurationError(
                 'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver)
@@ -136,9 +169,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 +185,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
             )
         )
 

+ 3 - 0
tests/unit/network_test.py

@@ -84,6 +84,7 @@ class NetworkTest(unittest.TestCase):
             {'Driver': 'overlay', 'Options': remote_options}, net
         )
 
+    @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
     def test_check_remote_network_config_driver_mismatch(self):
         net = Network(None, 'compose_test', 'net1', 'overlay')
         with pytest.raises(NetworkConfigChangedError) as e:
@@ -93,6 +94,7 @@ class NetworkTest(unittest.TestCase):
 
         assert 'driver has changed' in str(e.value)
 
+    @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
     def test_check_remote_network_config_options_mismatch(self):
         net = Network(None, 'compose_test', 'net1', 'overlay')
         with pytest.raises(NetworkConfigChangedError) as e:
@@ -146,6 +148,7 @@ class NetworkTest(unittest.TestCase):
             net
         )
 
+    @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
     def test_check_remote_network_labels_mismatch(self):
         net = Network(None, 'compose_test', 'net1', 'overlay', labels={
             'com.project.touhou.character': 'sakuya.izayoi'

+ 2 - 0
tests/unit/project_test.py

@@ -60,6 +60,7 @@ class ProjectTest(unittest.TestCase):
         assert project.get_service('db').options['image'] == 'busybox:latest'
         assert not project.networks.use_networking
 
+    @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
     def test_from_config_v2(self):
         config = Config(
             version=V2_0,
@@ -217,6 +218,7 @@ class ProjectTest(unittest.TestCase):
         )
         assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
 
+    @mock.patch('compose.network.Network.true_name', lambda n: n.full_name)
     def test_use_volumes_from_service_container(self):
         container_ids = ['aabbccddee', '12345']