mirror of https://github.com/docker/compose.git
Merge pull request #1356 from dnephin/use_labels_instead_of_names
Use labels instead of names to identify containers
This commit is contained in:
commit
1e6d912fbc
|
@ -1,4 +1,3 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from .service import Service # noqa:flake8
|
|
||||||
|
|
||||||
__version__ = '1.3.0dev'
|
__version__ = '1.3.0dev'
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -482,6 +484,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)
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
|
||||||
|
LABEL_ONE_OFF = 'com.docker.compose.oneoff'
|
||||||
|
LABEL_PROJECT = 'com.docker.compose.project'
|
||||||
|
LABEL_SERVICE = 'com.docker.compose.service'
|
||||||
|
LABEL_VERSION = 'com.docker.compose.version'
|
|
@ -4,6 +4,8 @@ from __future__ import absolute_import
|
||||||
import six
|
import six
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
from .const import LABEL_CONTAINER_NUMBER, LABEL_SERVICE
|
||||||
|
|
||||||
|
|
||||||
class Container(object):
|
class Container(object):
|
||||||
"""
|
"""
|
||||||
|
@ -58,14 +60,15 @@ class Container(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name_without_project(self):
|
def name_without_project(self):
|
||||||
return '_'.join(self.dictionary['Name'].split('_')[1:])
|
return '{0}_{1}'.format(self.labels.get(LABEL_SERVICE), self.number)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number(self):
|
def number(self):
|
||||||
try:
|
number = self.labels.get(LABEL_CONTAINER_NUMBER)
|
||||||
return int(self.name.split('_')[-1])
|
if not number:
|
||||||
except ValueError:
|
raise ValueError("Container {0} does not have a {1} label".format(
|
||||||
return None
|
self.short_id, LABEL_CONTAINER_NUMBER))
|
||||||
|
return int(number)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
|
@ -159,6 +162,7 @@ class Container(object):
|
||||||
self.has_been_inspected = True
|
self.has_been_inspected = True
|
||||||
return self.dictionary
|
return self.dictionary
|
||||||
|
|
||||||
|
# TODO: only used by tests, move to test module
|
||||||
def links(self):
|
def links(self):
|
||||||
links = []
|
links = []
|
||||||
for container in self.client.containers():
|
for container in self.client.containers():
|
||||||
|
|
|
@ -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,15 @@
|
||||||
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 .config import get_service_name_from_net, ConfigurationError
|
|
||||||
from .service import Service
|
|
||||||
from .container import Container
|
|
||||||
from docker.errors import APIError
|
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, check_for_legacy_containers
|
||||||
|
from .container import Container
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,6 +62,12 @@ class Project(object):
|
||||||
self.services = services
|
self.services = services
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
|
def labels(self, one_off=False):
|
||||||
|
return [
|
||||||
|
'{0}={1}'.format(LABEL_PROJECT, self.name),
|
||||||
|
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False"),
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dicts(cls, name, service_dicts, client):
|
def from_dicts(cls, name, service_dicts, client):
|
||||||
"""
|
"""
|
||||||
|
@ -75,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
|
||||||
|
@ -102,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:
|
||||||
|
@ -223,10 +235,21 @@ 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 = [
|
||||||
for container in self.client.containers(all=stopped)
|
Container.from_ps(self.client, container)
|
||||||
for service in self.get_services(service_names)
|
for container in self.client.containers(
|
||||||
if service.has_container(container, one_off=one_off)]
|
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):
|
def _inject_deps(self, acc, service):
|
||||||
net_name = service.get_net_name()
|
net_name = service.get_net_name()
|
||||||
|
|
|
@ -10,7 +10,15 @@ import six
|
||||||
from docker.errors import APIError
|
from docker.errors import APIError
|
||||||
from docker.utils import create_host_config, LogConfig
|
from docker.utils import create_host_config, LogConfig
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
from .config import DOCKER_CONFIG_KEYS, merge_environment
|
from .config import DOCKER_CONFIG_KEYS, merge_environment
|
||||||
|
from .const import (
|
||||||
|
LABEL_CONTAINER_NUMBER,
|
||||||
|
LABEL_ONE_OFF,
|
||||||
|
LABEL_PROJECT,
|
||||||
|
LABEL_SERVICE,
|
||||||
|
LABEL_VERSION,
|
||||||
|
)
|
||||||
from .container import Container, get_container_name
|
from .container import Container, get_container_name
|
||||||
from .progress_stream import stream_output, StreamOutputError
|
from .progress_stream import stream_output, StreamOutputError
|
||||||
|
|
||||||
|
@ -78,27 +86,28 @@ 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 = [
|
||||||
for container in self.client.containers(all=stopped)
|
Container.from_ps(self.client, container)
|
||||||
if self.has_container(container, one_off=one_off)]
|
for container in self.client.containers(
|
||||||
|
all=stopped,
|
||||||
|
filters={'label': self.labels(one_off=one_off)})]
|
||||||
|
|
||||||
def has_container(self, container, one_off=False):
|
if not containers:
|
||||||
"""Return True if `container` was created to fulfill this service."""
|
check_for_legacy_containers(
|
||||||
name = get_container_name(container)
|
self.client,
|
||||||
if not name or not is_valid_name(name, one_off):
|
self.project,
|
||||||
return False
|
[self.name],
|
||||||
project, name, _number = parse_name(name)
|
stopped=stopped,
|
||||||
return project == self.project and name == self.name
|
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`.
|
||||||
"""
|
"""
|
||||||
for container in self.client.containers():
|
labels = self.labels() + ['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]
|
||||||
if not self.has_container(container):
|
for container in self.client.containers(filters={'label': labels}):
|
||||||
continue
|
|
||||||
_, _, container_number = parse_name(get_container_name(container))
|
|
||||||
if container_number == number:
|
|
||||||
return Container.from_ps(self.client, container)
|
return Container.from_ps(self.client, container)
|
||||||
|
|
||||||
raise ValueError("No container found for %s_%s" % (self.name, number))
|
raise ValueError("No container found for %s_%s" % (self.name, number))
|
||||||
|
@ -138,7 +147,6 @@ class Service(object):
|
||||||
# Create enough containers
|
# Create enough containers
|
||||||
containers = self.containers(stopped=True)
|
containers = self.containers(stopped=True)
|
||||||
while len(containers) < desired_num:
|
while len(containers) < desired_num:
|
||||||
log.info("Creating %s..." % self._next_container_name(containers))
|
|
||||||
containers.append(self.create_container(detach=True))
|
containers.append(self.create_container(detach=True))
|
||||||
|
|
||||||
running_containers = []
|
running_containers = []
|
||||||
|
@ -178,6 +186,7 @@ class Service(object):
|
||||||
insecure_registry=False,
|
insecure_registry=False,
|
||||||
do_build=True,
|
do_build=True,
|
||||||
previous_container=None,
|
previous_container=None,
|
||||||
|
number=None,
|
||||||
**override_options):
|
**override_options):
|
||||||
"""
|
"""
|
||||||
Create a container for this service. If the image doesn't exist, attempt to pull
|
Create a container for this service. If the image doesn't exist, attempt to pull
|
||||||
|
@ -185,6 +194,7 @@ class Service(object):
|
||||||
"""
|
"""
|
||||||
container_options = self._get_container_create_options(
|
container_options = self._get_container_create_options(
|
||||||
override_options,
|
override_options,
|
||||||
|
number or self._next_container_number(one_off=one_off),
|
||||||
one_off=one_off,
|
one_off=one_off,
|
||||||
previous_container=previous_container,
|
previous_container=previous_container,
|
||||||
)
|
)
|
||||||
|
@ -209,7 +219,6 @@ class Service(object):
|
||||||
"""
|
"""
|
||||||
containers = self.containers(stopped=True)
|
containers = self.containers(stopped=True)
|
||||||
if not containers:
|
if not containers:
|
||||||
log.info("Creating %s..." % self._next_container_name(containers))
|
|
||||||
container = self.create_container(
|
container = self.create_container(
|
||||||
insecure_registry=insecure_registry,
|
insecure_registry=insecure_registry,
|
||||||
do_build=do_build,
|
do_build=do_build,
|
||||||
|
@ -256,6 +265,7 @@ class Service(object):
|
||||||
new_container = self.create_container(
|
new_container = self.create_container(
|
||||||
do_build=False,
|
do_build=False,
|
||||||
previous_container=container,
|
previous_container=container,
|
||||||
|
number=container.labels.get(LABEL_CONTAINER_NUMBER),
|
||||||
**override_options)
|
**override_options)
|
||||||
self.start_container(new_container)
|
self.start_container(new_container)
|
||||||
container.remove()
|
container.remove()
|
||||||
|
@ -280,7 +290,6 @@ class Service(object):
|
||||||
containers = self.containers(stopped=True)
|
containers = self.containers(stopped=True)
|
||||||
|
|
||||||
if not containers:
|
if not containers:
|
||||||
log.info("Creating %s..." % self._next_container_name(containers))
|
|
||||||
new_container = self.create_container(
|
new_container = self.create_container(
|
||||||
insecure_registry=insecure_registry,
|
insecure_registry=insecure_registry,
|
||||||
detach=detach,
|
detach=detach,
|
||||||
|
@ -302,14 +311,19 @@ class Service(object):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
def _next_container_name(self, all_containers, one_off=False):
|
def get_container_name(self, number, one_off=False):
|
||||||
bits = [self.project, self.name]
|
# TODO: Implement issue #652 here
|
||||||
if one_off:
|
return build_container_name(self.project, self.name, number, one_off)
|
||||||
bits.append('run')
|
|
||||||
return '_'.join(bits + [str(self._next_container_number(all_containers))])
|
|
||||||
|
|
||||||
def _next_container_number(self, all_containers):
|
# TODO: this would benefit from github.com/docker/docker/pull/11943
|
||||||
numbers = [parse_name(c.name).number for c in all_containers]
|
# to remove the need to inspect every container
|
||||||
|
def _next_container_number(self, one_off=False):
|
||||||
|
numbers = [
|
||||||
|
Container.from_ps(self.client, container).number
|
||||||
|
for container in self.client.containers(
|
||||||
|
all=True,
|
||||||
|
filters={'label': self.labels(one_off=one_off)})
|
||||||
|
]
|
||||||
return 1 if not numbers else max(numbers) + 1
|
return 1 if not numbers else max(numbers) + 1
|
||||||
|
|
||||||
def _get_links(self, link_to_self):
|
def _get_links(self, link_to_self):
|
||||||
|
@ -369,6 +383,7 @@ class Service(object):
|
||||||
def _get_container_create_options(
|
def _get_container_create_options(
|
||||||
self,
|
self,
|
||||||
override_options,
|
override_options,
|
||||||
|
number,
|
||||||
one_off=False,
|
one_off=False,
|
||||||
previous_container=None):
|
previous_container=None):
|
||||||
container_options = dict(
|
container_options = dict(
|
||||||
|
@ -376,9 +391,7 @@ class Service(object):
|
||||||
for k in DOCKER_CONFIG_KEYS if k in self.options)
|
for k in DOCKER_CONFIG_KEYS if k in self.options)
|
||||||
container_options.update(override_options)
|
container_options.update(override_options)
|
||||||
|
|
||||||
container_options['name'] = self._next_container_name(
|
container_options['name'] = self.get_container_name(number, one_off)
|
||||||
self.containers(stopped=True, one_off=one_off),
|
|
||||||
one_off)
|
|
||||||
|
|
||||||
# If a qualified hostname was given, split it into an
|
# If a qualified hostname was given, split it into an
|
||||||
# unqualified hostname and a domainname unless domainname
|
# unqualified hostname and a domainname unless domainname
|
||||||
|
@ -419,6 +432,11 @@ class Service(object):
|
||||||
if self.can_be_built():
|
if self.can_be_built():
|
||||||
container_options['image'] = self.full_name
|
container_options['image'] = self.full_name
|
||||||
|
|
||||||
|
container_options['labels'] = build_container_labels(
|
||||||
|
container_options.get('labels', {}),
|
||||||
|
self.labels(one_off=one_off),
|
||||||
|
number)
|
||||||
|
|
||||||
# Delete options which are only used when starting
|
# Delete options which are only used when starting
|
||||||
for key in DOCKER_START_KEYS:
|
for key in DOCKER_START_KEYS:
|
||||||
container_options.pop(key, None)
|
container_options.pop(key, None)
|
||||||
|
@ -520,6 +538,13 @@ class Service(object):
|
||||||
"""
|
"""
|
||||||
return '%s_%s' % (self.project, self.name)
|
return '%s_%s' % (self.project, self.name)
|
||||||
|
|
||||||
|
def labels(self, one_off=False):
|
||||||
|
return [
|
||||||
|
'{0}={1}'.format(LABEL_PROJECT, self.project),
|
||||||
|
'{0}={1}'.format(LABEL_SERVICE, self.name),
|
||||||
|
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False")
|
||||||
|
]
|
||||||
|
|
||||||
def can_be_scaled(self):
|
def can_be_scaled(self):
|
||||||
for port in self.options.get('ports', []):
|
for port in self.options.get('ports', []):
|
||||||
if ':' in str(port):
|
if ':' in str(port):
|
||||||
|
@ -585,23 +610,44 @@ def merge_volume_bindings(volumes_option, previous_container):
|
||||||
return volume_bindings
|
return volume_bindings
|
||||||
|
|
||||||
|
|
||||||
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
|
def build_container_name(project, service, number, one_off=False):
|
||||||
|
bits = [project, service]
|
||||||
|
|
||||||
def is_valid_name(name, one_off=False):
|
|
||||||
match = NAME_RE.match(name)
|
|
||||||
if match is None:
|
|
||||||
return False
|
|
||||||
if one_off:
|
if one_off:
|
||||||
return match.group(3) == 'run_'
|
bits.append('run')
|
||||||
else:
|
return '_'.join(bits + [str(number)])
|
||||||
return match.group(3) is None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_name(name):
|
def build_container_labels(label_options, service_labels, number, one_off=False):
|
||||||
match = NAME_RE.match(name)
|
labels = label_options or {}
|
||||||
(project, service_name, _, suffix) = match.groups()
|
labels.update(label.split('=', 1) for label in service_labels)
|
||||||
return ServiceName(project, service_name, int(suffix))
|
labels[LABEL_CONTAINER_NUMBER] = str(number)
|
||||||
|
labels[LABEL_VERSION] = __version__
|
||||||
|
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):
|
||||||
|
|
|
@ -21,6 +21,8 @@ class CLITestCase(DockerClientTestCase):
|
||||||
sys.exit = self.old_sys_exit
|
sys.exit = self.old_sys_exit
|
||||||
self.project.kill()
|
self.project.kill()
|
||||||
self.project.remove_stopped()
|
self.project.remove_stopped()
|
||||||
|
for container in self.project.containers(stopped=True, one_off=True):
|
||||||
|
container.remove(force=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project(self):
|
def project(self):
|
||||||
|
@ -62,6 +64,10 @@ class CLITestCase(DockerClientTestCase):
|
||||||
|
|
||||||
@patch('sys.stdout', new_callable=StringIO)
|
@patch('sys.stdout', new_callable=StringIO)
|
||||||
def test_ps_alternate_composefile(self, mock_stdout):
|
def test_ps_alternate_composefile(self, mock_stdout):
|
||||||
|
config_path = os.path.abspath(
|
||||||
|
'tests/fixtures/multiple-composefiles/compose2.yml')
|
||||||
|
self._project = self.command.get_project(config_path)
|
||||||
|
|
||||||
self.command.base_dir = 'tests/fixtures/multiple-composefiles'
|
self.command.base_dir = 'tests/fixtures/multiple-composefiles'
|
||||||
self.command.dispatch(['-f', 'compose2.yml', 'up', '-d'], None)
|
self.command.dispatch(['-f', 'compose2.yml', 'up', '-d'], None)
|
||||||
self.command.dispatch(['-f', 'compose2.yml', 'ps'], None)
|
self.command.dispatch(['-f', 'compose2.yml', 'ps'], None)
|
||||||
|
@ -416,7 +422,6 @@ class CLITestCase(DockerClientTestCase):
|
||||||
self.assertEqual(len(project.get_service('another').containers()), 0)
|
self.assertEqual(len(project.get_service('another').containers()), 0)
|
||||||
|
|
||||||
def test_port(self):
|
def test_port(self):
|
||||||
|
|
||||||
self.command.base_dir = 'tests/fixtures/ports-composefile'
|
self.command.base_dir = 'tests/fixtures/ports-composefile'
|
||||||
self.command.dispatch(['up', '-d'], None)
|
self.command.dispatch(['up', '-d'], None)
|
||||||
container = self.project.get_service('simple').get_container()
|
container = self.project.get_service('simple').get_container()
|
||||||
|
|
|
@ -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)
|
|
@ -8,11 +8,19 @@ import tempfile
|
||||||
import shutil
|
import shutil
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from compose import Service
|
from compose import __version__
|
||||||
|
from compose.const import (
|
||||||
|
LABEL_CONTAINER_NUMBER,
|
||||||
|
LABEL_ONE_OFF,
|
||||||
|
LABEL_PROJECT,
|
||||||
|
LABEL_SERVICE,
|
||||||
|
LABEL_VERSION,
|
||||||
|
)
|
||||||
from compose.service import (
|
from compose.service import (
|
||||||
CannotBeScaledError,
|
CannotBeScaledError,
|
||||||
build_extra_hosts,
|
|
||||||
ConfigError,
|
ConfigError,
|
||||||
|
Service,
|
||||||
|
build_extra_hosts,
|
||||||
)
|
)
|
||||||
from compose.container import Container
|
from compose.container import Container
|
||||||
from docker.errors import APIError
|
from docker.errors import APIError
|
||||||
|
@ -633,17 +641,18 @@ class ServiceTest(DockerClientTestCase):
|
||||||
'com.example.label-with-empty-value': "",
|
'com.example.label-with-empty-value': "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compose_labels = {
|
||||||
|
LABEL_CONTAINER_NUMBER: '1',
|
||||||
|
LABEL_ONE_OFF: 'False',
|
||||||
|
LABEL_PROJECT: 'composetest',
|
||||||
|
LABEL_SERVICE: 'web',
|
||||||
|
LABEL_VERSION: __version__,
|
||||||
|
}
|
||||||
|
expected = dict(labels_dict, **compose_labels)
|
||||||
|
|
||||||
service = self.create_service('web', labels=labels_dict)
|
service = self.create_service('web', labels=labels_dict)
|
||||||
labels = create_and_start_container(service).labels.items()
|
labels = create_and_start_container(service).labels
|
||||||
for pair in labels_dict.items():
|
self.assertEqual(labels, expected)
|
||||||
self.assertIn(pair, labels)
|
|
||||||
|
|
||||||
labels_list = ["%s=%s" % pair for pair in labels_dict.items()]
|
|
||||||
|
|
||||||
service = self.create_service('web', labels=labels_list)
|
|
||||||
labels = create_and_start_container(service).labels.items()
|
|
||||||
for pair in labels_dict.items():
|
|
||||||
self.assertIn(pair, labels)
|
|
||||||
|
|
||||||
def test_empty_labels(self):
|
def test_empty_labels(self):
|
||||||
labels_list = ['foo', 'bar']
|
labels_list = ['foo', 'bar']
|
||||||
|
|
|
@ -12,6 +12,7 @@ class DockerClientTestCase(unittest.TestCase):
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.client = docker_client()
|
cls.client = docker_client()
|
||||||
|
|
||||||
|
# TODO: update to use labels in #652
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for c in self.client.containers(all=True):
|
for c in self.client.containers(all=True):
|
||||||
if c['Names'] and 'composetest' in c['Names'][0]:
|
if c['Names'] and 'composetest' in c['Names'][0]:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import mock
|
||||||
import docker
|
import docker
|
||||||
|
|
||||||
from compose.container import Container
|
from compose.container import Container
|
||||||
|
from compose.container import get_container_name
|
||||||
|
|
||||||
|
|
||||||
class ContainerTest(unittest.TestCase):
|
class ContainerTest(unittest.TestCase):
|
||||||
|
@ -23,6 +24,13 @@ class ContainerTest(unittest.TestCase):
|
||||||
"NetworkSettings": {
|
"NetworkSettings": {
|
||||||
"Ports": {},
|
"Ports": {},
|
||||||
},
|
},
|
||||||
|
"Config": {
|
||||||
|
"Labels": {
|
||||||
|
"com.docker.compose.project": "composetest",
|
||||||
|
"com.docker.compose.service": "web",
|
||||||
|
"com.docker.compose.container-number": 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_from_ps(self):
|
def test_from_ps(self):
|
||||||
|
@ -65,10 +73,8 @@ class ContainerTest(unittest.TestCase):
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_number(self):
|
def test_number(self):
|
||||||
container = Container.from_ps(None,
|
container = Container(None, self.container_dict, has_been_inspected=True)
|
||||||
self.container_dict,
|
self.assertEqual(container.number, 7)
|
||||||
has_been_inspected=True)
|
|
||||||
self.assertEqual(container.number, 1)
|
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
container = Container.from_ps(None,
|
container = Container.from_ps(None,
|
||||||
|
@ -77,10 +83,8 @@ class ContainerTest(unittest.TestCase):
|
||||||
self.assertEqual(container.name, "composetest_db_1")
|
self.assertEqual(container.name, "composetest_db_1")
|
||||||
|
|
||||||
def test_name_without_project(self):
|
def test_name_without_project(self):
|
||||||
container = Container.from_ps(None,
|
container = Container(None, self.container_dict, has_been_inspected=True)
|
||||||
self.container_dict,
|
self.assertEqual(container.name_without_project, "web_7")
|
||||||
has_been_inspected=True)
|
|
||||||
self.assertEqual(container.name_without_project, "db_1")
|
|
||||||
|
|
||||||
def test_inspect_if_not_inspected(self):
|
def test_inspect_if_not_inspected(self):
|
||||||
mock_client = mock.create_autospec(docker.Client)
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
|
@ -130,3 +134,12 @@ class ContainerTest(unittest.TestCase):
|
||||||
self.assertEqual(container.get('Status'), "Up 8 seconds")
|
self.assertEqual(container.get('Status'), "Up 8 seconds")
|
||||||
self.assertEqual(container.get('HostConfig.VolumesFrom'), ["volume_id"])
|
self.assertEqual(container.get('HostConfig.VolumesFrom'), ["volume_id"])
|
||||||
self.assertEqual(container.get('Foo.Bar.DoesNotExist'), None)
|
self.assertEqual(container.get('Foo.Bar.DoesNotExist'), None)
|
||||||
|
|
||||||
|
|
||||||
|
class GetContainerNameTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_get_container_name(self):
|
||||||
|
self.assertIsNone(get_container_name({}))
|
||||||
|
self.assertEqual(get_container_name({'Name': 'myproject_db_1'}), 'myproject_db_1')
|
||||||
|
self.assertEqual(get_container_name({'Names': ['/myproject_db_1', '/myproject_web_1/db']}), 'myproject_db_1')
|
||||||
|
self.assertEqual(get_container_name({'Names': ['/swarm-host-1/myproject_db_1', '/swarm-host-1/myproject_web_1/db']}), 'myproject_db_1')
|
||||||
|
|
|
@ -7,15 +7,15 @@ import mock
|
||||||
import docker
|
import docker
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
|
||||||
from compose import Service
|
from compose.service import Service
|
||||||
from compose.container import Container
|
from compose.container import Container
|
||||||
|
from compose.const import LABEL_SERVICE, LABEL_PROJECT, LABEL_ONE_OFF
|
||||||
from compose.service import (
|
from compose.service import (
|
||||||
APIError,
|
APIError,
|
||||||
ConfigError,
|
ConfigError,
|
||||||
build_port_bindings,
|
build_port_bindings,
|
||||||
build_volume_binding,
|
build_volume_binding,
|
||||||
get_container_data_volumes,
|
get_container_data_volumes,
|
||||||
get_container_name,
|
|
||||||
merge_volume_bindings,
|
merge_volume_bindings,
|
||||||
parse_repository_tag,
|
parse_repository_tag,
|
||||||
parse_volume_spec,
|
parse_volume_spec,
|
||||||
|
@ -48,36 +48,27 @@ class ServiceTest(unittest.TestCase):
|
||||||
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_', image='foo'))
|
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_', image='foo'))
|
||||||
Service(name='foo', project='bar', image='foo')
|
Service(name='foo', project='bar', image='foo')
|
||||||
|
|
||||||
def test_get_container_name(self):
|
|
||||||
self.assertIsNone(get_container_name({}))
|
|
||||||
self.assertEqual(get_container_name({'Name': 'myproject_db_1'}), 'myproject_db_1')
|
|
||||||
self.assertEqual(get_container_name({'Names': ['/myproject_db_1', '/myproject_web_1/db']}), 'myproject_db_1')
|
|
||||||
self.assertEqual(get_container_name({'Names': ['/swarm-host-1/myproject_db_1', '/swarm-host-1/myproject_web_1/db']}), 'myproject_db_1')
|
|
||||||
|
|
||||||
def test_containers(self):
|
def test_containers(self):
|
||||||
service = Service('db', client=self.mock_client, image='foo', project='myproject')
|
service = Service('db', self.mock_client, 'myproject', image='foo')
|
||||||
|
|
||||||
self.mock_client.containers.return_value = []
|
self.mock_client.containers.return_value = []
|
||||||
self.assertEqual(service.containers(), [])
|
self.assertEqual(service.containers(), [])
|
||||||
|
|
||||||
|
def test_containers_with_containers(self):
|
||||||
self.mock_client.containers.return_value = [
|
self.mock_client.containers.return_value = [
|
||||||
{'Image': 'busybox', 'Id': 'OUT_1', 'Names': ['/myproject', '/foo/bar']},
|
dict(Name=str(i), Image='foo', Id=i) for i in range(3)
|
||||||
{'Image': 'busybox', 'Id': 'OUT_2', 'Names': ['/myproject_db']},
|
|
||||||
{'Image': 'busybox', 'Id': 'OUT_3', 'Names': ['/db_1']},
|
|
||||||
{'Image': 'busybox', 'Id': 'IN_1', 'Names': ['/myproject_db_1', '/myproject_web_1/db']},
|
|
||||||
]
|
]
|
||||||
self.assertEqual([c.id for c in service.containers()], ['IN_1'])
|
service = Service('db', self.mock_client, 'myproject', image='foo')
|
||||||
|
self.assertEqual([c.id for c in service.containers()], range(3))
|
||||||
|
|
||||||
def test_containers_prefixed(self):
|
expected_labels = [
|
||||||
service = Service('db', client=self.mock_client, image='foo', project='myproject')
|
'{0}=myproject'.format(LABEL_PROJECT),
|
||||||
|
'{0}=db'.format(LABEL_SERVICE),
|
||||||
self.mock_client.containers.return_value = [
|
'{0}=False'.format(LABEL_ONE_OFF),
|
||||||
{'Image': 'busybox', 'Id': 'OUT_1', 'Names': ['/swarm-host-1/myproject', '/swarm-host-1/foo/bar']},
|
|
||||||
{'Image': 'busybox', 'Id': 'OUT_2', 'Names': ['/swarm-host-1/myproject_db']},
|
|
||||||
{'Image': 'busybox', 'Id': 'OUT_3', 'Names': ['/swarm-host-1/db_1']},
|
|
||||||
{'Image': 'busybox', 'Id': 'IN_1', 'Names': ['/swarm-host-1/myproject_db_1', '/swarm-host-1/myproject_web_1/db']},
|
|
||||||
]
|
]
|
||||||
self.assertEqual([c.id for c in service.containers()], ['IN_1'])
|
|
||||||
|
self.mock_client.containers.assert_called_once_with(
|
||||||
|
all=False,
|
||||||
|
filters={'label': expected_labels})
|
||||||
|
|
||||||
def test_get_volumes_from_container(self):
|
def test_get_volumes_from_container(self):
|
||||||
container_id = 'aabbccddee'
|
container_id = 'aabbccddee'
|
||||||
|
@ -156,7 +147,7 @@ class ServiceTest(unittest.TestCase):
|
||||||
def test_split_domainname_none(self):
|
def test_split_domainname_none(self):
|
||||||
service = Service('foo', image='foo', hostname='name', client=self.mock_client)
|
service = Service('foo', image='foo', hostname='name', client=self.mock_client)
|
||||||
self.mock_client.containers.return_value = []
|
self.mock_client.containers.return_value = []
|
||||||
opts = service._get_container_create_options({'image': 'foo'})
|
opts = service._get_container_create_options({'image': 'foo'}, 1)
|
||||||
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
||||||
self.assertFalse('domainname' in opts, 'domainname')
|
self.assertFalse('domainname' in opts, 'domainname')
|
||||||
|
|
||||||
|
@ -167,7 +158,7 @@ class ServiceTest(unittest.TestCase):
|
||||||
image='foo',
|
image='foo',
|
||||||
client=self.mock_client)
|
client=self.mock_client)
|
||||||
self.mock_client.containers.return_value = []
|
self.mock_client.containers.return_value = []
|
||||||
opts = service._get_container_create_options({'image': 'foo'})
|
opts = service._get_container_create_options({'image': 'foo'}, 1)
|
||||||
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
||||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||||
|
|
||||||
|
@ -179,7 +170,7 @@ class ServiceTest(unittest.TestCase):
|
||||||
domainname='domain.tld',
|
domainname='domain.tld',
|
||||||
client=self.mock_client)
|
client=self.mock_client)
|
||||||
self.mock_client.containers.return_value = []
|
self.mock_client.containers.return_value = []
|
||||||
opts = service._get_container_create_options({'image': 'foo'})
|
opts = service._get_container_create_options({'image': 'foo'}, 1)
|
||||||
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
||||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||||
|
|
||||||
|
@ -191,7 +182,7 @@ class ServiceTest(unittest.TestCase):
|
||||||
image='foo',
|
image='foo',
|
||||||
client=self.mock_client)
|
client=self.mock_client)
|
||||||
self.mock_client.containers.return_value = []
|
self.mock_client.containers.return_value = []
|
||||||
opts = service._get_container_create_options({'image': 'foo'})
|
opts = service._get_container_create_options({'image': 'foo'}, 1)
|
||||||
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
|
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
|
||||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||||
|
|
||||||
|
@ -255,7 +246,7 @@ class ServiceTest(unittest.TestCase):
|
||||||
tag='sometag',
|
tag='sometag',
|
||||||
insecure_registry=True,
|
insecure_registry=True,
|
||||||
stream=True)
|
stream=True)
|
||||||
mock_log.info.assert_called_once_with(
|
mock_log.info.assert_called_with(
|
||||||
'Pulling foo (someimage:sometag)...')
|
'Pulling foo (someimage:sometag)...')
|
||||||
|
|
||||||
@mock.patch('compose.service.Container', autospec=True)
|
@mock.patch('compose.service.Container', autospec=True)
|
||||||
|
|
Loading…
Reference in New Issue