Skip to content

Commit

Permalink
Preserve container numbers, add slug to prevent name collisions
Browse files Browse the repository at this point in the history
Signed-off-by: Joffrey F <joffrey@docker.com>
  • Loading branch information
shin- committed Sep 11, 2018
1 parent 4e2de3c commit 70654cf
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 131 deletions.
14 changes: 6 additions & 8 deletions compose/cli/main.py
Expand Up @@ -474,16 +474,15 @@ def exec_command(self, options):
-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')

Expand Down Expand Up @@ -660,11 +659,10 @@ def port(self, options):
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)
Expand Down
1 change: 1 addition & 0 deletions compose/const.py
Expand Up @@ -15,6 +15,7 @@
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
Expand Down
13 changes: 9 additions & 4 deletions compose/container.py
Expand Up @@ -9,6 +9,7 @@
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
Expand Down Expand Up @@ -81,7 +82,7 @@ def service(self):
@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

Expand All @@ -91,11 +92,15 @@ def number(self):
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):
Expand Down
22 changes: 0 additions & 22 deletions compose/project.py
Expand Up @@ -31,7 +31,6 @@
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
Expand Down Expand Up @@ -198,25 +197,6 @@ def get_services_without_duplicate(self, service_names=None, include_deps=False)
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:
Expand Down Expand Up @@ -494,7 +474,6 @@ def up(self,
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):

Expand All @@ -505,7 +484,6 @@ def do(service):
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,
)
Expand Down
95 changes: 56 additions & 39 deletions 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
Expand Down Expand Up @@ -39,6 +40,7 @@
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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -216,17 +218,12 @@ def containers(self, stopped=False, one_off=False, filters={}, labels=None):
)
)

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

Expand Down Expand Up @@ -430,28 +427,32 @@ def _containers_have_diverged(self, containers):

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

containers, errors = parallel_execute(
[ServiceName(self.project, self.name, number, generate_random_id()) for number in [
index 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
return containers

def _execute_convergence_recreate(self, containers, scale, timeout, detached, start,
renew_anonymous_volumes):
Expand Down Expand Up @@ -514,8 +515,8 @@ def stop_and_remove(container):

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'))
Expand All @@ -524,7 +525,7 @@ def execute_convergence_plan(self, plan, timeout=None, detached=False,

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,
Expand Down Expand Up @@ -730,7 +731,17 @@ def get_volumes_from_names(self):
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
Expand Down Expand Up @@ -807,6 +818,7 @@ def _get_container_create_options(
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])
Expand All @@ -815,7 +827,7 @@ def _get_container_create_options(
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)

Expand Down Expand Up @@ -867,7 +879,9 @@ def _get_container_create_options(
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:
Expand Down Expand Up @@ -1105,12 +1119,12 @@ def labels(self, one_off=False, legacy=False):
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:
Expand Down Expand Up @@ -1367,11 +1381,13 @@ def mode(self):
# 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
Expand Down Expand Up @@ -1552,10 +1568,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:
Expand Down
1 change: 0 additions & 1 deletion script/test/versions.py
Expand Up @@ -50,7 +50,6 @@ def parse(cls, version):
stage = None
elif '-' in stage:
edition, stage = stage.split('-')

major, minor, patch = version.split('.', 3)
return cls(major, minor, patch, stage, edition)

Expand Down

0 comments on commit 70654cf

Please sign in to comment.