Skip to content

Commit

Permalink
Add migration warning and option to migrate to labels.
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Nephin <[email protected]>
  • Loading branch information
dnephin committed May 18, 2015
1 parent ed50a0a commit 6e98dec
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 28 deletions.
34 changes: 20 additions & 14 deletions compose/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import dockerpty

from .. import __version__
from .. import migration
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, CannotBeScaledError
from ..config import parse_environment
Expand Down Expand Up @@ -77,24 +78,26 @@ class TopLevelCommand(Command):
Options:
-f, --file FILE Specify an alternate compose file (default: docker-compose.yml)
-p, --project-name NAME Specify an alternate project name (default: directory name)
compose project to use labels
--verbose Show more output
-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):
Expand Down Expand Up @@ -483,6 +486,9 @@ def handler(signal, frame):
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)
35 changes: 35 additions & 0 deletions compose/migration.py
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 24 additions & 8 deletions compose/project.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand Down Expand Up @@ -82,6 +83,10 @@ def from_dicts(cls, name, service_dicts, client):
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
Expand Down Expand Up @@ -109,7 +114,7 @@ def get_services(self, service_names=None, include_deps=False):
"""
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:
Expand Down Expand Up @@ -230,10 +235,21 @@ def remove_stopped(self, service_names=None, **options):
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()
Expand Down
46 changes: 41 additions & 5 deletions compose/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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__)
Expand Down Expand Up @@ -86,10 +86,21 @@ def __init__(self, name, client=None, project='default', links=None, external_li
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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tests/integration/migration_test.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion tests/unit/container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def setUp(self):
"Labels": {
"com.docker.compose.project": "composetest",
"com.docker.compose.service": "web",
"com.docker.compose.container_number": 7,
"com.docker.compose.container-number": 7,
},
}
}
Expand Down

0 comments on commit 6e98dec

Please sign in to comment.