Add migration warning and option to migrate to labels.

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
Daniel Nephin 2015-05-09 12:53:59 -04:00
parent ed50a0a3a0
commit 62059d55e6
7 changed files with 148 additions and 29 deletions

View File

@ -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)

View File

@ -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
compose/migration.py Normal file
View 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)

View File

@ -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()

View File

@ -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

View 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)

View File

@ -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,
},
}
}