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_SERVICE = 'com.docker.compose.service'
LABEL_NETWORK = 'com.docker.compose.network' LABEL_NETWORK = 'com.docker.compose.network'
LABEL_VERSION = 'com.docker.compose.version' LABEL_VERSION = 'com.docker.compose.version'
LABEL_SLUG = 'com.docker.compose.slug'
LABEL_VOLUME = 'com.docker.compose.volume' LABEL_VOLUME = 'com.docker.compose.volume'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
NANOCPUS_SCALE = 1000000000 NANOCPUS_SCALE = 1000000000

View File

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

View File

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

View File

@ -40,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
@ -49,9 +50,11 @@ from .errors import OperationFailedError
from .parallel import parallel_execute from .parallel import parallel_execute
from .progress_stream import stream_output from .progress_stream import stream_output
from .progress_stream import StreamOutputError from .progress_stream import StreamOutputError
from .utils import generate_random_id
from .utils import json_hash from .utils import json_hash
from .utils import parse_bytes from .utils import parse_bytes
from .utils import parse_seconds_float from .utils import parse_seconds_float
from .utils import truncate_id
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -122,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')
@ -219,7 +222,6 @@ class Service(object):
"""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`.
""" """
for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]): for container in self.containers(labels=['{0}={1}'.format(LABEL_CONTAINER_NUMBER, number)]):
return container return container
@ -425,27 +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):
i = self._next_container_number()
def create_and_start(service, n): i = self._next_container_number()
container = service.create_container(number=n, quiet=True)
if not detached:
container.attach_log_stream()
if start:
self.start_container(container)
return container
containers, errors = parallel_execute( def create_and_start(service, n):
[ServiceName(self.project, self.name, index) for index in range(i, i + scale)], container = service.create_container(number=n, quiet=True)
lambda service_name: create_and_start(self, service_name.number), if not detached:
lambda service_name: self.get_container_name(service_name.service, service_name.number), container.attach_log_stream()
"Creating" if start:
) self.start_container(container)
for error in errors.values(): return container
raise OperationFailedError(error)
return containers containers, errors = parallel_execute(
[
ServiceName(self.project, self.name, index, generate_random_id())
for index in range(i, i + scale)
],
lambda service_name: create_and_start(self, service_name.number),
lambda service_name: self.get_container_name(
service_name.service, service_name.number, service_name.slug
),
"Creating"
)
for error in errors.values():
raise OperationFailedError(error)
return containers
def _execute_convergence_recreate(self, containers, scale, timeout, detached, start, def _execute_convergence_recreate(self, containers, scale, timeout, detached, start,
renew_anonymous_volumes): renew_anonymous_volumes):
@ -508,8 +516,8 @@ class Service(object):
def execute_convergence_plan(self, plan, timeout=None, detached=False, def execute_convergence_plan(self, plan, timeout=None, detached=False,
start=True, scale_override=None, start=True, scale_override=None,
rescale=True, project_services=None, rescale=True, reset_container_image=False,
reset_container_image=False, renew_anonymous_volumes=False): renew_anonymous_volumes=False):
(action, containers) = plan (action, containers) = plan
scale = scale_override if scale_override is not None else self.scale_num scale = scale_override if scale_override is not None else self.scale_num
containers = sorted(containers, key=attrgetter('number')) containers = sorted(containers, key=attrgetter('number'))
@ -518,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,
@ -568,7 +576,7 @@ class Service(object):
container.rename_to_tmp_name() container.rename_to_tmp_name()
new_container = self.create_container( new_container = self.create_container(
previous_container=container if not renew_anonymous_volumes else None, previous_container=container if not renew_anonymous_volumes else None,
number=container.labels.get(LABEL_CONTAINER_NUMBER), number=container.number,
quiet=True, quiet=True,
) )
if attach_logs: if attach_logs:
@ -723,8 +731,6 @@ class Service(object):
def get_volumes_from_names(self): def get_volumes_from_names(self):
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)]
# 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): def _next_container_number(self, one_off=False):
containers = itertools.chain( containers = itertools.chain(
self._fetch_containers( self._fetch_containers(
@ -813,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])
@ -821,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)
@ -873,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:
@ -1111,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:
@ -1373,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 + [str(number)]) return '_'.join(
bits + ([str(number), truncate_id(slug)] if slug else [str(number)])
)
# Images # Images
@ -1558,10 +1569,11 @@ def build_mount(mount_spec):
# Labels # Labels
def build_container_labels(label_options, service_labels, number, config_hash): def build_container_labels(label_options, service_labels, number, config_hash, slug):
labels = dict(label_options or {}) labels = dict(label_options or {})
labels.update(label.split('=', 1) for label in service_labels) labels.update(label.split('=', 1) for label in service_labels)
labels[LABEL_CONTAINER_NUMBER] = str(number) labels[LABEL_CONTAINER_NUMBER] = str(number)
labels[LABEL_SLUG] = slug
labels[LABEL_VERSION] = __version__ labels[LABEL_VERSION] = __version__
if config_hash: if config_hash:

View File

@ -7,6 +7,7 @@ import json
import json.decoder import json.decoder
import logging import logging
import ntpath import ntpath
import random
import six import six
from docker.errors import DockerException from docker.errors import DockerException
@ -151,3 +152,21 @@ def unquote_path(s):
if s[0] == '"' and s[-1] == '"': if s[0] == '"' and s[-1] == '"':
return s[1:-1] return s[1:-1]
return s 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 stage = None
elif '-' in stage: elif '-' in stage:
edition, stage = stage.split('-') edition, stage = stage.split('-')
major, minor, patch = version.split('.', 3) major, minor, patch = version.split('.', 3)
return cls(major, minor, patch, stage, edition) return cls(major, minor, patch, stage, edition)

View File

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

View File

@ -1,3 +1,3 @@
simple: simple:
image: busybox:latest 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() project.up()
containers = project.containers(['web']) 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): def test_containers_with_extra_service(self):
web = self.create_service('web') web = self.create_service('web')
@ -464,14 +465,14 @@ class ProjectTest(DockerClientTestCase):
project.up(['db']) project.up(['db'])
assert len(project.containers()) == 1 assert len(project.containers()) == 1
old_db_id = project.containers()[0].id
container, = project.containers() container, = project.containers()
old_db_id = container.id
db_volume_path = container.get_mount('/var/db')['Source'] db_volume_path = container.get_mount('/var/db')['Source']
project.up(strategy=ConvergenceStrategy.never) project.up(strategy=ConvergenceStrategy.never)
assert len(project.containers()) == 2 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.id == old_db_id
assert db_container.get_mount('/var/db')['Source'] == db_volume_path assert db_container.get_mount('/var/db')['Source'] == db_volume_path
@ -1944,7 +1945,7 @@ class ProjectTest(DockerClientTestCase):
containers = project.containers(stopped=True) containers = project.containers(stopped=True)
assert len(containers) == 1 assert len(containers) == 1
assert containers[0].name == 'underscoretest_svc1_1' assert containers[0].name.startswith('underscoretest_svc1_')
assert containers[0].project == '_underscoretest' assert containers[0].project == '_underscoretest'
full_vol_name = 'underscoretest_foo' full_vol_name = 'underscoretest_foo'
@ -1965,7 +1966,7 @@ class ProjectTest(DockerClientTestCase):
containers = project2.containers(stopped=True) containers = project2.containers(stopped=True)
assert len(containers) == 1 assert len(containers) == 1
assert containers[0].name == 'dashtest_svc1_1' assert containers[0].name.startswith('dashtest_svc1_')
assert containers[0].project == '-dashtest' assert containers[0].project == '-dashtest'
full_vol_name = 'dashtest_foo' 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_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
@ -67,7 +68,7 @@ class ServiceTest(DockerClientTestCase):
create_and_start_container(foo) create_and_start_container(foo)
assert len(foo.containers()) == 1 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 assert len(bar.containers()) == 0
create_and_start_container(bar) create_and_start_container(bar)
@ -77,8 +78,8 @@ class ServiceTest(DockerClientTestCase):
assert len(bar.containers()) == 2 assert len(bar.containers()) == 2
names = [c.name for c in bar.containers()] names = [c.name for c in bar.containers()]
assert 'composetest_bar_1' in names assert len(names) == 2
assert 'composetest_bar_2' in names assert all(name.startswith('composetest_bar_') for name in names)
def test_containers_one_off(self): def test_containers_one_off(self):
db = self.create_service('db') db = self.create_service('db')
@ -89,18 +90,18 @@ class ServiceTest(DockerClientTestCase):
def test_project_is_added_to_container_name(self): def test_project_is_added_to_container_name(self):
service = self.create_service('web') service = self.create_service('web')
create_and_start_container(service) 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): def test_create_container_with_one_off(self):
db = self.create_service('db') db = self.create_service('db')
container = db.create_container(one_off=True) 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): def test_create_container_with_one_off_when_existing_container_is_running(self):
db = self.create_service('db') db = self.create_service('db')
db.start() db.start()
container = db.create_container(one_off=True) 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): def test_create_container_with_unspecified_volume(self):
service = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) 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.Entrypoint') == ['top']
assert old_container.get('Config.Cmd') == ['-d', '1'] assert old_container.get('Config.Cmd') == ['-d', '1']
assert 'FOO=1' in old_container.get('Config.Env') 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) service.start_container(old_container)
old_container.inspect() # reload volume data old_container.inspect() # reload volume data
volume_path = old_container.get_mount('/etc')['Source'] 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.Entrypoint') == ['top']
assert new_container.get('Config.Cmd') == ['-d', '1'] assert new_container.get('Config.Cmd') == ['-d', '1']
assert 'FOO=2' in new_container.get('Config.Env') 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 assert new_container.get_mount('/etc')['Source'] == volume_path
if not is_cluster(self.client): if not is_cluster(self.client):
assert ( assert (
@ -836,13 +837,13 @@ class ServiceTest(DockerClientTestCase):
db = self.create_service('db') db = self.create_service('db')
web = self.create_service('web', links=[(db, None)]) web = self.create_service('web', links=[(db, None)])
create_and_start_container(db) db1 = create_and_start_container(db)
create_and_start_container(db) db2 = create_and_start_container(db)
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_1', 'db_1', db1.name, db1.name_without_project,
'composetest_db_2', 'db_2', db2.name, db2.name_without_project,
'db' 'db'
]) ])
@ -851,30 +852,33 @@ class ServiceTest(DockerClientTestCase):
db = self.create_service('db') db = self.create_service('db')
web = self.create_service('web', links=[(db, 'custom_link_name')]) web = self.create_service('web', links=[(db, 'custom_link_name')])
create_and_start_container(db) db1 = create_and_start_container(db)
create_and_start_container(db) db2 = create_and_start_container(db)
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_1', 'db_1', db1.name, db1.name_without_project,
'composetest_db_2', 'db_2', db2.name, db2.name_without_project,
'custom_link_name' 'custom_link_name'
]) ])
@no_cluster('No legacy links support in Swarm') @no_cluster('No legacy links support in Swarm')
def test_start_container_with_external_links(self): def test_start_container_with_external_links(self):
db = self.create_service('db') db = self.create_service('db')
web = self.create_service('web', external_links=['composetest_db_1', db_ctnrs = [create_and_start_container(db) for _ in range(3)]
'composetest_db_2', web = self.create_service(
'composetest_db_3:db_3']) '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) create_and_start_container(web)
assert set(get_links(web.containers()[0])) == set([ assert set(get_links(web.containers()[0])) == set([
'composetest_db_1', db_ctnrs[0].name,
'composetest_db_2', db_ctnrs[1].name,
'db_3' 'db_3'
]) ])
@ -892,14 +896,14 @@ class ServiceTest(DockerClientTestCase):
def test_start_one_off_container_creates_links_to_its_own_service(self): def test_start_one_off_container_creates_links_to_its_own_service(self):
db = self.create_service('db') db = self.create_service('db')
create_and_start_container(db) db1 = create_and_start_container(db)
create_and_start_container(db) db2 = create_and_start_container(db)
c = create_and_start_container(db, one_off=OneOffFilter.only) c = create_and_start_container(db, one_off=OneOffFilter.only)
assert set(get_links(c)) == set([ assert set(get_links(c)) == set([
'composetest_db_1', 'db_1', db1.name, db1.name_without_project,
'composetest_db_2', 'db_2', db2.name, db2.name_without_project,
'db' 'db'
]) ])
@ -1249,10 +1253,9 @@ class ServiceTest(DockerClientTestCase):
test that those containers are restarted and not removed/recreated. test that those containers are restarted and not removed/recreated.
""" """
service = self.create_service('web') service = self.create_service('web')
next_number = service._next_container_number() valid_numbers = [service._next_container_number(), service._next_container_number()]
valid_numbers = [next_number, next_number + 1] service.create_container(number=valid_numbers[0])
service.create_container(number=next_number) service.create_container(number=valid_numbers[1])
service.create_container(number=next_number + 1)
ParallelStreamWriter.instance = None ParallelStreamWriter.instance = None
with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr: with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr:
@ -1310,10 +1313,8 @@ class ServiceTest(DockerClientTestCase):
assert len(service.containers()) == 1 assert len(service.containers()) == 1
assert service.containers()[0].is_running assert service.containers()[0].is_running
assert ( assert "ERROR: for composetest_web_" in mock_stderr.getvalue()
"ERROR: for composetest_web_2 Cannot create container for service" assert "Cannot create container for service web: Boom" in mock_stderr.getvalue()
" web: Boom" in mock_stderr.getvalue()
)
def test_scale_with_unexpected_exception(self): def test_scale_with_unexpected_exception(self):
"""Test that when scaling if the API returns an error, that is not of type """Test that when scaling if the API returns an error, that is not of type
@ -1580,18 +1581,20 @@ class ServiceTest(DockerClientTestCase):
} }
compose_labels = { compose_labels = {
LABEL_CONTAINER_NUMBER: '1',
LABEL_ONE_OFF: 'False', LABEL_ONE_OFF: 'False',
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)
service = self.create_service('web', labels=labels_dict) 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(): for pair in expected.items():
assert pair in labels assert pair in labels
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': ''}
@ -1655,7 +1658,7 @@ class ServiceTest(DockerClientTestCase):
def test_duplicate_containers(self): def test_duplicate_containers(self):
service = self.create_service('web') 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) original = Container.create(service.client, **options)
assert set(service.containers(stopped=True)) == set([original]) assert set(service.containers(stopped=True)) == set([original])

View File

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

View File

@ -30,7 +30,8 @@ class ContainerTest(unittest.TestCase):
"Labels": { "Labels": {
"com.docker.compose.project": "composetest", "com.docker.compose.project": "composetest",
"com.docker.compose.service": "web", "com.docker.compose.service": "web",
"com.docker.compose.container-number": 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): 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_7" assert container.name_without_project == "web_7_092cd63296fd"
def test_name_without_project_custom_container_name(self): def test_name_without_project_custom_container_name(self):
self.container_dict['Name'] = "/custom_name_of_container" self.container_dict['Name'] = "/custom_name_of_container"

View File

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