mirror of
https://github.com/docker/compose.git
synced 2025-07-21 12:44:54 +02:00
Add migration warning and option to migrate to labels.
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
parent
ed50a0a3a0
commit
62059d55e6
@ -11,6 +11,7 @@ from docker.errors import APIError
|
|||||||
import dockerpty
|
import dockerpty
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
from .. import migration
|
||||||
from ..project import NoSuchService, ConfigurationError
|
from ..project import NoSuchService, ConfigurationError
|
||||||
from ..service import BuildError, CannotBeScaledError
|
from ..service import BuildError, CannotBeScaledError
|
||||||
from ..config import parse_environment
|
from ..config import parse_environment
|
||||||
@ -95,6 +96,7 @@ class TopLevelCommand(Command):
|
|||||||
start Start services
|
start Start services
|
||||||
stop Stop services
|
stop Stop services
|
||||||
up Create and start containers
|
up Create and start containers
|
||||||
|
migrate_to_labels Recreate containers to add labels
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def docopt_options(self):
|
def docopt_options(self):
|
||||||
@ -483,6 +485,9 @@ class TopLevelCommand(Command):
|
|||||||
params = {} if timeout is None else {'timeout': int(timeout)}
|
params = {} if timeout is None else {'timeout': int(timeout)}
|
||||||
project.stop(service_names=service_names, **params)
|
project.stop(service_names=service_names, **params)
|
||||||
|
|
||||||
|
def migrate_to_labels(self, project, _options):
|
||||||
|
migration.migrate_project_to_labels(project)
|
||||||
|
|
||||||
|
|
||||||
def list_containers(containers):
|
def list_containers(containers):
|
||||||
return ", ".join(c.name for c in containers)
|
return ", ".join(c.name for c in containers)
|
||||||
|
@ -64,7 +64,11 @@ class Container(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def number(self):
|
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
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
|
35
compose/migration.py
Normal file
35
compose/migration.py
Normal file
@ -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)
|
@ -1,13 +1,14 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
from docker.errors import APIError
|
||||||
|
|
||||||
from .config import get_service_name_from_net, ConfigurationError
|
from .config import get_service_name_from_net, ConfigurationError
|
||||||
from .const import LABEL_PROJECT, LABEL_ONE_OFF
|
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 .container import Container
|
||||||
from docker.errors import APIError
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -82,6 +83,10 @@ class Project(object):
|
|||||||
volumes_from=volumes_from, **service_dict))
|
volumes_from=volumes_from, **service_dict))
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_names(self):
|
||||||
|
return [service.name for service in self.services]
|
||||||
|
|
||||||
def get_service(self, name):
|
def get_service(self, name):
|
||||||
"""
|
"""
|
||||||
Retrieve a service by name. Raises NoSuchService
|
Retrieve a service by name. Raises NoSuchService
|
||||||
@ -109,7 +114,7 @@ class Project(object):
|
|||||||
"""
|
"""
|
||||||
if service_names is None or len(service_names) == 0:
|
if service_names is None or len(service_names) == 0:
|
||||||
return self.get_services(
|
return self.get_services(
|
||||||
service_names=[s.name for s in self.services],
|
service_names=self.service_names,
|
||||||
include_deps=include_deps
|
include_deps=include_deps
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -230,11 +235,22 @@ class Project(object):
|
|||||||
service.remove_stopped(**options)
|
service.remove_stopped(**options)
|
||||||
|
|
||||||
def containers(self, service_names=None, stopped=False, one_off=False):
|
def containers(self, service_names=None, stopped=False, one_off=False):
|
||||||
return [Container.from_ps(self.client, container)
|
containers = [
|
||||||
|
Container.from_ps(self.client, container)
|
||||||
for container in self.client.containers(
|
for container in self.client.containers(
|
||||||
all=stopped,
|
all=stopped,
|
||||||
filters={'label': self.labels(one_off=one_off)})]
|
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):
|
def _inject_deps(self, acc, service):
|
||||||
net_name = service.get_net_name()
|
net_name = service.get_net_name()
|
||||||
dep_names = (service.get_linked_names() +
|
dep_names = (service.get_linked_names() +
|
||||||
|
@ -19,7 +19,7 @@ from .const import (
|
|||||||
LABEL_SERVICE,
|
LABEL_SERVICE,
|
||||||
LABEL_VERSION,
|
LABEL_VERSION,
|
||||||
)
|
)
|
||||||
from .container import Container
|
from .container import Container, get_container_name
|
||||||
from .progress_stream import stream_output, StreamOutputError
|
from .progress_stream import stream_output, StreamOutputError
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -86,11 +86,22 @@ class Service(object):
|
|||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
def containers(self, stopped=False, one_off=False):
|
def containers(self, stopped=False, one_off=False):
|
||||||
return [Container.from_ps(self.client, container)
|
containers = [
|
||||||
|
Container.from_ps(self.client, container)
|
||||||
for container in self.client.containers(
|
for container in self.client.containers(
|
||||||
all=stopped,
|
all=stopped,
|
||||||
filters={'label': self.labels(one_off=one_off)})]
|
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):
|
def get_container(self, number=1):
|
||||||
"""Return a :class:`compose.container.Container` for this service. The
|
"""Return a :class:`compose.container.Container` for this service. The
|
||||||
container must be active, and match `number`.
|
container must be active, and match `number`.
|
||||||
@ -614,6 +625,31 @@ def build_container_labels(label_options, service_labels, number, one_off=False)
|
|||||||
return labels
|
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):
|
def parse_restart_spec(restart_config):
|
||||||
if not restart_config:
|
if not restart_config:
|
||||||
return None
|
return None
|
||||||
|
23
tests/integration/migration_test.py
Normal file
23
tests/integration/migration_test.py
Normal file
@ -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)
|
@ -28,7 +28,7 @@ class ContainerTest(unittest.TestCase):
|
|||||||
"Labels": {
|
"Labels": {
|
||||||
"com.docker.compose.project": "composetest",
|
"com.docker.compose.project": "composetest",
|
||||||
"com.docker.compose.service": "web",
|
"com.docker.compose.service": "web",
|
||||||
"com.docker.compose.container_number": 7,
|
"com.docker.compose.container-number": 7,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user