diff options
Diffstat (limited to 'tests/integration')
-rw-r--r-- | tests/integration/network_test.py | 17 | ||||
-rw-r--r-- | tests/integration/project_test.py | 683 | ||||
-rw-r--r-- | tests/integration/resilience_test.py | 5 | ||||
-rw-r--r-- | tests/integration/service_test.py | 393 | ||||
-rw-r--r-- | tests/integration/state_test.py | 19 | ||||
-rw-r--r-- | tests/integration/testcases.py | 122 | ||||
-rw-r--r-- | tests/integration/volume_test.py | 52 |
7 files changed, 1117 insertions, 174 deletions
diff --git a/tests/integration/network_test.py b/tests/integration/network_test.py new file mode 100644 index 00000000..2ff610fb --- /dev/null +++ b/tests/integration/network_test.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from .testcases import DockerClientTestCase +from compose.const import LABEL_NETWORK +from compose.const import LABEL_PROJECT +from compose.network import Network + + +class NetworkTest(DockerClientTestCase): + def test_network_default_labels(self): + net = Network(self.client, 'composetest', 'foonet') + net.ensure() + net_data = net.inspect() + labels = net_data['Labels'] + assert labels[LABEL_NETWORK] == net.name + assert labels[LABEL_PROJECT] == net.project diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 6e82e931..953dd52b 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,27 +1,53 @@ from __future__ import absolute_import from __future__ import unicode_literals +import os.path import random import py import pytest +from docker.errors import APIError from docker.errors import NotFound from .. import mock -from ..helpers import build_config +from ..helpers import build_config as load_config +from ..helpers import create_host_file from .testcases import DockerClientTestCase +from .testcases import SWARM_SKIP_CONTAINERS_ALL from compose.config import config from compose.config import ConfigurationError -from compose.config.config import V2_0 +from compose.config import types from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec +from compose.const import COMPOSEFILE_V2_0 as V2_0 +from compose.const import COMPOSEFILE_V2_1 as V2_1 +from compose.const import COMPOSEFILE_V2_2 as V2_2 +from compose.const import COMPOSEFILE_V3_1 as V3_1 from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.container import Container +from compose.errors import HealthCheckFailed +from compose.errors import NoHealthCheckConfigured from compose.project import Project from compose.project import ProjectError from compose.service import ConvergenceStrategy +from tests.integration.testcases import is_cluster +from tests.integration.testcases import no_cluster +from tests.integration.testcases import v2_1_only +from tests.integration.testcases import v2_2_only from tests.integration.testcases import v2_only +from tests.integration.testcases import v3_only + + +def build_config(**kwargs): + return config.Config( + version=kwargs.get('version'), + services=kwargs.get('services'), + volumes=kwargs.get('volumes'), + networks=kwargs.get('networks'), + secrets=kwargs.get('secrets'), + configs=kwargs.get('configs'), + ) class ProjectTest(DockerClientTestCase): @@ -36,6 +62,20 @@ class ProjectTest(DockerClientTestCase): containers = project.containers() self.assertEqual(len(containers), 2) + @pytest.mark.skipif(SWARM_SKIP_CONTAINERS_ALL, reason='Swarm /containers/json bug') + def test_containers_stopped(self): + web = self.create_service('web') + db = self.create_service('db') + project = Project('composetest', [web, db], self.client) + + project.up() + assert len(project.containers()) == 2 + assert len(project.containers(stopped=True)) == 2 + + project.stop() + assert len(project.containers()) == 0 + assert len(project.containers(stopped=True)) == 2 + def test_containers_with_service_names(self): web = self.create_service('web') db = self.create_service('db') @@ -66,7 +106,7 @@ class ProjectTest(DockerClientTestCase): def test_volumes_from_service(self): project = Project.from_config( name='composetest', - config_data=build_config({ + config_data=load_config({ 'data': { 'image': 'busybox:latest', 'volumes': ['/var/data'], @@ -89,10 +129,11 @@ class ProjectTest(DockerClientTestCase): volumes=['/var/data'], name='composetest_data_container', labels={LABEL_PROJECT: 'composetest'}, + host_config={}, ) project = Project.from_config( name='composetest', - config_data=build_config({ + config_data=load_config({ 'db': { 'image': 'busybox:latest', 'volumes_from': ['composetest_data_container'], @@ -104,12 +145,13 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(db._get_volumes_from(), [data_container.id + ':rw']) @v2_only() + @no_cluster('container networks not supported in Swarm') def test_network_mode_from_service(self): project = Project.from_config( name='composetest', client=self.client, - config_data=build_config({ - 'version': V2_0, + config_data=load_config({ + 'version': str(V2_0), 'services': { 'net': { 'image': 'busybox:latest', @@ -131,12 +173,13 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(web.network_mode.mode, 'container:' + net.containers()[0].id) @v2_only() + @no_cluster('container networks not supported in Swarm') def test_network_mode_from_container(self): def get_project(): return Project.from_config( name='composetest', - config_data=build_config({ - 'version': V2_0, + config_data=load_config({ + 'version': str(V2_0), 'services': { 'web': { 'image': 'busybox:latest', @@ -158,6 +201,7 @@ class ProjectTest(DockerClientTestCase): name='composetest_net_container', command='top', labels={LABEL_PROJECT: 'composetest'}, + host_config={}, ) net_container.start() @@ -167,10 +211,11 @@ class ProjectTest(DockerClientTestCase): web = project.get_service('web') self.assertEqual(web.network_mode.mode, 'container:' + net_container.id) + @no_cluster('container networks not supported in Swarm') def test_net_from_service_v1(self): project = Project.from_config( name='composetest', - config_data=build_config({ + config_data=load_config({ 'net': { 'image': 'busybox:latest', 'command': ["top"] @@ -190,11 +235,12 @@ class ProjectTest(DockerClientTestCase): net = project.get_service('net') self.assertEqual(web.network_mode.mode, 'container:' + net.containers()[0].id) + @no_cluster('container networks not supported in Swarm') def test_net_from_container_v1(self): def get_project(): return Project.from_config( name='composetest', - config_data=build_config({ + config_data=load_config({ 'web': { 'image': 'busybox:latest', 'net': 'container:composetest_net_container' @@ -214,6 +260,7 @@ class ProjectTest(DockerClientTestCase): name='composetest_net_container', command='top', labels={LABEL_PROJECT: 'composetest'}, + host_config={}, ) net_container.start() @@ -239,12 +286,12 @@ class ProjectTest(DockerClientTestCase): project.start(service_names=['web']) self.assertEqual( - set(c.name for c in project.containers()), + set(c.name for c in project.containers() if c.is_running), set([web_container_1.name, web_container_2.name])) project.start() self.assertEqual( - set(c.name for c in project.containers()), + set(c.name for c in project.containers() if c.is_running), set([web_container_1.name, web_container_2.name, db_container.name])) project.pause(service_names=['web']) @@ -264,10 +311,12 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len([c.name for c in project.containers() if c.is_paused]), 0) project.stop(service_names=['web'], timeout=1) - self.assertEqual(set(c.name for c in project.containers()), set([db_container.name])) + self.assertEqual( + set(c.name for c in project.containers() if c.is_running), set([db_container.name]) + ) project.kill(service_names=['db']) - self.assertEqual(len(project.containers()), 0) + self.assertEqual(len([c for c in project.containers() if c.is_running]), 0) self.assertEqual(len(project.containers(stopped=True)), 3) project.remove_stopped(service_names=['web']) @@ -282,11 +331,13 @@ class ProjectTest(DockerClientTestCase): project = Project('composetest', [web, db], self.client) project.create(['db']) - self.assertEqual(len(project.containers()), 0) - self.assertEqual(len(project.containers(stopped=True)), 1) - self.assertEqual(len(db.containers()), 0) - self.assertEqual(len(db.containers(stopped=True)), 1) - self.assertEqual(len(web.containers(stopped=True)), 0) + containers = project.containers(stopped=True) + assert len(containers) == 1 + assert not containers[0].is_running + db_containers = db.containers(stopped=True) + assert len(db_containers) == 1 + assert not db_containers[0].is_running + assert len(web.containers(stopped=True)) == 0 def test_create_twice(self): web = self.create_service('web') @@ -295,12 +346,14 @@ class ProjectTest(DockerClientTestCase): project.create(['db', 'web']) project.create(['db', 'web']) - self.assertEqual(len(project.containers()), 0) - self.assertEqual(len(project.containers(stopped=True)), 2) - self.assertEqual(len(db.containers()), 0) - self.assertEqual(len(db.containers(stopped=True)), 1) - self.assertEqual(len(web.containers()), 0) - self.assertEqual(len(web.containers(stopped=True)), 1) + containers = project.containers(stopped=True) + assert len(containers) == 2 + db_containers = db.containers(stopped=True) + assert len(db_containers) == 1 + assert not db_containers[0].is_running + web_containers = web.containers(stopped=True) + assert len(web_containers) == 1 + assert not web_containers[0].is_running def test_create_with_links(self): db = self.create_service('db') @@ -308,12 +361,11 @@ class ProjectTest(DockerClientTestCase): project = Project('composetest', [db, web], self.client) project.create(['web']) - self.assertEqual(len(project.containers()), 0) - self.assertEqual(len(project.containers(stopped=True)), 2) - self.assertEqual(len(db.containers()), 0) - self.assertEqual(len(db.containers(stopped=True)), 1) - self.assertEqual(len(web.containers()), 0) - self.assertEqual(len(web.containers(stopped=True)), 1) + # self.assertEqual(len(project.containers()), 0) + assert len(project.containers(stopped=True)) == 2 + assert not [c for c in project.containers(stopped=True) if c.is_running] + assert len(db.containers(stopped=True)) == 1 + assert len(web.containers(stopped=True)) == 1 def test_create_strategy_always(self): db = self.create_service('db') @@ -322,11 +374,11 @@ class ProjectTest(DockerClientTestCase): old_id = project.containers(stopped=True)[0].id project.create(['db'], strategy=ConvergenceStrategy.always) - self.assertEqual(len(project.containers()), 0) - self.assertEqual(len(project.containers(stopped=True)), 1) + assert len(project.containers(stopped=True)) == 1 db_container = project.containers(stopped=True)[0] - self.assertNotEqual(db_container.id, old_id) + assert not db_container.is_running + assert db_container.id != old_id def test_create_strategy_never(self): db = self.create_service('db') @@ -335,11 +387,11 @@ class ProjectTest(DockerClientTestCase): old_id = project.containers(stopped=True)[0].id project.create(['db'], strategy=ConvergenceStrategy.never) - self.assertEqual(len(project.containers()), 0) - self.assertEqual(len(project.containers(stopped=True)), 1) + assert len(project.containers(stopped=True)) == 1 db_container = project.containers(stopped=True)[0] - self.assertEqual(db_container.id, old_id) + assert not db_container.is_running + assert db_container.id == old_id def test_project_up(self): web = self.create_service('web') @@ -465,7 +517,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_starts_depends(self): project = Project.from_config( name='composetest', - config_data=build_config({ + config_data=load_config({ 'console': { 'image': 'busybox:latest', 'command': ["top"], @@ -500,7 +552,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_with_no_deps(self): project = Project.from_config( name='composetest', - config_data=build_config({ + config_data=load_config({ 'console': { 'image': 'busybox:latest', 'command': ["top"], @@ -529,10 +581,28 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(project.containers(stopped=True)), 2) self.assertEqual(len(project.get_service('web').containers()), 0) self.assertEqual(len(project.get_service('db').containers()), 1) - self.assertEqual(len(project.get_service('data').containers()), 0) self.assertEqual(len(project.get_service('data').containers(stopped=True)), 1) + assert not project.get_service('data').containers(stopped=True)[0].is_running self.assertEqual(len(project.get_service('console').containers()), 0) + def test_project_up_recreate_with_tmpfs_volume(self): + # https://github.com/docker/compose/issues/4751 + project = Project.from_config( + name='composetest', + config_data=load_config({ + 'version': '2.1', + 'services': { + 'foo': { + 'image': 'busybox:latest', + 'tmpfs': ['/dev/shm'], + 'volumes': ['/dev/shm'] + } + } + }), client=self.client + ) + project.up() + project.up(strategy=ConvergenceStrategy.always) + def test_unscale_after_restart(self): web = self.create_service('web') project = Project('composetest', [web], self.client) @@ -546,12 +616,12 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(service.containers()), 3) project.up() service = project.get_service('web') - self.assertEqual(len(service.containers()), 3) + self.assertEqual(len(service.containers()), 1) service.scale(1) self.assertEqual(len(service.containers()), 1) - project.up() + project.up(scale_override={'web': 3}) service = project.get_service('web') - self.assertEqual(len(service.containers()), 1) + self.assertEqual(len(service.containers()), 3) # does scale=0 ,makes any sense? after recreating at least 1 container is running service.scale(0) project.up() @@ -560,7 +630,7 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_project_up_networks(self): - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -572,7 +642,6 @@ class ProjectTest(DockerClientTestCase): 'baz': {'aliases': ['extra']}, }, }], - volumes={}, networks={ 'foo': {'driver': 'bridge'}, 'bar': {'driver': None}, @@ -606,14 +675,13 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_up_with_ipam_config(self): - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', 'image': 'busybox:latest', 'networks': {'front': None}, }], - volumes={}, networks={ 'front': { 'driver': 'bridge', @@ -666,12 +734,47 @@ class ProjectTest(DockerClientTestCase): } @v2_only() - def test_up_with_network_static_addresses(self): - config_data = config.Config( + def test_up_with_ipam_options(self): + config_data = build_config( version=V2_0, services=[{ 'name': 'web', 'image': 'busybox:latest', + 'networks': {'front': None}, + }], + networks={ + 'front': { + 'driver': 'bridge', + 'ipam': { + 'driver': 'default', + 'options': { + "com.docker.compose.network.test": "9-29-045" + } + }, + }, + }, + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + project.up() + + network = self.client.networks(names=['composetest_front'])[0] + + assert network['IPAM']['Options'] == { + "com.docker.compose.network.test": "9-29-045" + } + + @v2_1_only() + def test_up_with_network_static_addresses(self): + config_data = build_config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', 'command': 'top', 'networks': { 'static_test': { @@ -680,7 +783,6 @@ class ProjectTest(DockerClientTestCase): } }, }], - volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -695,7 +797,8 @@ class ProjectTest(DockerClientTestCase): {"subnet": "fe80::/64", "gateway": "fe80::1001:1"} ] - } + }, + 'enable_ipv6': True, } } ) @@ -706,22 +809,61 @@ class ProjectTest(DockerClientTestCase): ) project.up(detached=True) - network = self.client.networks(names=['static_test'])[0] service_container = project.get_service('web').containers()[0] - assert network['Options'] == { - "com.docker.network.enable_ipv6": "true" - } - IPAMConfig = (service_container.inspect().get('NetworkSettings', {}). get('Networks', {}).get('composetest_static_test', {}). get('IPAMConfig', {})) assert IPAMConfig.get('IPv4Address') == '172.16.100.100' assert IPAMConfig.get('IPv6Address') == 'fe80::1001:102' + @v2_1_only() + def test_up_with_enable_ipv6(self): + self.require_api_version('1.23') + config_data = build_config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top', + 'networks': { + 'static_test': { + 'ipv6_address': 'fe80::1001:102' + } + }, + }], + networks={ + 'static_test': { + 'driver': 'bridge', + 'enable_ipv6': True, + 'ipam': { + 'driver': 'default', + 'config': [ + {"subnet": "fe80::/64", + "gateway": "fe80::1001:1"} + ] + } + } + } + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + project.up(detached=True) + network = [n for n in self.client.networks() if 'static_test' in n['Name']][0] + service_container = project.get_service('web').containers()[0] + + assert network['EnableIPv6'] is True + ipam_config = (service_container.inspect().get('NetworkSettings', {}). + get('Networks', {}).get('composetest_static_test', {}). + get('IPAMConfig', {})) + assert ipam_config.get('IPv6Address') == 'fe80::1001:102' + @v2_only() def test_up_with_network_static_addresses_missing_subnet(self): - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -733,7 +875,6 @@ class ProjectTest(DockerClientTestCase): } }, }], - volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -756,11 +897,146 @@ class ProjectTest(DockerClientTestCase): with self.assertRaises(ProjectError): project.up() + @v2_1_only() + def test_up_with_network_link_local_ips(self): + config_data = build_config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'networks': { + 'linklocaltest': { + 'link_local_ips': ['169.254.8.8'] + } + } + }], + networks={ + 'linklocaltest': {'driver': 'bridge'} + } + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + project.up(detached=True) + + service_container = project.get_service('web').containers(stopped=True)[0] + ipam_config = service_container.inspect().get( + 'NetworkSettings', {} + ).get( + 'Networks', {} + ).get( + 'composetest_linklocaltest', {} + ).get('IPAMConfig', {}) + assert 'LinkLocalIPs' in ipam_config + assert ipam_config['LinkLocalIPs'] == ['169.254.8.8'] + + @v2_1_only() + def test_up_with_isolation(self): + self.require_api_version('1.24') + config_data = build_config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'isolation': 'default' + }], + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + project.up(detached=True) + service_container = project.get_service('web').containers(stopped=True)[0] + assert service_container.inspect()['HostConfig']['Isolation'] == 'default' + + @v2_1_only() + def test_up_with_invalid_isolation(self): + self.require_api_version('1.24') + config_data = build_config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'isolation': 'foobar' + }], + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + with self.assertRaises(ProjectError): + project.up() + + @v2_only() + def test_project_up_with_network_internal(self): + self.require_api_version('1.23') + config_data = build_config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'networks': {'internal': None}, + }], + networks={ + 'internal': {'driver': 'bridge', 'internal': True}, + }, + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + project.up() + + network = self.client.networks(names=['composetest_internal'])[0] + + assert network['Internal'] is True + + @v2_1_only() + def test_project_up_with_network_label(self): + self.require_api_version('1.23') + + network_name = 'network_with_label' + + config_data = build_config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'networks': {network_name: None} + }], + networks={ + network_name: {'labels': {'label_key': 'label_val'}} + } + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + + project.up() + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('composetest_') + ] + + assert [n['Name'] for n in networks] == ['composetest_{}'.format(network_name)] + assert 'label_key' in networks[0]['Labels'] + assert networks[0]['Labels']['label_key'] == 'label_val' + @v2_only() def test_project_up_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -768,7 +1044,6 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, - networks={}, ) project = Project.from_config( @@ -778,16 +1053,58 @@ class ProjectTest(DockerClientTestCase): project.up() self.assertEqual(len(project.containers()), 1) - volume_data = self.client.inspect_volume(full_vol_name) - self.assertEqual(volume_data['Name'], full_vol_name) + volume_data = self.get_volume_data(full_vol_name) + assert volume_data['Name'].split('/')[-1] == full_vol_name self.assertEqual(volume_data['Driver'], 'local') + @v2_1_only() + def test_project_up_with_volume_labels(self): + self.require_api_version('1.23') + + volume_name = 'volume_with_label' + + config_data = build_config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'volumes': [VolumeSpec.parse('{}:/data'.format(volume_name))] + }], + volumes={ + volume_name: { + 'labels': { + 'label_key': 'label_val' + } + } + }, + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + + project.up() + + volumes = [ + v for v in self.client.volumes().get('Volumes', []) + if v['Name'].split('/')[-1].startswith('composetest_') + ] + + assert set([v['Name'].split('/')[-1] for v in volumes]) == set( + ['composetest_{}'.format(volume_name)] + ) + + assert 'label_key' in volumes[0]['Labels'] + assert volumes[0]['Labels']['label_key'] == 'label_val' + @v2_only() def test_project_up_logging_with_multiple_files(self): base_file = config.ConfigFile( 'base.yml', { - 'version': V2_0, + 'version': str(V2_0), 'services': { 'simple': {'image': 'busybox:latest', 'command': 'top'}, 'another': { @@ -806,7 +1123,7 @@ class ProjectTest(DockerClientTestCase): override_file = config.ConfigFile( 'override.yml', { - 'version': V2_0, + 'version': str(V2_0), 'services': { 'another': { 'logging': { @@ -839,7 +1156,7 @@ class ProjectTest(DockerClientTestCase): base_file = config.ConfigFile( 'base.yml', { - 'version': V2_0, + 'version': str(V2_0), 'services': { 'simple': { 'image': 'busybox:latest', @@ -852,7 +1169,7 @@ class ProjectTest(DockerClientTestCase): override_file = config.ConfigFile( 'override.yml', { - 'version': V2_0, + 'version': str(V2_0), 'services': { 'simple': { 'ports': ['1234:1234'] @@ -870,11 +1187,39 @@ class ProjectTest(DockerClientTestCase): containers = project.containers() self.assertEqual(len(containers), 1) + @v2_2_only() + def test_project_up_config_scale(self): + config_data = build_config( + version=V2_2, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top', + 'scale': 3 + }] + ) + + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + project.up() + assert len(project.containers()) == 3 + + project.up(scale_override={'web': 2}) + assert len(project.containers()) == 2 + + project.up(scale_override={'web': 4}) + assert len(project.containers()) == 4 + + project.stop() + project.up() + assert len(project.containers()) == 3 + @v2_only() def test_initialize_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -882,7 +1227,6 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, - networks={}, ) project = Project.from_config( @@ -891,15 +1235,15 @@ class ProjectTest(DockerClientTestCase): ) project.volumes.initialize() - volume_data = self.client.inspect_volume(full_vol_name) - self.assertEqual(volume_data['Name'], full_vol_name) - self.assertEqual(volume_data['Driver'], 'local') + volume_data = self.get_volume_data(full_vol_name) + assert volume_data['Name'].split('/')[-1] == full_vol_name + assert volume_data['Driver'] == 'local' @v2_only() def test_project_up_implicit_volume_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -907,7 +1251,6 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, - networks={}, ) project = Project.from_config( @@ -916,15 +1259,52 @@ class ProjectTest(DockerClientTestCase): ) project.up() - volume_data = self.client.inspect_volume(full_vol_name) - self.assertEqual(volume_data['Name'], full_vol_name) + volume_data = self.get_volume_data(full_vol_name) + assert volume_data['Name'].split('/')[-1] == full_vol_name self.assertEqual(volume_data['Driver'], 'local') + @v3_only() + def test_project_up_with_secrets(self): + node = create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default')) + + config_data = build_config( + version=V3_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'cat /run/secrets/special', + 'secrets': [ + types.ServiceSecret.parse({'source': 'super', 'target': 'special'}), + ], + 'environment': ['constraint:node=={}'.format(node if node is not None else '*')] + }], + secrets={ + 'super': { + 'file': os.path.abspath('tests/fixtures/secrets/default'), + }, + }, + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + project.up() + project.stop() + + containers = project.containers(stopped=True) + assert len(containers) == 1 + container, = containers + + output = container.logs() + assert output == b"This is the secret\n" + @v2_only() def test_initialize_volumes_invalid_volume_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -932,22 +1312,22 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'foobar'}}, - networks={}, ) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) - with self.assertRaises(config.ConfigurationError): + with self.assertRaises(APIError if is_cluster(self.client) else config.ConfigurationError): project.volumes.initialize() @v2_only() + @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_updated_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -955,7 +1335,6 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, - networks={}, ) project = Project.from_config( name='composetest', @@ -963,8 +1342,8 @@ class ProjectTest(DockerClientTestCase): ) project.volumes.initialize() - volume_data = self.client.inspect_volume(full_vol_name) - self.assertEqual(volume_data['Name'], full_vol_name) + volume_data = self.get_volume_data(full_vol_name) + assert volume_data['Name'].split('/')[-1] == full_vol_name self.assertEqual(volume_data['Driver'], 'local') config_data = config_data._replace( @@ -986,7 +1365,7 @@ class ProjectTest(DockerClientTestCase): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -994,7 +1373,6 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, - networks={}, ) project = Project.from_config( name='composetest', @@ -1002,8 +1380,8 @@ class ProjectTest(DockerClientTestCase): ) project.volumes.initialize() - volume_data = self.client.inspect_volume(full_vol_name) - self.assertEqual(volume_data['Name'], full_vol_name) + volume_data = self.get_volume_data(full_vol_name) + assert volume_data['Name'].split('/')[-1] == full_vol_name self.assertEqual(volume_data['Driver'], 'local') config_data = config_data._replace( @@ -1015,17 +1393,18 @@ class ProjectTest(DockerClientTestCase): client=self.client ) project.volumes.initialize() - volume_data = self.client.inspect_volume(full_vol_name) - self.assertEqual(volume_data['Name'], full_vol_name) + volume_data = self.get_volume_data(full_vol_name) + assert volume_data['Name'].split('/')[-1] == full_vol_name self.assertEqual(volume_data['Driver'], 'local') @v2_only() + @no_cluster('inspect volume by name defect on Swarm Classic') def test_initialize_volumes_external_volumes(self): # Use composetest_ prefix so it gets garbage-collected in tearDown() vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) self.client.create_volume(vol_name) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -1033,9 +1412,8 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={ - vol_name: {'external': True, 'external_name': vol_name} + vol_name: {'external': True, 'name': vol_name} }, - networks=None, ) project = Project.from_config( name='composetest', @@ -1050,7 +1428,7 @@ class ProjectTest(DockerClientTestCase): def test_initialize_volumes_inexistent_external_volume(self): vol_name = '{0:x}'.format(random.getrandbits(32)) - config_data = config.Config( + config_data = build_config( version=V2_0, services=[{ 'name': 'web', @@ -1058,9 +1436,8 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={ - vol_name: {'external': True, 'external_name': vol_name} + vol_name: {'external': True, 'name': vol_name} }, - networks=None, ) project = Project.from_config( name='composetest', @@ -1080,7 +1457,7 @@ class ProjectTest(DockerClientTestCase): base_file = config.ConfigFile( 'base.yml', { - 'version': V2_0, + 'version': str(V2_0), 'services': { 'simple': { 'image': 'busybox:latest', @@ -1117,7 +1494,7 @@ class ProjectTest(DockerClientTestCase): } } - config_data = build_config(config_dict) + config_data = load_config(config_dict) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) @@ -1125,7 +1502,7 @@ class ProjectTest(DockerClientTestCase): config_dict['service2'] = config_dict['service1'] del config_dict['service1'] - config_data = build_config(config_dict) + config_data = load_config(config_dict) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) @@ -1145,3 +1522,115 @@ class ProjectTest(DockerClientTestCase): ctnr for ctnr in project._labeled_containers() if ctnr.labels.get(LABEL_SERVICE) == 'service1' ]) == 0 + + @v2_1_only() + def test_project_up_healthy_dependency(self): + config_dict = { + 'version': '2.1', + 'services': { + 'svc1': { + 'image': 'busybox:latest', + 'command': 'top', + 'healthcheck': { + 'test': 'exit 0', + 'retries': 1, + 'timeout': '10s', + 'interval': '1s' + }, + }, + 'svc2': { + 'image': 'busybox:latest', + 'command': 'top', + 'depends_on': { + 'svc1': {'condition': 'service_healthy'}, + } + } + } + } + config_data = load_config(config_dict) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + project.up() + containers = project.containers() + assert len(containers) == 2 + + svc1 = project.get_service('svc1') + svc2 = project.get_service('svc2') + assert 'svc1' in svc2.get_dependency_names() + assert svc1.is_healthy() + + @v2_1_only() + def test_project_up_unhealthy_dependency(self): + config_dict = { + 'version': '2.1', + 'services': { + 'svc1': { + 'image': 'busybox:latest', + 'command': 'top', + 'healthcheck': { + 'test': 'exit 1', + 'retries': 1, + 'timeout': '10s', + 'interval': '1s' + }, + }, + 'svc2': { + 'image': 'busybox:latest', + 'command': 'top', + 'depends_on': { + 'svc1': {'condition': 'service_healthy'}, + } + } + } + } + config_data = load_config(config_dict) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + with pytest.raises(ProjectError): + project.up() + containers = project.containers() + assert len(containers) == 1 + + svc1 = project.get_service('svc1') + svc2 = project.get_service('svc2') + assert 'svc1' in svc2.get_dependency_names() + with pytest.raises(HealthCheckFailed): + svc1.is_healthy() + + @v2_1_only() + def test_project_up_no_healthcheck_dependency(self): + config_dict = { + 'version': '2.1', + 'services': { + 'svc1': { + 'image': 'busybox:latest', + 'command': 'top', + 'healthcheck': { + 'disable': True + }, + }, + 'svc2': { + 'image': 'busybox:latest', + 'command': 'top', + 'depends_on': { + 'svc1': {'condition': 'service_healthy'}, + } + } + } + } + config_data = load_config(config_dict) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + with pytest.raises(ProjectError): + project.up() + containers = project.containers() + assert len(containers) == 1 + + svc1 = project.get_service('svc1') + svc2 = project.get_service('svc2') + assert 'svc1' in svc2.get_dependency_names() + with pytest.raises(NoHealthCheckConfigured): + svc1.is_healthy() diff --git a/tests/integration/resilience_test.py b/tests/integration/resilience_test.py index b544783a..2a2d1b56 100644 --- a/tests/integration/resilience_test.py +++ b/tests/integration/resilience_test.py @@ -20,6 +20,11 @@ class ResilienceTest(DockerClientTestCase): self.db.start_container(container) self.host_path = container.get_mount('/var/db')['Source'] + def tearDown(self): + del self.project + del self.db + super(ResilienceTest, self).tearDown() + def test_successful_recreate(self): self.project.up(strategy=ConvergenceStrategy.always) container = self.db.containers()[0] diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 053dee1b..3ddf991b 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import os import shutil import tempfile +from distutils.spawn import find_executable from os import path import pytest @@ -15,9 +16,12 @@ from .. import mock from .testcases import DockerClientTestCase from .testcases import get_links from .testcases import pull_busybox +from .testcases import SWARM_SKIP_CONTAINERS_ALL +from .testcases import SWARM_SKIP_CPU_SHARES from compose import __version__ from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec +from compose.const import IS_WINDOWS_PLATFORM from compose.const import LABEL_CONFIG_HASH from compose.const import LABEL_CONTAINER_NUMBER from compose.const import LABEL_ONE_OFF @@ -25,12 +29,21 @@ from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.const import LABEL_VERSION from compose.container import Container +from compose.errors import OperationFailedError from compose.project import OneOffFilter from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy from compose.service import NetworkMode +from compose.service import PidMode from compose.service import Service +from compose.utils import parse_nanoseconds_int +from tests.integration.testcases import is_cluster +from tests.integration.testcases import no_cluster +from tests.integration.testcases import v2_1_only +from tests.integration.testcases import v2_2_only +from tests.integration.testcases import v2_3_only from tests.integration.testcases import v2_only +from tests.integration.testcases import v3_only def create_and_start_container(service, **override_options): @@ -39,6 +52,7 @@ def create_and_start_container(service, **override_options): class ServiceTest(DockerClientTestCase): + def test_containers(self): foo = self.create_service('foo') bar = self.create_service('bar') @@ -93,6 +107,7 @@ class ServiceTest(DockerClientTestCase): service.start_container(container) self.assertEqual('foodriver', container.get('HostConfig.VolumeDriver')) + @pytest.mark.skipif(SWARM_SKIP_CPU_SHARES, reason='Swarm --cpu-shares bug') def test_create_container_with_cpu_shares(self): service = self.create_service('db', cpu_shares=73) container = service.create_container() @@ -105,6 +120,31 @@ class ServiceTest(DockerClientTestCase): container.start() self.assertEqual(container.get('HostConfig.CpuQuota'), 40000) + @v2_2_only() + def test_create_container_with_cpu_count(self): + self.require_api_version('1.25') + service = self.create_service('db', cpu_count=2) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.get('HostConfig.CpuCount'), 2) + + @v2_2_only() + @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='cpu_percent is not supported for Linux') + def test_create_container_with_cpu_percent(self): + self.require_api_version('1.25') + service = self.create_service('db', cpu_percent=12) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.get('HostConfig.CpuPercent'), 12) + + @v2_2_only() + def test_create_container_with_cpus(self): + self.require_api_version('1.25') + service = self.create_service('db', cpus=1) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.get('HostConfig.NanoCpus'), 1000000000) + def test_create_container_with_shm_size(self): self.require_api_version('1.22') service = self.create_service('db', shm_size=67108864) @@ -112,6 +152,30 @@ class ServiceTest(DockerClientTestCase): service.start_container(container) self.assertEqual(container.get('HostConfig.ShmSize'), 67108864) + def test_create_container_with_init_bool(self): + self.require_api_version('1.25') + service = self.create_service('db', init=True) + container = service.create_container() + service.start_container(container) + assert container.get('HostConfig.Init') is True + + @pytest.mark.xfail(True, reason='Option has been removed in Engine 17.06.0') + def test_create_container_with_init_path(self): + self.require_api_version('1.25') + docker_init_path = find_executable('docker-init') + service = self.create_service('db', init=docker_init_path) + container = service.create_container() + service.start_container(container) + assert container.get('HostConfig.InitPath') == docker_init_path + + @pytest.mark.xfail(True, reason='Some kernels/configs do not support pids_limit') + def test_create_container_with_pids_limit(self): + self.require_api_version('1.23') + service = self.create_service('db', pids_limit=10) + container = service.create_container() + service.start_container(container) + assert container.get('HostConfig.PidsLimit') == 10 + def test_create_container_with_extra_hosts_list(self): extra_hosts = ['somehost:162.242.195.82', 'otherhost:50.31.209.229'] service = self.create_service('db', extra_hosts=extra_hosts) @@ -140,6 +204,34 @@ class ServiceTest(DockerClientTestCase): service.start_container(container) assert container.get('HostConfig.ReadonlyRootfs') == read_only + def test_create_container_with_blkio_config(self): + blkio_config = { + 'weight': 300, + 'weight_device': [{'path': '/dev/sda', 'weight': 200}], + 'device_read_bps': [{'path': '/dev/sda', 'rate': 1024 * 1024 * 100}], + 'device_read_iops': [{'path': '/dev/sda', 'rate': 1000}], + 'device_write_bps': [{'path': '/dev/sda', 'rate': 1024 * 1024}], + 'device_write_iops': [{'path': '/dev/sda', 'rate': 800}] + } + service = self.create_service('web', blkio_config=blkio_config) + container = service.create_container() + assert container.get('HostConfig.BlkioWeight') == 300 + assert container.get('HostConfig.BlkioWeightDevice') == [{ + 'Path': '/dev/sda', 'Weight': 200 + }] + assert container.get('HostConfig.BlkioDeviceReadBps') == [{ + 'Path': '/dev/sda', 'Rate': 1024 * 1024 * 100 + }] + assert container.get('HostConfig.BlkioDeviceWriteBps') == [{ + 'Path': '/dev/sda', 'Rate': 1024 * 1024 + }] + assert container.get('HostConfig.BlkioDeviceReadIOps') == [{ + 'Path': '/dev/sda', 'Rate': 1000 + }] + assert container.get('HostConfig.BlkioDeviceWriteIOps') == [{ + 'Path': '/dev/sda', 'Rate': 800 + }] + def test_create_container_with_security_opt(self): security_opt = ['label:disable'] service = self.create_service('db', security_opt=security_opt) @@ -147,6 +239,15 @@ class ServiceTest(DockerClientTestCase): service.start_container(container) self.assertEqual(set(container.get('HostConfig.SecurityOpt')), set(security_opt)) + # @pytest.mark.xfail(True, reason='Not supported on most drivers') + @pytest.mark.skipif(True, reason='https://github.com/moby/moby/issues/34270') + def test_create_container_with_storage_opt(self): + storage_opt = {'size': '1G'} + service = self.create_service('db', storage_opt=storage_opt) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.get('HostConfig.StorageOpt'), storage_opt) + def test_create_container_with_mac_address(self): service = self.create_service('db', mac_address='02:42:ac:11:65:43') container = service.create_container() @@ -170,6 +271,24 @@ class ServiceTest(DockerClientTestCase): self.assertTrue(path.basename(actual_host_path) == path.basename(host_path), msg=("Last component differs: %s, %s" % (actual_host_path, host_path))) + def test_create_container_with_healthcheck_config(self): + one_second = parse_nanoseconds_int('1s') + healthcheck = { + 'test': ['true'], + 'interval': 2 * one_second, + 'timeout': 5 * one_second, + 'retries': 5, + 'start_period': 2 * one_second + } + service = self.create_service('db', healthcheck=healthcheck) + container = service.create_container() + remote_healthcheck = container.get('Config.Healthcheck') + assert remote_healthcheck['Test'] == healthcheck['test'] + assert remote_healthcheck['Interval'] == healthcheck['interval'] + assert remote_healthcheck['Timeout'] == healthcheck['timeout'] + assert remote_healthcheck['Retries'] == healthcheck['retries'] + assert remote_healthcheck['StartPeriod'] == healthcheck['start_period'] + def test_recreate_preserves_volume_with_trailing_slash(self): """When the Compose file specifies a trailing slash in the container path, make sure we copy the volume over when recreating. @@ -194,6 +313,7 @@ class ServiceTest(DockerClientTestCase): 'busybox', 'true', volumes={container_path: {}}, labels={'com.docker.compose.test_image': 'true'}, + host_config={} ) image = self.client.commit(tmp_container)['Id'] @@ -223,13 +343,16 @@ class ServiceTest(DockerClientTestCase): image='busybox:latest', command=["top"], labels={LABEL_PROJECT: 'composetest'}, + host_config={}, + environment=['affinity:container=={}'.format(volume_container_1.id)], ) host_service = self.create_service( 'host', volumes_from=[ VolumeFromSpec(volume_service, 'rw', 'service'), VolumeFromSpec(volume_container_2, 'rw', 'container') - ] + ], + environment=['affinity:container=={}'.format(volume_container_1.id)], ) host_container = host_service.create_container() host_service.start_container(host_container) @@ -266,9 +389,15 @@ class ServiceTest(DockerClientTestCase): self.assertIn('FOO=2', new_container.get('Config.Env')) self.assertEqual(new_container.name, 'composetest_db_1') self.assertEqual(new_container.get_mount('/etc')['Source'], volume_path) - self.assertIn( - 'affinity:container==%s' % old_container.id, - new_container.get('Config.Env')) + if not is_cluster(self.client): + assert ( + 'affinity:container==%s' % old_container.id in + new_container.get('Config.Env') + ) + else: + # In Swarm, the env marker is consumed and the container should be deployed + # on the same node. + assert old_container.get('Node.Name') == new_container.get('Node.Name') self.assertEqual(len(self.client.containers(all=True)), num_containers_before) self.assertNotEqual(old_container.id, new_container.id) @@ -295,8 +424,13 @@ class ServiceTest(DockerClientTestCase): ConvergencePlan('recreate', [orig_container])) assert new_container.get_mount('/etc')['Source'] == volume_path - assert ('affinity:container==%s' % orig_container.id in - new_container.get('Config.Env')) + if not is_cluster(self.client): + assert ('affinity:container==%s' % orig_container.id in + new_container.get('Config.Env')) + else: + # In Swarm, the env marker is consumed and the container should be deployed + # on the same node. + assert orig_container.get('Node.Name') == new_container.get('Node.Name') orig_container = new_container @@ -409,18 +543,21 @@ class ServiceTest(DockerClientTestCase): ) containers = service.execute_convergence_plan(ConvergencePlan('create', []), start=False) - 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 containers = service.execute_convergence_plan( ConvergencePlan('recreate', containers), start=False) - 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 service.execute_convergence_plan(ConvergencePlan('start', containers), start=False) - 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 def test_start_container_passes_through_options(self): db = self.create_service('db') @@ -432,6 +569,7 @@ class ServiceTest(DockerClientTestCase): create_and_start_container(db) self.assertEqual(db.containers()[0].environment['FOO'], 'BAR') + @no_cluster('No legacy links support in Swarm') def test_start_container_creates_links(self): db = self.create_service('db') web = self.create_service('web', links=[(db, None)]) @@ -448,6 +586,7 @@ class ServiceTest(DockerClientTestCase): 'db']) ) + @no_cluster('No legacy links support in Swarm') def test_start_container_creates_links_with_names(self): db = self.create_service('db') web = self.create_service('web', links=[(db, 'custom_link_name')]) @@ -464,6 +603,7 @@ class ServiceTest(DockerClientTestCase): 'custom_link_name']) ) + @no_cluster('No legacy links support in Swarm') def test_start_container_with_external_links(self): db = self.create_service('db') web = self.create_service('web', external_links=['composetest_db_1', @@ -482,6 +622,7 @@ class ServiceTest(DockerClientTestCase): 'db_3']), ) + @no_cluster('No legacy links support in Swarm') def test_start_normal_container_does_not_create_links_to_its_own_service(self): db = self.create_service('db') @@ -491,6 +632,7 @@ class ServiceTest(DockerClientTestCase): c = create_and_start_container(db) self.assertEqual(set(get_links(c)), set([])) + @no_cluster('No legacy links support in Swarm') def test_start_one_off_container_creates_links_to_its_own_service(self): db = self.create_service('db') @@ -517,7 +659,7 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) container.wait() self.assertIn(b'success', container.logs()) - self.assertEqual(len(self.client.images(name='composetest_test')), 1) + assert len(self.client.images(name='composetest_test')) >= 1 def test_start_container_uses_tagged_image_if_it_exists(self): self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test') @@ -544,7 +686,10 @@ class ServiceTest(DockerClientTestCase): with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: f.write("FROM busybox\n") - self.create_service('web', build={'context': base_dir}).build() + service = self.create_service('web', build={'context': base_dir}) + service.build() + self.addCleanup(self.client.remove_image, service.image_name) + assert self.client.inspect_image('composetest_web') def test_build_non_ascii_filename(self): @@ -557,7 +702,9 @@ class ServiceTest(DockerClientTestCase): with open(os.path.join(base_dir.encode('utf8'), b'foo\xE2bar'), 'w') as f: f.write("hello world\n") - self.create_service('web', build={'context': text_type(base_dir)}).build() + service = self.create_service('web', build={'context': text_type(base_dir)}) + service.build() + self.addCleanup(self.client.remove_image, service.image_name) assert self.client.inspect_image('composetest_web') def test_build_with_image_name(self): @@ -586,19 +733,107 @@ class ServiceTest(DockerClientTestCase): with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: f.write("FROM busybox\n") f.write("ARG build_version\n") + f.write("RUN echo ${build_version}\n") service = self.create_service('buildwithargs', build={'context': text_type(base_dir), 'args': {"build_version": "1"}}) service.build() + self.addCleanup(self.client.remove_image, service.image_name) + assert service.image() + assert "build_version=1" in service.image()['ContainerConfig']['Cmd'] + + def test_build_with_build_args_override(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write("FROM busybox\n") + f.write("ARG build_version\n") + f.write("RUN echo ${build_version}\n") + + service = self.create_service('buildwithargs', + build={'context': text_type(base_dir), + 'args': {"build_version": "1"}}) + service.build(build_args_override={'build_version': '2'}) + self.addCleanup(self.client.remove_image, service.image_name) + + assert service.image() + assert "build_version=2" in service.image()['ContainerConfig']['Cmd'] + + def test_build_with_build_labels(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write('FROM busybox\n') + + service = self.create_service('buildlabels', build={ + 'context': text_type(base_dir), + 'labels': {'com.docker.compose.test': 'true'} + }) + service.build() + self.addCleanup(self.client.remove_image, service.image_name) + + assert service.image() + assert service.image()['Config']['Labels']['com.docker.compose.test'] == 'true' + + @no_cluster('Container networks not on Swarm') + def test_build_with_network(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write('FROM busybox\n') + f.write('RUN ping -c1 google.local\n') + + net_container = self.client.create_container( + 'busybox', 'top', host_config=self.client.create_host_config( + extra_hosts={'google.local': '127.0.0.1'} + ), name='composetest_build_network' + ) + + self.addCleanup(self.client.remove_container, net_container, force=True) + self.client.start(net_container) + + service = self.create_service('buildwithnet', build={ + 'context': text_type(base_dir), + 'network': 'container:{}'.format(net_container['Id']) + }) + + service.build() + self.addCleanup(self.client.remove_image, service.image_name) + + assert service.image() + + @v2_3_only() + @no_cluster('Not supported on UCP 2.2.0-beta1') # FIXME: remove once support is added + def test_build_with_target(self): + self.require_api_version('1.30') + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write('FROM busybox as one\n') + f.write('LABEL com.docker.compose.test=true\n') + f.write('LABEL com.docker.compose.test.target=one\n') + f.write('FROM busybox as two\n') + f.write('LABEL com.docker.compose.test.target=two\n') + + service = self.create_service('buildtarget', build={ + 'context': text_type(base_dir), + 'target': 'one' + }) + + service.build() assert service.image() + assert service.image()['Config']['Labels']['com.docker.compose.test.target'] == 'one' - def test_start_container_stays_unpriviliged(self): + def test_start_container_stays_unprivileged(self): service = self.create_service('web') container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], False) - def test_start_container_becomes_priviliged(self): + def test_start_container_becomes_privileged(self): service = self.create_service('web', privileged=True) container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], True) @@ -631,20 +866,27 @@ class ServiceTest(DockerClientTestCase): '0.0.0.0:9001:9000/udp', ]) container = create_and_start_container(service).inspect() - self.assertEqual(container['NetworkSettings']['Ports'], { - '8000/tcp': [ - { - 'HostIp': '127.0.0.1', - 'HostPort': '8001', - }, - ], - '9000/udp': [ - { - 'HostIp': '0.0.0.0', - 'HostPort': '9001', - }, - ], - }) + assert container['NetworkSettings']['Ports']['8000/tcp'] == [{ + 'HostIp': '127.0.0.1', + 'HostPort': '8001', + }] + assert container['NetworkSettings']['Ports']['9000/udp'][0]['HostPort'] == '9001' + if not is_cluster(self.client): + assert container['NetworkSettings']['Ports']['9000/udp'][0]['HostIp'] == '0.0.0.0' + # self.assertEqual(container['NetworkSettings']['Ports'], { + # '8000/tcp': [ + # { + # 'HostIp': '127.0.0.1', + # 'HostPort': '8001', + # }, + # ], + # '9000/udp': [ + # { + # 'HostIp': '0.0.0.0', + # 'HostPort': '9001', + # }, + # ], + # }) def test_create_with_image_id(self): # Get image id for the current busybox:latest @@ -672,6 +914,10 @@ class ServiceTest(DockerClientTestCase): service.scale(0) self.assertEqual(len(service.containers()), 0) + @pytest.mark.skipif( + SWARM_SKIP_CONTAINERS_ALL, + reason='Swarm /containers/json bug' + ) def test_scale_with_stopped_containers(self): """ Given there are some stopped containers and scale is called with a @@ -732,15 +978,15 @@ class ServiceTest(DockerClientTestCase): message="testing", response={}, explanation="Boom")): - with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr: - service.scale(3) + with pytest.raises(OperationFailedError): + service.scale(3) - self.assertEqual(len(service.containers()), 1) - self.assertTrue(service.containers()[0].is_running) - self.assertIn( - "ERROR: for composetest_web_2 Cannot create container for service web: Boom", - mock_stderr.getvalue() + assert len(service.containers()) == 1 + assert service.containers()[0].is_running + assert ( + "ERROR: for composetest_web_2 Cannot create container for service" + " web: Boom" in mock_stderr.getvalue() ) def test_scale_with_unexpected_exception(self): @@ -792,7 +1038,8 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('app', container_name='custom-container') self.assertEqual(service.custom_container_name, 'custom-container') - service.scale(3) + with pytest.raises(OperationFailedError): + service.scale(3) captured_output = mock_log.warn.call_args[0][0] @@ -833,15 +1080,27 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(container.get('HostConfig.NetworkMode'), 'host') def test_pid_mode_none_defined(self): - service = self.create_service('web', pid=None) + service = self.create_service('web', pid_mode=None) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.PidMode'), '') def test_pid_mode_host(self): - service = self.create_service('web', pid='host') + service = self.create_service('web', pid_mode=PidMode('host')) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.PidMode'), 'host') + @v2_1_only() + def test_userns_mode_none_defined(self): + service = self.create_service('web', userns_mode=None) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.UsernsMode'), '') + + @v2_1_only() + def test_userns_mode_host(self): + service = self.create_service('web', userns_mode='host') + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.UsernsMode'), 'host') + def test_dns_no_value(self): service = self.create_service('web') container = create_and_start_container(service) @@ -852,11 +1111,42 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.Dns'), ['8.8.8.8', '9.9.9.9']) + def test_mem_swappiness(self): + service = self.create_service('web', mem_swappiness=11) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.MemorySwappiness'), 11) + + def test_mem_reservation(self): + service = self.create_service('web', mem_reservation='20m') + container = create_and_start_container(service) + assert container.get('HostConfig.MemoryReservation') == 20 * 1024 * 1024 + def test_restart_always_value(self): service = self.create_service('web', restart={'Name': 'always'}) container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'always') + def test_oom_score_adj_value(self): + service = self.create_service('web', oom_score_adj=500) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.OomScoreAdj'), 500) + + def test_group_add_value(self): + service = self.create_service('web', group_add=["root", "1"]) + container = create_and_start_container(service) + + host_container_groupadd = container.get('HostConfig.GroupAdd') + assert "root" in host_container_groupadd + assert "1" in host_container_groupadd + + def test_dns_opt_value(self): + service = self.create_service('web', dns_opt=["use-vc", "no-tld-query"]) + container = create_and_start_container(service) + + dns_opt = container.get('HostConfig.DnsOptions') + assert 'use-vc' in dns_opt + assert 'no-tld-query' in dns_opt + def test_restart_on_failure_value(self): service = self.create_service('web', restart={ 'Name': 'on-failure', @@ -915,6 +1205,22 @@ class ServiceTest(DockerClientTestCase): }.items(): self.assertEqual(env[k], v) + @v3_only() + def test_build_with_cachefrom(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write("FROM busybox\n") + + service = self.create_service('cache_from', + build={'context': base_dir, + 'cache_from': ['build1']}) + service.build() + self.addCleanup(self.client.remove_image, service.image_name) + + assert service.image() + @mock.patch.dict(os.environ) def test_resolve_env(self): os.environ['FILE_DEF'] = 'E1' @@ -943,7 +1249,7 @@ class ServiceTest(DockerClientTestCase): with mock.patch.object(self.client, '_version', '1.20'): service = self.create_service('web') service_config = service._get_container_host_config({}) - self.assertEquals(service_config['NetworkMode'], 'default') + self.assertEqual(service_config['NetworkMode'], 'default') def test_labels(self): labels_dict = { @@ -989,7 +1295,7 @@ class ServiceTest(DockerClientTestCase): one_off_container = service.create_container(one_off=True) self.assertNotEqual(one_off_container.name, 'my-web-container') - @pytest.mark.skipif(True, reason="Broken on 1.11.0rc1") + @pytest.mark.skipif(True, reason="Broken on 1.11.0 - 17.03.0") def test_log_drive_invalid(self): service = self.create_service('web', logging={'driver': 'xxx'}) expected_error_msg = "logger: no log driver named 'xxx' is registered" @@ -1047,6 +1353,7 @@ def converge(service, strategy=ConvergenceStrategy.changed): class ConfigHashTest(DockerClientTestCase): + def test_no_config_hash_when_one_off(self): web = self.create_service('web') container = web.create_container(one_off=True) diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py index 07b28e78..047dc704 100644 --- a/tests/integration/state_test.py +++ b/tests/integration/state_test.py @@ -6,9 +6,11 @@ from __future__ import absolute_import from __future__ import unicode_literals import py +from docker.errors import ImageNotFound from .testcases import DockerClientTestCase from .testcases import get_links +from .testcases import no_cluster from compose.config import config from compose.project import Project from compose.service import ConvergenceStrategy @@ -243,21 +245,34 @@ class ServiceStateTest(DockerClientTestCase): tag = 'latest' image = '{}:{}'.format(repo, tag) + def safe_remove_image(image): + try: + self.client.remove_image(image) + except ImageNotFound: + pass + image_id = self.client.images(name='busybox')[0]['Id'] self.client.tag(image_id, repository=repo, tag=tag) - self.addCleanup(self.client.remove_image, image) + self.addCleanup(safe_remove_image, image) web = self.create_service('web', image=image) container = web.create_container() # update the image - c = self.client.create_container(image, ['touch', '/hello.txt']) + c = self.client.create_container(image, ['touch', '/hello.txt'], host_config={}) + + # In the case of a cluster, there's a chance we pick up the old image when + # calculating the new hash. To circumvent that, untag the old image first + # See also: https://github.com/moby/moby/issues/26852 + self.client.remove_image(image, force=True) + self.client.commit(c, repository=repo, tag=tag) self.client.remove_container(c) web = self.create_service('web', image=image) self.assertEqual(('recreate', [container]), web.convergence_plan()) + @no_cluster('Can not guarantee the build will be run on the same node the service is deployed') def test_trigger_recreate_with_build(self): context = py.test.ensuretemp('test_trigger_recreate_with_build') self.addCleanup(context.remove) diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index 8d69d531..b72fb53a 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -4,20 +4,32 @@ from __future__ import unicode_literals import functools import os +import pytest +from docker.errors import APIError from docker.utils import version_lt -from pytest import skip from .. import unittest from compose.cli.docker_client import docker_client from compose.config.config import resolve_environment -from compose.config.config import V1 -from compose.config.config import V2_0 from compose.config.environment import Environment from compose.const import API_VERSIONS +from compose.const import COMPOSEFILE_V1 as V1 +from compose.const import COMPOSEFILE_V2_0 as V2_0 +from compose.const import COMPOSEFILE_V2_0 as V2_1 +from compose.const import COMPOSEFILE_V2_2 as V2_2 +from compose.const import COMPOSEFILE_V2_3 as V2_3 +from compose.const import COMPOSEFILE_V3_0 as V3_0 +from compose.const import COMPOSEFILE_V3_2 as V3_2 +from compose.const import COMPOSEFILE_V3_3 as V3_3 from compose.const import LABEL_PROJECT from compose.progress_stream import stream_output from compose.service import Service +SWARM_SKIP_CONTAINERS_ALL = os.environ.get('SWARM_SKIP_CONTAINERS_ALL', '0') != '0' +SWARM_SKIP_CPU_SHARES = os.environ.get('SWARM_SKIP_CPU_SHARES', '0') != '0' +SWARM_SKIP_RM_VOLUMES = os.environ.get('SWARM_SKIP_RM_VOLUMES', '0') != '0' +SWARM_ASSUME_MULTINODE = os.environ.get('SWARM_ASSUME_MULTINODE', '0') != '0' + def pull_busybox(client): client.pull('busybox:latest', stream=False) @@ -33,36 +45,58 @@ def get_links(container): return [format_link(link) for link in links] -def engine_version_too_low_for_v2(): +def engine_max_version(): if 'DOCKER_VERSION' not in os.environ: - return False + return V3_3 version = os.environ['DOCKER_VERSION'].partition('-')[0] - return version_lt(version, '1.10') + if version_lt(version, '1.10'): + return V1 + if version_lt(version, '1.12'): + return V2_0 + if version_lt(version, '1.13'): + return V2_1 + if version_lt(version, '17.06'): + return V3_2 + return V3_3 + + +def min_version_skip(version): + return pytest.mark.skipif( + engine_max_version() < version, + reason="Engine version %s is too low" % version + ) def v2_only(): - def decorator(f): - @functools.wraps(f) - def wrapper(self, *args, **kwargs): - if engine_version_too_low_for_v2(): - skip("Engine version is too low") - return - return f(self, *args, **kwargs) - return wrapper + return min_version_skip(V2_0) - return decorator + +def v2_1_only(): + return min_version_skip(V2_1) + + +def v2_2_only(): + return min_version_skip(V2_2) + + +def v2_3_only(): + return min_version_skip(V2_3) + + +def v3_only(): + return min_version_skip(V3_0) class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): - if engine_version_too_low_for_v2(): - version = API_VERSIONS[V1] - else: - version = API_VERSIONS[V2_0] - + version = API_VERSIONS[engine_max_version()] cls.client = docker_client(Environment(), version) + @classmethod + def tearDownClass(cls): + del cls.client + def tearDown(self): for c in self.client.containers( all=True, @@ -71,7 +105,11 @@ class DockerClientTestCase(unittest.TestCase): for i in self.client.images( filters={'label': 'com.docker.compose.test_image'}): - self.client.remove_image(i) + try: + self.client.remove_image(i, force=True) + except APIError as e: + if e.is_server_error(): + pass volumes = self.client.volumes().get('Volumes') or [] for v in volumes: @@ -106,4 +144,44 @@ class DockerClientTestCase(unittest.TestCase): def require_api_version(self, minimum): api_version = self.client.version()['ApiVersion'] if version_lt(api_version, minimum): - skip("API version is too low ({} < {})".format(api_version, minimum)) + pytest.skip("API version is too low ({} < {})".format(api_version, minimum)) + + def get_volume_data(self, volume_name): + if not is_cluster(self.client): + return self.client.inspect_volume(volume_name) + + volumes = self.client.volumes(filters={'name': volume_name})['Volumes'] + assert len(volumes) > 0 + return self.client.inspect_volume(volumes[0]['Name']) + + +def is_cluster(client): + if SWARM_ASSUME_MULTINODE: + return True + + def get_nodes_number(): + try: + return len(client.nodes()) + except APIError: + # If the Engine is not part of a Swarm, the SDK will raise + # an APIError + return 0 + + if not hasattr(is_cluster, 'nodes') or is_cluster.nodes is None: + # Only make the API call if the value hasn't been cached yet + is_cluster.nodes = get_nodes_number() + + return is_cluster.nodes > 1 + + +def no_cluster(reason): + def decorator(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if is_cluster(self.client): + pytest.skip("Test will not be run in cluster mode: %s" % reason) + return + return f(self, *args, **kwargs) + return wrapper + + return decorator diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index 706179ed..2a521d4c 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -1,9 +1,13 @@ from __future__ import absolute_import from __future__ import unicode_literals +import six from docker.errors import DockerException from .testcases import DockerClientTestCase +from .testcases import no_cluster +from compose.const import LABEL_PROJECT +from compose.const import LABEL_VOLUME from compose.volume import Volume @@ -17,13 +21,18 @@ class VolumeTest(DockerClientTestCase): self.client.remove_volume(volume.full_name) except DockerException: pass + del self.tmp_volumes + super(VolumeTest, self).tearDown() + + def create_volume(self, name, driver=None, opts=None, external=None, custom_name=False): + if external: + custom_name = True + if isinstance(external, six.text_type): + name = external - def create_volume(self, name, driver=None, opts=None, external=None): - if external and isinstance(external, bool): - external = name vol = Volume( self.client, 'composetest', name, driver=driver, driver_opts=opts, - external_name=external + external=bool(external), custom_name=custom_name ) self.tmp_volumes.append(vol) return vol @@ -31,26 +40,35 @@ class VolumeTest(DockerClientTestCase): def test_create_volume(self): vol = self.create_volume('volume01') vol.create() - info = self.client.inspect_volume(vol.full_name) - assert info['Name'] == vol.full_name + info = self.get_volume_data(vol.full_name) + assert info['Name'].split('/')[-1] == vol.full_name + + def test_create_volume_custom_name(self): + vol = self.create_volume('volume01', custom_name=True) + assert vol.name == vol.full_name + vol.create() + info = self.get_volume_data(vol.full_name) + assert info['Name'].split('/')[-1] == vol.name def test_recreate_existing_volume(self): vol = self.create_volume('volume01') vol.create() - info = self.client.inspect_volume(vol.full_name) - assert info['Name'] == vol.full_name + info = self.get_volume_data(vol.full_name) + assert info['Name'].split('/')[-1] == vol.full_name vol.create() - info = self.client.inspect_volume(vol.full_name) - assert info['Name'] == vol.full_name + info = self.get_volume_data(vol.full_name) + assert info['Name'].split('/')[-1] == vol.full_name + @no_cluster('inspect volume by name defect on Swarm Classic') def test_inspect_volume(self): vol = self.create_volume('volume01') vol.create() info = vol.inspect() assert info['Name'] == vol.full_name + @no_cluster('remove volume by name defect on Swarm Classic') def test_remove_volume(self): vol = Volume(self.client, 'composetest', 'volume01') vol.create() @@ -58,6 +76,7 @@ class VolumeTest(DockerClientTestCase): volumes = self.client.volumes()['Volumes'] assert len([v for v in volumes if v['Name'] == vol.full_name]) == 0 + @no_cluster('inspect volume by name defect on Swarm Classic') def test_external_volume(self): vol = self.create_volume('composetest_volume_ext', external=True) assert vol.external is True @@ -66,6 +85,7 @@ class VolumeTest(DockerClientTestCase): info = vol.inspect() assert info['Name'] == vol.name + @no_cluster('inspect volume by name defect on Swarm Classic') def test_external_aliased_volume(self): alias_name = 'composetest_alias01' vol = self.create_volume('volume01', external=alias_name) @@ -75,20 +95,32 @@ class VolumeTest(DockerClientTestCase): info = vol.inspect() assert info['Name'] == alias_name + @no_cluster('inspect volume by name defect on Swarm Classic') def test_exists(self): vol = self.create_volume('volume01') assert vol.exists() is False vol.create() assert vol.exists() is True + @no_cluster('inspect volume by name defect on Swarm Classic') def test_exists_external(self): vol = self.create_volume('volume01', external=True) assert vol.exists() is False vol.create() assert vol.exists() is True + @no_cluster('inspect volume by name defect on Swarm Classic') def test_exists_external_aliased(self): vol = self.create_volume('volume01', external='composetest_alias01') assert vol.exists() is False vol.create() assert vol.exists() is True + + @no_cluster('inspect volume by name defect on Swarm Classic') + def test_volume_default_labels(self): + vol = self.create_volume('volume01') + vol.create() + vol_data = vol.inspect() + labels = vol_data['Labels'] + assert labels[LABEL_VOLUME] == vol.name + assert labels[LABEL_PROJECT] == vol.project |