Make run behave in the same way as up

Signed-off-by: Eric Hripko <ehripko@bloomberg.net>
This commit is contained in:
Eric Hripko 2020-05-15 14:32:18 +01:00
parent 52d2fcc274
commit 06462cd604
6 changed files with 112 additions and 35 deletions

View File

@ -1298,31 +1298,28 @@ def build_one_off_container_options(options, detach, command):
def run_one_off_container(container_options, project, service, options, toplevel_options,
toplevel_environment):
if not options['--no-deps']:
deps = service.get_dependency_names()
if deps:
project.up(
service_names=deps,
start_deps=True,
strategy=ConvergenceStrategy.never,
rescale=False
)
project.initialize()
container = service.create_container(
quiet=True,
detach = options.get('--detach')
use_network_aliases = options.get('--use-aliases')
containers = project.up(
service_names=[service.name],
start_deps=not options['--no-deps'],
strategy=ConvergenceStrategy.never,
detached=detach,
rescale=False,
one_off=True,
**container_options)
override_options=container_options,
)
try:
container = next(c for c in containers if c.service == service.name)
except StopIteration:
raise OperationFailedError('Could not bring up the requested service')
use_network_aliases = options['--use-aliases']
if options.get('--detach'):
if detach:
service.start_container(container, use_network_aliases)
print(container.name)
return
def remove_container(force=False):
def remove_container():
if options['--rm']:
project.client.remove_container(container.id, force=True, v=True)
@ -1355,7 +1352,7 @@ def run_one_off_container(container_options, project, service, options, toplevel
exit_code = 1
except (signals.ShutdownException, signals.HangUpException):
project.client.kill(container.id)
remove_container(force=True)
remove_container()
sys.exit(2)
remove_container()

View File

@ -565,6 +565,8 @@ class Project(object):
renew_anonymous_volumes=False,
silent=False,
cli=False,
one_off=False,
override_options=None,
):
if cli:
@ -584,7 +586,11 @@ class Project(object):
for svc in services:
svc.ensure_image_exists(do_build=do_build, silent=silent, cli=cli)
plans = self._get_convergence_plans(
services, strategy, always_recreate_deps=always_recreate_deps)
services,
strategy,
always_recreate_deps=always_recreate_deps,
one_off=service_names if one_off else [],
)
def do(service):
@ -597,6 +603,7 @@ class Project(object):
start=start,
reset_container_image=reset_container_image,
renew_anonymous_volumes=renew_anonymous_volumes,
override_options=override_options,
)
def get_deps(service):
@ -628,7 +635,7 @@ class Project(object):
self.networks.initialize()
self.volumes.initialize()
def _get_convergence_plans(self, services, strategy, always_recreate_deps=False):
def _get_convergence_plans(self, services, strategy, always_recreate_deps=False, one_off=None):
plans = {}
for service in services:
@ -638,6 +645,7 @@ class Project(object):
if name in plans and
plans[name].action in ('recreate', 'create')
]
is_one_off = one_off and service.name in one_off
if updated_dependencies and strategy.allows_recreate:
log.debug('%s has upstream changes (%s)',
@ -649,11 +657,11 @@ class Project(object):
container_has_links = any(c.get('HostConfig.Links') for c in service.containers())
should_recreate_for_links = service_has_links ^ container_has_links
if always_recreate_deps or containers_stopped or should_recreate_for_links:
plan = service.convergence_plan(ConvergenceStrategy.always)
plan = service.convergence_plan(ConvergenceStrategy.always, is_one_off)
else:
plan = service.convergence_plan(strategy)
plan = service.convergence_plan(strategy, is_one_off)
else:
plan = service.convergence_plan(strategy)
plan = service.convergence_plan(strategy, is_one_off)
plans[service.name] = plan

View File

@ -388,9 +388,12 @@ class Service(object):
platform = self.default_platform
return platform
def convergence_plan(self, strategy=ConvergenceStrategy.changed):
def convergence_plan(self, strategy=ConvergenceStrategy.changed, one_off=False):
containers = self.containers(stopped=True)
if one_off:
return ConvergencePlan('one_off', [])
if not containers:
return ConvergencePlan('create', [])
@ -439,25 +442,37 @@ class Service(object):
return has_diverged
def _execute_convergence_create(self, scale, detached, start):
def _execute_convergence_create(self, scale, detached, start, one_off=False, override_options=None):
i = self._next_container_number()
def create_and_start(service, n):
container = service.create_container(number=n, quiet=True)
if one_off:
container = service.create_container(one_off=True, quiet=True, **override_options)
else:
container = service.create_container(number=n, quiet=True)
if not detached:
container.attach_log_stream()
if start:
if start and not one_off:
self.start_container(container)
return container
def get_name(service_name):
if one_off:
return "_".join([
service_name.project,
service_name.service,
"run",
])
return self.get_container_name(service_name.service, service_name.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),
get_name,
"Creating"
)
for error in errors.values():
@ -528,16 +543,20 @@ class Service(object):
def execute_convergence_plan(self, plan, timeout=None, detached=False,
start=True, scale_override=None,
rescale=True, reset_container_image=False,
renew_anonymous_volumes=False):
renew_anonymous_volumes=False, override_options=None):
(action, containers) = plan
scale = scale_override if scale_override is not None else self.scale_num
containers = sorted(containers, key=attrgetter('number'))
self.show_scale_warnings(scale)
if action == 'create':
if action in ['create', 'one_off']:
return self._execute_convergence_create(
scale, detached, start
scale,
detached,
start,
one_off=(action == 'one_off'),
override_options=override_options
)
# The create action needs always needs an initial scale, but otherwise,

