Preserve container numbers, add slug to prevent name collisions

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2018-08-31 16:18:19 -07:00
parent 4e2de3c1ff
commit 5916639383
11 changed files with 157 additions and 148 deletions

View File

@ -474,16 +474,15 @@ class TopLevelCommand(object):
-u, --user USER Run the command as this user. -u, --user USER Run the command as this user.
-T Disable pseudo-tty allocation. By default `docker-compose exec` -T Disable pseudo-tty allocation. By default `docker-compose exec`
allocates a TTY. allocates a TTY.
--index=index "index" of the container if there are multiple --index=index index of the container if there are multiple
instances of a service. If missing, Compose will pick an instances of a service [default: 1]
arbitrary container.
-e, --env KEY=VAL Set environment variables (can be used multiple times, -e, --env KEY=VAL Set environment variables (can be used multiple times,
not supported in API < 1.25) not supported in API < 1.25)
-w, --workdir DIR Path to workdir directory for this command. -w, --workdir DIR Path to workdir directory for this command.
""" """
environment = Environment.from_env_file(self.project_dir) environment = Environment.from_env_file(self.project_dir)
use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
index = options.get('--index') index = int(options.get('--index'))
service = self.project.get_service(options['SERVICE']) service = self.project.get_service(options['SERVICE'])
detach = options.get('--detach') detach = options.get('--detach')
@ -660,11 +659,10 @@ class TopLevelCommand(object):
Options: Options:
--protocol=proto tcp or udp [default: tcp] --protocol=proto tcp or udp [default: tcp]
--index=index "index" of the container if there are multiple --index=index index of the container if there are multiple
instances of a service. If missing, Compose will pick an instances of a service [default: 1]
arbitrary container.
""" """
index = options.get('--index') index = int(options.get('--index'))
service = self.project.get_service(options['SERVICE']) service = self.project.get_service(options['SERVICE'])
try: try:
container = service.get_container(number=index) container = service.get_container(number=index)

View File

@ -15,6 +15,7 @@ LABEL_PROJECT = 'com.docker.compose.project'
LABEL_SERVICE = 'com.docker.compose.service' LABEL_SERVICE = 'com.docker.compose.service'
LABEL_NETWORK = 'com.docker.compose.network' LABEL_NETWORK = 'com.docker.compose.network'
LABEL_VERSION = 'com.docker.compose.version' LABEL_VERSION = 'com.docker.compose.version'
LABEL_SLUG = 'com.docker.compose.slug'
LABEL_VOLUME = 'com.docker.compose.volume' LABEL_VOLUME = 'com.docker.compose.volume'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
NANOCPUS_SCALE = 1000000000 NANOCPUS_SCALE = 1000000000

View File

@ -9,6 +9,7 @@ from docker.errors import ImageNotFound
from .const import LABEL_CONTAINER_NUMBER from .const import LABEL_CONTAINER_NUMBER
from .const import LABEL_PROJECT from .const import LABEL_PROJECT
from .const import LABEL_SERVICE from .const import LABEL_SERVICE
from .const import LABEL_SLUG
from .const import LABEL_VERSION from .const import LABEL_VERSION
from .utils import truncate_id from .utils import truncate_id
from .version import ComposeVersion from .version import ComposeVersion
@ -81,7 +82,7 @@ class Container(object):
@property @property
def name_without_project(self): def name_without_project(self):
if self.name.startswith('{0}_{1}'.format(self.project, self.service)): if self.name.startswith('{0}_{1}'.format(self.project, self.service)):
return '{0}_{1}'.format(self.service, self.short_number) return '{0}_{1}{2}'.format(self.service, self.number, '_' + self.slug if self.slug else '')
else: else:
return self.name return self.name
@ -91,11 +92,15 @@ class Container(object):
if not number: if not number:
raise ValueError("Container {0} does not have a {1} label".format( raise ValueError("Container {0} does not have a {1} label".format(
self.short_id, LABEL_CONTAINER_NUMBER)) self.short_id, LABEL_CONTAINER_NUMBER))
return number return int(number)
@property @property
def short_number(self): def slug(self):
return truncate_id(self.number) return truncate_id(self.full_slug)
@property
def full_slug(self):
return self.labels.get(LABEL_SLUG)
@property @property
def ports(self): def ports(self):

View File

@ -31,7 +31,6 @@ from .service import ConvergenceStrategy
from .service import NetworkMode from .service import NetworkMode
from .service import PidMode from .service import PidMode
from .service import Service from .service import Service
from .service import ServiceName
from .service import ServiceNetworkMode from .service import ServiceNetworkMode
from .service import ServicePidMode from .service import ServicePidMode
from .utils import microseconds_from_time_nano from .utils import microseconds_from_time_nano
@ -198,25 +197,6 @@ class Project(object):
service.remove_duplicate_containers() service.remove_duplicate_containers()
return services return services
def get_scaled_services(self, services, scale_override):
"""
Returns a list of this project's services as scaled ServiceName objects.
services: a list of Service objects
scale_override: a dict with the scale to apply to each service (k: service_name, v: scale)
"""
service_names = []
for service in services:
if service.name in scale_override:
scale = scale_override[service.name]
else:
scale = service.scale_num
for i in range(1, scale + 1):
service_names.append(ServiceName(self.name, service.name, i))
return service_names
def get_links(self, service_dict): def get_links(self, service_dict):
links = [] links = []
if 'links' in service_dict: if 'links' in service_dict:
@ -494,7 +474,6 @@ class Project(object):
svc.ensure_image_exists(do_build=do_build, silent=silent) svc.ensure_image_exists(do_build=do_build, silent=silent)
plans = self._get_convergence_plans( plans = self._get_convergence_plans(
services, strategy, always_recreate_deps=always_recreate_deps) services, strategy, always_recreate_deps=always_recreate_deps)
scaled_services = self.get_scaled_services(services, scale_override)
def do(service): def do(service):
@ -505,7 +484,6 @@ class Project(object):
scale_override=scale_override.get(service.name), scale_override=scale_override.get(service.name),
rescale=rescale, rescale=rescale,
start=start, start=start,
project_services=scaled_services,
reset_container_image=reset_container_image, reset_container_image=reset_container_image,
renew_anonymous_volumes=renew_anonymous_volumes, renew_anonymous_volumes=renew_anonymous_volumes,
) )

