Sfoglia il codice sorgente

Add migration warning and option to migrate to labels.

Signed-off-by: Daniel Nephin <[email protected]>
Daniel Nephin 10 anni fa
parent
commit
62059d55e6

+ 19 - 14
compose/cli/main.py

@@ -11,6 +11,7 @@ from docker.errors import APIError
 import dockerpty
 
 from .. import __version__
+from .. import migration
 from ..project import NoSuchService, ConfigurationError
 from ..service import BuildError, CannotBeScaledError
 from ..config import parse_environment
@@ -81,20 +82,21 @@ class TopLevelCommand(Command):
       -v, --version             Print version and exit
 
     Commands:
-      build     Build or rebuild services
-      help      Get help on a command
-      kill      Kill containers
-      logs      View output from containers
-      port      Print the public port for a port binding
-      ps        List containers
-      pull      Pulls service images
-      restart   Restart services
-      rm        Remove stopped containers
-      run       Run a one-off command
-      scale     Set number of containers for a service
-      start     Start services
-      stop      Stop services
-      up        Create and start containers
+      build              Build or rebuild services
+      help               Get help on a command
+      kill               Kill containers
+      logs               View output from containers
+      port               Print the public port for a port binding
+      ps                 List containers
+      pull               Pulls service images
+      restart            Restart services
+      rm                 Remove stopped containers
+      run                Run a one-off command
+      scale              Set number of containers for a service
+      start              Start services
+      stop               Stop services
+      up                 Create and start containers
+      migrate_to_labels  Recreate containers to add labels
 
     """
     def docopt_options(self):
@@ -483,6 +485,9 @@ class TopLevelCommand(Command):
                 params = {} if timeout is None else {'timeout': int(timeout)}
                 project.stop(service_names=service_names, **params)
 
+    def migrate_to_labels(self, project, _options):
+        migration.migrate_project_to_labels(project)
+
 
 def list_containers(containers):
     return ", ".join(c.name for c in containers)

+ 5 - 1
compose/container.py

@@ -64,7 +64,11 @@ class Container(object):
 
     @property
     def number(self):
-        return int(self.labels.get(LABEL_CONTAINER_NUMBER) or 0)
+        number = self.labels.get(LABEL_CONTAINER_NUMBER)
+        if not number:
+            raise ValueError("Container {0} does not have a {1} label".format(
+                self.short_id, LABEL_CONTAINER_NUMBER))
+        return int(number)
 
     @property
     def ports(self):

+ 35 - 0
compose/migration.py

@@ -0,0 +1,35 @@
+import logging
+import re
+
+from .container import get_container_name, Container
+
+
+log = logging.getLogger(__name__)
+
+
+# TODO: remove this section when migrate_project_to_labels is removed
+NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
+
+
+def is_valid_name(name):
+    match = NAME_RE.match(name)
+    return match is not None
+
+
+def add_labels(project, container, name):
+    project_name, service_name, one_off, number = NAME_RE.match(name).groups()
+    if project_name != project.name or service_name not in project.service_names:
+        return
+    service = project.get_service(service_name)
+    service.recreate_container(container)
+
+
+def migrate_project_to_labels(project):
+    log.info("Running migration to labels for project %s", project.name)
+
+    client = project.client
+    for container in client.containers(all=True):
+        name = get_container_name(container)
+        if not is_valid_name(name):
+            continue
+        add_labels(project, Container.from_ps(client, container), name)

+ 24 - 8
compose/project.py

@@ -1,13 +1,14 @@
 from __future__ import unicode_literals
 from __future__ import absolute_import
 import logging
-
 from functools import reduce
+
+from docker.errors import APIError
+
 from .config import get_service_name_from_net, ConfigurationError
 from .const import LABEL_PROJECT, LABEL_ONE_OFF
-from .service import Service
+from .service import Service, check_for_legacy_containers
 from .container import Container
-from docker.errors import APIError
 
 log = logging.getLogger(__name__)
 
@@ -82,6 +83,10 @@ class Project(object):
                                             volumes_from=volumes_from, **service_dict))
         return project
 
+    @property
+    def service_names(self):
+        return [service.name for service in self.services]
+
     def get_service(self, name):
         """
         Retrieve a service by name. Raises NoSuchService