View File

@ -1780,6 +1780,14 @@ services:
assert len(db.containers()) == 1
assert len(console.containers()) == 0
def test_run_service_with_unhealthy_dependencies(self):
self.base_dir = 'tests/fixtures/v2-unhealthy-dependencies'
result = self.dispatch(['run', 'web', '/bin/true'], returncode=1)
assert re.search(
re.compile('for web .*is unhealthy.*', re.MULTILINE),
result.stderr
)
def test_run_service_with_scaled_dependencies(self):
self.base_dir = 'tests/fixtures/v2-dependencies'
self.dispatch(['up', '-d', '--scale', 'db=2', '--scale', 'console=0'])

View File

@ -0,0 +1,19 @@
version: "2.1"
services:
db:
image: busybox:1.31.0-uclibc
command: top
healthcheck:
test: exit 1
interval: 1s
timeout: 1s
retries: 1
web:
image: busybox:1.31.0-uclibc
command: top
depends_on:
db:
condition: service_healthy
console:
image: busybox:1.31.0-uclibc
command: top

View File

@ -18,6 +18,8 @@ from compose.cli.docopt_command import NoSuchCommand
from compose.cli.errors import UserError
from compose.cli.main import TopLevelCommand
from compose.const import IS_WINDOWS_PLATFORM
from compose.const import LABEL_SERVICE
from compose.container import Container
from compose.project import Project
@ -94,12 +96,26 @@ class CLITestCase(unittest.TestCase):
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
@mock.patch('compose.cli.main.RunOperation', autospec=True)
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
@mock.patch('compose.service.Container.create')
@mock.patch.dict(os.environ)
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
def test_run_interactive_passes_logs_false(
self,
mock_container_create,
mock_pseudo_terminal,
mock_run_operation,
):
os.environ['COMPOSE_INTERACTIVE_NO_CLI'] = 'true'
mock_client = mock.create_autospec(docker.APIClient)
mock_client.api_version = DEFAULT_DOCKER_API_VERSION
mock_client._general_configs = {}
mock_container_create.return_value = Container(mock_client, {
'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8',
'Config': {
'Labels': {
LABEL_SERVICE: 'service',
}
},
}, has_been_inspected=True)
project = Project.from_config(
name='composetest',
client=mock_client,
@ -132,10 +148,20 @@ class CLITestCase(unittest.TestCase):
_, _, call_kwargs = mock_run_operation.mock_calls[0]
assert call_kwargs['logs'] is False
def test_run_service_with_restart_always(self):
@mock.patch('compose.service.Container.create')
def test_run_service_with_restart_always(self, mock_container_create):
mock_client = mock.create_autospec(docker.APIClient)
mock_client.api_version = DEFAULT_DOCKER_API_VERSION
mock_client._general_configs = {}
mock_container_create.return_value = Container(mock_client, {
'Id': '37b35e0ba80d91009d37e16f249b32b84f72bda269985578ed6c75a0a13fcaa8',
'Name': 'composetest_service_37b35',
'Config': {
'Labels': {
LABEL_SERVICE: 'service',
}
},
}, has_been_inspected=True)
project = Project.from_config(
name='composetest',