diff --git a/compose/cli/main.py b/compose/cli/main.py index a2375516e9..0c5d5a7f64 100644 --- a/compose/cli/main.py +++ b/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) diff --git a/compose/container.py b/compose/container.py index 183d5fde89..3e462088fe 100644 --- a/compose/container.py +++ b/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): diff --git a/compose/migration.py b/compose/migration.py new file mode 100644 index 0000000000..16b5dd1678 --- /dev/null +++ b/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) diff --git a/compose/project.py b/compose/project.py index d22bdf4dc8..8ca144813c 100644 --- a/compose/project.py +++ b/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() diff --git a/compose/service.py b/compose/service.py index 3c62dbebb8..dc34a9bc25 100644 --- a/compose/service.py +++ b/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 diff --git a/tests/integration/migration_test.py b/tests/integration/migration_test.py new file mode 100644 index 0000000000..133d231481 --- /dev/null +++ b/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) diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index b04df6592e..2313d4b8ef 100644 --- a/tests/unit/container_test.py +++ b/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, }, } }