Merge pull request #6140 from docker/4688_no_sequential_ids

Add randomly generated slug to container names to prevent collisions
This commit is contained in:
Joffrey F 2018-09-19 15:12:41 -07:00 committed by GitHub
commit f80630ffcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 232 additions and 167 deletions

View File

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

View File

@ -9,7 +9,9 @@ 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
@ -80,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.number)
return '{0}_{1}{2}'.format(self.service, self.number, '_' + self.slug if self.slug else '')
else:
return self.name
@ -92,6 +94,14 @@ class Container(object):
self.short_id, LABEL_CONTAINER_NUMBER))
return int(number)
@property
def slug(self):
return truncate_id(self.full_slug)
@property
def full_slug(self):
return self.labels.get(LABEL_SLUG)
@property
def ports(self):
self.inspect_if_not_inspected()

View File

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

View File

@ -40,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
@ -49,9 +50,11 @@ from .errors import OperationFailedError
from .parallel import parallel_execute
from .progress_stream import stream_output
from .progress_stream import StreamOutputError
from .utils import generate_random_id
from .utils import json_hash
from .utils import parse_bytes
from .utils import parse_seconds_float
from .utils import truncate_id
log = logging.getLogger(__name__)
@ -122,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')
@ -219,7 +222,6 @@ class Service(object):
"""Return a :class:`compose.container.Container` for this service. The
container must be active, and match `number`.
"""
for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]):
return container
@ -425,27 +427,33 @@ class Service(object):
return has_diverged
def _execute_convergence_create(self, scale, detached, start, project_services=None):
i = self._next_container_number()
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, 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),
"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):
@ -508,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'))
@ -518,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,
@ -568,7 +576,7 @@ class Service(object):
container.rename_to_tmp_name()
new_container = self.create_container(
previous_container=container if not renew_anonymous_volumes else None,
number=container.labels.get(LABEL_CONTAINER_NUMBER),
number=container.number,
quiet=True,
)
if attach_logs:
@ -723,8 +731,6 @@ class Service(object):
def get_volumes_from_names(self):
return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)]
# TODO: this would benefit from github.com/docker/docker/pull/14699
# to remove the need to inspect every container
def _next_container_number(self, one_off=False):
containers = itertools.chain(
self._fetch_containers(
@ -813,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])
@ -821,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)
@ -873,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:
@ -1111,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:
@ -1373,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 + [str(number)])
return '_'.join(
bits + ([str(number), truncate_id(slug)] if slug else [str(number)])
)
# Images
@ -1558,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:

View File

@ -7,6 +7,7 @@ import json
import json.decoder
import logging
import ntpath
import random
import six
from docker.errors import DockerException
@ -151,3 +152,21 @@ def unquote_path(s):
if s[0] == '"' and s[-1] == '"':
return s[1:-1]
return s
def generate_random_id():
while True:
val = hex(random.getrandbits(32 * 8))[2:-1]
try:
int(truncate_id(val))
continue
except ValueError:
return val
def truncate_id(value):
if ':' in value:
value = value[value.index(':') + 1:]
if len(value) > 12:
return value[:12]
return value

View File

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

View File

