Sfoglia il codice sorgente

Preserve container numbers, add slug to prevent name collisions

Signed-off-by: Joffrey F <[email protected]>
Joffrey F 7 anni fa
parent
commit
5916639383

+ 6 - 8
compose/cli/main.py

@@ -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
-                              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,
             -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
-                              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'])
         service = self.project.get_service(options['SERVICE'])
         try:
         try:
             container = service.get_container(number=index)
             container = service.get_container(number=index)

+ 1 - 0
compose/const.py

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

+ 9 - 4
compose/container.py

@@ -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):
-        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
     @property
     def ports(self):
     def ports(self):

+ 0 - 22
compose/project.py

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

+ 57 - 39
compose/service.py

@@ -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)]):
-                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))
         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):
-                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,
     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,
-                                 reset_container_image=False, renew_anonymous_volumes=False):
+                                 rescale=True, reset_container_image=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:

+ 0 - 1
script/test/versions.py

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

+ 36 - 34
tests/acceptance/cli_test.py

@@ -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_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):
     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_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):
     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_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 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
-        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()
     @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 '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):
     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
-        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):
     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_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)
     @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'

+ 8 - 6
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_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),
-                '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)
         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),
-            'composetest_db_{}'.format(db_ctnrs[1].short_number),
+            db_ctnrs[0].name,
+            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': ''}

+ 4 - 4
tests/integration/state_test.py

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

+ 4 - 3
tests/unit/container_test.py

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

+ 32 - 27
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 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(),
-            previous_container=prev_container)
+            1,
+            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'}),
         )
         )