diff options
Diffstat (limited to 'tests/acceptance/cli_test.py')
-rw-r--r-- | tests/acceptance/cli_test.py | 865 |
1 files changed, 783 insertions, 82 deletions
diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index ffba3002..bba2238e 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals import datetime import json import os +import os.path +import re import signal import subprocess import time @@ -11,19 +14,27 @@ from collections import Counter from collections import namedtuple from operator import attrgetter -import py +import pytest +import six import yaml from docker import errors from .. import mock +from ..helpers import create_host_file from compose.cli.command import get_project +from compose.config.errors import DuplicateOverrideFileFound from compose.container import Container from compose.project import OneOffFilter +from compose.utils import nanoseconds_from_time_seconds from tests.integration.testcases import DockerClientTestCase from tests.integration.testcases import get_links +from tests.integration.testcases import is_cluster +from tests.integration.testcases import no_cluster from tests.integration.testcases import pull_busybox +from tests.integration.testcases import SWARM_SKIP_RM_VOLUMES +from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only - +from tests.integration.testcases import v3_only ProcessResult = namedtuple('ProcessResult', 'stdout stderr') @@ -61,7 +72,8 @@ def wait_on_condition(condition, delay=0.1, timeout=40): def kill_service(service): for container in service.containers(): - container.kill() + if container.is_running: + container.kill() class ContainerCountCondition(object): @@ -71,7 +83,7 @@ class ContainerCountCondition(object): self.expected = expected def __call__(self): - return len(self.project.containers()) == self.expected + return len([c for c in self.project.containers() if c.is_running]) == self.expected def __str__(self): return "waiting for counter count == %s" % self.expected @@ -100,19 +112,25 @@ class CLITestCase(DockerClientTestCase): def setUp(self): super(CLITestCase, self).setUp() self.base_dir = 'tests/fixtures/simple-composefile' + self.override_dir = None def tearDown(self): if self.base_dir: self.project.kill() - self.project.remove_stopped() + self.project.down(None, True) for container in self.project.containers(stopped=True, one_off=OneOffFilter.only): container.remove(force=True) - networks = self.client.networks() for n in networks: - if n['Name'].startswith('{}_'.format(self.project.name)): + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)): self.client.remove_network(n['Name']) + volumes = self.client.volumes().get('Volumes') or [] + for v in volumes: + if v['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)): + self.client.remove_volume(v['Name']) + if hasattr(self, '_project'): + del self._project super(CLITestCase, self).tearDown() @@ -120,7 +138,7 @@ class CLITestCase(DockerClientTestCase): def project(self): # Hack: allow project to be overridden if not hasattr(self, '_project'): - self._project = get_project(self.base_dir) + self._project = get_project(self.base_dir, override_dir=self.override_dir) return self._project def dispatch(self, options, project_options=None, returncode=0): @@ -141,10 +159,16 @@ class CLITestCase(DockerClientTestCase): def test_help(self): self.base_dir = 'tests/fixtures/no-composefile' result = self.dispatch(['help', 'up'], returncode=0) - assert 'Usage: up [options] [SERVICE...]' in result.stdout + assert 'Usage: up [options] [--scale SERVICE=NUM...] [SERVICE...]' in result.stdout # Prevent tearDown from trying to create a project self.base_dir = None + def test_help_nonexistent(self): + self.base_dir = 'tests/fixtures/no-composefile' + result = self.dispatch(['help', 'foobar'], returncode=1) + assert 'No such command' in result.stderr + self.base_dir = None + def test_shorthand_host_opt(self): self.dispatch( ['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')), @@ -152,11 +176,32 @@ class CLITestCase(DockerClientTestCase): returncode=0 ) + def test_host_not_reachable(self): + result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1) + assert "Couldn't connect to Docker daemon" in result.stderr + + def test_host_not_reachable_volumes_from_container(self): + self.base_dir = 'tests/fixtures/volumes-from-container' + + container = self.client.create_container( + 'busybox', 'true', name='composetest_data_container', + host_config={} + ) + self.addCleanup(self.client.remove_container, container) + + result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1) + assert "Couldn't connect to Docker daemon" in result.stderr + def test_config_list_services(self): self.base_dir = 'tests/fixtures/v2-full' result = self.dispatch(['config', '--services']) assert set(result.stdout.rstrip().split('\n')) == {'web', 'other'} + def test_config_list_volumes(self): + self.base_dir = 'tests/fixtures/v2-full' + result = self.dispatch(['config', '--volumes']) + assert set(result.stdout.rstrip().split('\n')) == {'data'} + def test_config_quiet_with_error(self): self.base_dir = None result = self.dispatch([ @@ -191,7 +236,7 @@ class CLITestCase(DockerClientTestCase): 'other': { 'image': 'busybox:latest', 'command': 'top', - 'volumes': ['/data:rw'], + 'volumes': ['/data'], }, }, } @@ -219,9 +264,11 @@ class CLITestCase(DockerClientTestCase): 'image': 'busybox', 'restart': 'on-failure:5', }, + 'restart-null': { + 'image': 'busybox', + 'restart': '' + }, }, - 'networks': {}, - 'volumes': {}, } def test_config_external_network(self): @@ -238,11 +285,75 @@ class CLITestCase(DockerClientTestCase): } } + def test_config_external_volume_v2(self): + self.base_dir = 'tests/fixtures/volumes' + result = self.dispatch(['-f', 'external-volumes-v2.yml', 'config']) + json_result = yaml.load(result.stdout) + assert 'volumes' in json_result + assert json_result['volumes'] == { + 'foo': { + 'external': True, + }, + 'bar': { + 'external': { + 'name': 'some_bar', + }, + } + } + + def test_config_external_volume_v2_x(self): + self.base_dir = 'tests/fixtures/volumes' + result = self.dispatch(['-f', 'external-volumes-v2-x.yml', 'config']) + json_result = yaml.load(result.stdout) + assert 'volumes' in json_result + assert json_result['volumes'] == { + 'foo': { + 'external': True, + 'name': 'some_foo', + }, + 'bar': { + 'external': True, + 'name': 'some_bar', + } + } + + def test_config_external_volume_v3_x(self): + self.base_dir = 'tests/fixtures/volumes' + result = self.dispatch(['-f', 'external-volumes-v3-x.yml', 'config']) + json_result = yaml.load(result.stdout) + assert 'volumes' in json_result + assert json_result['volumes'] == { + 'foo': { + 'external': True, + }, + 'bar': { + 'external': { + 'name': 'some_bar', + }, + } + } + + def test_config_external_volume_v3_4(self): + self.base_dir = 'tests/fixtures/volumes' + result = self.dispatch(['-f', 'external-volumes-v3-4.yml', 'config']) + json_result = yaml.load(result.stdout) + assert 'volumes' in json_result + assert json_result['volumes'] == { + 'foo': { + 'external': True, + 'name': 'some_foo', + }, + 'bar': { + 'external': True, + 'name': 'some_bar', + } + } + def test_config_v1(self): self.base_dir = 'tests/fixtures/v1-config' result = self.dispatch(['config']) assert yaml.load(result.stdout) == { - 'version': '2.0', + 'version': '2.1', 'services': { 'net': { 'image': 'busybox', @@ -250,7 +361,7 @@ class CLITestCase(DockerClientTestCase): }, 'volume': { 'image': 'busybox', - 'volumes': ['/data:rw'], + 'volumes': ['/data'], 'network_mode': 'bridge', }, 'app': { @@ -259,8 +370,73 @@ class CLITestCase(DockerClientTestCase): 'network_mode': 'service:net', }, }, - 'networks': {}, - 'volumes': {}, + } + + @v3_only() + def test_config_v3(self): + self.base_dir = 'tests/fixtures/v3-full' + result = self.dispatch(['config']) + + assert yaml.load(result.stdout) == { + 'version': '3.2', + 'volumes': { + 'foobar': { + 'labels': { + 'com.docker.compose.test': 'true', + }, + }, + }, + 'services': { + 'web': { + 'image': 'busybox', + 'deploy': { + 'mode': 'replicated', + 'replicas': 6, + 'labels': ['FOO=BAR'], + 'update_config': { + 'parallelism': 3, + 'delay': '10s', + 'failure_action': 'continue', + 'monitor': '60s', + 'max_failure_ratio': 0.3, + }, + 'resources': { + 'limits': { + 'cpus': '0.001', + 'memory': '50M', + }, + 'reservations': { + 'cpus': '0.0001', + 'memory': '20M', + }, + }, + 'restart_policy': { + 'condition': 'on_failure', + 'delay': '5s', + 'max_attempts': 3, + 'window': '120s', + }, + 'placement': { + 'constraints': ['node=foo'], + }, + }, + + 'healthcheck': { + 'test': 'cat /etc/passwd', + 'interval': '10s', + 'timeout': '1s', + 'retries': 5, + }, + 'volumes': [ + '/host/path:/container/path:ro', + 'foobar:/container/volumepath:rw', + '/anonymous', + 'foobar:/container/volumepath2:nocopy' + ], + + 'stop_grace_period': '20s', + }, + }, } def test_ps(self): @@ -308,19 +484,38 @@ class CLITestCase(DockerClientTestCase): def test_pull_with_ignore_pull_failures(self): result = self.dispatch([ '-f', 'ignore-pull-failures.yml', - 'pull', '--ignore-pull-failures']) + 'pull', '--ignore-pull-failures'] + ) assert 'Pulling simple (busybox:latest)...' in result.stderr assert 'Pulling another (nonexisting-image:latest)...' in result.stderr - assert 'Error: image library/nonexisting-image' in result.stderr - assert 'not found' in result.stderr + assert ('repository nonexisting-image not found' in result.stderr or + 'image library/nonexisting-image:latest not found' in result.stderr or + 'pull access denied for nonexisting-image' in result.stderr) + + def test_pull_with_parallel_failure(self): + result = self.dispatch([ + '-f', 'ignore-pull-failures.yml', 'pull', '--parallel'], + returncode=1 + ) + + self.assertRegexpMatches(result.stderr, re.compile('^Pulling simple', re.MULTILINE)) + self.assertRegexpMatches(result.stderr, re.compile('^Pulling another', re.MULTILINE)) + self.assertRegexpMatches(result.stderr, + re.compile('^ERROR: for another .*does not exist.*', re.MULTILINE)) + self.assertRegexpMatches(result.stderr, + re.compile('''^(ERROR: )?(b')?.* nonexisting-image''', + re.MULTILINE)) + + def test_pull_with_quiet(self): + assert self.dispatch(['pull', '--quiet']).stderr == '' + assert self.dispatch(['pull', '--quiet']).stdout == '' def test_build_plain(self): self.base_dir = 'tests/fixtures/simple-dockerfile' self.dispatch(['build', 'simple']) result = self.dispatch(['build', 'simple']) - assert BUILD_CACHE_TEXT in result.stdout assert BUILD_PULL_TEXT not in result.stdout def test_build_no_cache(self): @@ -338,7 +533,9 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['build', 'simple'], None) result = self.dispatch(['build', '--pull', 'simple']) - assert BUILD_CACHE_TEXT in result.stdout + if not is_cluster(self.client): + # If previous build happened on another node, cache won't be available + assert BUILD_CACHE_TEXT in result.stdout assert BUILD_PULL_TEXT in result.stdout def test_build_no_cache_pull(self): @@ -351,6 +548,7 @@ class CLITestCase(DockerClientTestCase): assert BUILD_CACHE_TEXT not in result.stdout assert BUILD_PULL_TEXT in result.stdout + @pytest.mark.xfail(reason='17.10.0 RC bug remove after GA https://github.com/moby/moby/issues/35116') def test_build_failed(self): self.base_dir = 'tests/fixtures/simple-failing-dockerfile' self.dispatch(['build', 'simple'], returncode=1) @@ -364,6 +562,7 @@ class CLITestCase(DockerClientTestCase): ] assert len(containers) == 1 + @pytest.mark.xfail(reason='17.10.0 RC bug remove after GA https://github.com/moby/moby/issues/35116') def test_build_failed_forcerm(self): self.base_dir = 'tests/fixtures/simple-failing-dockerfile' self.dispatch(['build', '--force-rm', 'simple'], returncode=1) @@ -378,9 +577,15 @@ class CLITestCase(DockerClientTestCase): ] assert not containers + def test_build_shm_size_build_option(self): + pull_busybox(self.client) + self.base_dir = 'tests/fixtures/build-shm-size' + result = self.dispatch(['build', '--no-cache'], None) + assert 'shm_size: 96' in result.stdout + def test_bundle_with_digests(self): self.base_dir = 'tests/fixtures/bundle-with-digests/' - tmpdir = py.test.ensuretemp('cli_test_bundle') + tmpdir = pytest.ensuretemp('cli_test_bundle') self.addCleanup(tmpdir.remove) filename = str(tmpdir.join('example.dab')) @@ -404,46 +609,136 @@ class CLITestCase(DockerClientTestCase): }, } + def test_build_override_dir(self): + self.base_dir = 'tests/fixtures/build-path-override-dir' + self.override_dir = os.path.abspath('tests/fixtures') + result = self.dispatch([ + '--project-directory', self.override_dir, + 'build']) + + assert 'Successfully built' in result.stdout + + def test_build_override_dir_invalid_path(self): + config_path = os.path.abspath('tests/fixtures/build-path-override-dir/docker-compose.yml') + result = self.dispatch([ + '-f', config_path, + 'build'], returncode=1) + + assert 'does not exist, is not accessible, or is not a valid URL' in result.stderr + def test_create(self): self.dispatch(['create']) service = self.project.get_service('simple') another = self.project.get_service('another') - self.assertEqual(len(service.containers()), 0) - self.assertEqual(len(another.containers()), 0) - self.assertEqual(len(service.containers(stopped=True)), 1) - self.assertEqual(len(another.containers(stopped=True)), 1) + service_containers = service.containers(stopped=True) + another_containers = another.containers(stopped=True) + assert len(service_containers) == 1 + assert len(another_containers) == 1 + assert not service_containers[0].is_running + assert not another_containers[0].is_running def test_create_with_force_recreate(self): self.dispatch(['create'], None) service = self.project.get_service('simple') - self.assertEqual(len(service.containers()), 0) - self.assertEqual(len(service.containers(stopped=True)), 1) + service_containers = service.containers(stopped=True) + assert len(service_containers) == 1 + assert not service_containers[0].is_running old_ids = [c.id for c in service.containers(stopped=True)] self.dispatch(['create', '--force-recreate'], None) - self.assertEqual(len(service.containers()), 0) - self.assertEqual(len(service.containers(stopped=True)), 1) + service_containers = service.containers(stopped=True) + assert len(service_containers) == 1 + assert not service_containers[0].is_running - new_ids = [c.id for c in service.containers(stopped=True)] + new_ids = [c.id for c in service_containers] - self.assertNotEqual(old_ids, new_ids) + assert old_ids != new_ids def test_create_with_no_recreate(self): self.dispatch(['create'], None) service = self.project.get_service('simple') - self.assertEqual(len(service.containers()), 0) - self.assertEqual(len(service.containers(stopped=True)), 1) + service_containers = service.containers(stopped=True) + assert len(service_containers) == 1 + assert not service_containers[0].is_running old_ids = [c.id for c in service.containers(stopped=True)] self.dispatch(['create', '--no-recreate'], None) - self.assertEqual(len(service.containers()), 0) - self.assertEqual(len(service.containers(stopped=True)), 1) + service_containers = service.containers(stopped=True) + assert len(service_containers) == 1 + assert not service_containers[0].is_running - new_ids = [c.id for c in service.containers(stopped=True)] + new_ids = [c.id for c in service_containers] - self.assertEqual(old_ids, new_ids) + assert old_ids == new_ids + + def test_run_one_off_with_volume(self): + self.base_dir = 'tests/fixtures/simple-composefile-volume-ready' + volume_path = os.path.abspath(os.path.join(os.getcwd(), self.base_dir, 'files')) + node = create_host_file(self.client, os.path.join(volume_path, 'example.txt')) + + self.dispatch([ + 'run', + '-v', '{}:/data'.format(volume_path), + '-e', 'constraint:node=={}'.format(node if node is not None else '*'), + 'simple', + 'test', '-f', '/data/example.txt' + ], returncode=0) + + service = self.project.get_service('simple') + container_data = service.containers(one_off=OneOffFilter.only, stopped=True)[0] + mount = container_data.get('Mounts')[0] + assert mount['Source'] == volume_path + assert mount['Destination'] == '/data' + assert mount['Type'] == 'bind' + + def test_run_one_off_with_multiple_volumes(self): + self.base_dir = 'tests/fixtures/simple-composefile-volume-ready' + volume_path = os.path.abspath(os.path.join(os.getcwd(), self.base_dir, 'files')) + node = create_host_file(self.client, os.path.join(volume_path, 'example.txt')) + + self.dispatch([ + 'run', + '-v', '{}:/data'.format(volume_path), + '-v', '{}:/data1'.format(volume_path), + '-e', 'constraint:node=={}'.format(node if node is not None else '*'), + 'simple', + 'test', '-f', '/data/example.txt' + ], returncode=0) + + self.dispatch([ + 'run', + '-v', '{}:/data'.format(volume_path), + '-v', '{}:/data1'.format(volume_path), + '-e', 'constraint:node=={}'.format(node if node is not None else '*'), + 'simple', + 'test', '-f' '/data1/example.txt' + ], returncode=0) + + def test_run_one_off_with_volume_merge(self): + self.base_dir = 'tests/fixtures/simple-composefile-volume-ready' + volume_path = os.path.abspath(os.path.join(os.getcwd(), self.base_dir, 'files')) + create_host_file(self.client, os.path.join(volume_path, 'example.txt')) + + self.dispatch([ + '-f', 'docker-compose.merge.yml', + 'run', + '-v', '{}:/data'.format(volume_path), + 'simple', + 'test', '-f', '/data/example.txt' + ], returncode=0) + + service = self.project.get_service('simple') + container_data = service.containers(one_off=OneOffFilter.only, stopped=True)[0] + mounts = container_data.get('Mounts') + assert len(mounts) == 2 + config_mount = [m for m in mounts if m['Destination'] == '/data1'][0] + override_mount = [m for m in mounts if m['Destination'] == '/data'][0] + + assert config_mount['Type'] == 'volume' + assert override_mount['Source'] == volume_path + assert override_mount['Type'] == 'bind' def test_create_with_force_recreate_and_no_recreate(self): self.dispatch( @@ -511,7 +806,7 @@ class CLITestCase(DockerClientTestCase): network_name = self.project.networks.networks['default'].full_name networks = self.client.networks(names=[network_name]) self.assertEqual(len(networks), 1) - self.assertEqual(networks[0]['Driver'], 'bridge') + assert networks[0]['Driver'] == 'bridge' if not is_cluster(self.client) else 'overlay' assert 'com.docker.network.bridge.enable_icc' not in networks[0]['Options'] network = self.client.inspect_network(networks[0]['Id']) @@ -534,6 +829,45 @@ class CLITestCase(DockerClientTestCase): assert self.lookup(container, service.name) @v2_only() + def test_up_no_start(self): + self.base_dir = 'tests/fixtures/v2-full' + self.dispatch(['up', '--no-start'], None) + + services = self.project.get_services() + + default_network = self.project.networks.networks['default'].full_name + front_network = self.project.networks.networks['front'].full_name + networks = self.client.networks(names=[default_network, front_network]) + assert len(networks) == 2 + + for service in services: + containers = service.containers(stopped=True) + assert len(containers) == 1 + + container = containers[0] + assert not container.is_running + assert container.get('State.Status') == 'created' + + volumes = self.project.volumes.volumes + assert 'data' in volumes + volume = volumes['data'] + + # The code below is a Swarm-compatible equivalent to volume.exists() + remote_volumes = [ + v for v in self.client.volumes().get('Volumes', []) + if v['Name'].split('/')[-1] == volume.full_name + ] + assert len(remote_volumes) > 0 + + @v2_only() + def test_up_no_ansi(self): + self.base_dir = 'tests/fixtures/v2-simple' + result = self.dispatch(['--no-ansi', 'up', '-d'], None) + assert "%c[2K\r" % 27 not in result.stderr + assert "%c[1A" % 27 not in result.stderr + assert "%c[1B" % 27 not in result.stderr + + @v2_only() def test_up_with_default_network_config(self): filename = 'default-network-config.yml' @@ -557,11 +891,11 @@ class CLITestCase(DockerClientTestCase): networks = [ n for n in self.client.networks() - if n['Name'].startswith('{}_'.format(self.project.name)) + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] # Two networks were created: back and front - assert sorted(n['Name'] for n in networks) == [back_name, front_name] + assert sorted(n['Name'].split('/')[-1] for n in networks) == [back_name, front_name] web_container = self.project.get_service('web').containers()[0] back_aliases = web_container.get( @@ -576,6 +910,24 @@ class CLITestCase(DockerClientTestCase): assert 'ahead' in front_aliases @v2_only() + def test_up_with_network_internal(self): + self.require_api_version('1.23') + filename = 'network-internal.yml' + self.base_dir = 'tests/fixtures/networks' + self.dispatch(['-f', filename, 'up', '-d'], None) + internal_net = '{}_internal'.format(self.project.name) + + networks = [ + n for n in self.client.networks() + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) + ] + + # One network was created: internal + assert sorted(n['Name'].split('/')[-1] for n in networks) == [internal_net] + + assert networks[0]['Internal'] is True + + @v2_only() def test_up_with_network_static_addresses(self): filename = 'network-static-addresses.yml' ipv4_address = '172.16.100.100' @@ -586,11 +938,11 @@ class CLITestCase(DockerClientTestCase): networks = [ n for n in self.client.networks() - if n['Name'].startswith('{}_'.format(self.project.name)) + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] # One networks was created: front - assert sorted(n['Name'] for n in networks) == [static_net] + assert sorted(n['Name'].split('/')[-1] for n in networks) == [static_net] web_container = self.project.get_service('web').containers()[0] ipam_config = web_container.get( @@ -609,14 +961,19 @@ class CLITestCase(DockerClientTestCase): networks = [ n for n in self.client.networks() - if n['Name'].startswith('{}_'.format(self.project.name)) + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] # Two networks were created: back and front - assert sorted(n['Name'] for n in networks) == [back_name, front_name] + assert sorted(n['Name'].split('/')[-1] for n in networks) == [back_name, front_name] - back_network = [n for n in networks if n['Name'] == back_name][0] - front_network = [n for n in networks if n['Name'] == front_name][0] + # lookup by ID instead of name in case of duplicates + back_network = self.client.inspect_network( + [n for n in networks if n['Name'] == back_name][0]['Id'] + ) + front_network = self.client.inspect_network( + [n for n in networks if n['Name'] == front_name][0]['Id'] + ) web_container = self.project.get_service('web').containers()[0] app_container = self.project.get_service('app').containers()[0] @@ -653,8 +1010,12 @@ class CLITestCase(DockerClientTestCase): assert 'Service "web" uses an undefined network "foo"' in result.stderr @v2_only() + @no_cluster('container networks not supported in Swarm') def test_up_with_network_mode(self): - c = self.client.create_container('busybox', 'top', name='composetest_network_mode_container') + c = self.client.create_container( + 'busybox', 'top', name='composetest_network_mode_container', + host_config={} + ) self.addCleanup(self.client.remove_container, c, force=True) self.client.start(c) container_mode_source = 'container:{}'.format(c['Id']) @@ -668,7 +1029,7 @@ class CLITestCase(DockerClientTestCase): networks = [ n for n in self.client.networks() - if n['Name'].startswith('{}_'.format(self.project.name)) + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] assert not networks @@ -705,7 +1066,7 @@ class CLITestCase(DockerClientTestCase): network_names = ['{}_{}'.format(self.project.name, n) for n in ['foo', 'bar']] for name in network_names: - self.client.create_network(name) + self.client.create_network(name, attachable=True) self.dispatch(['-f', filename, 'up', '-d']) container = self.project.containers()[0] @@ -723,17 +1084,57 @@ class CLITestCase(DockerClientTestCase): networks = [ n['Name'] for n in self.client.networks() - if n['Name'].startswith('{}_'.format(self.project.name)) + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] assert not networks network_name = 'composetest_external_network' - self.client.create_network(network_name) + self.client.create_network(network_name, attachable=True) self.dispatch(['-f', filename, 'up', '-d']) container = self.project.containers()[0] assert list(container.get('NetworkSettings.Networks')) == [network_name] + @v2_1_only() + def test_up_with_network_labels(self): + filename = 'network-label.yml' + + self.base_dir = 'tests/fixtures/networks' + self._project = get_project(self.base_dir, [filename]) + + self.dispatch(['-f', filename, 'up', '-d'], returncode=0) + + network_with_label = '{}_network_with_label'.format(self.project.name) + + networks = [ + n for n in self.client.networks() + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) + ] + + assert [n['Name'].split('/')[-1] for n in networks] == [network_with_label] + assert 'label_key' in networks[0]['Labels'] + assert networks[0]['Labels']['label_key'] == 'label_val' + + @v2_1_only() + def test_up_with_volume_labels(self): + filename = 'volume-label.yml' + + self.base_dir = 'tests/fixtures/volumes' + self._project = get_project(self.base_dir, [filename]) + + self.dispatch(['-f', filename, 'up', '-d'], returncode=0) + + volume_with_label = '{}_volume_with_label'.format(self.project.name) + + volumes = [ + v for v in self.client.volumes().get('Volumes', []) + if v['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) + ] + + assert set([v['Name'].split('/')[-1] for v in volumes]) == set([volume_with_label]) + assert 'label_key' in volumes[0]['Labels'] + assert volumes[0]['Labels']['label_key'] == 'label_val' + @v2_only() def test_up_no_services(self): self.base_dir = 'tests/fixtures/no-services' @@ -741,7 +1142,7 @@ class CLITestCase(DockerClientTestCase): network_names = [ n['Name'] for n in self.client.networks() - if n['Name'].startswith('{}_'.format(self.project.name)) + if n['Name'].split('/')[-1].startswith('{}_'.format(self.project.name)) ] assert network_names == [] @@ -776,6 +1177,7 @@ class CLITestCase(DockerClientTestCase): assert "Unsupported config option for services.bar: 'net'" in result.stderr + @no_cluster("Legacy networking not supported on Swarm") def test_up_with_net_v1(self): self.base_dir = 'tests/fixtures/net-container' self.dispatch(['up', '-d'], None) @@ -789,6 +1191,50 @@ class CLITestCase(DockerClientTestCase): assert foo_container.get('HostConfig.NetworkMode') == \ 'container:{}'.format(bar_container.id) + @v3_only() + def test_up_with_healthcheck(self): + def wait_on_health_status(container, status): + def condition(): + container.inspect() + return container.get('State.Health.Status') == status + + return wait_on_condition(condition, delay=0.5) + + self.base_dir = 'tests/fixtures/healthcheck' + self.dispatch(['up', '-d'], None) + + passes = self.project.get_service('passes') + passes_container = passes.containers()[0] + + assert passes_container.get('Config.Healthcheck') == { + "Test": ["CMD-SHELL", "/bin/true"], + "Interval": nanoseconds_from_time_seconds(1), + "Timeout": nanoseconds_from_time_seconds(30 * 60), + "Retries": 1, + } + + wait_on_health_status(passes_container, 'healthy') + + fails = self.project.get_service('fails') + fails_container = fails.containers()[0] + + assert fails_container.get('Config.Healthcheck') == { + "Test": ["CMD", "/bin/false"], + "Interval": nanoseconds_from_time_seconds(2.5), + "Retries": 2, + } + + wait_on_health_status(fails_container, 'unhealthy') + + disabled = self.project.get_service('disabled') + disabled_container = disabled.containers()[0] + + assert disabled_container.get('Config.Healthcheck') == { + "Test": ["NONE"], + } + + assert 'Health' not in disabled_container.get('State') + def test_up_with_no_deps(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['up', '-d', '--no-deps', 'web'], None) @@ -871,10 +1317,44 @@ class CLITestCase(DockerClientTestCase): wait_on_condition(ContainerCountCondition(self.project, 0)) def test_up_handles_abort_on_container_exit(self): - start_process(self.base_dir, ['up', '--abort-on-container-exit']) - wait_on_condition(ContainerCountCondition(self.project, 2)) - self.project.stop(['simple']) + self.base_dir = 'tests/fixtures/abort-on-container-exit-0' + proc = start_process(self.base_dir, ['up', '--abort-on-container-exit']) wait_on_condition(ContainerCountCondition(self.project, 0)) + proc.wait() + self.assertEqual(proc.returncode, 0) + + def test_up_handles_abort_on_container_exit_code(self): + self.base_dir = 'tests/fixtures/abort-on-container-exit-1' + proc = start_process(self.base_dir, ['up', '--abort-on-container-exit']) + wait_on_condition(ContainerCountCondition(self.project, 0)) + proc.wait() + self.assertEqual(proc.returncode, 1) + + @v2_only() + @no_cluster('Container PID mode does not work across clusters') + def test_up_with_pid_mode(self): + c = self.client.create_container( + 'busybox', 'top', name='composetest_pid_mode_container', + host_config={} + ) + self.addCleanup(self.client.remove_container, c, force=True) + self.client.start(c) + container_mode_source = 'container:{}'.format(c['Id']) + + self.base_dir = 'tests/fixtures/pid-mode' + + self.dispatch(['up', '-d'], None) + + service_mode_source = 'container:{}'.format( + self.project.get_service('container').containers()[0].id) + service_mode_container = self.project.get_service('service').containers()[0] + assert service_mode_container.get('HostConfig.PidMode') == service_mode_source + + container_mode_container = self.project.get_service('container').containers()[0] + assert container_mode_container.get('HostConfig.PidMode') == container_mode_source + + host_mode_container = self.project.get_service('host').containers()[0] + assert host_mode_container.get('HostConfig.PidMode') == 'host' def test_exec_without_tty(self): self.base_dir = 'tests/fixtures/links-composefile' @@ -882,8 +1362,8 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(self.project.containers()), 1) stdout, stderr = self.dispatch(['exec', '-T', 'console', 'ls', '-1d', '/']) - self.assertEquals(stdout, "/\n") - self.assertEquals(stderr, "") + self.assertEqual(stderr, "") + self.assertEqual(stdout, "/\n") def test_exec_custom_user(self): self.base_dir = 'tests/fixtures/links-composefile' @@ -891,8 +1371,8 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(self.project.containers()), 1) stdout, stderr = self.dispatch(['exec', '-T', '--user=operator', 'console', 'whoami']) - self.assertEquals(stdout, "operator\n") - self.assertEquals(stderr, "") + self.assertEqual(stdout, "operator\n") + self.assertEqual(stderr, "") def test_run_service_without_links(self): self.base_dir = 'tests/fixtures/links-composefile' @@ -923,6 +1403,17 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(db.containers()), 1) self.assertEqual(len(console.containers()), 0) + 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']) + db = self.project.get_service('db') + console = self.project.get_service('console') + assert len(db.containers()) == 2 + assert len(console.containers()) == 0 + self.dispatch(['run', 'web', '/bin/true'], None) + assert len(db.containers()) == 2 + assert len(console.containers()) == 0 + def test_run_with_no_deps(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['run', '--no-deps', 'web', '/bin/true']) @@ -964,6 +1455,37 @@ class CLITestCase(DockerClientTestCase): [u'/bin/true'], ) + @pytest.mark.skipif(SWARM_SKIP_RM_VOLUMES, reason='Swarm DELETE /containers/<id> bug') + def test_run_rm(self): + self.base_dir = 'tests/fixtures/volume' + proc = start_process(self.base_dir, ['run', '--rm', 'test']) + wait_on_condition(ContainerStateCondition( + self.project.client, + 'volume_test_run_1', + 'running')) + service = self.project.get_service('test') + containers = service.containers(one_off=OneOffFilter.only) + self.assertEqual(len(containers), 1) + mounts = containers[0].get('Mounts') + for mount in mounts: + if mount['Destination'] == '/container-path': + anonymous_name = mount['Name'] + break + os.kill(proc.pid, signal.SIGINT) + wait_on_process(proc, 1) + + self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 0) + + volumes = self.client.volumes()['Volumes'] + assert volumes is not None + for volume in service.options.get('volumes'): + if volume.internal == '/container-named-path': + name = volume.external + break + volume_names = [v['Name'].split('/')[-1] for v in volumes] + assert name in volume_names + assert anonymous_name not in volume_names + def test_run_service_with_dockerfile_entrypoint(self): self.base_dir = 'tests/fixtures/entrypoint-dockerfile' self.dispatch(['run', 'test']) @@ -1031,7 +1553,7 @@ class CLITestCase(DockerClientTestCase): container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] self.assertEqual(user, container.get('Config.User')) - def test_run_service_with_environement_overridden(self): + def test_run_service_with_environment_overridden(self): name = 'service' self.base_dir = 'tests/fixtures/environment-composefile' self.dispatch([ @@ -1043,9 +1565,9 @@ class CLITestCase(DockerClientTestCase): ]) service = self.project.get_service(name) container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] - # env overriden + # env overridden self.assertEqual('notbar', container.environment['foo']) - # keep environement from yaml + # keep environment from yaml self.assertEqual('world', container.environment['hello']) # added option from command line self.assertEqual('beta', container.environment['alpha']) @@ -1084,13 +1606,12 @@ class CLITestCase(DockerClientTestCase): container.stop() # check the ports - self.assertNotEqual(port_random, None) - self.assertIn("0.0.0.0", port_random) - self.assertEqual(port_assigned, "0.0.0.0:49152") - self.assertEqual(port_range[0], "0.0.0.0:49153") - self.assertEqual(port_range[1], "0.0.0.0:49154") + assert port_random is not None + assert port_assigned.endswith(':49152') + assert port_range[0].endswith(':49153') + assert port_range[1].endswith(':49154') - def test_run_service_with_explicitly_maped_ports(self): + def test_run_service_with_explicitly_mapped_ports(self): # create one off container self.base_dir = 'tests/fixtures/ports-composefile' self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple']) @@ -1104,10 +1625,10 @@ class CLITestCase(DockerClientTestCase): container.stop() # check the ports - self.assertEqual(port_short, "0.0.0.0:30000") - self.assertEqual(port_full, "0.0.0.0:30001") + assert port_short.endswith(':30000') + assert port_full.endswith(':30001') - def test_run_service_with_explicitly_maped_ip_ports(self): + def test_run_service_with_explicitly_mapped_ip_ports(self): # create one off container self.base_dir = 'tests/fixtures/ports-composefile' self.dispatch([ @@ -1254,6 +1775,23 @@ class CLITestCase(DockerClientTestCase): 'exited')) @mock.patch.dict(os.environ) + def test_run_unicode_env_values_from_system(self): + value = 'ą, ć, ę, ł, ń, ó, ś, ź, ż' + if six.PY2: # os.environ doesn't support unicode values in Py2 + os.environ['BAR'] = value.encode('utf-8') + else: # ... and doesn't support byte values in Py3 + os.environ['BAR'] = value + self.base_dir = 'tests/fixtures/unicode-environment' + result = self.dispatch(['run', 'simple']) + + if six.PY2: # Can't retrieve output on Py3. See issue #3670 + assert value == result.stdout.strip() + + container = self.project.containers(one_off=OneOffFilter.only, stopped=True)[0] + environment = container.get('Config.Env') + assert 'FOO={}'.format(value) in environment + + @mock.patch.dict(os.environ) def test_run_env_values_from_system(self): os.environ['FOO'] = 'bar' os.environ['BAR'] = 'baz' @@ -1278,6 +1816,27 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(service.containers(stopped=True)), 1) self.dispatch(['rm', '-f'], None) self.assertEqual(len(service.containers(stopped=True)), 0) + service = self.project.get_service('simple') + service.create_container() + self.dispatch(['rm', '-fs'], None) + self.assertEqual(len(service.containers(stopped=True)), 0) + + def test_rm_stop(self): + self.dispatch(['up', '-d'], None) + simple = self.project.get_service('simple') + another = self.project.get_service('another') + assert len(simple.containers()) == 1 + assert len(another.containers()) == 1 + self.dispatch(['rm', '-fs'], None) + assert len(simple.containers(stopped=True)) == 0 + assert len(another.containers(stopped=True)) == 0 + + self.dispatch(['up', '-d'], None) + assert len(simple.containers()) == 1 + assert len(another.containers()) == 1 + self.dispatch(['rm', '-fs', 'another'], None) + assert len(simple.containers()) == 1 + assert len(another.containers(stopped=True)) == 0 def test_rm_all(self): service = self.project.get_service('simple') @@ -1383,7 +1942,13 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['logs', '-f']) - assert result.stdout.count('\n') == 5 + if not is_cluster(self.client): + assert result.stdout.count('\n') == 5 + else: + # Sometimes logs are picked up from old containers that haven't yet + # been removed (removal in Swarm is async) + assert result.stdout.count('\n') >= 5 + assert 'simple' in result.stdout assert 'another' in result.stdout assert 'exited with code 0' in result.stdout @@ -1439,7 +2004,10 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up']) result = self.dispatch(['logs', '--tail', '2']) - assert result.stdout.count('\n') == 3 + 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 def test_kill(self): self.dispatch(['up', '-d'], None) @@ -1526,6 +2094,59 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(project.get_service('simple').containers()), 0) self.assertEqual(len(project.get_service('another').containers()), 0) + def test_scale_v2_2(self): + self.base_dir = 'tests/fixtures/scale' + result = self.dispatch(['scale', 'web=1'], returncode=1) + assert 'incompatible with the v2.2 format' in result.stderr + + def test_up_scale_scale_up(self): + self.base_dir = 'tests/fixtures/scale' + project = self.project + + self.dispatch(['up', '-d']) + assert len(project.get_service('web').containers()) == 2 + assert len(project.get_service('db').containers()) == 1 + + self.dispatch(['up', '-d', '--scale', 'web=3']) + assert len(project.get_service('web').containers()) == 3 + assert len(project.get_service('db').containers()) == 1 + + def test_up_scale_scale_down(self): + self.base_dir = 'tests/fixtures/scale' + project = self.project + + self.dispatch(['up', '-d']) + assert len(project.get_service('web').containers()) == 2 + assert len(project.get_service('db').containers()) == 1 + + self.dispatch(['up', '-d', '--scale', 'web=1']) + assert len(project.get_service('web').containers()) == 1 + assert len(project.get_service('db').containers()) == 1 + + def test_up_scale_reset(self): + self.base_dir = 'tests/fixtures/scale' + project = self.project + + self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'db=3']) + assert len(project.get_service('web').containers()) == 3 + assert len(project.get_service('db').containers()) == 3 + + self.dispatch(['up', '-d']) + assert len(project.get_service('web').containers()) == 2 + assert len(project.get_service('db').containers()) == 1 + + def test_up_scale_to_zero(self): + self.base_dir = 'tests/fixtures/scale' + project = self.project + + self.dispatch(['up', '-d']) + assert len(project.get_service('web').containers()) == 2 + assert len(project.get_service('db').containers()) == 1 + + self.dispatch(['up', '-d', '--scale', 'web=0', '--scale', 'db=0']) + assert len(project.get_service('web').containers()) == 0 + assert len(project.get_service('db').containers()) == 0 + def test_port(self): self.base_dir = 'tests/fixtures/ports-composefile' self.dispatch(['up', '-d'], None) @@ -1535,9 +2156,22 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['port', 'simple', str(number)]) return result.stdout.rstrip() - self.assertEqual(get_port(3000), container.get_local_port(3000)) - self.assertEqual(get_port(3001), "0.0.0.0:49152") - self.assertEqual(get_port(3002), "0.0.0.0:49153") + assert get_port(3000) == container.get_local_port(3000) + assert ':49152' in get_port(3001) + assert ':49153' in get_port(3002) + + def test_expanded_port(self): + self.base_dir = 'tests/fixtures/ports-composefile' + self.dispatch(['-f', 'expanded-notation.yml', 'up', '-d']) + container = self.project.get_service('simple').get_container() + + def get_port(number): + result = self.dispatch(['port', 'simple', str(number)]) + return result.stdout.rstrip() + + assert get_port(3000) == container.get_local_port(3000) + assert ':53222' in get_port(3001) + assert ':53223' in get_port(3002) def test_port_with_scale(self): self.base_dir = 'tests/fixtures/ports-composefile-scale' @@ -1590,12 +2224,14 @@ class CLITestCase(DockerClientTestCase): assert len(lines) == 2 container, = self.project.containers() - expected_template = ( - ' container {} {} (image=busybox:latest, ' - 'name=simplecomposefile_simple_1)') + expected_template = ' container {} {}' + expected_meta_info = ['image=busybox:latest', 'name=simplecomposefile_simple_1'] assert expected_template.format('create', container.id) in lines[0] assert expected_template.format('start', container.id) in lines[1] + for line in lines: + for info in expected_meta_info: + assert info in line assert has_timestamp(lines[0]) @@ -1638,7 +2274,6 @@ class CLITestCase(DockerClientTestCase): 'docker-compose.yml', 'docker-compose.override.yml', 'extra.yml', - ] self._project = get_project(self.base_dir, config_paths) self.dispatch( @@ -1655,7 +2290,6 @@ class CLITestCase(DockerClientTestCase): web, other, db = containers self.assertEqual(web.human_readable_command, 'top') - self.assertTrue({'db', 'other'} <= set(get_links(web))) self.assertEqual(db.human_readable_command, 'top') self.assertEqual(other.human_readable_command, 'top') @@ -1687,3 +2321,70 @@ class CLITestCase(DockerClientTestCase): "BAZ=2", ]) self.assertTrue(expected_env <= set(web.get('Config.Env'))) + + def test_top_services_not_running(self): + self.base_dir = 'tests/fixtures/top' + result = self.dispatch(['top']) + assert len(result.stdout) == 0 + + def test_top_services_running(self): + self.base_dir = 'tests/fixtures/top' + self.dispatch(['up', '-d']) + result = self.dispatch(['top']) + + self.assertIn('top_service_a', result.stdout) + self.assertIn('top_service_b', result.stdout) + self.assertNotIn('top_not_a_service', result.stdout) + + def test_top_processes_running(self): + self.base_dir = 'tests/fixtures/top' + self.dispatch(['up', '-d']) + result = self.dispatch(['top']) + assert result.stdout.count("top") == 4 + + def test_forward_exitval(self): + self.base_dir = 'tests/fixtures/exit-code-from' + proc = start_process( + self.base_dir, + ['up', '--abort-on-container-exit', '--exit-code-from', 'another']) + + result = wait_on_process(proc, returncode=1) + + assert 'exitcodefrom_another_1 exited with code 1' in result.stdout + + def test_images(self): + self.project.get_service('simple').create_container() + result = self.dispatch(['images']) + assert 'busybox' in result.stdout + assert 'simplecomposefile_simple_1' in result.stdout + + def test_images_default_composefile(self): + self.base_dir = 'tests/fixtures/multiple-composefiles' + self.dispatch(['up', '-d']) + result = self.dispatch(['images']) + + assert 'busybox' in result.stdout + assert 'multiplecomposefiles_another_1' in result.stdout + assert 'multiplecomposefiles_simple_1' in result.stdout + + def test_up_with_override_yaml(self): + self.base_dir = 'tests/fixtures/override-yaml-files' + self._project = get_project(self.base_dir, []) + self.dispatch( + [ + 'up', '-d', + ], + None) + + containers = self.project.containers() + self.assertEqual(len(containers), 2) + + web, db = containers + self.assertEqual(web.human_readable_command, 'sleep 100') + self.assertEqual(db.human_readable_command, 'top') + + def test_up_with_duplicate_override_yaml_files(self): + self.base_dir = 'tests/fixtures/duplicate-override-yaml-files' + with self.assertRaises(DuplicateOverrideFileFound): + get_project(self.base_dir, []) + self.base_dir = None |