@ -99,7 +99,14 @@ class ContainerStateCondition(object):
def __call__(self):
try:
container = self.client.inspect_container(self.name)
if self.name.endswith('*'):
ctnrs = self.client.containers(all=True, filters={'name': self.name[:-1]})
if len(ctnrs) > 0:
container = self.client.inspect_container(ctnrs[0]['Id'])
else:
return False
else:
container = self.client.inspect_container(self.name)
return container['State']['Status'] == self.status
except errors.APIError:
return False
@ -1019,11 +1026,15 @@ class CLITestCase(DockerClientTestCase):
def test_up_attached(self):
self.base_dir = 'tests/fixtures/echo-services'
result = self.dispatch(['up', '--no-color'])
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_1 | simple' in result.stdout
assert 'another_1 | another' in result.stdout
assert 'simple_1 exited with code 0' in result.stdout
assert 'another_1 exited with code 0' 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):
@ -1727,11 +1738,12 @@ class CLITestCase(DockerClientTestCase):
def test_run_rm(self):
self.base_dir = 'tests/fixtures/volume'
proc = start_process(self.base_dir, ['run', '--rm', 'test'])
service = self.project.get_service('test')
wait_on_condition(ContainerStateCondition(
self.project.client,
'volume_test_run_1',
'running'))
service = self.project.get_service('test')
'volume_test_run_*',
'running')
)
containers = service.containers(one_off=OneOffFilter.only)
assert len(containers) == 1
mounts = containers[0].get('Mounts')
@ -2054,39 +2066,39 @@ class CLITestCase(DockerClientTestCase):
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
wait_on_condition(ContainerStateCondition(
self.project.client,
'simple-composefile_simple_run_1',
'simple-composefile_simple_run_*',
'running'))
os.kill(proc.pid, signal.SIGINT)
wait_on_condition(ContainerStateCondition(
self.project.client,
'simple-composefile_simple_run_1',
'simple-composefile_simple_run_*',
'exited'))
def test_run_handles_sigterm(self):
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
wait_on_condition(ContainerStateCondition(
self.project.client,
'simple-composefile_simple_run_1',
'simple-composefile_simple_run_*',
'running'))
os.kill(proc.pid, signal.SIGTERM)
wait_on_condition(ContainerStateCondition(
self.project.client,
'simple-composefile_simple_run_1',
'simple-composefile_simple_run_*',
'exited'))
def test_run_handles_sighup(self):
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
wait_on_condition(ContainerStateCondition(
self.project.client,
'simple-composefile_simple_run_1',
'simple-composefile_simple_run_*',
'running'))
os.kill(proc.pid, signal.SIGHUP)
wait_on_condition(ContainerStateCondition(
self.project.client,
'simple-composefile_simple_run_1',
'simple-composefile_simple_run_*',
'exited'))
@mock.patch.dict(os.environ)
@ -2286,34 +2298,44 @@ class CLITestCase(DockerClientTestCase):
proc = start_process(self.base_dir, ['logs', '-f'])
self.dispatch(['up', '-d', 'another'])
wait_on_condition(ContainerStateCondition(
self.project.client,
'logs-composefile_another_1',
'exited'))
another_name = self.project.get_service('another').get_container().name_without_project
wait_on_condition(
ContainerStateCondition(
self.project.client,
'logs-composefile_another_*',
'exited'
)
)
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_1 exited with code 0' in result.stdout
assert 'logs-composefile_simple_1 exited with code 137' 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'
proc = start_process(self.base_dir, ['up'])
wait_on_condition(ContainerStateCondition(
self.project.client,
'logs-restart-composefile_another_1',
'exited'))
wait_on_condition(
ContainerStateCondition(
self.project.client,
'logs-restart-composefile_another_*',
'exited'
)
)
self.dispatch(['kill', 'simple'])
result = wait_on_process(proc)
assert result.stdout.count('logs-restart-composefile_another_1 exited with code 1') == 3
assert len(re.findall(
r'logs-restart-composefile_another_1_[a-f0-9]{12} exited with code 1',
result.stdout
)) == 3
assert result.stdout.count('world') == 3
def test_logs_default(self):
@ -2346,10 +2368,10 @@ class CLITestCase(DockerClientTestCase):
self.dispatch(['up'])
result = self.dispatch(['logs', '--tail', '2'])
assert 'c\n' in result.stdout
assert 'd\n' in result.stdout
assert 'a\n' not in result.stdout
assert 'b\n' not in result.stdout
assert 'y\n' in result.stdout
assert 'z\n' in result.stdout
assert 'w\n' not in result.stdout
assert 'x\n' not in result.stdout
def test_kill(self):
self.dispatch(['up', '-d'], None)
@ -2523,9 +2545,9 @@ class CLITestCase(DockerClientTestCase):
result = self.dispatch(['port', '--index=' + str(index), 'simple', str(number)])
return result.stdout.rstrip()
assert get_port(3000) == containers[0].get_local_port(3000)
assert get_port(3000, index=1) == containers[0].get_local_port(3000)
assert get_port(3000, index=2) == containers[1].get_local_port(3000)
assert get_port(3000) in (containers[0].get_local_port(3000), containers[1].get_local_port(3000))
assert get_port(3000, index=containers[0].number) == containers[0].get_local_port(3000)
assert get_port(3000, index=containers[1].number) == containers[1].get_local_port(3000)
assert get_port(3002) == ""
def test_events_json(self):
@ -2561,7 +2583,7 @@ class CLITestCase(DockerClientTestCase):
container, = self.project.containers()
expected_template = ' container {} {}'
expected_meta_info = ['image=busybox:latest', 'name=simple-composefile_simple_1']
expected_meta_info = ['image=busybox:latest', 'name=simple-composefile_simple_']
assert expected_template.format('create', container.id) in lines[0]
assert expected_template.format('start', container.id) in lines[1]
@ -2643,8 +2665,11 @@ class CLITestCase(DockerClientTestCase):
assert len(containers) == 2
web = containers[1]
db_name = containers[0].name_without_project
assert set(get_links(web)) == set(['db', 'mydb_1', 'extends_mydb_1'])
assert set(get_links(web)) == set(
['db', db_name, 'extends_{}'.format(db_name)]
)
expected_env = set([
"FOO=1",
@ -2677,11 +2702,11 @@ class CLITestCase(DockerClientTestCase):
self.base_dir = 'tests/fixtures/exit-code-from'
proc = start_process(
self.base_dir,
['up', '--abort-on-container-exit', '--exit-code-from', 'another'])
['up', '--abort-on-container-exit', '--exit-code-from', 'another']
)
result = wait_on_process(proc, returncode=1)
assert 'exit-code-from_another_1 exited with code 1' in 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'
@ -2690,13 +2715,14 @@ class CLITestCase(DockerClientTestCase):
['up', '--abort-on-container-exit', '--exit-code-from', 'simple']
)
result = wait_on_process(proc, returncode=137) # SIGKILL
assert 'exit-code-from_another_1 exited with code 1' 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()
result = self.dispatch(['images'])
assert 'busybox' in result.stdout
assert 'simple-composefile_simple_1' in result.stdout
assert 'simple-composefile_simple_' in result.stdout
def test_images_default_composefile(self):
self.base_dir = 'tests/fixtures/multiple-composefiles'