@@ -109,7 +114,7 @@ class Project(object):
         """
         if service_names is None or len(service_names) == 0:
             return self.get_services(
-                service_names=[s.name for s in self.services],
+                service_names=self.service_names,
                 include_deps=include_deps
             )
         else:
@@ -230,10 +235,21 @@ class Project(object):
             service.remove_stopped(**options)
 
     def containers(self, service_names=None, stopped=False, one_off=False):
-        return [Container.from_ps(self.client, container)
-                for container in self.client.containers(
-                    all=stopped,
-                    filters={'label': self.labels(one_off=one_off)})]
+        containers = [
+            Container.from_ps(self.client, container)
+            for container in self.client.containers(
+                all=stopped,
+                filters={'label': self.labels(one_off=one_off)})]
+
+        if not containers:
+            check_for_legacy_containers(
+                self.client,
+                self.name,
+                self.service_names,
+                stopped=stopped,
+                one_off=one_off)
+
+        return containers
 
     def _inject_deps(self, acc, service):
         net_name = service.get_net_name()

+ 41 - 5
compose/service.py

@@ -19,7 +19,7 @@ from .const import (
     LABEL_SERVICE,
     LABEL_VERSION,
 )
-from .container import Container
+from .container import Container, get_container_name
 from .progress_stream import stream_output, StreamOutputError
 
 log = logging.getLogger(__name__)
@@ -86,10 +86,21 @@ class Service(object):
         self.options = options
 
     def containers(self, stopped=False, one_off=False):
-        return [Container.from_ps(self.client, container)
-                for container in self.client.containers(
-                    all=stopped,
-                    filters={'label': self.labels(one_off=one_off)})]
+        containers = [
+            Container.from_ps(self.client, container)
+            for container in self.client.containers(
+                all=stopped,
+                filters={'label': self.labels(one_off=one_off)})]
+
+        if not containers:
+            check_for_legacy_containers(
+                self.client,
+                self.project,
+                [self.name],
+                stopped=stopped,
+                one_off=one_off)
+
+        return containers
 
     def get_container(self, number=1):
         """Return a :class:`compose.container.Container` for this service. The
@@ -614,6 +625,31 @@ def build_container_labels(label_options, service_labels, number, one_off=False)
     return labels
 
 
+def check_for_legacy_containers(
+        client,
+        project,
+        services,
+        stopped=False,
+        one_off=False):
+    """Check if there are containers named using the old naming convention
+    and warn the user that those containers may need to be migrated to
+    using labels, so that compose can find them.
+    """
+    for container in client.containers(all=stopped):
+        name = get_container_name(container)
+        for service in services:
+            prefix = '%s_%s_%s' % (project, service, 'run_' if one_off else '')
+            if not name.startswith(prefix):
+                continue
+
+            log.warn(
+                "Compose found a found a container named %s without any "
+                "labels. As of compose 1.3.0 containers are identified with "
+                "labels instead of naming convention. If you'd like compose "
+                "to use this container, please run "
+                "`docker-compose --migrate-to-labels`" % (name,))
+
+
 def parse_restart_spec(restart_config):
     if not restart_config:
         return None

+ 23 - 0
tests/integration/migration_test.py

@@ -0,0 +1,23 @@
+import mock
+
+from compose import service, migration
+from compose.project import Project
+from .testcases import DockerClientTestCase
+
+
+class ProjectTest(DockerClientTestCase):
+
+    def test_migration_to_labels(self):
+        web = self.create_service('web')
+        db = self.create_service('db')
+        project = Project('composetest', [web, db], self.client)
+
+        self.client.create_container(name='composetest_web_1', **web.options)
+        self.client.create_container(name='composetest_db_1', **db.options)
+
+        with mock.patch.object(service, 'log', autospec=True) as mock_log:
+            self.assertEqual(project.containers(stopped=True), [])
+            self.assertEqual(mock_log.warn.call_count, 2)
+
+        migration.migrate_project_to_labels(project)
+        self.assertEqual(len(project.containers(stopped=True)), 2)

+ 1 - 1
tests/unit/container_test.py

@@ -28,7 +28,7 @@ class ContainerTest(unittest.TestCase):
                 "Labels": {
                     "com.docker.compose.project": "composetest",
                     "com.docker.compose.service": "web",
-                    "com.docker.compose.container_number": 7,
+                    "com.docker.compose.container-number": 7,
                 },
             }
         }