View File

@ -1,6 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools
import logging import logging
import os import os
import re import re
@ -39,6 +40,7 @@ from .const import LABEL_CONTAINER_NUMBER
from .const import LABEL_ONE_OFF from .const import LABEL_ONE_OFF
from .const import LABEL_PROJECT from .const import LABEL_PROJECT
from .const import LABEL_SERVICE from .const import LABEL_SERVICE
from .const import LABEL_SLUG
from .const import LABEL_VERSION from .const import LABEL_VERSION
from .const import NANOCPUS_SCALE from .const import NANOCPUS_SCALE
from .container import Container from .container import Container
@ -123,7 +125,7 @@ class NoSuchImageError(Exception):
pass pass
ServiceName = namedtuple('ServiceName', 'project service number') ServiceName = namedtuple('ServiceName', 'project service number slug')
ConvergencePlan = namedtuple('ConvergencePlan', 'action containers') ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
@ -216,17 +218,12 @@ class Service(object):
) )
) )
def get_container(self, number=None): 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`.
""" """
if number is not None and len(number) == 64: for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]):
for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]): return container
return container
else:
for container in self.containers():
if number is None or container.number.startswith(number):
return container
raise ValueError("No container found for %s_%s" % (self.name, number)) raise ValueError("No container found for %s_%s" % (self.name, number))
@ -430,28 +427,33 @@ class Service(object):
return has_diverged return has_diverged
def _execute_convergence_create(self, scale, detached, start, project_services=None): def _execute_convergence_create(self, scale, detached, start):
def create_and_start(service, n): i = self._next_container_number()
container = service.create_container(number=n, quiet=True)
if not detached:
container.attach_log_stream()
if start:
self.start_container(container)
return container
containers, errors = parallel_execute( def create_and_start(service, n):
[ServiceName(self.project, self.name, number) for number in [ container = service.create_container(number=n, quiet=True)
self._next_container_number() for _ in range(scale) if not detached:
]], container.attach_log_stream()
lambda service_name: create_and_start(self, service_name.number), if start:
lambda service_name: self.get_container_name(service_name.service, service_name.number), self.start_container(container)
"Creating" return container
)
for error in errors.values():
raise OperationFailedError(error)
return containers containers, errors = parallel_execute(
[
ServiceName(self.project, self.name, index, generate_random_id())
for index in range(i, i + scale)
],
lambda service_name: create_and_start(self, service_name.number),
lambda service_name: self.get_container_name(
service_name.service, service_name.number, service_name.slug
),
"Creating"
)
for error in errors.values():
raise OperationFailedError(error)
return containers
def _execute_convergence_recreate(self, containers, scale, timeout, detached, start, def _execute_convergence_recreate(self, containers, scale, timeout, detached, start,
renew_anonymous_volumes): renew_anonymous_volumes):
@ -514,8 +516,8 @@ class Service(object):
def execute_convergence_plan(self, plan, timeout=None, detached=False, def execute_convergence_plan(self, plan, timeout=None, detached=False,
start=True, scale_override=None, start=True, scale_override=None,
rescale=True, project_services=None, rescale=True, reset_container_image=False,
reset_container_image=False, renew_anonymous_volumes=False): renew_anonymous_volumes=False):
(action, containers) = plan (action, containers) = plan
scale = scale_override if scale_override is not None else self.scale_num scale = scale_override if scale_override is not None else self.scale_num
containers = sorted(containers, key=attrgetter('number')) containers = sorted(containers, key=attrgetter('number'))
@ -524,7 +526,7 @@ class Service(object):
if action == 'create': if action == 'create':
return self._execute_convergence_create( return self._execute_convergence_create(
scale, detached, start, project_services scale, detached, start
) )
# The create action needs always needs an initial scale, but otherwise, # The create action needs always needs an initial scale, but otherwise,
@ -730,7 +732,17 @@ class Service(object):
return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)] return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)]
def _next_container_number(self, one_off=False): def _next_container_number(self, one_off=False):
return generate_random_id() containers = itertools.chain(
self._fetch_containers(
all=True,
filters={'label': self.labels(one_off=one_off)}
), self._fetch_containers(
all=True,
filters={'label': self.labels(one_off=one_off, legacy=True)}
)
)
numbers = [c.number for c in containers]
return 1 if not numbers else max(numbers) + 1
def _fetch_containers(self, **fetch_options): def _fetch_containers(self, **fetch_options):
# Account for containers that might have been removed since we fetched # Account for containers that might have been removed since we fetched
@ -807,6 +819,7 @@ class Service(object):
one_off=False, one_off=False,
previous_container=None): previous_container=None):
add_config_hash = (not one_off and not override_options) add_config_hash = (not one_off and not override_options)
slug = generate_random_id() if previous_container is None else previous_container.full_slug
container_options = dict( container_options = dict(
(k, self.options[k]) (k, self.options[k])
@ -815,7 +828,7 @@ class Service(object):
container_options.update(override_options) container_options.update(override_options)
if not container_options.get('name'): if not container_options.get('name'):
container_options['name'] = self.get_container_name(self.name, number, one_off) container_options['name'] = self.get_container_name(self.name, number, slug, one_off)
container_options.setdefault('detach', True) container_options.setdefault('detach', True)
@ -867,7 +880,9 @@ class Service(object):
container_options.get('labels', {}), container_options.get('labels', {}),
self.labels(one_off=one_off), self.labels(one_off=one_off),
number, number,
self.config_hash if add_config_hash else None) self.config_hash if add_config_hash else None,
slug
)
# Delete options which are only used in HostConfig # Delete options which are only used in HostConfig
for key in HOST_CONFIG_KEYS: for key in HOST_CONFIG_KEYS:
@ -1105,12 +1120,12 @@ class Service(object):
def custom_container_name(self): def custom_container_name(self):
return self.options.get('container_name') return self.options.get('container_name')
def get_container_name(self, service_name, number, one_off=False): def get_container_name(self, service_name, number, slug, one_off=False):
if self.custom_container_name and not one_off: if self.custom_container_name and not one_off:
return self.custom_container_name return self.custom_container_name
container_name = build_container_name( container_name = build_container_name(
self.project, service_name, number, one_off, self.project, service_name, number, slug, one_off,
) )
ext_links_origins = [l.split(':')[0] for l in self.options.get('external_links', [])] ext_links_origins = [l.split(':')[0] for l in self.options.get('external_links', [])]
if container_name in ext_links_origins: if container_name in ext_links_origins:
@ -1367,11 +1382,13 @@ class ServiceNetworkMode(object):
# Names # Names
def build_container_name(project, service, number, one_off=False): def build_container_name(project, service, number, slug, one_off=False):
bits = [project.lstrip('-_'), service] bits = [project.lstrip('-_'), service]
if one_off: if one_off:
bits.append('run') bits.append('run')
return '_'.join(bits + [truncate_id(number)]) return '_'.join(
bits + ([str(number), truncate_id(slug)] if slug else [str(number)])
)
# Images # Images
@ -1552,10 +1569,11 @@ def build_mount(mount_spec):
# Labels # Labels
def build_container_labels(label_options, service_labels, number, config_hash): def build_container_labels(label_options, service_labels, number, config_hash, slug):
labels = dict(label_options or {}) labels = dict(label_options or {})
labels.update(label.split('=', 1) for label in service_labels) labels.update(label.split('=', 1) for label in service_labels)
labels[LABEL_CONTAINER_NUMBER] = str(number) labels[LABEL_CONTAINER_NUMBER] = str(number)
labels[LABEL_SLUG] = slug
labels[LABEL_VERSION] = __version__ labels[LABEL_VERSION] = __version__
if config_hash: if config_hash:

View File

@ -50,7 +50,6 @@ class Version(namedtuple('_Version', 'major minor patch stage edition')):
stage = None stage = None
elif '-' in stage: elif '-' in stage:
edition, stage = stage.split('-') edition, stage = stage.split('-')
major, minor, patch = version.split('.', 3) major, minor, patch = version.split('.', 3)
return cls(major, minor, patch, stage, edition) return cls(major, minor, patch, stage, edition)

View File

@ -547,16 +547,16 @@ class CLITestCase(DockerClientTestCase):
def test_ps(self): def test_ps(self):
self.project.get_service('simple').create_container() self.project.get_service('simple').create_container()
result = self.dispatch(['ps']) result = self.dispatch(['ps'])
assert 'simple-composefile_simple_' in result.stdout assert 'simple-composefile_simple_1' in result.stdout
def test_ps_default_composefile(self): def test_ps_default_composefile(self):
self.base_dir = 'tests/fixtures/multiple-composefiles' self.base_dir = 'tests/fixtures/multiple-composefiles'
self.dispatch(['up', '-d']) self.dispatch(['up', '-d'])
result = self.dispatch(['ps']) result = self.dispatch(['ps'])
assert 'multiple-composefiles_simple_' in result.stdout assert 'multiple-composefiles_simple_1' in result.stdout
assert 'multiple-composefiles_another_' in result.stdout assert 'multiple-composefiles_another_1' in result.stdout
assert 'multiple-composefiles_yetanother_' not in result.stdout assert 'multiple-composefiles_yetanother_1' not in result.stdout
def test_ps_alternate_composefile(self): def test_ps_alternate_composefile(self):
config_path = os.path.abspath( config_path = os.path.abspath(
@ -567,9 +567,9 @@ class CLITestCase(DockerClientTestCase):
self.dispatch(['-f', 'compose2.yml', 'up', '-d']) self.dispatch(['-f', 'compose2.yml', 'up', '-d'])
result = self.dispatch(['-f', 'compose2.yml', 'ps']) result = self.dispatch(['-f', 'compose2.yml', 'ps'])
assert 'multiple-composefiles_simple_' not in result.stdout assert 'multiple-composefiles_simple_1' not in result.stdout
assert 'multiple-composefiles_another_' not in result.stdout assert 'multiple-composefiles_another_1' not in result.stdout
assert 'multiple-composefiles_yetanother_' in result.stdout assert 'multiple-composefiles_yetanother_1' in result.stdout
def test_ps_services_filter_option(self): def test_ps_services_filter_option(self):
self.base_dir = 'tests/fixtures/ps-services-filter' self.base_dir = 'tests/fixtures/ps-services-filter'
@ -963,13 +963,13 @@ class CLITestCase(DockerClientTestCase):
assert len(self.project.containers(one_off=OneOffFilter.only, stopped=True)) == 2 assert len(self.project.containers(one_off=OneOffFilter.only, stopped=True)) == 2
result = self.dispatch(['down', '--rmi=local', '--volumes']) result = self.dispatch(['down', '--rmi=local', '--volumes'])
assert 'Stopping v2-full_web_' in result.stderr assert 'Stopping v2-full_web_1' in result.stderr
assert 'Stopping v2-full_other_' in result.stderr assert 'Stopping v2-full_other_1' in result.stderr
assert 'Stopping v2-full_web_run_' in result.stderr assert 'Stopping v2-full_web_run_2' in result.stderr
assert 'Removing v2-full_web_' in result.stderr assert 'Removing v2-full_web_1' in result.stderr
assert 'Removing v2-full_other_' in result.stderr assert 'Removing v2-full_other_1' in result.stderr
assert 'Removing v2-full_web_run_' in result.stderr assert 'Removing v2-full_web_run_1' in result.stderr
assert 'Removing v2-full_web_run_' in result.stderr assert 'Removing v2-full_web_run_2' in result.stderr
assert 'Removing volume v2-full_data' in result.stderr assert 'Removing volume v2-full_data' in result.stderr
assert 'Removing image v2-full_web' in result.stderr assert 'Removing image v2-full_web' in result.stderr
assert 'Removing image busybox' not in result.stderr assert 'Removing image busybox' not in result.stderr
@ -1026,13 +1026,15 @@ class CLITestCase(DockerClientTestCase):
def test_up_attached(self): def test_up_attached(self):
self.base_dir = 'tests/fixtures/echo-services' self.base_dir = 'tests/fixtures/echo-services'
result = self.dispatch(['up', '--no-color']) result = self.dispatch(['up', '--no-color'])
simple_num = self.project.get_service('simple').containers(stopped=True)[0].short_number simple_name = self.project.get_service('simple').containers(stopped=True)[0].name_without_project
another_num = self.project.get_service('another').containers(stopped=True)[0].short_number another_name = self.project.get_service('another').containers(
stopped=True
)[0].name_without_project
assert 'simple_{} | simple'.format(simple_num) in result.stdout assert '{} | simple'.format(simple_name) in result.stdout
assert 'another_{} | another'.format(another_num) in result.stdout assert '{} | another'.format(another_name) in result.stdout
assert 'simple_{} exited with code 0'.format(simple_num) in result.stdout assert '{} exited with code 0'.format(simple_name) in result.stdout
assert 'another_{} exited with code 0'.format(another_num) in result.stdout assert '{} exited with code 0'.format(another_name) in result.stdout
@v2_only() @v2_only()
def test_up(self): def test_up(self):
@ -2296,24 +2298,24 @@ class CLITestCase(DockerClientTestCase):
proc = start_process(self.base_dir, ['logs', '-f']) proc = start_process(self.base_dir, ['logs', '-f'])
self.dispatch(['up', '-d', 'another']) self.dispatch(['up', '-d', 'another'])
another_num = self.project.get_service('another').get_container().short_number another_name = self.project.get_service('another').get_container().name_without_project
wait_on_condition( wait_on_condition(
ContainerStateCondition( ContainerStateCondition(
self.project.client, self.project.client,
'logs-composefile_another_{}'.format(another_num), 'logs-composefile_another_*',
'exited' 'exited'
) )
) )
simple_num = self.project.get_service('simple').get_container().short_number simple_name = self.project.get_service('simple').get_container().name_without_project
self.dispatch(['kill', 'simple']) self.dispatch(['kill', 'simple'])
result = wait_on_process(proc) result = wait_on_process(proc)
assert 'hello' in result.stdout assert 'hello' in result.stdout
assert 'test' in result.stdout assert 'test' in result.stdout
assert 'logs-composefile_another_{} exited with code 0'.format(another_num) in result.stdout assert '{} exited with code 0'.format(another_name) in result.stdout
assert 'logs-composefile_simple_{} exited with code 137'.format(simple_num) in result.stdout assert '{} exited with code 137'.format(simple_name) in result.stdout
def test_logs_follow_logs_from_restarted_containers(self): def test_logs_follow_logs_from_restarted_containers(self):
self.base_dir = 'tests/fixtures/logs-restart-composefile' self.base_dir = 'tests/fixtures/logs-restart-composefile'
@ -2331,7 +2333,7 @@ class CLITestCase(DockerClientTestCase):
result = wait_on_process(proc) result = wait_on_process(proc)
assert len(re.findall( assert len(re.findall(
r'logs-restart-composefile_another_[a-f0-9]{12} exited with code 1', r'logs-restart-composefile_another_1_[a-f0-9]{12} exited with code 1',
result.stdout result.stdout
)) == 3 )) == 3
assert result.stdout.count('world') == 3 assert result.stdout.count('world') == 3
@ -2663,10 +2665,10 @@ class CLITestCase(DockerClientTestCase):
assert len(containers) == 2 assert len(containers) == 2
web = containers[1] web = containers[1]
db_num = containers[0].short_number db_name = containers[0].name_without_project
assert set(get_links(web)) == set( assert set(get_links(web)) == set(
['db', 'mydb_{}'.format(db_num), 'extends_mydb_{}'.format(db_num)] ['db', db_name, 'extends_{}'.format(db_name)]
) )
expected_env = set([ expected_env = set([
@ -2704,7 +2706,7 @@ class CLITestCase(DockerClientTestCase):
) )
result = wait_on_process(proc, returncode=1) result = wait_on_process(proc, returncode=1)
assert re.findall(r'exit-code-from_another_[a-f0-9]{12} exited with code 1', result.stdout) assert re.findall(r'exit-code-from_another_1_[a-f0-9]{12} exited with code 1', result.stdout)
def test_exit_code_from_signal_stop(self): def test_exit_code_from_signal_stop(self):
self.base_dir = 'tests/fixtures/exit-code-from' self.base_dir = 'tests/fixtures/exit-code-from'
@ -2713,8 +2715,8 @@ class CLITestCase(DockerClientTestCase):
['up', '--abort-on-container-exit', '--exit-code-from', 'simple'] ['up', '--abort-on-container-exit', '--exit-code-from', 'simple']
) )
result = wait_on_process(proc, returncode=137) # SIGKILL result = wait_on_process(proc, returncode=137) # SIGKILL
num = self.project.get_service('another').containers(stopped=True)[0].short_number name = self.project.get_service('another').containers(stopped=True)[0].name_without_project
assert 'exit-code-from_another_{} exited with code 1'.format(num) in result.stdout assert '{} exited with code 1'.format(name) in result.stdout
def test_images(self): def test_images(self):
self.project.get_service('simple').create_container() self.project.get_service('simple').create_container()
@ -2728,8 +2730,8 @@ class CLITestCase(DockerClientTestCase):
result = self.dispatch(['images']) result = self.dispatch(['images'])
assert 'busybox' in result.stdout assert 'busybox' in result.stdout
assert 'multiple-composefiles_another_' in result.stdout assert 'multiple-composefiles_another_1' in result.stdout
assert 'multiple-composefiles_simple_' in result.stdout assert 'multiple-composefiles_simple_1' in result.stdout
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
def test_images_tagless_image(self): def test_images_tagless_image(self):
@ -2749,7 +2751,7 @@ class CLITestCase(DockerClientTestCase):
self.project.get_service('foo').create_container() self.project.get_service('foo').create_container()
result = self.dispatch(['images']) result = self.dispatch(['images'])
assert '<none>' in result.stdout assert '<none>' in result.stdout
assert 'tagless-image_foo_' in result.stdout assert 'tagless-image_foo_1' in result.stdout
def test_up_with_override_yaml(self): def test_up_with_override_yaml(self):
self.base_dir = 'tests/fixtures/override-yaml-files' self.base_dir = 'tests/fixtures/override-yaml-files'

View File

@ -32,6 +32,7 @@ from compose.const import LABEL_CONTAINER_NUMBER
from compose.const import LABEL_ONE_OFF from compose.const import LABEL_ONE_OFF
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
from compose.const import LABEL_SERVICE from compose.const import LABEL_SERVICE
from compose.const import LABEL_SLUG
from compose.const import LABEL_VERSION from compose.const import LABEL_VERSION
from compose.container import Container from compose.container import Container
from compose.errors import OperationFailedError from compose.errors import OperationFailedError
@ -867,17 +868,17 @@ class ServiceTest(DockerClientTestCase):
db_ctnrs = [create_and_start_container(db) for _ in range(3)] db_ctnrs = [create_and_start_container(db) for _ in range(3)]
web = self.create_service( web = self.create_service(
'web', external_links=[ 'web', external_links=[
'composetest_db_{}'.format(db_ctnrs[0].short_number), db_ctnrs[0].name,
'composetest_db_{}'.format(db_ctnrs[1].short_number), db_ctnrs[1].name,
'composetest_db_{}:db_3'.format(db_ctnrs[2].short_number) '{}:db_3'.format(db_ctnrs[2].name)
] ]
) )
create_and_start_container(web) create_and_start_container(web)
assert set(get_links(web.containers()[0])) == set([ assert set(get_links(web.containers()[0])) == set([
'composetest_db_{}'.format(db_ctnrs[0].short_number), db_ctnrs[0].name,
'composetest_db_{}'.format(db_ctnrs[1].short_number), db_ctnrs[1].name,
'db_3' 'db_3'
]) ])
@ -1584,6 +1585,7 @@ class ServiceTest(DockerClientTestCase):
LABEL_PROJECT: 'composetest', LABEL_PROJECT: 'composetest',
LABEL_SERVICE: 'web', LABEL_SERVICE: 'web',
LABEL_VERSION: __version__, LABEL_VERSION: __version__,
LABEL_CONTAINER_NUMBER: '1'
} }
expected = dict(labels_dict, **compose_labels) expected = dict(labels_dict, **compose_labels)
@ -1592,7 +1594,7 @@ class ServiceTest(DockerClientTestCase):
labels = ctnr.labels.items() labels = ctnr.labels.items()
for pair in expected.items(): for pair in expected.items():
assert pair in labels assert pair in labels
assert ctnr.labels[LABEL_CONTAINER_NUMBER] == ctnr.number assert ctnr.labels[LABEL_SLUG] == ctnr.full_slug
def test_empty_labels(self): def test_empty_labels(self):
labels_dict = {'foo': '', 'bar': ''} labels_dict = {'foo': '', 'bar': ''}

View File

@ -198,14 +198,14 @@ class ProjectWithDependenciesTest(ProjectTestCase):
db, = [c for c in containers if c.service == 'db'] db, = [c for c in containers if c.service == 'db']
assert set(get_links(web)) == { assert set(get_links(web)) == {
'composetest_db_{}'.format(db.short_number), 'composetest_db_{}_{}'.format(db.number, db.slug),
'db', 'db',
'db_{}'.format(db.short_number) 'db_{}_{}'.format(db.number, db.slug)
} }
assert set(get_links(nginx)) == { assert set(get_links(nginx)) == {
'composetest_web_{}'.format(web.short_number), 'composetest_web_{}_{}'.format(web.number, web.slug),
'web', 'web',
'web_{}'.format(web.short_number) 'web_{}_{}'.format(web.number, web.slug)
} }

View File

@ -30,7 +30,8 @@ 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": "092cd63296fdc446ad432d3905dd1fcbe12a2ba6b52", "com.docker.compose.container-number": "7",
"com.docker.compose.slug": "092cd63296fdc446ad432d3905dd1fcbe12a2ba6b52"
}, },
} }
} }
@ -77,7 +78,7 @@ class ContainerTest(unittest.TestCase):
def test_number(self): def test_number(self):
container = Container(None, self.container_dict, has_been_inspected=True) container = Container(None, self.container_dict, has_been_inspected=True)
assert container.number == "092cd63296fdc446ad432d3905dd1fcbe12a2ba6b52" assert container.number == 7
def test_name(self): def test_name(self):
container = Container.from_ps(None, container = Container.from_ps(None,
@ -88,7 +89,7 @@ class ContainerTest(unittest.TestCase):
def test_name_without_project(self): def test_name_without_project(self):
self.container_dict['Name'] = "/composetest_web_7" self.container_dict['Name'] = "/composetest_web_7"
container = Container(None, self.container_dict, has_been_inspected=True) container = Container(None, self.container_dict, has_been_inspected=True)
assert container.name_without_project == "web_092cd63296fd" assert container.name_without_project == "web_7_092cd63296fd"
def test_name_without_project_custom_container_name(self): def test_name_without_project_custom_container_name(self):
self.container_dict['Name'] = "/custom_name_of_container" self.container_dict['Name'] = "/custom_name_of_container"

View File

@ -41,7 +41,6 @@ from compose.service import parse_repository_tag
from compose.service import Service from compose.service import Service
from compose.service import ServiceNetworkMode from compose.service import ServiceNetworkMode
from compose.service import warn_on_masked_volume from compose.service import warn_on_masked_volume
from compose.utils import generate_random_id as generate_id
class ServiceTest(unittest.TestCase): class ServiceTest(unittest.TestCase):
@ -82,7 +81,8 @@ class ServiceTest(unittest.TestCase):
service = Service('db', self.mock_client, 'myproject', image='foo') service = Service('db', self.mock_client, 'myproject', image='foo')
assert [c.id for c in service.containers()] == ['1'] assert [c.id for c in service.containers()] == ['1']
assert service.get_container().id == '1' assert service._next_container_number() == 2
assert service.get_container(1).id == '1'
def test_get_volumes_from_container(self): def test_get_volumes_from_container(self):
container_id = 'aabbccddee' container_id = 'aabbccddee'
@ -164,7 +164,7 @@ class ServiceTest(unittest.TestCase):
client=self.mock_client, client=self.mock_client,
mem_limit=1000000000, mem_limit=1000000000,
memswap_limit=2000000000) memswap_limit=2000000000)
service._get_container_create_options({'some': 'overrides'}, generate_id()) service._get_container_create_options({'some': 'overrides'}, 1)
assert self.mock_client.create_host_config.called assert self.mock_client.create_host_config.called
assert self.mock_client.create_host_config.call_args[1]['mem_limit'] == 1000000000 assert self.mock_client.create_host_config.call_args[1]['mem_limit'] == 1000000000
@ -173,10 +173,10 @@ class ServiceTest(unittest.TestCase):
def test_self_reference_external_link(self): def test_self_reference_external_link(self):
service = Service( service = Service(
name='foo', name='foo',
external_links=['default_foo_bdfa3ed91e2c'] external_links=['default_foo_1_bdfa3ed91e2c']
) )
with pytest.raises(DependencyError): with pytest.raises(DependencyError):
service.get_container_name('foo', 'bdfa3ed91e2c') service.get_container_name('foo', 1, 'bdfa3ed91e2c')
def test_mem_reservation(self): def test_mem_reservation(self):
self.mock_client.create_host_config.return_value = {} self.mock_client.create_host_config.return_value = {}
@ -188,7 +188,7 @@ class ServiceTest(unittest.TestCase):
client=self.mock_client, client=self.mock_client,
mem_reservation='512m' mem_reservation='512m'
) )
service._get_container_create_options({'some': 'overrides'}, generate_id()) service._get_container_create_options({'some': 'overrides'}, 1)
assert self.mock_client.create_host_config.called is True assert self.mock_client.create_host_config.called is True
assert self.mock_client.create_host_config.call_args[1]['mem_reservation'] == '512m' assert self.mock_client.create_host_config.call_args[1]['mem_reservation'] == '512m'
@ -201,7 +201,7 @@ class ServiceTest(unittest.TestCase):
hostname='name', hostname='name',
client=self.mock_client, client=self.mock_client,
cgroup_parent='test') cgroup_parent='test')
service._get_container_create_options({'some': 'overrides'}, generate_id()) service._get_container_create_options({'some': 'overrides'}, 1)
assert self.mock_client.create_host_config.called assert self.mock_client.create_host_config.called
assert self.mock_client.create_host_config.call_args[1]['cgroup_parent'] == 'test' assert self.mock_client.create_host_config.call_args[1]['cgroup_parent'] == 'test'
@ -218,7 +218,7 @@ class ServiceTest(unittest.TestCase):
client=self.mock_client, client=self.mock_client,
log_driver='syslog', log_driver='syslog',
logging=logging) logging=logging)
service._get_container_create_options({'some': 'overrides'}, generate_id()) service._get_container_create_options({'some': 'overrides'}, 1)
assert self.mock_client.create_host_config.called assert self.mock_client.create_host_config.called
assert self.mock_client.create_host_config.call_args[1]['log_config'] == { assert self.mock_client.create_host_config.call_args[1]['log_config'] == {
@ -233,7 +233,7 @@ class ServiceTest(unittest.TestCase):
image='foo', image='foo',
client=self.mock_client, client=self.mock_client,
stop_grace_period="1m35s") stop_grace_period="1m35s")
opts = service._get_container_create_options({'image': 'foo'}, generate_id()) opts = service._get_container_create_options({'image': 'foo'}, 1)
assert opts['stop_timeout'] == 95 assert opts['stop_timeout'] == 95
def test_split_domainname_none(self): def test_split_domainname_none(self):
@ -242,7 +242,7 @@ class ServiceTest(unittest.TestCase):
image='foo', image='foo',
hostname='name.domain.tld', hostname='name.domain.tld',
client=self.mock_client) client=self.mock_client)
opts = service._get_container_create_options({'image': 'foo'}, generate_id()) opts = service._get_container_create_options({'image': 'foo'}, 1)
assert opts['hostname'] == 'name.domain.tld', 'hostname' assert opts['hostname'] == 'name.domain.tld', 'hostname'
assert not ('domainname' in opts), 'domainname' assert not ('domainname' in opts), 'domainname'
@ -253,7 +253,7 @@ class ServiceTest(unittest.TestCase):
hostname='name.domain.tld', hostname='name.domain.tld',
image='foo', image='foo',
client=self.mock_client) client=self.mock_client)
opts = service._get_container_create_options({'image': 'foo'}, generate_id()) opts = service._get_container_create_options({'image': 'foo'}, 1)
assert opts['hostname'] == 'name', 'hostname' assert opts['hostname'] == 'name', 'hostname'
assert opts['domainname'] == 'domain.tld', 'domainname' assert opts['domainname'] == 'domain.tld', 'domainname'
@ -265,7 +265,7 @@ class ServiceTest(unittest.TestCase):
image='foo', image='foo',
domainname='domain.tld', domainname='domain.tld',
client=self.mock_client) client=self.mock_client)
opts = service._get_container_create_options({'image': 'foo'}, generate_id()) opts = service._get_container_create_options({'image': 'foo'}, 1)
assert opts['hostname'] == 'name', 'hostname' assert opts['hostname'] == 'name', 'hostname'
assert opts['domainname'] == 'domain.tld', 'domainname' assert opts['domainname'] == 'domain.tld', 'domainname'
@ -277,7 +277,7 @@ class ServiceTest(unittest.TestCase):
domainname='domain.tld', domainname='domain.tld',
image='foo', image='foo',
client=self.mock_client) client=self.mock_client)
opts = service._get_container_create_options({'image': 'foo'}, generate_id()) opts = service._get_container_create_options({'image': 'foo'}, 1)
assert opts['hostname'] == 'name.sub', 'hostname' assert opts['hostname'] == 'name.sub', 'hostname'
assert opts['domainname'] == 'domain.tld', 'domainname' assert opts['domainname'] == 'domain.tld', 'domainname'
@ -288,7 +288,7 @@ class ServiceTest(unittest.TestCase):
use_networking=False, use_networking=False,
client=self.mock_client, client=self.mock_client,
) )
opts = service._get_container_create_options({'image': 'foo'}, generate_id()) opts = service._get_container_create_options({'image': 'foo'}, 1)
assert opts.get('hostname') is None assert opts.get('hostname') is None
def test_get_container_create_options_with_name_option(self): def test_get_container_create_options_with_name_option(self):
@ -317,11 +317,13 @@ class ServiceTest(unittest.TestCase):
self.mock_client.inspect_image.return_value = {'Id': 'abcd'} self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
prev_container = mock.Mock( prev_container = mock.Mock(
id='ababab', id='ababab',
image_config={'ContainerConfig': {}}) image_config={'ContainerConfig': {}}
)
prev_container.full_slug = 'abcdefff1234'
prev_container.get.return_value = None prev_container.get.return_value = None
opts = service._get_container_create_options( opts = service._get_container_create_options(
{}, generate_id(), previous_container=prev_container {}, 1, previous_container=prev_container
) )
assert service.options['labels'] == labels assert service.options['labels'] == labels
@ -354,11 +356,13 @@ class ServiceTest(unittest.TestCase):
}.get(key, None) }.get(key, None)
prev_container.get.side_effect = container_get prev_container.get.side_effect = container_get
prev_container.full_slug = 'abcdefff1234'
opts = service._get_container_create_options( opts = service._get_container_create_options(
{}, {},
generate_id(), 1,
previous_container=prev_container) previous_container=prev_container
)
assert opts['environment'] == ['affinity:container==ababab'] assert opts['environment'] == ['affinity:container==ababab']
@ -369,10 +373,11 @@ class ServiceTest(unittest.TestCase):
id='ababab', id='ababab',
image_config={'ContainerConfig': {}}) image_config={'ContainerConfig': {}})
prev_container.get.return_value = None prev_container.get.return_value = None
prev_container.full_slug = 'abcdefff1234'
opts = service._get_container_create_options( opts = service._get_container_create_options(
{}, {},
generate_id(), 1,
previous_container=prev_container) previous_container=prev_container)
assert opts['environment'] == [] assert opts['environment'] == []
@ -385,11 +390,11 @@ class ServiceTest(unittest.TestCase):
@mock.patch('compose.service.Container', autospec=True) @mock.patch('compose.service.Container', autospec=True)
def test_get_container(self, mock_container_class): def test_get_container(self, mock_container_class):
container_dict = dict(Name='default_foo_bdfa3ed91e2c') container_dict = dict(Name='default_foo_2_bdfa3ed91e2c')
self.mock_client.containers.return_value = [container_dict] self.mock_client.containers.return_value = [container_dict]
service = Service('foo', image='foo', client=self.mock_client) service = Service('foo', image='foo', client=self.mock_client)
container = service.get_container(number="bdfa3ed91e2c") container = service.get_container(number=2)
assert container == mock_container_class.from_ps.return_value assert container == mock_container_class.from_ps.return_value
mock_container_class.from_ps.assert_called_once_with( mock_container_class.from_ps.assert_called_once_with(
self.mock_client, container_dict) self.mock_client, container_dict)
@ -462,7 +467,7 @@ class ServiceTest(unittest.TestCase):
@mock.patch('compose.service.Container', autospec=True) @mock.patch('compose.service.Container', autospec=True)
def test_recreate_container(self, _): def test_recreate_container(self, _):
mock_container = mock.create_autospec(Container) mock_container = mock.create_autospec(Container)
mock_container.number = generate_id() mock_container.full_slug = 'abcdefff1234'
service = Service('foo', client=self.mock_client, image='someimage') service = Service('foo', client=self.mock_client, image='someimage')
service.image = lambda: {'Id': 'abc123'} service.image = lambda: {'Id': 'abc123'}
new_container = service.recreate_container(mock_container) new_container = service.recreate_container(mock_container)
@ -476,7 +481,7 @@ class ServiceTest(unittest.TestCase):
@mock.patch('compose.service.Container', autospec=True) @mock.patch('compose.service.Container', autospec=True)
def test_recreate_container_with_timeout(self, _): def test_recreate_container_with_timeout(self, _):
mock_container = mock.create_autospec(Container) mock_container = mock.create_autospec(Container)
mock_container.number = generate_id() mock_container.full_slug = 'abcdefff1234'
self.mock_client.inspect_image.return_value = {'Id': 'abc123'} self.mock_client.inspect_image.return_value = {'Id': 'abc123'}
service = Service('foo', client=self.mock_client, image='someimage') service = Service('foo', client=self.mock_client, image='someimage')
service.recreate_container(mock_container, timeout=1) service.recreate_container(mock_container, timeout=1)
@ -713,7 +718,7 @@ class ServiceTest(unittest.TestCase):
for api_version in set(API_VERSIONS.values()): for api_version in set(API_VERSIONS.values()):
self.mock_client.api_version = api_version self.mock_client.api_version = api_version
assert service._get_container_create_options( assert service._get_container_create_options(
{}, generate_id() {}, 1
)['labels'][LABEL_CONFIG_HASH] == config_hash )['labels'][LABEL_CONFIG_HASH] == config_hash
def test_remove_image_none(self): def test_remove_image_none(self):
@ -972,7 +977,7 @@ class ServiceTest(unittest.TestCase):
service = Service('foo', client=self.mock_client, environment=environment) service = Service('foo', client=self.mock_client, environment=environment)
create_opts = service._get_container_create_options(override_options, generate_id()) create_opts = service._get_container_create_options(override_options, 1)
assert set(create_opts['environment']) == set(format_environment({ assert set(create_opts['environment']) == set(format_environment({
'HTTP_PROXY': default_proxy_config['httpProxy'], 'HTTP_PROXY': default_proxy_config['httpProxy'],
'http_proxy': default_proxy_config['httpProxy'], 'http_proxy': default_proxy_config['httpProxy'],
@ -1297,7 +1302,7 @@ class ServiceVolumesTest(unittest.TestCase):
service._get_container_create_options( service._get_container_create_options(
override_options={}, override_options={},
number=generate_id(), number=1,
) )
assert set(self.mock_client.create_host_config.call_args[1]['binds']) == set([ assert set(self.mock_client.create_host_config.call_args[1]['binds']) == set([
@ -1340,7 +1345,7 @@ class ServiceVolumesTest(unittest.TestCase):
service._get_container_create_options( service._get_container_create_options(
override_options={}, override_options={},
number=generate_id(), number=1,
previous_container=Container(self.mock_client, {'Id': '123123123'}), previous_container=Container(self.mock_client, {'Id': '123123123'}),
) )