View File

@ -1,3 +1,3 @@
simple:
image: busybox:latest
command: sh -c "echo a && echo b && echo c && echo d"
command: sh -c "echo w && echo x && echo y && echo z"

View File

@ -90,7 +90,8 @@ class ProjectTest(DockerClientTestCase):
project.up()
containers = project.containers(['web'])
assert [c.name for c in containers] == ['composetest_web_1']
assert len(containers) == 1
assert containers[0].name.startswith('composetest_web_')
def test_containers_with_extra_service(self):
web = self.create_service('web')
@ -464,14 +465,14 @@ class ProjectTest(DockerClientTestCase):
project.up(['db'])
assert len(project.containers()) == 1
old_db_id = project.containers()[0].id
container, = project.containers()
old_db_id = container.id
db_volume_path = container.get_mount('/var/db')['Source']
project.up(strategy=ConvergenceStrategy.never)
assert len(project.containers()) == 2
db_container = [c for c in project.containers() if 'db' in c.name][0]
db_container = [c for c in project.containers() if c.name == container.name][0]
assert db_container.id == old_db_id
assert db_container.get_mount('/var/db')['Source'] == db_volume_path
@ -1944,7 +1945,7 @@ class ProjectTest(DockerClientTestCase):
containers = project.containers(stopped=True)
assert len(containers) == 1
assert containers[0].name == 'underscoretest_svc1_1'
assert containers[0].name.startswith('underscoretest_svc1_')
assert containers[0].project == '_underscoretest'
full_vol_name = 'underscoretest_foo'
@ -1965,7 +1966,7 @@ class ProjectTest(DockerClientTestCase):
containers = project2.containers(stopped=True)
assert len(containers) == 1
assert containers[0].name == 'dashtest_svc1_1'
assert containers[0].name.startswith('dashtest_svc1_')
assert containers[0].project == '-dashtest'
full_vol_name = 'dashtest_foo'

View File

