From 5916639383d334145f0566502f80d4152528a158 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 31 Aug 2018 16:18:19 -0700 Subject: [PATCH] Preserve container numbers, add slug to prevent name collisions Signed-off-by: Joffrey F --- compose/cli/main.py | 14 ++--- compose/const.py | 1 + compose/container.py | 13 +++-- compose/project.py | 22 ------- compose/service.py | 96 ++++++++++++++++++------------- script/test/versions.py | 1 - tests/acceptance/cli_test.py | 70 +++++++++++----------- tests/integration/service_test.py | 14 +++-- tests/integration/state_test.py | 8 +-- tests/unit/container_test.py | 7 ++- tests/unit/service_test.py | 59 ++++++++++--------- 11 files changed, 157 insertions(+), 148 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 2cee9e033..e0acf0711 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -474,16 +474,15 @@ class TopLevelCommand(object): -u, --user USER Run the command as this user. -T Disable pseudo-tty allocation. By default `docker-compose exec` allocates a TTY. - --index=index "index" of the container if there are multiple - instances of a service. If missing, Compose will pick an - arbitrary container. + --index=index index of the container if there are multiple + instances of a service [default: 1] -e, --env KEY=VAL Set environment variables (can be used multiple times, not supported in API < 1.25) -w, --workdir DIR Path to workdir directory for this command. """ environment = Environment.from_env_file(self.project_dir) 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']) detach = options.get('--detach') @@ -660,11 +659,10 @@ class TopLevelCommand(object): Options: --protocol=proto tcp or udp [default: tcp] - --index=index "index" of the container if there are multiple - instances of a service. If missing, Compose will pick an - arbitrary container. + --index=index index of the container if there are multiple + instances of a service [default: 1] """ - index = options.get('--index') + index = int(options.get('--index')) service = self.project.get_service(options['SERVICE']) try: container = service.get_container(number=index) diff --git a/compose/const.py b/compose/const.py index ffb68db01..f4b9489e1 100644 --- a/compose/const.py +++ b/compose/const.py @@ -15,6 +15,7 @@ LABEL_PROJECT = 'com.docker.compose.project' LABEL_SERVICE = 'com.docker.compose.service' LABEL_NETWORK = 'com.docker.compose.network' LABEL_VERSION = 'com.docker.compose.version' +LABEL_SLUG = 'com.docker.compose.slug' LABEL_VOLUME = 'com.docker.compose.volume' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' NANOCPUS_SCALE = 1000000000 diff --git a/compose/container.py b/compose/container.py index 9b5bbba04..3ee45c8f3 100644 --- a/compose/container.py +++ b/compose/container.py @@ -9,6 +9,7 @@ from docker.errors import ImageNotFound from .const import LABEL_CONTAINER_NUMBER from .const import LABEL_PROJECT from .const import LABEL_SERVICE +from .const import LABEL_SLUG from .const import LABEL_VERSION from .utils import truncate_id from .version import ComposeVersion @@ -81,7 +82,7 @@ class Container(object): @property def name_without_project(self): 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: return self.name @@ -91,11 +92,15 @@ class Container(object): if not number: raise ValueError("Container {0} does not have a {1} label".format( self.short_id, LABEL_CONTAINER_NUMBER)) - return number + return int(number) @property - def short_number(self): - return truncate_id(self.number) + def slug(self): + return truncate_id(self.full_slug) + + @property + def full_slug(self): + return self.labels.get(LABEL_SLUG) @property def ports(self): diff --git a/compose/project.py b/compose/project.py index 22ef8be44..4340577c9 100644 --- a/compose/project.py +++ b/compose/project.py @@ -31,7 +31,6 @@ from .service import ConvergenceStrategy from .service import NetworkMode from .service import PidMode from .service import Service -from .service import ServiceName from .service import ServiceNetworkMode from .service import ServicePidMode from .utils import microseconds_from_time_nano @@ -198,25 +197,6 @@ class Project(object): service.remove_duplicate_containers() 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): links = [] if 'links' in service_dict: @@ -494,7 +474,6 @@ class Project(object): svc.ensure_image_exists(do_build=do_build, silent=silent) plans = self._get_convergence_plans( services, strategy, always_recreate_deps=always_recreate_deps) - scaled_services = self.get_scaled_services(services, scale_override) def do(service): @@ -505,7 +484,6 @@ class Project(object): scale_override=scale_override.get(service.name), rescale=rescale, start=start, - project_services=scaled_services, reset_container_image=reset_container_image, renew_anonymous_volumes=renew_anonymous_volumes, ) diff --git a/compose/service.py b/compose/service.py index 5989217d7..199be8f1f 100644 --- a/compose/service.py +++ b/compose/service.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import itertools import logging import os import re @@ -39,6 +40,7 @@ from .const import LABEL_CONTAINER_NUMBER from .const import LABEL_ONE_OFF from .const import LABEL_PROJECT from .const import LABEL_SERVICE +from .const import LABEL_SLUG from .const import LABEL_VERSION from .const import NANOCPUS_SCALE from .container import Container @@ -123,7 +125,7 @@ class NoSuchImageError(Exception): pass -ServiceName = namedtuple('ServiceName', 'project service number') +ServiceName = namedtuple('ServiceName', 'project service number slug') 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 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)]): - return container - else: - for container in self.containers(): - if number is None or container.number.startswith(number): - return container + for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]): + return container raise ValueError("No container found for %s_%s" % (self.name, number)) @@ -430,28 +427,33 @@ class Service(object): 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): - container = service.create_container(number=n, quiet=True) - if not detached: - container.attach_log_stream() - if start: - self.start_container(container) - return container + i = self._next_container_number() - containers, errors = parallel_execute( - [ServiceName(self.project, self.name, number) for number in [ - self._next_container_number() for _ in range(scale) - ]], - lambda service_name: create_and_start(self, service_name.number), - lambda service_name: self.get_container_name(service_name.service, service_name.number), - "Creating" - ) - for error in errors.values(): - raise OperationFailedError(error) + def create_and_start(service, n): + container = service.create_container(number=n, quiet=True) + if not detached: + container.attach_log_stream() + if start: + self.start_container(container) + return container - 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, renew_anonymous_volumes): @@ -514,8 +516,8 @@ class Service(object): def execute_convergence_plan(self, plan, timeout=None, detached=False, start=True, scale_override=None, - rescale=True, project_services=None, - reset_container_image=False, renew_anonymous_volumes=False): + rescale=True, reset_container_image=False, + renew_anonymous_volumes=False): (action, containers) = plan scale = scale_override if scale_override is not None else self.scale_num containers = sorted(containers, key=attrgetter('number')) @@ -524,7 +526,7 @@ class Service(object): if action == '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, @@ -730,7 +732,17 @@ class Service(object): return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)] 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): # Account for containers that might have been removed since we fetched @@ -807,6 +819,7 @@ class Service(object): one_off=False, previous_container=None): 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( (k, self.options[k]) @@ -815,7 +828,7 @@ class Service(object): container_options.update(override_options) 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) @@ -867,7 +880,9 @@ class Service(object): container_options.get('labels', {}), self.labels(one_off=one_off), 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 for key in HOST_CONFIG_KEYS: @@ -1105,12 +1120,12 @@ class Service(object): def custom_container_name(self): 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: return self.custom_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', [])] if container_name in ext_links_origins: @@ -1367,11 +1382,13 @@ class ServiceNetworkMode(object): # 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] if one_off: bits.append('run') - return '_'.join(bits + [truncate_id(number)]) + return '_'.join( + bits + ([str(number), truncate_id(slug)] if slug else [str(number)]) + ) # Images @@ -1552,10 +1569,11 @@ def build_mount(mount_spec): # 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.update(label.split('=', 1) for label in service_labels) labels[LABEL_CONTAINER_NUMBER] = str(number) + labels[LABEL_SLUG] = slug labels[LABEL_VERSION] = __version__ if config_hash: diff --git a/script/test/versions.py b/script/test/versions.py index 0dd27538f..6d273a9e6 100755 --- a/script/test/versions.py +++ b/script/test/versions.py @@ -50,7 +50,6 @@ class Version(namedtuple('_Version', 'major minor patch stage edition')): stage = None elif '-' in stage: edition, stage = stage.split('-') - major, minor, patch = version.split('.', 3) return cls(major, minor, patch, stage, edition) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index a41250d3d..015180bc7 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -547,16 +547,16 @@ class CLITestCase(DockerClientTestCase): def test_ps(self): self.project.get_service('simple').create_container() 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): self.base_dir = 'tests/fixtures/multiple-composefiles' self.dispatch(['up', '-d']) result = self.dispatch(['ps']) - assert 'multiple-composefiles_simple_' in result.stdout - assert 'multiple-composefiles_another_' in result.stdout - assert 'multiple-composefiles_yetanother_' not in result.stdout + assert 'multiple-composefiles_simple_1' in result.stdout + assert 'multiple-composefiles_another_1' in result.stdout + assert 'multiple-composefiles_yetanother_1' not in result.stdout def test_ps_alternate_composefile(self): config_path = os.path.abspath( @@ -567,9 +567,9 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['-f', 'compose2.yml', 'up', '-d']) result = self.dispatch(['-f', 'compose2.yml', 'ps']) - assert 'multiple-composefiles_simple_' not in result.stdout - assert 'multiple-composefiles_another_' not in result.stdout - assert 'multiple-composefiles_yetanother_' in result.stdout + assert 'multiple-composefiles_simple_1' not in result.stdout + assert 'multiple-composefiles_another_1' not in result.stdout + assert 'multiple-composefiles_yetanother_1' in result.stdout def test_ps_services_filter_option(self): 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 result = self.dispatch(['down', '--rmi=local', '--volumes']) - assert 'Stopping v2-full_web_' in result.stderr - assert 'Stopping v2-full_other_' in result.stderr - assert 'Stopping v2-full_web_run_' in result.stderr - assert 'Removing v2-full_web_' in result.stderr - assert 'Removing v2-full_other_' in result.stderr - assert 'Removing v2-full_web_run_' in result.stderr - assert 'Removing v2-full_web_run_' in result.stderr + assert 'Stopping v2-full_web_1' in result.stderr + assert 'Stopping v2-full_other_1' in result.stderr + assert 'Stopping v2-full_web_run_2' in result.stderr + assert 'Removing v2-full_web_1' in result.stderr + assert 'Removing v2-full_other_1' in result.stderr + assert 'Removing v2-full_web_run_1' in result.stderr + assert 'Removing v2-full_web_run_2' in result.stderr assert 'Removing volume v2-full_data' in result.stderr assert 'Removing image v2-full_web' in result.stderr assert 'Removing image busybox' not in result.stderr @@ -1026,13 +1026,15 @@ class CLITestCase(DockerClientTestCase): def test_up_attached(self): self.base_dir = 'tests/fixtures/echo-services' result = self.dispatch(['up', '--no-color']) - simple_num = self.project.get_service('simple').containers(stopped=True)[0].short_number - another_num = self.project.get_service('another').containers(stopped=True)[0].short_number + simple_name = self.project.get_service('simple').containers(stopped=True)[0].name_without_project + another_name = self.project.get_service('another').containers( + stopped=True + )[0].name_without_project - assert 'simple_{} | simple'.format(simple_num) in result.stdout - assert 'another_{} | another'.format(another_num) in result.stdout - assert 'simple_{} exited with code 0'.format(simple_num) in result.stdout - assert 'another_{} exited with code 0'.format(another_num) in result.stdout + assert '{} | simple'.format(simple_name) in result.stdout + assert '{} | another'.format(another_name) in result.stdout + assert '{} exited with code 0'.format(simple_name) in result.stdout + assert '{} exited with code 0'.format(another_name) in result.stdout @v2_only() def test_up(self): @@ -2296,24 +2298,24 @@ class CLITestCase(DockerClientTestCase): proc = start_process(self.base_dir, ['logs', '-f']) 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( ContainerStateCondition( self.project.client, - 'logs-composefile_another_{}'.format(another_num), + 'logs-composefile_another_*', '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']) result = wait_on_process(proc) assert 'hello' in result.stdout assert 'test' in result.stdout - assert 'logs-composefile_another_{} exited with code 0'.format(another_num) in result.stdout - assert 'logs-composefile_simple_{} exited with code 137'.format(simple_num) in result.stdout + assert '{} exited with code 0'.format(another_name) in result.stdout + assert '{} exited with code 137'.format(simple_name) in result.stdout def test_logs_follow_logs_from_restarted_containers(self): self.base_dir = 'tests/fixtures/logs-restart-composefile' @@ -2331,7 +2333,7 @@ class CLITestCase(DockerClientTestCase): result = wait_on_process(proc) 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 )) == 3 assert result.stdout.count('world') == 3 @@ -2663,10 +2665,10 @@ class CLITestCase(DockerClientTestCase): assert len(containers) == 2 web = containers[1] - db_num = containers[0].short_number + db_name = containers[0].name_without_project 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([ @@ -2704,7 +2706,7 @@ class CLITestCase(DockerClientTestCase): ) 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): self.base_dir = 'tests/fixtures/exit-code-from' @@ -2713,8 +2715,8 @@ class CLITestCase(DockerClientTestCase): ['up', '--abort-on-container-exit', '--exit-code-from', 'simple'] ) result = wait_on_process(proc, returncode=137) # SIGKILL - num = self.project.get_service('another').containers(stopped=True)[0].short_number - assert 'exit-code-from_another_{} exited with code 1'.format(num) in result.stdout + name = self.project.get_service('another').containers(stopped=True)[0].name_without_project + assert '{} exited with code 1'.format(name) in result.stdout def test_images(self): self.project.get_service('simple').create_container() @@ -2728,8 +2730,8 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['images']) assert 'busybox' in result.stdout - assert 'multiple-composefiles_another_' in result.stdout - assert 'multiple-composefiles_simple_' in result.stdout + assert 'multiple-composefiles_another_1' in result.stdout + assert 'multiple-composefiles_simple_1' in result.stdout @mock.patch.dict(os.environ) def test_images_tagless_image(self): @@ -2749,7 +2751,7 @@ class CLITestCase(DockerClientTestCase): self.project.get_service('foo').create_container() result = self.dispatch(['images']) assert '' 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): self.base_dir = 'tests/fixtures/override-yaml-files' diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index d7422e2f6..db40409f8 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -32,6 +32,7 @@ from compose.const import LABEL_CONTAINER_NUMBER from compose.const import LABEL_ONE_OFF from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE +from compose.const import LABEL_SLUG from compose.const import LABEL_VERSION from compose.container import Container from compose.errors import OperationFailedError @@ -867,17 +868,17 @@ class ServiceTest(DockerClientTestCase): db_ctnrs = [create_and_start_container(db) for _ in range(3)] web = self.create_service( 'web', external_links=[ - 'composetest_db_{}'.format(db_ctnrs[0].short_number), - 'composetest_db_{}'.format(db_ctnrs[1].short_number), - 'composetest_db_{}:db_3'.format(db_ctnrs[2].short_number) + db_ctnrs[0].name, + db_ctnrs[1].name, + '{}:db_3'.format(db_ctnrs[2].name) ] ) create_and_start_container(web) assert set(get_links(web.containers()[0])) == set([ - 'composetest_db_{}'.format(db_ctnrs[0].short_number), - 'composetest_db_{}'.format(db_ctnrs[1].short_number), + db_ctnrs[0].name, + db_ctnrs[1].name, 'db_3' ]) @@ -1584,6 +1585,7 @@ class ServiceTest(DockerClientTestCase): LABEL_PROJECT: 'composetest', LABEL_SERVICE: 'web', LABEL_VERSION: __version__, + LABEL_CONTAINER_NUMBER: '1' } expected = dict(labels_dict, **compose_labels) @@ -1592,7 +1594,7 @@ class ServiceTest(DockerClientTestCase): labels = ctnr.labels.items() for pair in expected.items(): 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): labels_dict = {'foo': '', 'bar': ''} diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py index 7652c06c8..a41986f46 100644 --- a/tests/integration/state_test.py +++ b/tests/integration/state_test.py @@ -198,14 +198,14 @@ class ProjectWithDependenciesTest(ProjectTestCase): db, = [c for c in containers if c.service == 'db'] assert set(get_links(web)) == { - 'composetest_db_{}'.format(db.short_number), + 'composetest_db_{}_{}'.format(db.number, db.slug), 'db', - 'db_{}'.format(db.short_number) + 'db_{}_{}'.format(db.number, db.slug) } assert set(get_links(nginx)) == { - 'composetest_web_{}'.format(web.short_number), + 'composetest_web_{}_{}'.format(web.number, web.slug), 'web', - 'web_{}'.format(web.short_number) + 'web_{}_{}'.format(web.number, web.slug) } diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 4f2f08302..64c9cc344 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -30,7 +30,8 @@ class ContainerTest(unittest.TestCase): "Labels": { "com.docker.compose.project": "composetest", "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): container = Container(None, self.container_dict, has_been_inspected=True) - assert container.number == "092cd63296fdc446ad432d3905dd1fcbe12a2ba6b52" + assert container.number == 7 def test_name(self): container = Container.from_ps(None, @@ -88,7 +89,7 @@ class ContainerTest(unittest.TestCase): def test_name_without_project(self): self.container_dict['Name'] = "/composetest_web_7" 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): self.container_dict['Name'] = "/custom_name_of_container" diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index ac234e624..d5dbcbea6 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -41,7 +41,6 @@ from compose.service import parse_repository_tag from compose.service import Service from compose.service import ServiceNetworkMode from compose.service import warn_on_masked_volume -from compose.utils import generate_random_id as generate_id class ServiceTest(unittest.TestCase): @@ -82,7 +81,8 @@ class ServiceTest(unittest.TestCase): service = Service('db', self.mock_client, 'myproject', image='foo') 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): container_id = 'aabbccddee' @@ -164,7 +164,7 @@ class ServiceTest(unittest.TestCase): client=self.mock_client, mem_limit=1000000000, 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.call_args[1]['mem_limit'] == 1000000000 @@ -173,10 +173,10 @@ class ServiceTest(unittest.TestCase): def test_self_reference_external_link(self): service = Service( name='foo', - external_links=['default_foo_bdfa3ed91e2c'] + external_links=['default_foo_1_bdfa3ed91e2c'] ) with pytest.raises(DependencyError): - service.get_container_name('foo', 'bdfa3ed91e2c') + service.get_container_name('foo', 1, 'bdfa3ed91e2c') def test_mem_reservation(self): self.mock_client.create_host_config.return_value = {} @@ -188,7 +188,7 @@ class ServiceTest(unittest.TestCase): client=self.mock_client, 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.call_args[1]['mem_reservation'] == '512m' @@ -201,7 +201,7 @@ class ServiceTest(unittest.TestCase): hostname='name', client=self.mock_client, 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.call_args[1]['cgroup_parent'] == 'test' @@ -218,7 +218,7 @@ class ServiceTest(unittest.TestCase): client=self.mock_client, log_driver='syslog', 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.call_args[1]['log_config'] == { @@ -233,7 +233,7 @@ class ServiceTest(unittest.TestCase): image='foo', client=self.mock_client, 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 def test_split_domainname_none(self): @@ -242,7 +242,7 @@ class ServiceTest(unittest.TestCase): image='foo', hostname='name.domain.tld', 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 not ('domainname' in opts), 'domainname' @@ -253,7 +253,7 @@ class ServiceTest(unittest.TestCase): hostname='name.domain.tld', image='foo', 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['domainname'] == 'domain.tld', 'domainname' @@ -265,7 +265,7 @@ class ServiceTest(unittest.TestCase): image='foo', domainname='domain.tld', 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['domainname'] == 'domain.tld', 'domainname' @@ -277,7 +277,7 @@ class ServiceTest(unittest.TestCase): domainname='domain.tld', image='foo', 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['domainname'] == 'domain.tld', 'domainname' @@ -288,7 +288,7 @@ class ServiceTest(unittest.TestCase): use_networking=False, 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 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'} prev_container = mock.Mock( id='ababab', - image_config={'ContainerConfig': {}}) + image_config={'ContainerConfig': {}} + ) + prev_container.full_slug = 'abcdefff1234' prev_container.get.return_value = None opts = service._get_container_create_options( - {}, generate_id(), previous_container=prev_container + {}, 1, previous_container=prev_container ) assert service.options['labels'] == labels @@ -354,11 +356,13 @@ class ServiceTest(unittest.TestCase): }.get(key, None) prev_container.get.side_effect = container_get + prev_container.full_slug = 'abcdefff1234' opts = service._get_container_create_options( {}, - generate_id(), - previous_container=prev_container) + 1, + previous_container=prev_container + ) assert opts['environment'] == ['affinity:container==ababab'] @@ -369,10 +373,11 @@ class ServiceTest(unittest.TestCase): id='ababab', image_config={'ContainerConfig': {}}) prev_container.get.return_value = None + prev_container.full_slug = 'abcdefff1234' opts = service._get_container_create_options( {}, - generate_id(), + 1, previous_container=prev_container) assert opts['environment'] == [] @@ -385,11 +390,11 @@ class ServiceTest(unittest.TestCase): @mock.patch('compose.service.Container', autospec=True) 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] 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 mock_container_class.from_ps.assert_called_once_with( self.mock_client, container_dict) @@ -462,7 +467,7 @@ class ServiceTest(unittest.TestCase): @mock.patch('compose.service.Container', autospec=True) def test_recreate_container(self, _): 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.image = lambda: {'Id': 'abc123'} new_container = service.recreate_container(mock_container) @@ -476,7 +481,7 @@ class ServiceTest(unittest.TestCase): @mock.patch('compose.service.Container', autospec=True) def test_recreate_container_with_timeout(self, _): 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'} service = Service('foo', client=self.mock_client, image='someimage') service.recreate_container(mock_container, timeout=1) @@ -713,7 +718,7 @@ class ServiceTest(unittest.TestCase): for api_version in set(API_VERSIONS.values()): self.mock_client.api_version = api_version assert service._get_container_create_options( - {}, generate_id() + {}, 1 )['labels'][LABEL_CONFIG_HASH] == config_hash def test_remove_image_none(self): @@ -972,7 +977,7 @@ class ServiceTest(unittest.TestCase): 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({ '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( override_options={}, - number=generate_id(), + number=1, ) 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( override_options={}, - number=generate_id(), + number=1, previous_container=Container(self.mock_client, {'Id': '123123123'}), )