@ -32,6 +32,7 @@ from compose.const import LABEL_CONTAINER_NUMBER
from compose.const import LABEL_ONE_OFF
from compose.const import LABEL_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
@ -67,7 +68,7 @@ class ServiceTest(DockerClientTestCase):
create_and_start_container(foo)
assert len(foo.containers()) == 1
assert foo.containers()[0].name == 'composetest_foo_1'
assert foo.containers()[0].name.startswith('composetest_foo_')
assert len(bar.containers()) == 0
create_and_start_container(bar)
@ -77,8 +78,8 @@ class ServiceTest(DockerClientTestCase):
assert len(bar.containers()) == 2
names = [c.name for c in bar.containers()]
assert 'composetest_bar_1' in names
assert 'composetest_bar_2' in names
assert len(names) == 2
assert all(name.startswith('composetest_bar_') for name in names)
def test_containers_one_off(self):
db = self.create_service('db')
@ -89,18 +90,18 @@ class ServiceTest(DockerClientTestCase):
def test_project_is_added_to_container_name(self):
service = self.create_service('web')
create_and_start_container(service)
assert service.containers()[0].name == 'composetest_web_1'
assert service.containers()[0].name.startswith('composetest_web_')
def test_create_container_with_one_off(self):
db = self.create_service('db')
container = db.create_container(one_off=True)
assert container.name == 'composetest_db_run_1'
assert container.name.startswith('composetest_db_run_')
def test_create_container_with_one_off_when_existing_container_is_running(self):
db = self.create_service('db')
db.start()
container = db.create_container(one_off=True)
assert container.name == 'composetest_db_run_1'
assert container.name.startswith('composetest_db_run_')
def test_create_container_with_unspecified_volume(self):
service = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
@ -489,7 +490,7 @@ class ServiceTest(DockerClientTestCase):
assert old_container.get('Config.Entrypoint') == ['top']
assert old_container.get('Config.Cmd') == ['-d', '1']
assert 'FOO=1' in old_container.get('Config.Env')
assert old_container.name == 'composetest_db_1'
assert old_container.name.startswith('composetest_db_')
service.start_container(old_container)
old_container.inspect() # reload volume data
volume_path = old_container.get_mount('/etc')['Source']
@ -503,7 +504,7 @@ class ServiceTest(DockerClientTestCase):
assert new_container.get('Config.Entrypoint') == ['top']
assert new_container.get('Config.Cmd') == ['-d', '1']
assert 'FOO=2' in new_container.get('Config.Env')
assert new_container.name == 'composetest_db_1'
assert new_container.name.startswith('composetest_db_')
assert new_container.get_mount('/etc')['Source'] == volume_path
if not is_cluster(self.client):
assert (
@ -836,13 +837,13 @@ class ServiceTest(DockerClientTestCase):
db = self.create_service('db')
web = self.create_service('web', links=[(db, None)])
create_and_start_container(db)
create_and_start_container(db)
db1 = create_and_start_container(db)
db2 = create_and_start_container(db)
create_and_start_container(web)
assert set(get_links(web.containers()[0])) == set([
'composetest_db_1', 'db_1',
'composetest_db_2', 'db_2',
db1.name, db1.name_without_project,
db2.name, db2.name_without_project,
'db'
])
@ -851,30 +852,33 @@ class ServiceTest(DockerClientTestCase):
db = self.create_service('db')
web = self.create_service('web', links=[(db, 'custom_link_name')])
create_and_start_container(db)
create_and_start_container(db)
db1 = create_and_start_container(db)
db2 = create_and_start_container(db)
create_and_start_container(web)
assert set(get_links(web.containers()[0])) == set([
'composetest_db_1', 'db_1',
'composetest_db_2', 'db_2',
db1.name, db1.name_without_project,
db2.name, db2.name_without_project,
'custom_link_name'
])
@no_cluster('No legacy links support in Swarm')
def test_start_container_with_external_links(self):
db = self.create_service('db')
web = self.create_service('web', external_links=['composetest_db_1',
'composetest_db_2',
'composetest_db_3:db_3'])
db_ctnrs = [create_and_start_container(db) for _ in range(3)]
web = self.create_service(
'web', external_links=[
db_ctnrs[0].name,
db_ctnrs[1].name,
'{}:db_3'.format(db_ctnrs[2].name)
]
)
for _ in range(3):
create_and_start_container(db)
create_and_start_container(web)
assert set(get_links(web.containers()[0])) == set([
'composetest_db_1',
'composetest_db_2',
db_ctnrs[0].name,
db_ctnrs[1].name,
'db_3'
])
@ -892,14 +896,14 @@ class ServiceTest(DockerClientTestCase):
def test_start_one_off_container_creates_links_to_its_own_service(self):
db = self.create_service('db')
create_and_start_container(db)
create_and_start_container(db)
db1 = create_and_start_container(db)
db2 = create_and_start_container(db)
c = create_and_start_container(db, one_off=OneOffFilter.only)
assert set(get_links(c)) == set([
'composetest_db_1', 'db_1',
'composetest_db_2', 'db_2',
db1.name, db1.name_without_project,
db2.name, db2.name_without_project,
'db'
])
@ -1249,10 +1253,9 @@ class ServiceTest(DockerClientTestCase):
test that those containers are restarted and not removed/recreated.
"""
service = self.create_service('web')
next_number = service._next_container_number()
valid_numbers = [next_number, next_number + 1]
service.create_container(number=next_number)
service.create_container(number=next_number + 1)
valid_numbers = [service._next_container_number(), service._next_container_number()]
service.create_container(number=valid_numbers[0])
service.create_container(number=valid_numbers[1])
ParallelStreamWriter.instance = None
with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
@ -1310,10 +1313,8 @@ class ServiceTest(DockerClientTestCase):
assert len(service.containers()) == 1
assert service.containers()[0].is_running
assert (
"ERROR: for composetest_web_2 Cannot create container for service"
" web: Boom" in mock_stderr.getvalue()
)
assert "ERROR: for composetest_web_" in mock_stderr.getvalue()
assert "Cannot create container for service web: Boom" in mock_stderr.getvalue()
def test_scale_with_unexpected_exception(self):
"""Test that when scaling if the API returns an error, that is not of type
@ -1580,18 +1581,20 @@ class ServiceTest(DockerClientTestCase):
}
compose_labels = {
LABEL_CONTAINER_NUMBER: '1',
LABEL_ONE_OFF: 'False',
LABEL_PROJECT: 'composetest',
LABEL_SERVICE: 'web',
LABEL_VERSION: __version__,
LABEL_CONTAINER_NUMBER: '1'
}
expected = dict(labels_dict, **compose_labels)
service = self.create_service('web', labels=labels_dict)
labels = create_and_start_container(service).labels.items()
ctnr = create_and_start_container(service)
labels = ctnr.labels.items()
for pair in expected.items():
assert pair in labels
assert ctnr.labels[LABEL_SLUG] == ctnr.full_slug
def test_empty_labels(self):
labels_dict = {'foo': '', 'bar': ''}
@ -1655,7 +1658,7 @@ class ServiceTest(DockerClientTestCase):
def test_duplicate_containers(self):
service = self.create_service('web')
options = service._get_container_create_options({}, 1)
options = service._get_container_create_options({}, service._next_container_number())
original = Container.create(service.client, **options)
assert set(service.containers(stopped=True)) == set([original])

View File

@ -55,8 +55,8 @@ class BasicProjectTest(ProjectTestCase):
def test_partial_change(self):
old_containers = self.run_up(self.cfg)
old_db = [c for c in old_containers if c.name_without_project == 'db_1'][0]
old_web = [c for c in old_containers if c.name_without_project == 'web_1'][0]
old_db = [c for c in old_containers if c.name_without_project.startswith('db_')][0]
old_web = [c for c in old_containers if c.name_without_project.startswith('web_')][0]
self.cfg['web']['command'] = '/bin/true'
@ -71,7 +71,7 @@ class BasicProjectTest(ProjectTestCase):
created = list(new_containers - old_containers)
assert len(created) == 1
assert created[0].name_without_project == 'web_1'
assert created[0].name_without_project == old_web.name_without_project
assert created[0].get('Config.Cmd') == ['/bin/true']
def test_all_change(self):
@ -114,7 +114,7 @@ class ProjectWithDependenciesTest(ProjectTestCase):
def test_up(self):
containers = self.run_up(self.cfg)
assert set(c.name_without_project for c in containers) == set(['db_1', 'web_1', 'nginx_1'])
assert set(c.service for c in containers) == set(['db', 'web', 'nginx'])
def test_change_leaf(self):
old_containers = self.run_up(self.cfg)
@ -122,7 +122,7 @@ class ProjectWithDependenciesTest(ProjectTestCase):
self.cfg['nginx']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg)
assert set(c.name_without_project for c in new_containers - old_containers) == set(['nginx_1'])
assert set(c.service for c in new_containers - old_containers) == set(['nginx'])
def test_change_middle(self):
old_containers = self.run_up(self.cfg)
@ -130,7 +130,7 @@ class ProjectWithDependenciesTest(ProjectTestCase):
self.cfg['web']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg)
assert set(c.name_without_project for c in new_containers - old_containers) == set(['web_1'])
assert set(c.service for c in new_containers - old_containers) == set(['web'])
def test_change_middle_always_recreate_deps(self):
old_containers = self.run_up(self.cfg, always_recreate_deps=True)
@ -138,8 +138,7 @@ class ProjectWithDependenciesTest(ProjectTestCase):
self.cfg['web']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg, always_recreate_deps=True)
assert set(c.name_without_project
for c in new_containers - old_containers) == {'web_1', 'nginx_1'}
assert set(c.service for c in new_containers - old_containers) == {'web', 'nginx'}
def test_change_root(self):
old_containers = self.run_up(self.cfg)
@ -147,7 +146,7 @@ class ProjectWithDependenciesTest(ProjectTestCase):
self.cfg['db']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg)
assert set(c.name_without_project for c in new_containers - old_containers) == set(['db_1'])
assert set(c.service for c in new_containers - old_containers) == set(['db'])
def test_change_root_always_recreate_deps(self):
old_containers = self.run_up(self.cfg, always_recreate_deps=True)
@ -155,8 +154,9 @@ class ProjectWithDependenciesTest(ProjectTestCase):
self.cfg['db']['environment'] = {'NEW_VAR': '1'}
new_containers = self.run_up(self.cfg, always_recreate_deps=True)
assert set(c.name_without_project
for c in new_containers - old_containers) == {'db_1', 'web_1', 'nginx_1'}
assert set(c.service for c in new_containers - old_containers) == {
'db', 'web', 'nginx'
}
def test_change_root_no_recreate(self):
old_containers = self.run_up(self.cfg)
@ -195,9 +195,18 @@ class ProjectWithDependenciesTest(ProjectTestCase):
web, = [c for c in containers if c.service == 'web']
nginx, = [c for c in containers if c.service == 'nginx']
db, = [c for c in containers if c.service == 'db']
assert set(get_links(web)) == {'composetest_db_1', 'db', 'db_1'}
assert set(get_links(nginx)) == {'composetest_web_1', 'web', 'web_1'}
assert set(get_links(web)) == {
'composetest_db_{}_{}'.format(db.number, db.slug),
'db',
'db_{}_{}'.format(db.number, db.slug)
}
assert set(get_links(nginx)) == {
'composetest_web_{}_{}'.format(web.number, web.slug),
'web',
'web_{}_{}'.format(web.number, web.slug)
}
class ServiceStateTest(DockerClientTestCase):

View File

@ -30,7 +30,8 @@ class ContainerTest(unittest.TestCase):
"Labels": {
"com.docker.compose.project": "composetest",
"com.docker.compose.service": "web",
"com.docker.compose.container-number": 7,
"com.docker.compose.container-number": "7",
"com.docker.compose.slug": "092cd63296fdc446ad432d3905dd1fcbe12a2ba6b52"
},
}
}
@ -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_7"
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"

View File

@ -173,10 +173,10 @@ class ServiceTest(unittest.TestCase):
def test_self_reference_external_link(self):
service = Service(
name='foo',
external_links=['default_foo_1']
external_links=['default_foo_1_bdfa3ed91e2c']
)
with pytest.raises(DependencyError):
service.get_container_name('foo', 1)
service.get_container_name('foo', 1, 'bdfa3ed91e2c')
def test_mem_reservation(self):
self.mock_client.create_host_config.return_value = {}
@ -317,13 +317,14 @@ 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(
{},
1,
previous_container=prev_container)
{}, 1, previous_container=prev_container
)
assert service.options['labels'] == labels
assert service.options['environment'] == environment
@ -355,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(
{},
1,
previous_container=prev_container)
previous_container=prev_container
)
assert opts['environment'] == ['affinity:container==ababab']
@ -370,6 +373,7 @@ 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(
{},
@ -386,7 +390,7 @@ 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_2')
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)
@ -463,6 +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.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,6 +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.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)
@ -711,9 +717,9 @@ 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({}, 1)['labels'][LABEL_CONFIG_HASH] == (
config_hash
)
assert service._get_container_create_options(
{}, 1
)['labels'][LABEL_CONFIG_HASH] == config_hash
def test_remove_image_none(self):
web = Service('web', image='example', client=self.mock_client)