diff options
author | Felipe Sateler <fsateler@debian.org> | 2019-11-22 21:15:41 -0300 |
---|---|---|
committer | Felipe Sateler <fsateler@debian.org> | 2019-11-22 21:15:41 -0300 |
commit | 97b16e5404375cc6cca4469045984cac0eabd335 (patch) | |
tree | b9cfdfec00f4a6afceed718cbb155651d23a51fc /tests | |
parent | 813ff34b5328e530d94c95cd8235431cde391e4c (diff) | |
parent | d66f980dd002ce94c3196b1a74dc8c1a0788be06 (diff) |
Update upstream source from tag 'upstream/1.25.0'
Update to upstream version '1.25.0'
with Debian dir 01225dadf264cb86293071829641cb341942031d
Diffstat (limited to 'tests')
91 files changed, 2185 insertions, 449 deletions
diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 07570580..a03d5656 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import datetime import json -import os import os.path import re import signal @@ -12,6 +11,7 @@ import subprocess import time from collections import Counter from collections import namedtuple +from functools import reduce from operator import attrgetter import pytest @@ -20,6 +20,7 @@ import yaml from docker import errors from .. import mock +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from ..helpers import create_host_file from compose.cli.command import get_project from compose.config.errors import DuplicateOverrideFileFound @@ -41,7 +42,7 @@ ProcessResult = namedtuple('ProcessResult', 'stdout stderr') BUILD_CACHE_TEXT = 'Using cache' -BUILD_PULL_TEXT = 'Status: Image is up to date for busybox:latest' +BUILD_PULL_TEXT = 'Status: Image is up to date for busybox:1.27.2' def start_process(base_dir, options): @@ -63,6 +64,12 @@ def wait_on_process(proc, returncode=0): return ProcessResult(stdout.decode('utf-8'), stderr.decode('utf-8')) +def dispatch(base_dir, options, project_options=None, returncode=0): + project_options = project_options or [] + proc = start_process(base_dir, project_options + options) + return wait_on_process(proc, returncode=returncode) + + def wait_on_condition(condition, delay=0.1, timeout=40): start_time = time.time() while not condition(): @@ -99,7 +106,14 @@ class ContainerStateCondition(object): def __call__(self): try: - container = self.client.inspect_container(self.name) + if self.name.endswith('*'): + ctnrs = self.client.containers(all=True, filters={'name': self.name[:-1]}) + if len(ctnrs) > 0: + container = self.client.inspect_container(ctnrs[0]['Id']) + else: + return False + else: + container = self.client.inspect_container(self.name) return container['State']['Status'] == self.status except errors.APIError: return False @@ -143,9 +157,7 @@ class CLITestCase(DockerClientTestCase): return self._project def dispatch(self, options, project_options=None, returncode=0): - project_options = project_options or [] - proc = start_process(self.base_dir, project_options + options) - return wait_on_process(proc, returncode=returncode) + return dispatch(self.base_dir, options, project_options, returncode) def execute(self, container, cmd): # Remove once Hijack and CloseNotifier sign a peace treaty @@ -164,6 +176,13 @@ class CLITestCase(DockerClientTestCase): # Prevent tearDown from trying to create a project self.base_dir = None + def test_quiet_build(self): + self.base_dir = 'tests/fixtures/build-args' + result = self.dispatch(['build'], None) + quietResult = self.dispatch(['build', '-q'], None) + assert result.stdout != "" + assert quietResult.stdout == "" + def test_help_nonexistent(self): self.base_dir = 'tests/fixtures/no-composefile' result = self.dispatch(['help', 'foobar'], returncode=1) @@ -222,6 +241,16 @@ class CLITestCase(DockerClientTestCase): self.base_dir = 'tests/fixtures/v2-full' assert self.dispatch(['config', '--quiet']).stdout == '' + def test_config_with_hash_option(self): + self.base_dir = 'tests/fixtures/v2-full' + result = self.dispatch(['config', '--hash=*']) + for service in self.project.get_services(): + assert '{} {}\n'.format(service.name, service.config_hash) in result.stdout + + svc = self.project.get_service('other') + result = self.dispatch(['config', '--hash=other']) + assert result.stdout == '{} {}\n'.format(svc.name, svc.config_hash) + def test_config_default(self): self.base_dir = 'tests/fixtures/v2-full' result = self.dispatch(['config']) @@ -242,7 +271,7 @@ class CLITestCase(DockerClientTestCase): 'volumes_from': ['service:other:rw'], }, 'other': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'volumes': ['/data'], }, @@ -293,6 +322,51 @@ class CLITestCase(DockerClientTestCase): } } + def test_config_with_dot_env(self): + self.base_dir = 'tests/fixtures/default-env-file' + result = self.dispatch(['config']) + json_result = yaml.load(result.stdout) + assert json_result == { + 'services': { + 'web': { + 'command': 'true', + 'image': 'alpine:latest', + 'ports': ['5643/tcp', '9999/tcp'] + } + }, + 'version': '2.4' + } + + def test_config_with_env_file(self): + self.base_dir = 'tests/fixtures/default-env-file' + result = self.dispatch(['--env-file', '.env2', 'config']) + json_result = yaml.load(result.stdout) + assert json_result == { + 'services': { + 'web': { + 'command': 'false', + 'image': 'alpine:latest', + 'ports': ['5644/tcp', '9998/tcp'] + } + }, + 'version': '2.4' + } + + def test_config_with_dot_env_and_override_dir(self): + self.base_dir = 'tests/fixtures/default-env-file' + result = self.dispatch(['--project-directory', 'alt/', 'config']) + json_result = yaml.load(result.stdout) + assert json_result == { + 'services': { + 'web': { + 'command': 'echo uwu', + 'image': 'alpine:3.10.1', + 'ports': ['3341/tcp', '4449/tcp'] + } + }, + 'version': '2.4' + } + def test_config_external_volume_v2(self): self.base_dir = 'tests/fixtures/volumes' result = self.dispatch(['-f', 'external-volumes-v2.yml', 'config']) @@ -481,18 +555,20 @@ class CLITestCase(DockerClientTestCase): assert yaml.load(result.stdout) == { 'version': '2.3', 'volumes': {'foo': {'driver': 'default'}}, + 'networks': {'bar': {}}, 'services': { 'foo': { 'command': '/bin/true', - 'image': 'alpine:3.7', + 'image': 'alpine:3.10.1', 'scale': 3, 'restart': 'always:7', 'mem_limit': '300M', 'mem_reservation': '100M', 'cpus': 0.7, - 'volumes': ['foo:/bar:rw'] + 'volumes': ['foo:/bar:rw'], + 'networks': {'bar': None}, } - } + }, } def test_ps(self): @@ -550,15 +626,25 @@ class CLITestCase(DockerClientTestCase): assert 'with_build' in running.stdout assert 'with_image' in running.stdout + def test_ps_all(self): + self.project.get_service('simple').create_container(one_off='blahblah') + result = self.dispatch(['ps']) + assert 'simple-composefile_simple_run_' not in result.stdout + + result2 = self.dispatch(['ps', '--all']) + assert 'simple-composefile_simple_run_' in result2.stdout + def test_pull(self): result = self.dispatch(['pull']) assert 'Pulling simple' in result.stderr assert 'Pulling another' in result.stderr + assert 'done' in result.stderr + assert 'failed' not in result.stderr def test_pull_with_digest(self): result = self.dispatch(['-f', 'digest.yml', 'pull', '--no-parallel']) - assert 'Pulling simple (busybox:latest)...' in result.stderr + assert 'Pulling simple ({})...'.format(BUSYBOX_IMAGE_WITH_TAG) in result.stderr assert ('Pulling digest (busybox@' 'sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b520' '04ee8502d)...') in result.stderr @@ -569,12 +655,19 @@ class CLITestCase(DockerClientTestCase): 'pull', '--ignore-pull-failures', '--no-parallel'] ) - assert 'Pulling simple (busybox:latest)...' in result.stderr + assert 'Pulling simple ({})...'.format(BUSYBOX_IMAGE_WITH_TAG) in result.stderr assert 'Pulling another (nonexisting-image:latest)...' 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_build(self): + result = self.dispatch(['-f', 'pull-with-build.yml', 'pull']) + + assert 'Pulling simple' not in result.stderr + assert 'Pulling from_simple' not in result.stderr + assert 'Pulling another ...' in result.stderr + def test_pull_with_quiet(self): assert self.dispatch(['pull', '--quiet']).stderr == '' assert self.dispatch(['pull', '--quiet']).stdout == '' @@ -600,15 +693,15 @@ class CLITestCase(DockerClientTestCase): self.base_dir = 'tests/fixtures/links-composefile' result = self.dispatch(['pull', '--no-parallel', 'web']) assert sorted(result.stderr.split('\n'))[1:] == [ - 'Pulling web (busybox:latest)...', + 'Pulling web (busybox:1.27.2)...', ] def test_pull_with_include_deps(self): self.base_dir = 'tests/fixtures/links-composefile' result = self.dispatch(['pull', '--no-parallel', '--include-deps', 'web']) assert sorted(result.stderr.split('\n'))[1:] == [ - 'Pulling db (busybox:latest)...', - 'Pulling web (busybox:latest)...', + 'Pulling db (busybox:1.27.2)...', + 'Pulling web (busybox:1.27.2)...', ] def test_build_plain(self): @@ -689,6 +782,27 @@ class CLITestCase(DockerClientTestCase): ] assert not containers + @pytest.mark.xfail(True, reason='Flaky on local') + def test_build_rm(self): + containers = [ + Container.from_ps(self.project.client, c) + for c in self.project.client.containers(all=True) + ] + + assert not containers + + self.base_dir = 'tests/fixtures/simple-dockerfile' + self.dispatch(['build', '--no-rm', 'simple'], returncode=0) + + containers = [ + Container.from_ps(self.project.client, c) + for c in self.project.client.containers(all=True) + ] + assert containers + + for c in self.project.client.containers(all=True): + self.addCleanup(self.project.client.remove_container, c, force=True) + def test_build_shm_size_build_option(self): pull_busybox(self.client) self.base_dir = 'tests/fixtures/build-shm-size' @@ -771,6 +885,13 @@ class CLITestCase(DockerClientTestCase): assert 'does not exist, is not accessible, or is not a valid URL' in result.stderr + def test_build_parallel(self): + self.base_dir = 'tests/fixtures/build-multiple-composefile' + result = self.dispatch(['build', '--parallel']) + assert 'Successfully tagged build-multiple-composefile_a:latest' in result.stdout + assert 'Successfully tagged build-multiple-composefile_b:latest' in result.stdout + assert 'Successfully built' in result.stdout + def test_create(self): self.dispatch(['create']) service = self.project.get_service('simple') @@ -909,11 +1030,11 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['down', '--rmi=local', '--volumes']) assert 'Stopping v2-full_web_1' in result.stderr assert 'Stopping v2-full_other_1' in result.stderr - assert 'Stopping v2-full_web_run_2' in result.stderr + assert 'Stopping v2-full_web_run_' in result.stderr assert 'Removing v2-full_web_1' in result.stderr assert 'Removing v2-full_other_1' in result.stderr - assert 'Removing v2-full_web_run_1' in result.stderr - assert 'Removing v2-full_web_run_2' in result.stderr + assert 'Removing v2-full_web_run_' in result.stderr + assert 'Removing v2-full_web_run_' in result.stderr assert 'Removing volume v2-full_data' in result.stderr assert 'Removing image v2-full_web' in result.stderr assert 'Removing image busybox' not in result.stderr @@ -970,11 +1091,15 @@ class CLITestCase(DockerClientTestCase): def test_up_attached(self): self.base_dir = 'tests/fixtures/echo-services' result = self.dispatch(['up', '--no-color']) + simple_name = self.project.get_service('simple').containers(stopped=True)[0].name_without_project + another_name = self.project.get_service('another').containers( + stopped=True + )[0].name_without_project - assert 'simple_1 | simple' in result.stdout - assert 'another_1 | another' in result.stdout - assert 'simple_1 exited with code 0' in result.stdout - assert 'another_1 exited with code 0' in result.stdout + assert '{} | simple'.format(simple_name) in result.stdout + assert '{} | another'.format(another_name) in result.stdout + assert '{} exited with code 0'.format(simple_name) in result.stdout + assert '{} exited with code 0'.format(another_name) in result.stdout @v2_only() def test_up(self): @@ -1040,6 +1165,22 @@ class CLITestCase(DockerClientTestCase): assert len(remote_volumes) > 0 @v2_only() + def test_up_no_start_remove_orphans(self): + self.base_dir = 'tests/fixtures/v2-simple' + self.dispatch(['up', '--no-start'], None) + + services = self.project.get_services() + + stopped = reduce((lambda prev, next: prev.containers( + stopped=True) + next.containers(stopped=True)), services) + assert len(stopped) == 2 + + self.dispatch(['-f', 'one-container.yml', 'up', '--no-start', '--remove-orphans'], None) + stopped2 = reduce((lambda prev, next: prev.containers( + stopped=True) + next.containers(stopped=True)), services) + assert len(stopped2) == 1 + + @v2_only() def test_up_no_ansi(self): self.base_dir = 'tests/fixtures/v2-simple' result = self.dispatch(['--no-ansi', 'up', '-d'], None) @@ -1311,7 +1452,7 @@ class CLITestCase(DockerClientTestCase): 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 set([v['Name'].split('/')[-1] for v in volumes]) == {volume_with_label} assert 'label_key' in volumes[0]['Labels'] assert volumes[0]['Labels']['label_key'] == 'label_val' @@ -1678,11 +1819,12 @@ class CLITestCase(DockerClientTestCase): def test_run_rm(self): self.base_dir = 'tests/fixtures/volume' proc = start_process(self.base_dir, ['run', '--rm', 'test']) + service = self.project.get_service('test') wait_on_condition(ContainerStateCondition( self.project.client, - 'volume_test_run_1', - 'running')) - service = self.project.get_service('test') + 'volume_test_run_*', + 'running') + ) containers = service.containers(one_off=OneOffFilter.only) assert len(containers) == 1 mounts = containers[0].get('Mounts') @@ -1975,7 +2117,7 @@ class CLITestCase(DockerClientTestCase): for _, config in networks.items(): # TODO: once we drop support for API <1.24, this can be changed to: # assert config['Aliases'] == [container.short_id] - aliases = set(config['Aliases'] or []) - set([container.short_id]) + aliases = set(config['Aliases'] or []) - {container.short_id} assert not aliases @v2_only() @@ -1995,7 +2137,7 @@ class CLITestCase(DockerClientTestCase): for _, config in networks.items(): # TODO: once we drop support for API <1.24, this can be changed to: # assert config['Aliases'] == [container.short_id] - aliases = set(config['Aliases'] or []) - set([container.short_id]) + aliases = set(config['Aliases'] or []) - {container.short_id} assert not aliases assert self.lookup(container, 'app') @@ -2005,39 +2147,39 @@ class CLITestCase(DockerClientTestCase): proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) wait_on_condition(ContainerStateCondition( self.project.client, - 'simple-composefile_simple_run_1', + 'simple-composefile_simple_run_*', 'running')) os.kill(proc.pid, signal.SIGINT) wait_on_condition(ContainerStateCondition( self.project.client, - 'simple-composefile_simple_run_1', + 'simple-composefile_simple_run_*', 'exited')) def test_run_handles_sigterm(self): proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) wait_on_condition(ContainerStateCondition( self.project.client, - 'simple-composefile_simple_run_1', + 'simple-composefile_simple_run_*', 'running')) os.kill(proc.pid, signal.SIGTERM) wait_on_condition(ContainerStateCondition( self.project.client, - 'simple-composefile_simple_run_1', + 'simple-composefile_simple_run_*', 'exited')) def test_run_handles_sighup(self): proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) wait_on_condition(ContainerStateCondition( self.project.client, - 'simple-composefile_simple_run_1', + 'simple-composefile_simple_run_*', 'running')) os.kill(proc.pid, signal.SIGHUP) wait_on_condition(ContainerStateCondition( self.project.client, - 'simple-composefile_simple_run_1', + 'simple-composefile_simple_run_*', 'exited')) @mock.patch.dict(os.environ) @@ -2160,6 +2302,7 @@ class CLITestCase(DockerClientTestCase): def test_start_no_containers(self): result = self.dispatch(['start'], returncode=1) + assert 'failed' in result.stderr assert 'No containers to start' in result.stderr @v2_only() @@ -2230,6 +2373,7 @@ class CLITestCase(DockerClientTestCase): assert 'another' in result.stdout assert 'exited with code 0' in result.stdout + @pytest.mark.skip(reason="race condition between up and logs") def test_logs_follow_logs_from_new_containers(self): self.base_dir = 'tests/fixtures/logs-composefile' self.dispatch(['up', '-d', 'simple']) @@ -2237,20 +2381,47 @@ class CLITestCase(DockerClientTestCase): proc = start_process(self.base_dir, ['logs', '-f']) self.dispatch(['up', '-d', 'another']) - wait_on_condition(ContainerStateCondition( - self.project.client, - 'logs-composefile_another_1', - 'exited')) + another_name = self.project.get_service('another').get_container().name_without_project + wait_on_condition( + ContainerStateCondition( + self.project.client, + 'logs-composefile_another_*', + 'exited' + ) + ) + simple_name = self.project.get_service('simple').get_container().name_without_project self.dispatch(['kill', 'simple']) result = wait_on_process(proc) assert 'hello' in result.stdout assert 'test' in result.stdout - assert 'logs-composefile_another_1 exited with code 0' in result.stdout - assert 'logs-composefile_simple_1 exited with code 137' in result.stdout + assert '{} exited with code 0'.format(another_name) in result.stdout + assert '{} exited with code 137'.format(simple_name) in result.stdout + + @pytest.mark.skip(reason="race condition between up and logs") + def test_logs_follow_logs_from_restarted_containers(self): + self.base_dir = 'tests/fixtures/logs-restart-composefile' + proc = start_process(self.base_dir, ['up']) + + wait_on_condition( + ContainerStateCondition( + self.project.client, + 'logs-restart-composefile_another_*', + 'exited' + ) + ) + self.dispatch(['kill', 'simple']) + + result = wait_on_process(proc) + assert result.stdout.count( + r'logs-restart-composefile_another_1 exited with code 1' + ) == 3 + assert result.stdout.count('world') == 3 + + @pytest.mark.skip(reason="race condition between up and logs") def test_logs_default(self): self.base_dir = 'tests/fixtures/logs-composefile' self.dispatch(['up', '-d']) @@ -2274,17 +2445,17 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up', '-d']) result = self.dispatch(['logs', '-f', '-t']) - assert re.search('(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})', result.stdout) + assert re.search(r'(\d{4})-(\d{2})-(\d{2})T(\d{2})\:(\d{2})\:(\d{2})', result.stdout) def test_logs_tail(self): self.base_dir = 'tests/fixtures/logs-tail-composefile' self.dispatch(['up']) result = self.dispatch(['logs', '--tail', '2']) - assert 'c\n' in result.stdout - assert 'd\n' in result.stdout - assert 'a\n' not in result.stdout - assert 'b\n' not in result.stdout + assert 'y\n' in result.stdout + assert 'z\n' in result.stdout + assert 'w\n' not in result.stdout + assert 'x\n' not in result.stdout def test_kill(self): self.dispatch(['up', '-d'], None) @@ -2377,10 +2548,12 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up', '-d']) assert len(project.get_service('web').containers()) == 2 assert len(project.get_service('db').containers()) == 1 + assert len(project.get_service('worker').containers()) == 0 - self.dispatch(['up', '-d', '--scale', 'web=3']) + self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'worker=1']) assert len(project.get_service('web').containers()) == 3 assert len(project.get_service('db').containers()) == 1 + assert len(project.get_service('worker').containers()) == 1 def test_up_scale_scale_down(self): self.base_dir = 'tests/fixtures/scale' @@ -2389,22 +2562,26 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up', '-d']) assert len(project.get_service('web').containers()) == 2 assert len(project.get_service('db').containers()) == 1 + assert len(project.get_service('worker').containers()) == 0 self.dispatch(['up', '-d', '--scale', 'web=1']) assert len(project.get_service('web').containers()) == 1 assert len(project.get_service('db').containers()) == 1 + assert len(project.get_service('worker').containers()) == 0 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']) + self.dispatch(['up', '-d', '--scale', 'web=3', '--scale', 'db=3', '--scale', 'worker=3']) assert len(project.get_service('web').containers()) == 3 assert len(project.get_service('db').containers()) == 3 + assert len(project.get_service('worker').containers()) == 3 self.dispatch(['up', '-d']) assert len(project.get_service('web').containers()) == 2 assert len(project.get_service('db').containers()) == 1 + assert len(project.get_service('worker').containers()) == 0 def test_up_scale_to_zero(self): self.base_dir = 'tests/fixtures/scale' @@ -2413,10 +2590,12 @@ class CLITestCase(DockerClientTestCase): self.dispatch(['up', '-d']) assert len(project.get_service('web').containers()) == 2 assert len(project.get_service('db').containers()) == 1 + assert len(project.get_service('worker').containers()) == 0 - self.dispatch(['up', '-d', '--scale', 'web=0', '--scale', 'db=0']) + self.dispatch(['up', '-d', '--scale', 'web=0', '--scale', 'db=0', '--scale', 'worker=0']) assert len(project.get_service('web').containers()) == 0 assert len(project.get_service('db').containers()) == 0 + assert len(project.get_service('worker').containers()) == 0 def test_port(self): self.base_dir = 'tests/fixtures/ports-composefile' @@ -2458,9 +2637,9 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['port', '--index=' + str(index), 'simple', str(number)]) return result.stdout.rstrip() - assert get_port(3000) == containers[0].get_local_port(3000) - assert get_port(3000, index=1) == containers[0].get_local_port(3000) - assert get_port(3000, index=2) == containers[1].get_local_port(3000) + assert get_port(3000) in (containers[0].get_local_port(3000), containers[1].get_local_port(3000)) + assert get_port(3000, index=containers[0].number) == containers[0].get_local_port(3000) + assert get_port(3000, index=containers[1].number) == containers[1].get_local_port(3000) assert get_port(3002) == "" def test_events_json(self): @@ -2496,7 +2675,7 @@ class CLITestCase(DockerClientTestCase): container, = self.project.containers() expected_template = ' container {} {}' - expected_meta_info = ['image=busybox:latest', 'name=simple-composefile_simple_1'] + expected_meta_info = ['image=busybox:1.27.2', 'name=simple-composefile_simple_'] assert expected_template.format('create', container.id) in lines[0] assert expected_template.format('start', container.id) in lines[1] @@ -2568,7 +2747,7 @@ class CLITestCase(DockerClientTestCase): self.base_dir = 'tests/fixtures/extends' self.dispatch(['up', '-d'], None) - assert set([s.name for s in self.project.services]) == set(['mydb', 'myweb']) + assert set([s.name for s in self.project.services]) == {'mydb', 'myweb'} # Sort by name so we get [db, web] containers = sorted( @@ -2578,14 +2757,11 @@ class CLITestCase(DockerClientTestCase): assert len(containers) == 2 web = containers[1] + db_name = containers[0].name_without_project - assert set(get_links(web)) == set(['db', 'mydb_1', 'extends_mydb_1']) + assert set(get_links(web)) == {'db', db_name, 'extends_{}'.format(db_name)} - expected_env = set([ - "FOO=1", - "BAR=2", - "BAZ=2", - ]) + expected_env = {"FOO=1", "BAR=2", "BAZ=2"} assert expected_env <= set(web.get('Config.Env')) def test_top_services_not_running(self): @@ -2612,17 +2788,27 @@ class CLITestCase(DockerClientTestCase): self.base_dir = 'tests/fixtures/exit-code-from' proc = start_process( self.base_dir, - ['up', '--abort-on-container-exit', '--exit-code-from', 'another']) + ['up', '--abort-on-container-exit', '--exit-code-from', 'another'] + ) result = wait_on_process(proc, returncode=1) - assert 'exit-code-from_another_1 exited with code 1' in result.stdout + def test_exit_code_from_signal_stop(self): + self.base_dir = 'tests/fixtures/exit-code-from' + proc = start_process( + self.base_dir, + ['up', '--abort-on-container-exit', '--exit-code-from', 'simple'] + ) + result = wait_on_process(proc, returncode=137) # SIGKILL + name = self.project.get_service('another').containers(stopped=True)[0].name_without_project + assert '{} exited with code 1'.format(name) in result.stdout + def test_images(self): self.project.get_service('simple').create_container() result = self.dispatch(['images']) assert 'busybox' in result.stdout - assert 'simple-composefile_simple_1' in result.stdout + assert 'simple-composefile_simple_' in result.stdout def test_images_default_composefile(self): self.base_dir = 'tests/fixtures/multiple-composefiles' @@ -2630,8 +2816,8 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['images']) assert 'busybox' in result.stdout - assert 'multiple-composefiles_another_1' in result.stdout - assert 'multiple-composefiles_simple_1' in result.stdout + assert '_another_1' in result.stdout + assert '_simple_1' in result.stdout @mock.patch.dict(os.environ) def test_images_tagless_image(self): @@ -2670,3 +2856,13 @@ class CLITestCase(DockerClientTestCase): with pytest.raises(DuplicateOverrideFileFound): get_project(self.base_dir, []) self.base_dir = None + + def test_images_use_service_tag(self): + pull_busybox(self.client) + self.base_dir = 'tests/fixtures/images-service-tag' + self.dispatch(['up', '-d', '--build']) + result = self.dispatch(['images']) + + assert re.search(r'foo1.+test[ \t]+dev', result.stdout) is not None + assert re.search(r'foo2.+test[ \t]+prod', result.stdout) is not None + assert re.search(r'foo3.+test[ \t]+latest', result.stdout) is not None diff --git a/tests/fixtures/UpperCaseDir/docker-compose.yml b/tests/fixtures/UpperCaseDir/docker-compose.yml index b25beaf4..09cc9519 100644 --- a/tests/fixtures/UpperCaseDir/docker-compose.yml +++ b/tests/fixtures/UpperCaseDir/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/abort-on-container-exit-0/docker-compose.yml b/tests/fixtures/abort-on-container-exit-0/docker-compose.yml index ce41697b..77307ef2 100644 --- a/tests/fixtures/abort-on-container-exit-0/docker-compose.yml +++ b/tests/fixtures/abort-on-container-exit-0/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: ls . diff --git a/tests/fixtures/abort-on-container-exit-1/docker-compose.yml b/tests/fixtures/abort-on-container-exit-1/docker-compose.yml index 7ec9b7e1..23290964 100644 --- a/tests/fixtures/abort-on-container-exit-1/docker-compose.yml +++ b/tests/fixtures/abort-on-container-exit-1/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: ls /thecakeisalie diff --git a/tests/fixtures/build-args/Dockerfile b/tests/fixtures/build-args/Dockerfile index 93ebcb9c..d1534068 100644 --- a/tests/fixtures/build-args/Dockerfile +++ b/tests/fixtures/build-args/Dockerfile @@ -1,4 +1,4 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc LABEL com.docker.compose.test_image=true ARG favorite_th_character RUN echo "Favorite Touhou Character: ${favorite_th_character}" diff --git a/tests/fixtures/build-ctx/Dockerfile b/tests/fixtures/build-ctx/Dockerfile index dd864b83..4acac9c7 100644 --- a/tests/fixtures/build-ctx/Dockerfile +++ b/tests/fixtures/build-ctx/Dockerfile @@ -1,3 +1,3 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc LABEL com.docker.compose.test_image=true CMD echo "success" diff --git a/tests/fixtures/build-memory/Dockerfile b/tests/fixtures/build-memory/Dockerfile index b27349b9..076b84d7 100644 --- a/tests/fixtures/build-memory/Dockerfile +++ b/tests/fixtures/build-memory/Dockerfile @@ -1,4 +1,4 @@ -FROM busybox +FROM busybox:1.31.0-uclibc # Report the memory (through the size of the group memory) RUN echo "memory:" $(cat /sys/fs/cgroup/memory/memory.limit_in_bytes) diff --git a/tests/fixtures/build-multiple-composefile/a/Dockerfile b/tests/fixtures/build-multiple-composefile/a/Dockerfile new file mode 100644 index 00000000..52ed15ec --- /dev/null +++ b/tests/fixtures/build-multiple-composefile/a/Dockerfile @@ -0,0 +1,4 @@ + +FROM busybox:1.31.0-uclibc +RUN echo a +CMD top diff --git a/tests/fixtures/build-multiple-composefile/b/Dockerfile b/tests/fixtures/build-multiple-composefile/b/Dockerfile new file mode 100644 index 00000000..932d851d --- /dev/null +++ b/tests/fixtures/build-multiple-composefile/b/Dockerfile @@ -0,0 +1,4 @@ + +FROM busybox:1.31.0-uclibc +RUN echo b +CMD top diff --git a/tests/fixtures/build-multiple-composefile/docker-compose.yml b/tests/fixtures/build-multiple-composefile/docker-compose.yml new file mode 100644 index 00000000..efa70d7e --- /dev/null +++ b/tests/fixtures/build-multiple-composefile/docker-compose.yml @@ -0,0 +1,8 @@ + +version: "2" + +services: + a: + build: ./a + b: + build: ./b diff --git a/tests/fixtures/compatibility-mode/docker-compose.yml b/tests/fixtures/compatibility-mode/docker-compose.yml index aac6fd4c..4b63fadf 100644 --- a/tests/fixtures/compatibility-mode/docker-compose.yml +++ b/tests/fixtures/compatibility-mode/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.5' services: foo: - image: alpine:3.7 + image: alpine:3.10.1 command: /bin/true deploy: replicas: 3 @@ -16,7 +16,13 @@ services: memory: 100M volumes: - foo:/bar + networks: + - bar volumes: foo: driver: default + +networks: + bar: + attachable: true diff --git a/tests/fixtures/default-env-file/.env2 b/tests/fixtures/default-env-file/.env2 new file mode 100644 index 00000000..d754523f --- /dev/null +++ b/tests/fixtures/default-env-file/.env2 @@ -0,0 +1,4 @@ +IMAGE=alpine:latest +COMMAND=false +PORT1=5644 +PORT2=9998 diff --git a/tests/fixtures/default-env-file/alt/.env b/tests/fixtures/default-env-file/alt/.env new file mode 100644 index 00000000..981c7207 --- /dev/null +++ b/tests/fixtures/default-env-file/alt/.env @@ -0,0 +1,4 @@ +IMAGE=alpine:3.10.1 +COMMAND=echo uwu +PORT1=3341 +PORT2=4449 diff --git a/tests/fixtures/default-env-file/docker-compose.yml b/tests/fixtures/default-env-file/docker-compose.yml index aa8e4409..79363586 100644 --- a/tests/fixtures/default-env-file/docker-compose.yml +++ b/tests/fixtures/default-env-file/docker-compose.yml @@ -1,4 +1,6 @@ -web: +version: '2.4' +services: + web: image: ${IMAGE} command: ${COMMAND} ports: diff --git a/tests/fixtures/dockerfile-with-volume/Dockerfile b/tests/fixtures/dockerfile-with-volume/Dockerfile index 0d376ec4..f38e1d57 100644 --- a/tests/fixtures/dockerfile-with-volume/Dockerfile +++ b/tests/fixtures/dockerfile-with-volume/Dockerfile @@ -1,4 +1,4 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc LABEL com.docker.compose.test_image=true VOLUME /data CMD top diff --git a/tests/fixtures/duplicate-override-yaml-files/docker-compose.yml b/tests/fixtures/duplicate-override-yaml-files/docker-compose.yml index 5f2909d6..6880435b 100644 --- a/tests/fixtures/duplicate-override-yaml-files/docker-compose.yml +++ b/tests/fixtures/duplicate-override-yaml-files/docker-compose.yml @@ -1,10 +1,10 @@ web: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "sleep 100" links: - db db: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "sleep 200" diff --git a/tests/fixtures/echo-services/docker-compose.yml b/tests/fixtures/echo-services/docker-compose.yml index 8014f3d9..75fc45d9 100644 --- a/tests/fixtures/echo-services/docker-compose.yml +++ b/tests/fixtures/echo-services/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: echo simple another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: echo another diff --git a/tests/fixtures/entrypoint-dockerfile/Dockerfile b/tests/fixtures/entrypoint-dockerfile/Dockerfile index 49f4416c..30ec50ba 100644 --- a/tests/fixtures/entrypoint-dockerfile/Dockerfile +++ b/tests/fixtures/entrypoint-dockerfile/Dockerfile @@ -1,4 +1,4 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc LABEL com.docker.compose.test_image=true ENTRYPOINT ["printf"] CMD ["default", "args"] diff --git a/tests/fixtures/env-file-override/.env.conf b/tests/fixtures/env-file-override/.env.conf new file mode 100644 index 00000000..90b8b495 --- /dev/null +++ b/tests/fixtures/env-file-override/.env.conf @@ -0,0 +1,2 @@ +WHEREAMI +DEFAULT_CONF_LOADED=true diff --git a/tests/fixtures/env-file-override/.env.override b/tests/fixtures/env-file-override/.env.override new file mode 100644 index 00000000..398fa51b --- /dev/null +++ b/tests/fixtures/env-file-override/.env.override @@ -0,0 +1 @@ +WHEREAMI=override diff --git a/tests/fixtures/env-file-override/docker-compose.yml b/tests/fixtures/env-file-override/docker-compose.yml new file mode 100644 index 00000000..fdae6d82 --- /dev/null +++ b/tests/fixtures/env-file-override/docker-compose.yml @@ -0,0 +1,6 @@ +version: '3.7' +services: + test: + image: busybox + env_file: .env.conf + entrypoint: env diff --git a/tests/fixtures/environment-composefile/docker-compose.yml b/tests/fixtures/environment-composefile/docker-compose.yml index 9d99fee0..5650c7c8 100644 --- a/tests/fixtures/environment-composefile/docker-compose.yml +++ b/tests/fixtures/environment-composefile/docker-compose.yml @@ -1,5 +1,5 @@ service: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top environment: diff --git a/tests/fixtures/environment-exec/docker-compose.yml b/tests/fixtures/environment-exec/docker-compose.yml index 813606eb..e284ba8c 100644 --- a/tests/fixtures/environment-exec/docker-compose.yml +++ b/tests/fixtures/environment-exec/docker-compose.yml @@ -2,7 +2,7 @@ version: "2.2" services: service: - image: busybox:latest + image: busybox:1.27.2 command: top environment: diff --git a/tests/fixtures/exit-code-from/docker-compose.yml b/tests/fixtures/exit-code-from/docker-compose.yml index 687e78b9..c38bd549 100644 --- a/tests/fixtures/exit-code-from/docker-compose.yml +++ b/tests/fixtures/exit-code-from/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: sh -c "echo hello && tail -f /dev/null" another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: /bin/false diff --git a/tests/fixtures/expose-composefile/docker-compose.yml b/tests/fixtures/expose-composefile/docker-compose.yml index d14a468d..c2a3dc42 100644 --- a/tests/fixtures/expose-composefile/docker-compose.yml +++ b/tests/fixtures/expose-composefile/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top expose: - '3000' diff --git a/tests/fixtures/images-service-tag/Dockerfile b/tests/fixtures/images-service-tag/Dockerfile new file mode 100644 index 00000000..1e1a1b2e --- /dev/null +++ b/tests/fixtures/images-service-tag/Dockerfile @@ -0,0 +1,2 @@ +FROM busybox:1.31.0-uclibc +RUN touch /foo diff --git a/tests/fixtures/images-service-tag/docker-compose.yml b/tests/fixtures/images-service-tag/docker-compose.yml new file mode 100644 index 00000000..a46b32bf --- /dev/null +++ b/tests/fixtures/images-service-tag/docker-compose.yml @@ -0,0 +1,11 @@ +version: "2.4" +services: + foo1: + build: . + image: test:dev + foo2: + build: . + image: test:prod + foo3: + build: . + image: test:latest diff --git a/tests/fixtures/links-composefile/docker-compose.yml b/tests/fixtures/links-composefile/docker-compose.yml index 930fd4c7..0a2f3d9e 100644 --- a/tests/fixtures/links-composefile/docker-compose.yml +++ b/tests/fixtures/links-composefile/docker-compose.yml @@ -1,11 +1,11 @@ db: - image: busybox:latest + image: busybox:1.27.2 command: top web: - image: busybox:latest + image: busybox:1.27.2 command: top links: - db:db console: - image: busybox:latest + image: busybox:1.27.2 command: top diff --git a/tests/fixtures/logging-composefile-legacy/docker-compose.yml b/tests/fixtures/logging-composefile-legacy/docker-compose.yml index ee994107..efac1d6a 100644 --- a/tests/fixtures/logging-composefile-legacy/docker-compose.yml +++ b/tests/fixtures/logging-composefile-legacy/docker-compose.yml @@ -1,9 +1,9 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top log_driver: "none" another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top log_driver: "json-file" log_opt: diff --git a/tests/fixtures/logging-composefile/docker-compose.yml b/tests/fixtures/logging-composefile/docker-compose.yml index 466d13e5..ac231b89 100644 --- a/tests/fixtures/logging-composefile/docker-compose.yml +++ b/tests/fixtures/logging-composefile/docker-compose.yml @@ -1,12 +1,12 @@ version: "2" services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top logging: driver: "none" another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top logging: driver: "json-file" diff --git a/tests/fixtures/logs-composefile/docker-compose.yml b/tests/fixtures/logs-composefile/docker-compose.yml index b719c91e..3ffaa984 100644 --- a/tests/fixtures/logs-composefile/docker-compose.yml +++ b/tests/fixtures/logs-composefile/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest - command: sh -c "echo hello && tail -f /dev/null" + image: busybox:1.31.0-uclibc + command: sh -c "sleep 1 && echo hello && tail -f /dev/null" another: - image: busybox:latest - command: sh -c "echo test" + image: busybox:1.31.0-uclibc + command: sh -c "sleep 1 && echo test" diff --git a/tests/fixtures/logs-restart-composefile/docker-compose.yml b/tests/fixtures/logs-restart-composefile/docker-compose.yml new file mode 100644 index 00000000..2179d54d --- /dev/null +++ b/tests/fixtures/logs-restart-composefile/docker-compose.yml @@ -0,0 +1,7 @@ +simple: + image: busybox:1.31.0-uclibc + command: sh -c "echo hello && tail -f /dev/null" +another: + image: busybox:1.31.0-uclibc + command: sh -c "sleep 2 && echo world && /bin/false" + restart: "on-failure:2" diff --git a/tests/fixtures/logs-tail-composefile/docker-compose.yml b/tests/fixtures/logs-tail-composefile/docker-compose.yml index 80d8feae..18dad986 100644 --- a/tests/fixtures/logs-tail-composefile/docker-compose.yml +++ b/tests/fixtures/logs-tail-composefile/docker-compose.yml @@ -1,3 +1,3 @@ simple: - image: busybox:latest - command: sh -c "echo a && echo b && echo c && echo d" + image: busybox:1.31.0-uclibc + command: sh -c "echo w && echo x && echo y && echo z" diff --git a/tests/fixtures/longer-filename-composefile/docker-compose.yaml b/tests/fixtures/longer-filename-composefile/docker-compose.yaml index a4eba2d0..5dadce44 100644 --- a/tests/fixtures/longer-filename-composefile/docker-compose.yaml +++ b/tests/fixtures/longer-filename-composefile/docker-compose.yaml @@ -1,3 +1,3 @@ definedinyamlnotyml: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/multiple-composefiles/compose2.yml b/tests/fixtures/multiple-composefiles/compose2.yml index 56803380..530d92df 100644 --- a/tests/fixtures/multiple-composefiles/compose2.yml +++ b/tests/fixtures/multiple-composefiles/compose2.yml @@ -1,3 +1,3 @@ yetanother: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/multiple-composefiles/docker-compose.yml b/tests/fixtures/multiple-composefiles/docker-compose.yml index b25beaf4..09cc9519 100644 --- a/tests/fixtures/multiple-composefiles/docker-compose.yml +++ b/tests/fixtures/multiple-composefiles/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/networks/default-network-config.yml b/tests/fixtures/networks/default-network-config.yml index 4bd0989b..556ca980 100644 --- a/tests/fixtures/networks/default-network-config.yml +++ b/tests/fixtures/networks/default-network-config.yml @@ -1,10 +1,10 @@ version: "2" services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top networks: default: diff --git a/tests/fixtures/networks/docker-compose.yml b/tests/fixtures/networks/docker-compose.yml index c11fa682..b911c752 100644 --- a/tests/fixtures/networks/docker-compose.yml +++ b/tests/fixtures/networks/docker-compose.yml @@ -2,17 +2,17 @@ version: "2" services: web: - image: busybox + image: alpine:3.10.1 command: top networks: ["front"] app: - image: busybox + image: alpine:3.10.1 command: top networks: ["front", "back"] links: - "db:database" db: - image: busybox + image: alpine:3.10.1 command: top networks: ["back"] diff --git a/tests/fixtures/networks/external-default.yml b/tests/fixtures/networks/external-default.yml index 5c9426b8..42a39565 100644 --- a/tests/fixtures/networks/external-default.yml +++ b/tests/fixtures/networks/external-default.yml @@ -1,10 +1,10 @@ version: "2" services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top networks: default: diff --git a/tests/fixtures/no-links-composefile/docker-compose.yml b/tests/fixtures/no-links-composefile/docker-compose.yml index 75a6a085..54936f30 100644 --- a/tests/fixtures/no-links-composefile/docker-compose.yml +++ b/tests/fixtures/no-links-composefile/docker-compose.yml @@ -1,9 +1,9 @@ db: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top web: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top console: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/override-files/docker-compose.yml b/tests/fixtures/override-files/docker-compose.yml index 6c3d4e17..0119ec73 100644 --- a/tests/fixtures/override-files/docker-compose.yml +++ b/tests/fixtures/override-files/docker-compose.yml @@ -1,10 +1,10 @@ version: '2.2' services: web: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "sleep 200" depends_on: - db db: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "sleep 200" diff --git a/tests/fixtures/override-files/extra.yml b/tests/fixtures/override-files/extra.yml index 492c3795..d03c5096 100644 --- a/tests/fixtures/override-files/extra.yml +++ b/tests/fixtures/override-files/extra.yml @@ -6,5 +6,5 @@ services: - other other: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "top" diff --git a/tests/fixtures/override-yaml-files/docker-compose.yml b/tests/fixtures/override-yaml-files/docker-compose.yml index 5f2909d6..6880435b 100644 --- a/tests/fixtures/override-yaml-files/docker-compose.yml +++ b/tests/fixtures/override-yaml-files/docker-compose.yml @@ -1,10 +1,10 @@ web: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "sleep 100" links: - db db: - image: busybox:latest + image: busybox:1.31.0-uclibc command: "sleep 200" diff --git a/tests/fixtures/ports-composefile-scale/docker-compose.yml b/tests/fixtures/ports-composefile-scale/docker-compose.yml index 1a2bb485..bdd39cef 100644 --- a/tests/fixtures/ports-composefile-scale/docker-compose.yml +++ b/tests/fixtures/ports-composefile-scale/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: /bin/sleep 300 ports: - '3000' diff --git a/tests/fixtures/ports-composefile/docker-compose.yml b/tests/fixtures/ports-composefile/docker-compose.yml index c213068d..f4987027 100644 --- a/tests/fixtures/ports-composefile/docker-compose.yml +++ b/tests/fixtures/ports-composefile/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top ports: - '3000' diff --git a/tests/fixtures/ports-composefile/expanded-notation.yml b/tests/fixtures/ports-composefile/expanded-notation.yml index 09a7a2bf..6510e428 100644 --- a/tests/fixtures/ports-composefile/expanded-notation.yml +++ b/tests/fixtures/ports-composefile/expanded-notation.yml @@ -1,7 +1,7 @@ version: '3.2' services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top ports: - target: 3000 diff --git a/tests/fixtures/ps-services-filter/docker-compose.yml b/tests/fixtures/ps-services-filter/docker-compose.yml index 3d860937..180f515a 100644 --- a/tests/fixtures/ps-services-filter/docker-compose.yml +++ b/tests/fixtures/ps-services-filter/docker-compose.yml @@ -1,5 +1,5 @@ with_image: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top with_build: build: ../build-ctx/ diff --git a/tests/fixtures/run-labels/docker-compose.yml b/tests/fixtures/run-labels/docker-compose.yml index e8cd5006..e3b237fd 100644 --- a/tests/fixtures/run-labels/docker-compose.yml +++ b/tests/fixtures/run-labels/docker-compose.yml @@ -1,5 +1,5 @@ service: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top labels: diff --git a/tests/fixtures/run-workdir/docker-compose.yml b/tests/fixtures/run-workdir/docker-compose.yml index dc3ea86a..9d092a55 100644 --- a/tests/fixtures/run-workdir/docker-compose.yml +++ b/tests/fixtures/run-workdir/docker-compose.yml @@ -1,4 +1,4 @@ service: - image: busybox:latest + image: busybox:1.31.0-uclibc working_dir: /etc command: /bin/true diff --git a/tests/fixtures/scale/docker-compose.yml b/tests/fixtures/scale/docker-compose.yml index a0d3b771..53ae1342 100644 --- a/tests/fixtures/scale/docker-compose.yml +++ b/tests/fixtures/scale/docker-compose.yml @@ -5,5 +5,9 @@ services: command: top scale: 2 db: - image: busybox - command: top + image: busybox + command: top + worker: + image: busybox + command: top + scale: 0 diff --git a/tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml b/tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml index fe717151..45b626d0 100644 --- a/tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml +++ b/tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml @@ -1,7 +1,7 @@ version: '2.2' services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc volumes: - datastore:/data1 diff --git a/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml b/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml index 98a7d23b..088d71c9 100644 --- a/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml +++ b/tests/fixtures/simple-composefile-volume-ready/docker-compose.yml @@ -1,2 +1,2 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc diff --git a/tests/fixtures/simple-composefile/digest.yml b/tests/fixtures/simple-composefile/digest.yml index 08f1d993..79f043ba 100644 --- a/tests/fixtures/simple-composefile/digest.yml +++ b/tests/fixtures/simple-composefile/digest.yml @@ -1,5 +1,5 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top digest: image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d diff --git a/tests/fixtures/simple-composefile/docker-compose.yml b/tests/fixtures/simple-composefile/docker-compose.yml index b25beaf4..b66a0652 100644 --- a/tests/fixtures/simple-composefile/docker-compose.yml +++ b/tests/fixtures/simple-composefile/docker-compose.yml @@ -1,6 +1,6 @@ simple: - image: busybox:latest + image: busybox:1.27.2 command: top another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/simple-composefile/ignore-pull-failures.yml b/tests/fixtures/simple-composefile/ignore-pull-failures.yml index a28f7922..7e7d560d 100644 --- a/tests/fixtures/simple-composefile/ignore-pull-failures.yml +++ b/tests/fixtures/simple-composefile/ignore-pull-failures.yml @@ -1,5 +1,5 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top another: image: nonexisting-image:latest diff --git a/tests/fixtures/simple-composefile/pull-with-build.yml b/tests/fixtures/simple-composefile/pull-with-build.yml new file mode 100644 index 00000000..3bff35c5 --- /dev/null +++ b/tests/fixtures/simple-composefile/pull-with-build.yml @@ -0,0 +1,11 @@ +version: "3" +services: + build_simple: + image: simple + build: . + command: top + from_simple: + image: simple + another: + image: busybox:1.31.0-uclibc + command: top diff --git a/tests/fixtures/simple-dockerfile/Dockerfile b/tests/fixtures/simple-dockerfile/Dockerfile index dd864b83..098ff3eb 100644 --- a/tests/fixtures/simple-dockerfile/Dockerfile +++ b/tests/fixtures/simple-dockerfile/Dockerfile @@ -1,3 +1,3 @@ -FROM busybox:latest +FROM busybox:1.27.2 LABEL com.docker.compose.test_image=true CMD echo "success" diff --git a/tests/fixtures/simple-failing-dockerfile/Dockerfile b/tests/fixtures/simple-failing-dockerfile/Dockerfile index c2d06b16..205021a2 100644 --- a/tests/fixtures/simple-failing-dockerfile/Dockerfile +++ b/tests/fixtures/simple-failing-dockerfile/Dockerfile @@ -1,4 +1,4 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc LABEL com.docker.compose.test_image=true LABEL com.docker.compose.test_failing_image=true # With the following label the container wil be cleaned up automatically diff --git a/tests/fixtures/sleeps-composefile/docker-compose.yml b/tests/fixtures/sleeps-composefile/docker-compose.yml index 7c8d84f8..26feb502 100644 --- a/tests/fixtures/sleeps-composefile/docker-compose.yml +++ b/tests/fixtures/sleeps-composefile/docker-compose.yml @@ -3,8 +3,8 @@ version: "2" services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: sleep 200 another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: sleep 200 diff --git a/tests/fixtures/stop-signal-composefile/docker-compose.yml b/tests/fixtures/stop-signal-composefile/docker-compose.yml index 04f58aa9..9f99b0c7 100644 --- a/tests/fixtures/stop-signal-composefile/docker-compose.yml +++ b/tests/fixtures/stop-signal-composefile/docker-compose.yml @@ -1,5 +1,5 @@ simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: - sh - '-c' diff --git a/tests/fixtures/tagless-image/Dockerfile b/tests/fixtures/tagless-image/Dockerfile index 56741055..92305555 100644 --- a/tests/fixtures/tagless-image/Dockerfile +++ b/tests/fixtures/tagless-image/Dockerfile @@ -1,2 +1,2 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc RUN touch /blah diff --git a/tests/fixtures/top/docker-compose.yml b/tests/fixtures/top/docker-compose.yml index d632a836..36a3917d 100644 --- a/tests/fixtures/top/docker-compose.yml +++ b/tests/fixtures/top/docker-compose.yml @@ -1,6 +1,6 @@ service_a: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top service_b: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/unicode-environment/docker-compose.yml b/tests/fixtures/unicode-environment/docker-compose.yml index a41af4f0..307678cd 100644 --- a/tests/fixtures/unicode-environment/docker-compose.yml +++ b/tests/fixtures/unicode-environment/docker-compose.yml @@ -1,7 +1,7 @@ version: '2' services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: sh -c 'echo $$FOO' environment: FOO: ${BAR} diff --git a/tests/fixtures/user-composefile/docker-compose.yml b/tests/fixtures/user-composefile/docker-compose.yml index 3eb7d397..11283d9d 100644 --- a/tests/fixtures/user-composefile/docker-compose.yml +++ b/tests/fixtures/user-composefile/docker-compose.yml @@ -1,4 +1,4 @@ service: - image: busybox:latest + image: busybox:1.31.0-uclibc user: notauser command: id diff --git a/tests/fixtures/v2-dependencies/docker-compose.yml b/tests/fixtures/v2-dependencies/docker-compose.yml index 2e14b94b..45ec8501 100644 --- a/tests/fixtures/v2-dependencies/docker-compose.yml +++ b/tests/fixtures/v2-dependencies/docker-compose.yml @@ -1,13 +1,13 @@ version: "2.0" services: db: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top web: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top depends_on: - db console: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/v2-full/Dockerfile b/tests/fixtures/v2-full/Dockerfile index 51ed0d90..6fa7a726 100644 --- a/tests/fixtures/v2-full/Dockerfile +++ b/tests/fixtures/v2-full/Dockerfile @@ -1,4 +1,4 @@ -FROM busybox:latest +FROM busybox:1.31.0-uclibc RUN echo something CMD top diff --git a/tests/fixtures/v2-full/docker-compose.yml b/tests/fixtures/v2-full/docker-compose.yml index a973dd0c..20c14f0f 100644 --- a/tests/fixtures/v2-full/docker-compose.yml +++ b/tests/fixtures/v2-full/docker-compose.yml @@ -18,7 +18,7 @@ services: - other other: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top volumes: - /data diff --git a/tests/fixtures/v2-simple/docker-compose.yml b/tests/fixtures/v2-simple/docker-compose.yml index c99ae02f..ac754eee 100644 --- a/tests/fixtures/v2-simple/docker-compose.yml +++ b/tests/fixtures/v2-simple/docker-compose.yml @@ -1,8 +1,8 @@ version: "2" services: simple: - image: busybox:latest + image: busybox:1.27.2 command: top another: - image: busybox:latest + image: busybox:1.27.2 command: top diff --git a/tests/fixtures/v2-simple/links-invalid.yml b/tests/fixtures/v2-simple/links-invalid.yml index 481aa404..a88eb1d5 100644 --- a/tests/fixtures/v2-simple/links-invalid.yml +++ b/tests/fixtures/v2-simple/links-invalid.yml @@ -1,10 +1,10 @@ version: "2" services: simple: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top links: - another another: - image: busybox:latest + image: busybox:1.31.0-uclibc command: top diff --git a/tests/fixtures/v2-simple/one-container.yml b/tests/fixtures/v2-simple/one-container.yml new file mode 100644 index 00000000..2d5c2ca6 --- /dev/null +++ b/tests/fixtures/v2-simple/one-container.yml @@ -0,0 +1,5 @@ +version: "2" +services: + simple: + image: busybox:1.31.0-uclibc + command: top diff --git a/tests/helpers.py b/tests/helpers.py index dd129981..327715ee 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,6 +7,10 @@ from compose.config.config import ConfigDetails from compose.config.config import ConfigFile from compose.config.config import load +BUSYBOX_IMAGE_NAME = 'busybox' +BUSYBOX_DEFAULT_TAG = '1.31.0-uclibc' +BUSYBOX_IMAGE_WITH_TAG = '{}:{}'.format(BUSYBOX_IMAGE_NAME, BUSYBOX_DEFAULT_TAG) + def build_config(contents, **kwargs): return load(build_config_details(contents, **kwargs)) @@ -22,7 +26,7 @@ def build_config_details(contents, working_dir='working_dir', filename='filename def create_custom_host_file(client, filename, content): dirname = os.path.dirname(filename) container = client.create_container( - 'busybox:latest', + BUSYBOX_IMAGE_WITH_TAG, ['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)], volumes={dirname: {}}, host_config=client.create_host_config( diff --git a/tests/integration/environment_test.py b/tests/integration/environment_test.py new file mode 100644 index 00000000..671e6531 --- /dev/null +++ b/tests/integration/environment_test.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import tempfile + +from ddt import data +from ddt import ddt + +from .. import mock +from ..acceptance.cli_test import dispatch +from compose.cli.command import get_project +from compose.cli.command import project_from_options +from compose.config.environment import Environment +from tests.integration.testcases import DockerClientTestCase + + +@ddt +class EnvironmentTest(DockerClientTestCase): + @classmethod + def setUpClass(cls): + super(EnvironmentTest, cls).setUpClass() + cls.compose_file = tempfile.NamedTemporaryFile(mode='w+b') + cls.compose_file.write(bytes("""version: '3.2' +services: + svc: + image: busybox:1.31.0-uclibc + environment: + TEST_VARIABLE: ${TEST_VARIABLE}""", encoding='utf-8')) + cls.compose_file.flush() + + @classmethod + def tearDownClass(cls): + super(EnvironmentTest, cls).tearDownClass() + cls.compose_file.close() + + @data('events', + 'exec', + 'kill', + 'logs', + 'pause', + 'ps', + 'restart', + 'rm', + 'start', + 'stop', + 'top', + 'unpause') + def _test_no_warning_on_missing_host_environment_var_on_silent_commands(self, cmd): + options = {'COMMAND': cmd, '--file': [EnvironmentTest.compose_file.name]} + with mock.patch('compose.config.environment.log') as fake_log: + # Note that the warning silencing and the env variables check is + # done in `project_from_options` + # So no need to have a proper options map, the `COMMAND` key is enough + project_from_options('.', options) + assert fake_log.warn.call_count == 0 + + +class EnvironmentOverrideFileTest(DockerClientTestCase): + def test_env_file_override(self): + base_dir = 'tests/fixtures/env-file-override' + dispatch(base_dir, ['--env-file', '.env.override', 'up']) + project = get_project(project_dir=base_dir, + config_path=['docker-compose.yml'], + environment=Environment.from_env_file(base_dir, '.env.override'), + override_dir=base_dir) + containers = project.containers(stopped=True) + assert len(containers) == 1 + assert "WHEREAMI=override" in containers[0].get('Config.Env') + assert "DEFAULT_CONF_LOADED=true" in containers[0].get('Config.Env') + dispatch(base_dir, ['--env-file', '.env.override', 'down'], None) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 3960d12e..4c88f3d6 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import copy import json import os import random @@ -14,6 +15,7 @@ from docker.errors import NotFound from .. import mock from ..helpers import build_config as load_config +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from ..helpers import create_host_file from .testcases import DockerClientTestCase from .testcases import SWARM_SKIP_CONTAINERS_ALL @@ -90,7 +92,8 @@ class ProjectTest(DockerClientTestCase): project.up() containers = project.containers(['web']) - assert [c.name for c in containers] == ['composetest_web_1'] + assert len(containers) == 1 + assert containers[0].name.startswith('composetest_web_') def test_containers_with_extra_service(self): web = self.create_service('web') @@ -102,18 +105,35 @@ class ProjectTest(DockerClientTestCase): self.create_service('extra').create_container() project = Project('composetest', [web, db], self.client) - assert set(project.containers(stopped=True)) == set([web_1, db_1]) + assert set(project.containers(stopped=True)) == {web_1, db_1} + + def test_parallel_pull_with_no_image(self): + config_data = build_config( + version=V2_3, + services=[{ + 'name': 'web', + 'build': {'context': '.'}, + }], + ) + + project = Project.from_config( + name='composetest', + config_data=config_data, + client=self.client + ) + + project.pull(parallel_pull=True) def test_volumes_from_service(self): project = Project.from_config( name='composetest', config_data=load_config({ 'data': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['/var/data'], }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': ['data'], }, }), @@ -126,7 +146,7 @@ class ProjectTest(DockerClientTestCase): def test_volumes_from_container(self): data_container = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, volumes=['/var/data'], name='composetest_data_container', labels={LABEL_PROJECT: 'composetest'}, @@ -136,7 +156,7 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': ['composetest_data_container'], }, }), @@ -155,11 +175,11 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'net': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'service:net', 'command': ["top"] }, @@ -183,7 +203,7 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'container:composetest_net_container' }, }, @@ -198,7 +218,7 @@ class ProjectTest(DockerClientTestCase): net_container = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, name='composetest_net_container', command='top', labels={LABEL_PROJECT: 'composetest'}, @@ -218,11 +238,11 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'net': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'net': 'container:net', 'command': ["top"] }, @@ -243,7 +263,7 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'net': 'container:composetest_net_container' }, }), @@ -257,7 +277,7 @@ class ProjectTest(DockerClientTestCase): net_container = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, name='composetest_net_container', command='top', labels={LABEL_PROJECT: 'composetest'}, @@ -286,24 +306,20 @@ class ProjectTest(DockerClientTestCase): db_container = db.create_container() project.start(service_names=['web']) - assert set(c.name for c in project.containers() if c.is_running) == set( - [web_container_1.name, web_container_2.name] - ) + assert set(c.name for c in project.containers() if c.is_running) == { + web_container_1.name, web_container_2.name} project.start() - assert set(c.name for c in project.containers() if c.is_running) == set( - [web_container_1.name, web_container_2.name, db_container.name] - ) + assert set(c.name for c in project.containers() if c.is_running) == { + web_container_1.name, web_container_2.name, db_container.name} project.pause(service_names=['web']) - assert set([c.name for c in project.containers() if c.is_paused]) == set( - [web_container_1.name, web_container_2.name] - ) + assert set([c.name for c in project.containers() if c.is_paused]) == { + web_container_1.name, web_container_2.name} project.pause() - assert set([c.name for c in project.containers() if c.is_paused]) == set( - [web_container_1.name, web_container_2.name, db_container.name] - ) + assert set([c.name for c in project.containers() if c.is_paused]) == { + web_container_1.name, web_container_2.name, db_container.name} project.unpause(service_names=['db']) assert len([c.name for c in project.containers() if c.is_paused]) == 2 @@ -312,7 +328,7 @@ class ProjectTest(DockerClientTestCase): assert len([c.name for c in project.containers() if c.is_paused]) == 0 project.stop(service_names=['web'], timeout=1) - assert set(c.name for c in project.containers() if c.is_running) == set([db_container.name]) + assert set(c.name for c in project.containers() if c.is_running) == {db_container.name} project.kill(service_names=['db']) assert len([c for c in project.containers() if c.is_running]) == 0 @@ -431,7 +447,7 @@ class ProjectTest(DockerClientTestCase): project.up(strategy=ConvergenceStrategy.always) assert len(project.containers()) == 2 - db_container = [c for c in project.containers() if 'db' in c.name][0] + db_container = [c for c in project.containers() if c.service == 'db'][0] assert db_container.id != old_db_id assert db_container.get('Volumes./etc') == db_volume_path @@ -451,7 +467,7 @@ class ProjectTest(DockerClientTestCase): project.up(strategy=ConvergenceStrategy.always) assert len(project.containers()) == 2 - db_container = [c for c in project.containers() if 'db' in c.name][0] + db_container = [c for c in project.containers() if c.service == 'db'][0] assert db_container.id != old_db_id assert db_container.get_mount('/etc')['Source'] == db_volume_path @@ -464,14 +480,14 @@ class ProjectTest(DockerClientTestCase): project.up(['db']) assert len(project.containers()) == 1 - old_db_id = project.containers()[0].id container, = project.containers() + old_db_id = container.id db_volume_path = container.get_mount('/var/db')['Source'] project.up(strategy=ConvergenceStrategy.never) assert len(project.containers()) == 2 - db_container = [c for c in project.containers() if 'db' in c.name][0] + db_container = [c for c in project.containers() if c.name == container.name][0] assert db_container.id == old_db_id assert db_container.get_mount('/var/db')['Source'] == db_volume_path @@ -498,7 +514,7 @@ class ProjectTest(DockerClientTestCase): assert len(new_containers) == 2 assert [c.is_running for c in new_containers] == [True, True] - db_container = [c for c in new_containers if 'db' in c.name][0] + db_container = [c for c in new_containers if c.service == 'db'][0] assert db_container.id == old_db_id assert db_container.get_mount('/var/db')['Source'] == db_volume_path @@ -534,20 +550,20 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'console': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], }, 'data': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'volumes_from': ['data'], }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'links': ['db'], }, @@ -569,20 +585,20 @@ class ProjectTest(DockerClientTestCase): name='composetest', config_data=load_config({ 'console': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], }, 'data': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"] }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'volumes_from': ['data'], }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': ["top"], 'links': ['db'], }, @@ -608,7 +624,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'foo': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'tmpfs': ['/dev/shm'], 'volumes': ['/dev/shm'] } @@ -649,7 +665,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'networks': { 'foo': None, @@ -694,7 +710,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'front': None}, }], networks={ @@ -754,7 +770,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'front': None}, }], networks={ @@ -789,7 +805,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'networks': { 'static_test': { @@ -841,7 +857,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': { 'n1': { 'priority': p1, @@ -904,7 +920,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'networks': { 'static_test': { @@ -947,7 +963,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': { 'static_test': { 'ipv4_address': '172.16.100.100', @@ -983,7 +999,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': { 'linklocaltest': { 'link_local_ips': ['169.254.8.8'] @@ -1020,7 +1036,7 @@ class ProjectTest(DockerClientTestCase): 'name': 'web', 'volumes': [VolumeSpec.parse('foo:/container-path')], 'networks': {'foo': {}}, - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }], networks={ 'foo': { @@ -1056,7 +1072,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'isolation': 'default' }], ) @@ -1076,7 +1092,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'isolation': 'foobar' }], ) @@ -1096,7 +1112,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'runtime': 'runc' }], ) @@ -1116,7 +1132,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'runtime': 'foobar' }], ) @@ -1136,7 +1152,7 @@ class ProjectTest(DockerClientTestCase): version=V2_3, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'runtime': 'nvidia' }], ) @@ -1156,7 +1172,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'internal': None}, }], networks={ @@ -1185,7 +1201,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {network_name: None} }], networks={ @@ -1218,7 +1234,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, @@ -1245,7 +1261,7 @@ class ProjectTest(DockerClientTestCase): version=V2_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': [VolumeSpec.parse('{}:/data'.format(volume_name))] }], volumes={ @@ -1284,9 +1300,9 @@ class ProjectTest(DockerClientTestCase): { 'version': str(V2_0), 'services': { - 'simple': {'image': 'busybox:latest', 'command': 'top'}, + 'simple': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, 'another': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'logging': { 'driver': "json-file", @@ -1337,7 +1353,7 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'simple': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'ports': ['1234:1234'] }, @@ -1371,7 +1387,7 @@ class ProjectTest(DockerClientTestCase): version=V2_2, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'scale': 3 }] @@ -1401,7 +1417,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {}}, @@ -1425,7 +1441,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {}}, @@ -1449,7 +1465,7 @@ class ProjectTest(DockerClientTestCase): version=V3_1, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'cat /run/secrets/special', 'secrets': [ types.ServiceSecret.parse({'source': 'super', 'target': 'special'}), @@ -1478,6 +1494,60 @@ class ProjectTest(DockerClientTestCase): output = container.logs() assert output == b"This is the secret\n" + @v3_only() + def test_project_up_with_added_secrets(self): + node = create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default')) + + config_input1 = { + 'version': V3_1, + 'services': [ + { + 'name': 'web', + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'cat /run/secrets/special', + 'environment': ['constraint:node=={}'.format(node if node is not None else '')] + } + + ], + 'secrets': { + 'super': { + 'file': os.path.abspath('tests/fixtures/secrets/default') + } + } + } + config_input2 = copy.deepcopy(config_input1) + # Add the secret + config_input2['services'][0]['secrets'] = [ + types.ServiceSecret.parse({'source': 'super', 'target': 'special'}) + ] + + config_data1 = build_config(**config_input1) + config_data2 = build_config(**config_input2) + + # First up with non-secret + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data1, + ) + project.up() + + # Then up with secret + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data2, + ) + 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)) @@ -1486,7 +1556,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'foobar'}}, @@ -1509,7 +1579,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, @@ -1551,7 +1621,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={ @@ -1593,7 +1663,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, @@ -1632,7 +1702,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={ @@ -1656,7 +1726,7 @@ class ProjectTest(DockerClientTestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top' }], volumes={ @@ -1684,7 +1754,7 @@ class ProjectTest(DockerClientTestCase): 'version': str(V2_0), 'services': { 'simple': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'volumes': ['{0}:/data'.format(vol_name)] }, @@ -1713,7 +1783,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_orphans(self): config_dict = { 'service1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', } } @@ -1750,7 +1820,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_ignore_orphans(self): config_dict = { 'service1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', } } @@ -1778,7 +1848,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'healthcheck': { 'test': 'exit 0', @@ -1788,7 +1858,7 @@ class ProjectTest(DockerClientTestCase): }, }, 'svc2': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'depends_on': { 'svc1': {'condition': 'service_healthy'}, @@ -1815,7 +1885,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'healthcheck': { 'test': 'exit 1', @@ -1825,7 +1895,7 @@ class ProjectTest(DockerClientTestCase): }, }, 'svc2': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'depends_on': { 'svc1': {'condition': 'service_healthy'}, @@ -1854,14 +1924,14 @@ class ProjectTest(DockerClientTestCase): 'version': '2.1', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'healthcheck': { 'disable': True }, }, 'svc2': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'depends_on': { 'svc1': {'condition': 'service_healthy'}, @@ -1898,7 +1968,7 @@ class ProjectTest(DockerClientTestCase): 'version': '2.3', 'services': { 'svc1': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'security_opt': ['seccomp:"{}"'.format(profile_path)] } @@ -1915,3 +1985,65 @@ class ProjectTest(DockerClientTestCase): assert len(remote_secopts) == 1 assert remote_secopts[0].startswith('seccomp=') assert json.loads(remote_secopts[0].lstrip('seccomp=')) == seccomp_data + + @no_cluster('inspect volume by name defect on Swarm Classic') + def test_project_up_name_starts_with_illegal_char(self): + config_dict = { + 'version': '2.3', + 'services': { + 'svc1': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'ls', + 'volumes': ['foo:/foo:rw'], + 'networks': ['bar'], + }, + }, + 'volumes': { + 'foo': {}, + }, + 'networks': { + 'bar': {}, + } + } + config_data = load_config(config_dict) + project = Project.from_config( + name='_underscoretest', config_data=config_data, client=self.client + ) + project.up() + self.addCleanup(project.down, None, True) + + containers = project.containers(stopped=True) + assert len(containers) == 1 + assert containers[0].name.startswith('underscoretest_svc1_') + assert containers[0].project == '_underscoretest' + + full_vol_name = 'underscoretest_foo' + vol_data = self.get_volume_data(full_vol_name) + assert vol_data + assert vol_data['Labels'][LABEL_PROJECT] == '_underscoretest' + + full_net_name = '_underscoretest_bar' + net_data = self.client.inspect_network(full_net_name) + assert net_data + assert net_data['Labels'][LABEL_PROJECT] == '_underscoretest' + + project2 = Project.from_config( + name='-dashtest', config_data=config_data, client=self.client + ) + project2.up() + self.addCleanup(project2.down, None, True) + + containers = project2.containers(stopped=True) + assert len(containers) == 1 + assert containers[0].name.startswith('dashtest_svc1_') + assert containers[0].project == '-dashtest' + + full_vol_name = 'dashtest_foo' + vol_data = self.get_volume_data(full_vol_name) + assert vol_data + assert vol_data['Labels'][LABEL_PROJECT] == '-dashtest' + + full_net_name = '-dashtest_bar' + net_data = self.client.inspect_network(full_net_name) + assert net_data + assert net_data['Labels'][LABEL_PROJECT] == '-dashtest' diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index d8f4d094..c50aab08 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -15,6 +15,7 @@ from six import StringIO from six import text_type from .. import mock +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from .testcases import docker_client from .testcases import DockerClientTestCase from .testcases import get_links @@ -37,6 +38,8 @@ from compose.container import Container from compose.errors import OperationFailedError from compose.parallel import ParallelStreamWriter from compose.project import OneOffFilter +from compose.project import Project +from compose.service import BuildAction from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy from compose.service import NetworkMode @@ -67,7 +70,7 @@ class ServiceTest(DockerClientTestCase): create_and_start_container(foo) assert len(foo.containers()) == 1 - assert foo.containers()[0].name == 'composetest_foo_1' + assert foo.containers()[0].name.startswith('composetest_foo_') assert len(bar.containers()) == 0 create_and_start_container(bar) @@ -77,8 +80,8 @@ class ServiceTest(DockerClientTestCase): assert len(bar.containers()) == 2 names = [c.name for c in bar.containers()] - assert 'composetest_bar_1' in names - assert 'composetest_bar_2' in names + assert len(names) == 2 + assert all(name.startswith('composetest_bar_') for name in names) def test_containers_one_off(self): db = self.create_service('db') @@ -89,18 +92,18 @@ class ServiceTest(DockerClientTestCase): def test_project_is_added_to_container_name(self): service = self.create_service('web') create_and_start_container(service) - assert service.containers()[0].name == 'composetest_web_1' + assert service.containers()[0].name.startswith('composetest_web_') def test_create_container_with_one_off(self): db = self.create_service('db') container = db.create_container(one_off=True) - assert container.name == 'composetest_db_run_1' + assert container.name.startswith('composetest_db_run_') def test_create_container_with_one_off_when_existing_container_is_running(self): db = self.create_service('db') db.start() container = db.create_container(one_off=True) - assert container.name == 'composetest_db_run_1' + assert container.name.startswith('composetest_db_run_') def test_create_container_with_unspecified_volume(self): service = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')]) @@ -373,7 +376,7 @@ class ServiceTest(DockerClientTestCase): self.client.create_volume(volume_name) service = Service('db', client=client, volumes=[ MountSpec(type='volume', source=volume_name, target=container_path) - ], image='busybox:latest', command=['top'], project='composetest') + ], image=BUSYBOX_IMAGE_WITH_TAG, command=['top'], project='composetest') container = service.create_container() service.start_container(container) mount = container.get_mount(container_path) @@ -388,7 +391,7 @@ class ServiceTest(DockerClientTestCase): container_path = '/container-tmpfs' service = Service('db', client=client, volumes=[ MountSpec(type='tmpfs', target=container_path) - ], image='busybox:latest', command=['top'], project='composetest') + ], image=BUSYBOX_IMAGE_WITH_TAG, command=['top'], project='composetest') container = service.create_container() service.start_container(container) mount = container.get_mount(container_path) @@ -424,6 +427,22 @@ class ServiceTest(DockerClientTestCase): new_container = service.recreate_container(old_container) assert new_container.get_mount('/data')['Source'] == volume_path + def test_recreate_volume_to_mount(self): + # https://github.com/docker/compose/issues/6280 + service = Service( + project='composetest', + name='db', + client=self.client, + build={'context': 'tests/fixtures/dockerfile-with-volume'}, + volumes=[MountSpec.parse({ + 'type': 'volume', + 'target': '/data', + })] + ) + old_container = create_and_start_container(service) + new_container = service.recreate_container(old_container) + assert new_container.get_mount('/data')['Source'] + def test_duplicate_volume_trailing_slash(self): """ When an image specifies a volume, and the Compose file specifies a host path @@ -458,7 +477,7 @@ class ServiceTest(DockerClientTestCase): volume_container_1 = volume_service.create_container() volume_container_2 = Container.create( self.client, - image='busybox:latest', + image=BUSYBOX_IMAGE_WITH_TAG, command=["top"], labels={LABEL_PROJECT: 'composetest'}, host_config={}, @@ -489,7 +508,7 @@ class ServiceTest(DockerClientTestCase): assert old_container.get('Config.Entrypoint') == ['top'] assert old_container.get('Config.Cmd') == ['-d', '1'] assert 'FOO=1' in old_container.get('Config.Env') - assert old_container.name == 'composetest_db_1' + assert old_container.name.startswith('composetest_db_') service.start_container(old_container) old_container.inspect() # reload volume data volume_path = old_container.get_mount('/etc')['Source'] @@ -503,7 +522,7 @@ class ServiceTest(DockerClientTestCase): assert new_container.get('Config.Entrypoint') == ['top'] assert new_container.get('Config.Cmd') == ['-d', '1'] assert 'FOO=2' in new_container.get('Config.Env') - assert new_container.name == 'composetest_db_1' + assert new_container.name.startswith('composetest_db_') assert new_container.get_mount('/etc')['Source'] == volume_path if not is_cluster(self.client): assert ( @@ -679,8 +698,8 @@ class ServiceTest(DockerClientTestCase): new_container, = service.execute_convergence_plan( ConvergencePlan('recreate', [old_container])) - mock_log.warn.assert_called_once_with(mock.ANY) - _, args, kwargs = mock_log.warn.mock_calls[0] + mock_log.warning.assert_called_once_with(mock.ANY) + _, args, kwargs = mock_log.warning.mock_calls[0] assert "Service \"db\" is using volume \"/data\" from the previous container" in args[0] assert [mount['Destination'] for mount in new_container.get('Mounts')] == ['/data'] @@ -836,13 +855,13 @@ class ServiceTest(DockerClientTestCase): db = self.create_service('db') web = self.create_service('web', links=[(db, None)]) - create_and_start_container(db) - create_and_start_container(db) + db1 = create_and_start_container(db) + db2 = create_and_start_container(db) create_and_start_container(web) assert set(get_links(web.containers()[0])) == set([ - 'composetest_db_1', 'db_1', - 'composetest_db_2', 'db_2', + db1.name, db1.name_without_project, + db2.name, db2.name_without_project, 'db' ]) @@ -851,30 +870,33 @@ class ServiceTest(DockerClientTestCase): db = self.create_service('db') web = self.create_service('web', links=[(db, 'custom_link_name')]) - create_and_start_container(db) - create_and_start_container(db) + db1 = create_and_start_container(db) + db2 = create_and_start_container(db) create_and_start_container(web) assert set(get_links(web.containers()[0])) == set([ - 'composetest_db_1', 'db_1', - 'composetest_db_2', 'db_2', + db1.name, db1.name_without_project, + db2.name, db2.name_without_project, 'custom_link_name' ]) @no_cluster('No legacy links support in Swarm') def test_start_container_with_external_links(self): db = self.create_service('db') - web = self.create_service('web', external_links=['composetest_db_1', - 'composetest_db_2', - 'composetest_db_3:db_3']) + db_ctnrs = [create_and_start_container(db) for _ in range(3)] + web = self.create_service( + 'web', external_links=[ + db_ctnrs[0].name, + db_ctnrs[1].name, + '{}:db_3'.format(db_ctnrs[2].name) + ] + ) - for _ in range(3): - create_and_start_container(db) create_and_start_container(web) assert set(get_links(web.containers()[0])) == set([ - 'composetest_db_1', - 'composetest_db_2', + db_ctnrs[0].name, + db_ctnrs[1].name, 'db_3' ]) @@ -892,14 +914,14 @@ class ServiceTest(DockerClientTestCase): def test_start_one_off_container_creates_links_to_its_own_service(self): db = self.create_service('db') - create_and_start_container(db) - create_and_start_container(db) + db1 = create_and_start_container(db) + db2 = create_and_start_container(db) c = create_and_start_container(db, one_off=OneOffFilter.only) assert set(get_links(c)) == set([ - 'composetest_db_1', 'db_1', - 'composetest_db_2', 'db_2', + db1.name, db1.name_without_project, + db2.name, db2.name_without_project, 'db' ]) @@ -946,6 +968,43 @@ class ServiceTest(DockerClientTestCase): assert self.client.inspect_image('composetest_web') + def test_build_cli(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('web', + build={'context': base_dir}, + environment={ + 'COMPOSE_DOCKER_CLI_BUILD': '1', + 'DOCKER_BUILDKIT': '1', + }) + service.build(cli=True) + self.addCleanup(self.client.remove_image, service.image_name) + assert self.client.inspect_image('composetest_web') + + def test_up_build_cli(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") + + web = self.create_service('web', + build={'context': base_dir}, + environment={ + 'COMPOSE_DOCKER_CLI_BUILD': '1', + 'DOCKER_BUILDKIT': '1', + }) + project = Project('composetest', [web], self.client) + project.up(do_build=BuildAction.force) + + containers = project.containers(['web']) + assert len(containers) == 1 + assert containers[0].name.startswith('composetest_web_') + def test_build_non_ascii_filename(self): base_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base_dir) @@ -1137,6 +1196,21 @@ class ServiceTest(DockerClientTestCase): service.build() assert service.image() + def test_build_with_illegal_leading_chars(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\nRUN echo "Embodiment of Scarlet Devil"\n') + service = Service( + 'build_leading_slug', client=self.client, + project='___-composetest', build={ + 'context': text_type(base_dir) + } + ) + assert service.image_name == 'composetest_build_leading_slug' + service.build() + assert service.image() + def test_start_container_stays_unprivileged(self): service = self.create_service('web') container = create_and_start_container(service).inspect() @@ -1198,9 +1272,8 @@ class ServiceTest(DockerClientTestCase): # }) def test_create_with_image_id(self): - # Get image id for the current busybox:latest pull_busybox(self.client) - image_id = self.client.inspect_image('busybox:latest')['Id'][:12] + image_id = self.client.inspect_image(BUSYBOX_IMAGE_WITH_TAG)['Id'][:12] service = self.create_service('foo', image=image_id) service.create_container() @@ -1234,17 +1307,15 @@ class ServiceTest(DockerClientTestCase): test that those containers are restarted and not removed/recreated. """ service = self.create_service('web') - next_number = service._next_container_number() - valid_numbers = [next_number, next_number + 1] - service.create_container(number=next_number) - service.create_container(number=next_number + 1) + service.create_container(number=1) + service.create_container(number=2) ParallelStreamWriter.instance = None with mock.patch('sys.stderr', new_callable=StringIO) as mock_stderr: service.scale(2) for container in service.containers(): assert container.is_running - assert container.number in valid_numbers + assert container.number in [1, 2] captured_output = mock_stderr.getvalue() assert 'Creating' not in captured_output @@ -1295,10 +1366,8 @@ class ServiceTest(DockerClientTestCase): assert len(service.containers()) == 1 assert service.containers()[0].is_running - assert ( - "ERROR: for composetest_web_2 Cannot create container for service" - " web: Boom" in mock_stderr.getvalue() - ) + assert "ERROR: for composetest_web_" in mock_stderr.getvalue() + assert "Cannot create container for service web: Boom" in mock_stderr.getvalue() def test_scale_with_unexpected_exception(self): """Test that when scaling if the API returns an error, that is not of type @@ -1352,7 +1421,7 @@ class ServiceTest(DockerClientTestCase): with pytest.raises(OperationFailedError): service.scale(3) - captured_output = mock_log.warn.call_args[0][0] + captured_output = mock_log.warning.call_args[0][0] assert len(service.containers()) == 1 assert "Remove the custom name to scale the service." in captured_output @@ -1565,16 +1634,17 @@ class ServiceTest(DockerClientTestCase): } compose_labels = { - LABEL_CONTAINER_NUMBER: '1', LABEL_ONE_OFF: 'False', LABEL_PROJECT: 'composetest', LABEL_SERVICE: 'web', LABEL_VERSION: __version__, + LABEL_CONTAINER_NUMBER: '1' } expected = dict(labels_dict, **compose_labels) service = self.create_service('web', labels=labels_dict) - labels = create_and_start_container(service).labels.items() + ctnr = create_and_start_container(service) + labels = ctnr.labels.items() for pair in expected.items(): assert pair in labels @@ -1640,7 +1710,7 @@ class ServiceTest(DockerClientTestCase): def test_duplicate_containers(self): service = self.create_service('web') - options = service._get_container_create_options({}, 1) + options = service._get_container_create_options({}, service._next_container_number()) original = Container.create(service.client, **options) assert set(service.containers(stopped=True)) == set([original]) diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py index 5992a02a..714945ee 100644 --- a/tests/integration/state_test.py +++ b/tests/integration/state_test.py @@ -5,9 +5,12 @@ by `docker-compose up`. from __future__ import absolute_import from __future__ import unicode_literals +import copy + import py from docker.errors import ImageNotFound +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from .testcases import DockerClientTestCase from .testcases import get_links from .testcases import no_cluster @@ -40,8 +43,8 @@ class BasicProjectTest(ProjectTestCase): super(BasicProjectTest, self).setUp() self.cfg = { - 'db': {'image': 'busybox:latest', 'command': 'top'}, - 'web': {'image': 'busybox:latest', 'command': 'top'}, + 'db': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, + 'web': {'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top'}, } def test_no_change(self): @@ -55,8 +58,8 @@ class BasicProjectTest(ProjectTestCase): def test_partial_change(self): old_containers = self.run_up(self.cfg) - old_db = [c for c in old_containers if c.name_without_project == 'db_1'][0] - old_web = [c for c in old_containers if c.name_without_project == 'web_1'][0] + old_db = [c for c in old_containers if c.name_without_project.startswith('db_')][0] + old_web = [c for c in old_containers if c.name_without_project.startswith('web_')][0] self.cfg['web']['command'] = '/bin/true' @@ -71,7 +74,7 @@ class BasicProjectTest(ProjectTestCase): created = list(new_containers - old_containers) assert len(created) == 1 - assert created[0].name_without_project == 'web_1' + assert created[0].name_without_project == old_web.name_without_project assert created[0].get('Config.Cmd') == ['/bin/true'] def test_all_change(self): @@ -97,16 +100,16 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg = { 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', }, 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', 'links': ['db'], }, 'nginx': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', 'links': ['web'], }, @@ -114,7 +117,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): def test_up(self): containers = self.run_up(self.cfg) - assert set(c.name_without_project for c in containers) == set(['db_1', 'web_1', 'nginx_1']) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) def test_change_leaf(self): old_containers = self.run_up(self.cfg) @@ -122,7 +125,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['nginx']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.name_without_project for c in new_containers - old_containers) == set(['nginx_1']) + assert set(c.service for c in new_containers - old_containers) == set(['nginx']) def test_change_middle(self): old_containers = self.run_up(self.cfg) @@ -130,7 +133,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.name_without_project for c in new_containers - old_containers) == set(['web_1']) + assert set(c.service for c in new_containers - old_containers) == set(['web']) def test_change_middle_always_recreate_deps(self): old_containers = self.run_up(self.cfg, always_recreate_deps=True) @@ -138,8 +141,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['web']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg, always_recreate_deps=True) - assert set(c.name_without_project - for c in new_containers - old_containers) == {'web_1', 'nginx_1'} + assert set(c.service for c in new_containers - old_containers) == {'web', 'nginx'} def test_change_root(self): old_containers = self.run_up(self.cfg) @@ -147,7 +149,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg) - assert set(c.name_without_project for c in new_containers - old_containers) == set(['db_1']) + assert set(c.service for c in new_containers - old_containers) == set(['db']) def test_change_root_always_recreate_deps(self): old_containers = self.run_up(self.cfg, always_recreate_deps=True) @@ -155,8 +157,9 @@ class ProjectWithDependenciesTest(ProjectTestCase): self.cfg['db']['environment'] = {'NEW_VAR': '1'} new_containers = self.run_up(self.cfg, always_recreate_deps=True) - assert set(c.name_without_project - for c in new_containers - old_containers) == {'db_1', 'web_1', 'nginx_1'} + assert set(c.service for c in new_containers - old_containers) == { + 'db', 'web', 'nginx' + } def test_change_root_no_recreate(self): old_containers = self.run_up(self.cfg) @@ -171,7 +174,7 @@ class ProjectWithDependenciesTest(ProjectTestCase): def test_service_removed_while_down(self): next_cfg = { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'tail -f /dev/null', }, 'nginx': self.cfg['nginx'], @@ -195,9 +198,155 @@ class ProjectWithDependenciesTest(ProjectTestCase): web, = [c for c in containers if c.service == 'web'] nginx, = [c for c in containers if c.service == 'nginx'] + db, = [c for c in containers if c.service == 'db'] + + assert set(get_links(web)) == { + 'composetest_db_1', + 'db', + 'db_1', + } + assert set(get_links(nginx)) == { + 'composetest_web_1', + 'web', + 'web_1', + } + + +class ProjectWithDependsOnDependenciesTest(ProjectTestCase): + def setUp(self): + super(ProjectWithDependsOnDependenciesTest, self).setUp() + + self.cfg = { + 'version': '2', + 'services': { + 'db': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'tail -f /dev/null', + }, + 'web': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'tail -f /dev/null', + 'depends_on': ['db'], + }, + 'nginx': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'command': 'tail -f /dev/null', + 'depends_on': ['web'], + }, + } + } + + def test_up(self): + local_cfg = copy.deepcopy(self.cfg) + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + def test_change_leaf(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['nginx']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg) + + assert set(c.service for c in new_containers - old_containers) == set(['nginx']) + + def test_change_middle(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg) + + assert set(c.service for c in new_containers - old_containers) == set(['web']) + + def test_change_middle_always_recreate_deps(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg, always_recreate_deps=True) + + local_cfg['services']['web']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg, always_recreate_deps=True) + + assert set(c.service for c in new_containers - old_containers) == set(['web', 'nginx']) + + def test_change_root(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg) + + assert set(c.service for c in new_containers - old_containers) == set(['db']) + + def test_change_root_always_recreate_deps(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg, always_recreate_deps=True) + + local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up(local_cfg, always_recreate_deps=True) + + assert set(c.service for c in new_containers - old_containers) == set(['db', 'web', 'nginx']) + + def test_change_root_no_recreate(self): + local_cfg = copy.deepcopy(self.cfg) + old_containers = self.run_up(local_cfg) + + local_cfg['services']['db']['environment'] = {'NEW_VAR': '1'} + new_containers = self.run_up( + local_cfg, + strategy=ConvergenceStrategy.never) + + assert new_containers - old_containers == set() + + def test_service_removed_while_down(self): + local_cfg = copy.deepcopy(self.cfg) + next_cfg = copy.deepcopy(self.cfg) + del next_cfg['services']['db'] + del next_cfg['services']['web']['depends_on'] + + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + project = self.make_project(local_cfg) + project.stop(timeout=1) + + next_containers = self.run_up(next_cfg) + assert set(c.service for c in next_containers) == set(['web', 'nginx']) + + def test_service_removed_while_up(self): + local_cfg = copy.deepcopy(self.cfg) + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + del local_cfg['services']['db'] + del local_cfg['services']['web']['depends_on'] + + containers = self.run_up(local_cfg) + assert set(c.service for c in containers) == set(['web', 'nginx']) + + def test_dependency_removed(self): + local_cfg = copy.deepcopy(self.cfg) + next_cfg = copy.deepcopy(self.cfg) + del next_cfg['services']['nginx']['depends_on'] + + containers = self.run_up(local_cfg, service_names=['nginx']) + assert set(c.service for c in containers) == set(['db', 'web', 'nginx']) + + project = self.make_project(local_cfg) + project.stop(timeout=1) + + next_containers = self.run_up(next_cfg, service_names=['nginx']) + assert set(c.service for c in next_containers if c.is_running) == set(['nginx']) + + def test_dependency_added(self): + local_cfg = copy.deepcopy(self.cfg) + + del local_cfg['services']['nginx']['depends_on'] + containers = self.run_up(local_cfg, service_names=['nginx']) + assert set(c.service for c in containers) == set(['nginx']) - assert set(get_links(web)) == {'composetest_db_1', 'db', 'db_1'} - assert set(get_links(nginx)) == {'composetest_web_1', 'web', 'web_1'} + local_cfg['services']['nginx']['depends_on'] = ['db'] + containers = self.run_up(local_cfg, service_names=['nginx']) + assert set(c.service for c in containers) == set(['nginx', 'db']) class ServiceStateTest(DockerClientTestCase): @@ -237,7 +386,7 @@ class ServiceStateTest(DockerClientTestCase): assert ('recreate', [container]) == web.convergence_plan() def test_trigger_recreate_with_nonexistent_image_tag(self): - web = self.create_service('web', image="busybox:latest") + web = self.create_service('web', image=BUSYBOX_IMAGE_WITH_TAG) container = web.create_container() web = self.create_service('web', image="nonexistent-image") diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index 4440d771..fe70d1f7 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -9,6 +9,7 @@ from docker.errors import APIError from docker.utils import version_lt from .. import unittest +from ..helpers import BUSYBOX_IMAGE_WITH_TAG from compose.cli.docker_client import docker_client from compose.config.config import resolve_environment from compose.config.environment import Environment @@ -32,7 +33,7 @@ SWARM_ASSUME_MULTINODE = os.environ.get('SWARM_ASSUME_MULTINODE', '0') != '0' def pull_busybox(client): - client.pull('busybox:latest', stream=False) + client.pull(BUSYBOX_IMAGE_WITH_TAG, stream=False) def get_links(container): @@ -123,7 +124,7 @@ class DockerClientTestCase(unittest.TestCase): def create_service(self, name, **kwargs): if 'image' not in kwargs and 'build' not in kwargs: - kwargs['image'] = 'busybox:latest' + kwargs['image'] = BUSYBOX_IMAGE_WITH_TAG if 'command' not in kwargs: kwargs['command'] = ["top"] @@ -139,7 +140,9 @@ class DockerClientTestCase(unittest.TestCase): def check_build(self, *args, **kwargs): kwargs.setdefault('rm', True) build_output = self.client.build(*args, **kwargs) - stream_output(build_output, open('/dev/null', 'w')) + with open(os.devnull, 'w') as devnull: + for event in stream_output(build_output, devnull): + pass def require_api_version(self, minimum): api_version = self.client.version()['ApiVersion'] diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py index 88f75405..8faebb7f 100644 --- a/tests/unit/bundle_test.py +++ b/tests/unit/bundle_test.py @@ -10,6 +10,7 @@ from compose import service from compose.cli.errors import UserError from compose.config.config import Config from compose.const import COMPOSEFILE_V2_0 as V2_0 +from compose.service import NoSuchImageError @pytest.fixture @@ -35,6 +36,16 @@ def test_get_image_digest_image_uses_digest(mock_service): assert not mock_service.image.called +def test_get_image_digest_from_repository(mock_service): + mock_service.options['image'] = 'abcd' + mock_service.image_name = 'abcd' + mock_service.image.side_effect = NoSuchImageError(None) + mock_service.get_image_registry_data.return_value = {'Descriptor': {'digest': 'digest'}} + + digest = bundle.get_image_digest(mock_service) + assert digest == 'abcd@digest' + + def test_get_image_digest_no_image(mock_service): with pytest.raises(UserError) as exc: bundle.get_image_digest(service.Service(name='theservice')) @@ -83,7 +94,7 @@ def test_to_bundle(): configs={} ) - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: output = bundle.to_bundle(config, image_digests) assert mock_log.mock_calls == [ @@ -117,7 +128,7 @@ def test_convert_service_to_bundle(): 'privileged': True, } - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: config = bundle.convert_service_to_bundle(name, service_dict, image_digest) mock_log.assert_called_once_with( @@ -166,7 +177,7 @@ def test_make_service_networks_default(): name = 'theservice' service_dict = {} - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: networks = bundle.make_service_networks(name, service_dict) assert not mock_log.called @@ -184,7 +195,7 @@ def test_make_service_networks(): }, } - with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + with mock.patch('compose.bundle.log.warning', autospec=True) as mock_log: networks = bundle.make_service_networks(name, service_dict) mock_log.assert_called_once_with( diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index be91ea31..772c136e 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -247,5 +247,5 @@ class TestGetTlsVersion(object): environment = {'COMPOSE_TLS_VERSION': 'TLSv5_5'} with mock.patch('compose.cli.docker_client.log') as mock_log: tls_version = get_tls_version(environment) - mock_log.warn.assert_called_once_with(mock.ANY) + mock_log.warning.assert_called_once_with(mock.ANY) assert tls_version is None diff --git a/tests/unit/cli/log_printer_test.py b/tests/unit/cli/log_printer_test.py index d0c4b56b..5e387241 100644 --- a/tests/unit/cli/log_printer_test.py +++ b/tests/unit/cli/log_printer_test.py @@ -152,6 +152,17 @@ class TestWatchEvents(object): *thread_args) assert container_id in thread_map + def test_container_attach_event(self, thread_map, mock_presenters): + container_id = 'abcd' + mock_container = mock.Mock(is_restarting=False) + mock_container.attach_log_stream.side_effect = APIError("race condition") + event_die = {'action': 'die', 'id': container_id} + event_start = {'action': 'start', 'id': container_id, 'container': mock_container} + event_stream = [event_die, event_start] + thread_args = 'foo', 'bar' + watch_events(thread_map, event_stream, mock_presenters, thread_args) + assert mock_container.attach_log_stream.called + def test_other_event(self, thread_map, mock_presenters): container_id = 'abcd' event_stream = [{'action': 'create', 'id': container_id}] @@ -193,7 +204,7 @@ class TestConsumeQueue(object): queue.put(item) generator = consume_queue(queue, True) - assert next(generator) is 'foobar-1' + assert next(generator) == 'foobar-1' def test_item_is_none_when_timeout_is_hit(self): queue = Queue() diff --git a/tests/unit/cli/main_test.py b/tests/unit/cli/main_test.py index 1a2dfbcf..aadb9d45 100644 --- a/tests/unit/cli/main_test.py +++ b/tests/unit/cli/main_test.py @@ -9,9 +9,11 @@ import pytest from compose import container from compose.cli.errors import UserError from compose.cli.formatter import ConsoleWarningFormatter +from compose.cli.main import build_one_off_container_options from compose.cli.main import call_docker from compose.cli.main import convergence_strategy_from_opts from compose.cli.main import filter_containers_to_service_names +from compose.cli.main import get_docker_start_call from compose.cli.main import setup_console_handler from compose.cli.main import warn_for_swarm_mode from compose.service import ConvergenceStrategy @@ -63,7 +65,65 @@ class TestCLIMainTestCase(object): with mock.patch('compose.cli.main.log') as fake_log: warn_for_swarm_mode(mock_client) - assert fake_log.warn.call_count == 1 + assert fake_log.warning.call_count == 1 + + def test_build_one_off_container_options(self): + command = 'build myservice' + detach = False + options = { + '-e': ['MYVAR=MYVALUE'], + '-T': True, + '--label': ['MYLABEL'], + '--entrypoint': 'bash', + '--user': 'MYUSER', + '--service-ports': [], + '--publish': '', + '--name': 'MYNAME', + '--workdir': '.', + '--volume': [], + 'stdin_open': False, + } + + expected_container_options = { + 'command': command, + 'tty': False, + 'stdin_open': False, + 'detach': detach, + 'entrypoint': 'bash', + 'environment': {'MYVAR': 'MYVALUE'}, + 'labels': {'MYLABEL': ''}, + 'name': 'MYNAME', + 'ports': [], + 'restart': None, + 'user': 'MYUSER', + 'working_dir': '.', + } + + container_options = build_one_off_container_options(options, detach, command) + assert container_options == expected_container_options + + def test_get_docker_start_call(self): + container_id = 'my_container_id' + + mock_container_options = {'detach': False, 'stdin_open': True} + expected_docker_start_call = ['start', '--attach', '--interactive', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call + + mock_container_options = {'detach': False, 'stdin_open': False} + expected_docker_start_call = ['start', '--attach', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call + + mock_container_options = {'detach': True, 'stdin_open': True} + expected_docker_start_call = ['start', '--interactive', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call + + mock_container_options = {'detach': True, 'stdin_open': False} + expected_docker_start_call = ['start', container_id] + docker_start_call = get_docker_start_call(mock_container_options, container_id) + assert expected_docker_start_call == docker_start_call class TestSetupConsoleHandlerTestCase(object): @@ -123,13 +183,13 @@ def mock_find_executable(exe): class TestCallDocker(object): def test_simple_no_options(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {}) + call_docker(['ps'], {}, {}) assert fake_call.call_args[0][0] == ['docker', 'ps'] def test_simple_tls_option(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {'--tls': True}) + call_docker(['ps'], {'--tls': True}, {}) assert fake_call.call_args[0][0] == ['docker', '--tls', 'ps'] @@ -140,7 +200,7 @@ class TestCallDocker(object): '--tlscacert': './ca.pem', '--tlscert': './cert.pem', '--tlskey': './key.pem', - }) + }, {}) assert fake_call.call_args[0][0] == [ 'docker', '--tls', '--tlscacert', './ca.pem', '--tlscert', @@ -149,16 +209,33 @@ class TestCallDocker(object): def test_with_host_option(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {'--host': 'tcp://mydocker.net:2333'}) + call_docker(['ps'], {'--host': 'tcp://mydocker.net:2333'}, {}) assert fake_call.call_args[0][0] == [ 'docker', '--host', 'tcp://mydocker.net:2333', 'ps' ] + def test_with_http_host(self): + with mock.patch('subprocess.call') as fake_call: + call_docker(['ps'], {'--host': 'http://mydocker.net:2333'}, {}) + + assert fake_call.call_args[0][0] == [ + 'docker', '--host', 'tcp://mydocker.net:2333', 'ps', + ] + def test_with_host_option_shorthand_equal(self): with mock.patch('subprocess.call') as fake_call: - call_docker(['ps'], {'--host': '=tcp://mydocker.net:2333'}) + call_docker(['ps'], {'--host': '=tcp://mydocker.net:2333'}, {}) assert fake_call.call_args[0][0] == [ 'docker', '--host', 'tcp://mydocker.net:2333', 'ps' ] + + def test_with_env(self): + with mock.patch('subprocess.call') as fake_call: + call_docker(['ps'], {}, {'DOCKER_HOST': 'tcp://mydocker.net:2333'}) + + assert fake_call.call_args[0][0] == [ + 'docker', 'ps' + ] + assert fake_call.call_args[1]['env'] == {'DOCKER_HOST': 'tcp://mydocker.net:2333'} diff --git a/tests/unit/cli/utils_test.py b/tests/unit/cli/utils_test.py index 26524ff3..7a762890 100644 --- a/tests/unit/cli/utils_test.py +++ b/tests/unit/cli/utils_test.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import unittest +from compose.cli.utils import human_readable_file_size from compose.utils import unquote_path @@ -21,3 +22,27 @@ class UnquotePathTest(unittest.TestCase): assert unquote_path('""hello""') == '"hello"' assert unquote_path('"hel"lo"') == 'hel"lo' assert unquote_path('"hello""') == 'hello"' + + +class HumanReadableFileSizeTest(unittest.TestCase): + def test_100b(self): + assert human_readable_file_size(100) == '100 B' + + def test_1kb(self): + assert human_readable_file_size(1000) == '1 kB' + assert human_readable_file_size(1024) == '1.024 kB' + + def test_1023b(self): + assert human_readable_file_size(1023) == '1.023 kB' + + def test_999b(self): + assert human_readable_file_size(999) == '999 B' + + def test_units(self): + assert human_readable_file_size((10 ** 3) ** 0) == '1 B' + assert human_readable_file_size((10 ** 3) ** 1) == '1 kB' + assert human_readable_file_size((10 ** 3) ** 2) == '1 MB' + assert human_readable_file_size((10 ** 3) ** 3) == '1 GB' + assert human_readable_file_size((10 ** 3) ** 4) == '1 TB' + assert human_readable_file_size((10 ** 3) ** 5) == '1 PB' + assert human_readable_file_size((10 ** 3) ** 6) == '1 EB' diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 7c8a1423..a7522f93 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -171,7 +171,10 @@ class CLITestCase(unittest.TestCase): '--workdir': None, }) - assert mock_client.create_host_config.call_args[1]['restart_policy']['Name'] == 'always' + # NOTE: The "run" command is supposed to be a one-off tool; therefore restart policy "no" + # (the default) is enforced despite explicit wish for "always" in the project + # configuration file + assert not mock_client.create_host_config.call_args[1].get('restart_policy') command = TopLevelCommand(project) command.run({ diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 8a75648a..0d3f49b9 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -8,14 +8,17 @@ import os import shutil import tempfile from operator import itemgetter +from random import shuffle import py import pytest import yaml from ...helpers import build_config_details +from ...helpers import BUSYBOX_IMAGE_WITH_TAG from compose.config import config from compose.config import types +from compose.config.config import ConfigFile from compose.config.config import resolve_build_args from compose.config.config import resolve_environment from compose.config.environment import Environment @@ -42,7 +45,7 @@ from tests import unittest DEFAULT_VERSION = V2_0 -def make_service_dict(name, service_dict, working_dir, filename=None): +def make_service_dict(name, service_dict, working_dir='.', filename=None): """Test helper function to construct a ServiceExtendsResolver """ resolver = config.ServiceExtendsResolver( @@ -328,7 +331,7 @@ class ConfigTest(unittest.TestCase): ) assert 'Unexpected type for "version" key in "filename.yml"' \ - in mock_logging.warn.call_args[0][0] + in mock_logging.warning.call_args[0][0] service_dicts = config_data.services assert service_sort(service_dicts) == service_sort([ @@ -342,7 +345,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError): config.load( build_config_details( - {'web': 'busybox:latest'}, + {'web': BUSYBOX_IMAGE_WITH_TAG}, 'working_dir', 'filename.yml' ) @@ -352,7 +355,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError): config.load( build_config_details( - {'version': '2', 'services': {'web': 'busybox:latest'}}, + {'version': '2', 'services': {'web': BUSYBOX_IMAGE_WITH_TAG}}, 'working_dir', 'filename.yml' ) @@ -363,7 +366,7 @@ class ConfigTest(unittest.TestCase): config.load( build_config_details({ 'version': '2', - 'services': {'web': 'busybox:latest'}, + 'services': {'web': BUSYBOX_IMAGE_WITH_TAG}, 'networks': { 'invalid': {'foo', 'bar'} } @@ -612,6 +615,38 @@ class ConfigTest(unittest.TestCase): excinfo.exconly() ) + def test_config_integer_service_name_raise_validation_error_v2_when_no_interpolate(self): + with pytest.raises(ConfigurationError) as excinfo: + config.load( + build_config_details( + { + 'version': '2', + 'services': {1: {'image': 'busybox'}} + }, + 'working_dir', + 'filename.yml' + ), + interpolate=False + ) + + assert ( + "In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'." in + excinfo.exconly() + ) + + def test_config_integer_service_property_raise_validation_error(self): + with pytest.raises(ConfigurationError) as excinfo: + config.load( + build_config_details({ + 'version': '2.1', + 'services': {'foobar': {'image': 'busybox', 1234: 'hah'}} + }, 'working_dir', 'filename.yml') + ) + + assert ( + "Unsupported config option for services.foobar: '1234'" in excinfo.exconly() + ) + def test_config_invalid_service_name_raise_validation_error(self): with pytest.raises(ConfigurationError) as excinfo: config.load( @@ -814,15 +849,15 @@ class ConfigTest(unittest.TestCase): def test_load_sorts_in_dependency_order(self): config_details = build_config_details({ 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'links': ['db'], }, 'db': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': ['volume:ro'] }, 'volume': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['/tmp'], } }) @@ -1071,8 +1106,43 @@ class ConfigTest(unittest.TestCase): details = config.ConfigDetails('.', [base_file, override_file]) web_service = config.load(details).services[0] assert web_service['networks'] == { - 'foobar': {'aliases': ['foo', 'bar']}, - 'baz': None + 'foobar': {'aliases': ['bar', 'foo']}, + 'baz': {} + } + + def test_load_with_multiple_files_mismatched_networks_format_inverse_order(self): + base_file = config.ConfigFile( + 'override.yaml', + { + 'version': '2', + 'services': { + 'web': { + 'networks': ['baz'] + } + } + } + ) + override_file = config.ConfigFile( + 'base.yaml', + { + 'version': '2', + 'services': { + 'web': { + 'image': 'example/web', + 'networks': { + 'foobar': {'aliases': ['foo', 'bar']} + } + } + }, + 'networks': {'foobar': {}, 'baz': {}} + } + ) + + details = config.ConfigDetails('.', [base_file, override_file]) + web_service = config.load(details).services[0] + assert web_service['networks'] == { + 'foobar': {'aliases': ['bar', 'foo']}, + 'baz': {} } def test_load_with_multiple_files_v2(self): @@ -1212,7 +1282,7 @@ class ConfigTest(unittest.TestCase): 'version': '2', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['data0028:/data:ro'], }, }, @@ -1228,7 +1298,7 @@ class ConfigTest(unittest.TestCase): 'version': '2', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['./data0028:/data:ro'], }, }, @@ -1244,7 +1314,7 @@ class ConfigTest(unittest.TestCase): 'base.yaml', { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': ['data0028:/data:ro'], }, } @@ -1261,7 +1331,7 @@ class ConfigTest(unittest.TestCase): 'version': '2.3', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': [ { 'target': '/anonymous', 'type': 'volume' @@ -1291,7 +1361,7 @@ class ConfigTest(unittest.TestCase): assert tmpfs_mount.target == '/tmpfs' assert not tmpfs_mount.is_named_volume - assert host_mount.source == os.path.normpath('/abc') + assert host_mount.source == '/abc' assert host_mount.target == '/xyz' assert not host_mount.is_named_volume @@ -1306,7 +1376,7 @@ class ConfigTest(unittest.TestCase): 'version': '3.4', 'services': { 'web': { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes': [ {'type': 'bind', 'source': './web', 'target': '/web'}, ], @@ -1322,6 +1392,86 @@ class ConfigTest(unittest.TestCase): assert mount.type == 'bind' assert mount.source == expected_source + def test_load_bind_mount_relative_path_with_tilde(self): + base_file = config.ConfigFile( + 'base.yaml', { + 'version': '3.4', + 'services': { + 'web': { + 'image': BUSYBOX_IMAGE_WITH_TAG, + 'volumes': [ + {'type': 'bind', 'source': '~/web', 'target': '/web'}, + ], + }, + }, + }, + ) + + details = config.ConfigDetails('.', [base_file]) + config_data = config.load(details) + mount = config_data.services[0].get('volumes')[0] + assert mount.target == '/web' + assert mount.type == 'bind' + assert ( + not mount.source.startswith('~') and mount.source.endswith( + '{}web'.format(os.path.sep) + ) + ) + + def test_config_invalid_ipam_config(self): + with pytest.raises(ConfigurationError) as excinfo: + config.load( + build_config_details( + { + 'version': str(V2_1), + 'networks': { + 'foo': { + 'driver': 'default', + 'ipam': { + 'driver': 'default', + 'config': ['172.18.0.0/16'], + } + } + } + }, + filename='filename.yml', + ) + ) + assert ('networks.foo.ipam.config contains an invalid type,' + ' it should be an object') in excinfo.exconly() + + def test_config_valid_ipam_config(self): + ipam_config = { + 'subnet': '172.28.0.0/16', + 'ip_range': '172.28.5.0/24', + 'gateway': '172.28.5.254', + 'aux_addresses': { + 'host1': '172.28.1.5', + 'host2': '172.28.1.6', + 'host3': '172.28.1.7', + }, + } + networks = config.load( + build_config_details( + { + 'version': str(V2_1), + 'networks': { + 'foo': { + 'driver': 'default', + 'ipam': { + 'driver': 'default', + 'config': [ipam_config], + } + } + } + }, + filename='filename.yml', + ) + ).networks + + assert 'foo' in networks + assert networks['foo']['ipam']['config'] == [ipam_config] + def test_config_valid_service_names(self): for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']: services = config.load( @@ -2145,7 +2295,7 @@ class ConfigTest(unittest.TestCase): def test_merge_mixed_ports(self): base = { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'ports': [ { @@ -2162,7 +2312,7 @@ class ConfigTest(unittest.TestCase): actual = config.merge_service_dicts(base, override, V3_1) assert actual == { - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'command': 'top', 'ports': [types.ServicePort('1245', '1245', 'udp', None, None)] } @@ -2589,6 +2739,45 @@ class ConfigTest(unittest.TestCase): ['c 7:128 rwm', 'x 3:244 rw', 'f 0:128 n'] ) + def test_merge_isolation(self): + base = { + 'image': 'bar', + 'isolation': 'default', + } + + override = { + 'isolation': 'hyperv', + } + + actual = config.merge_service_dicts(base, override, V2_3) + assert actual == { + 'image': 'bar', + 'isolation': 'hyperv', + } + + def test_merge_storage_opt(self): + base = { + 'image': 'bar', + 'storage_opt': { + 'size': '1G', + 'readonly': 'false', + } + } + + override = { + 'storage_opt': { + 'size': '2G', + 'encryption': 'aes', + } + } + + actual = config.merge_service_dicts(base, override, V2_3) + assert actual['storage_opt'] == { + 'size': '2G', + 'readonly': 'false', + 'encryption': 'aes', + } + def test_external_volume_config(self): config_details = build_config_details({ 'version': '2', @@ -2938,6 +3127,41 @@ class ConfigTest(unittest.TestCase): ) config.load(config_details) + def test_config_duplicate_mount_points(self): + config1 = build_config_details( + { + 'version': '3.5', + 'services': { + 'web': { + 'image': 'busybox', + 'volumes': ['/tmp/foo:/tmp/foo', '/tmp/foo:/tmp/foo:rw'] + } + } + } + ) + + config2 = build_config_details( + { + 'version': '3.5', + 'services': { + 'web': { + 'image': 'busybox', + 'volumes': ['/x:/y', '/z:/y'] + } + } + } + ) + + with self.assertRaises(ConfigurationError) as e: + config.load(config1) + self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % ( + ', '.join(['/tmp/foo:/tmp/foo:rw']*2))) + + with self.assertRaises(ConfigurationError) as e: + config.load(config2) + self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % ( + ', '.join(['/x:/y:rw', '/z:/y:rw']))) + class NetworkModeTest(unittest.TestCase): @@ -3263,6 +3487,25 @@ class InterpolationTest(unittest.TestCase): } @mock.patch.dict(os.environ) + def test_config_file_with_options_environment_file(self): + project_dir = 'tests/fixtures/default-env-file' + service_dicts = config.load( + config.find( + project_dir, None, Environment.from_env_file(project_dir, '.env2') + ) + ).services + + assert service_dicts[0] == { + 'name': 'web', + 'image': 'alpine:latest', + 'ports': [ + types.ServicePort.parse('5644')[0], + types.ServicePort.parse('9998')[0] + ], + 'command': 'false' + } + + @mock.patch.dict(os.environ) def test_config_file_with_environment_variable(self): project_dir = 'tests/fixtures/environment-interpolation' os.environ.update( @@ -3329,8 +3572,8 @@ class InterpolationTest(unittest.TestCase): with mock.patch('compose.config.environment.log') as log: config.load(config_details) - assert 2 == log.warn.call_count - warnings = sorted(args[0][0] for args in log.warn.call_args_list) + assert 2 == log.warning.call_count + warnings = sorted(args[0][0] for args in log.warning.call_args_list) assert 'BAR' in warnings[0] assert 'FOO' in warnings[1] @@ -3360,8 +3603,8 @@ class InterpolationTest(unittest.TestCase): with mock.patch('compose.config.config.log') as log: config.load(config_details, compatibility=True) - assert log.warn.call_count == 1 - warn_message = log.warn.call_args[0][0] + assert log.warning.call_count == 1 + warn_message = log.warning.call_args[0][0] assert warn_message.startswith( 'The following deploy sub-keys are not supported in compatibility mode' ) @@ -3378,7 +3621,7 @@ class InterpolationTest(unittest.TestCase): 'version': '3.5', 'services': { 'foo': { - 'image': 'alpine:3.7', + 'image': 'alpine:3.10.1', 'deploy': { 'replicas': 3, 'restart_policy': { @@ -3390,6 +3633,9 @@ class InterpolationTest(unittest.TestCase): 'reservations': {'memory': '100M'}, }, }, + 'credential_spec': { + 'file': 'spec.json' + }, }, }, }) @@ -3397,17 +3643,18 @@ class InterpolationTest(unittest.TestCase): with mock.patch('compose.config.config.log') as log: cfg = config.load(config_details, compatibility=True) - assert log.warn.call_count == 0 + assert log.warning.call_count == 0 service_dict = cfg.services[0] assert service_dict == { - 'image': 'alpine:3.7', + 'image': 'alpine:3.10.1', 'scale': 3, 'restart': {'MaximumRetryCount': 7, 'Name': 'always'}, 'mem_limit': '300M', 'mem_reservation': '100M', 'cpus': 0.7, - 'name': 'foo' + 'name': 'foo', + 'security_opt': ['credentialspec=file://spec.json'], } @mock.patch.dict(os.environ) @@ -3483,6 +3730,13 @@ class VolumeConfigTest(unittest.TestCase): assert d['volumes'] == [VolumeSpec.parse('/host/path:/container/path')] @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths') + def test_volumes_order_is_preserved(self): + volumes = ['/{0}:/{0}'.format(i) for i in range(0, 6)] + shuffle(volumes) + cfg = make_service_dict('foo', {'build': '.', 'volumes': volumes}) + assert cfg['volumes'] == volumes + + @pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths') @mock.patch.dict(os.environ) def test_volume_binding_with_home(self): os.environ['HOME'] = '/home/user' @@ -3569,35 +3823,35 @@ class MergePathMappingTest(object): {self.config_name: ['/foo:/code', '/data']}, {}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/foo:/code', '/data']) + assert set(service_dict[self.config_name]) == {'/foo:/code', '/data'} def test_no_base(self): service_dict = config.merge_service_dicts( {}, {self.config_name: ['/bar:/code']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code']) + assert set(service_dict[self.config_name]) == {'/bar:/code'} def test_override_explicit_path(self): service_dict = config.merge_service_dicts( {self.config_name: ['/foo:/code', '/data']}, {self.config_name: ['/bar:/code']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data']) + assert set(service_dict[self.config_name]) == {'/bar:/code', '/data'} def test_add_explicit_path(self): service_dict = config.merge_service_dicts( {self.config_name: ['/foo:/code', '/data']}, {self.config_name: ['/bar:/code', '/quux:/data']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code', '/quux:/data']) + assert set(service_dict[self.config_name]) == {'/bar:/code', '/quux:/data'} def test_remove_explicit_path(self): service_dict = config.merge_service_dicts( {self.config_name: ['/foo:/code', '/quux:/data']}, {self.config_name: ['/bar:/code', '/data']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data']) + assert set(service_dict[self.config_name]) == {'/bar:/code', '/data'} class MergeVolumesTest(unittest.TestCase, MergePathMappingTest): @@ -3703,8 +3957,95 @@ class MergePortsTest(unittest.TestCase, MergeListsTest): class MergeNetworksTest(unittest.TestCase, MergeListsTest): config_name = 'networks' - base_config = ['frontend', 'backend'] - override_config = ['monitoring'] + base_config = {'default': {'aliases': ['foo.bar', 'foo.baz']}} + override_config = {'default': {'ipv4_address': '123.234.123.234'}} + + def test_no_network_overrides(self): + service_dict = config.merge_service_dicts( + {self.config_name: self.base_config}, + {self.config_name: self.override_config}, + DEFAULT_VERSION) + assert service_dict[self.config_name] == { + 'default': { + 'aliases': ['foo.bar', 'foo.baz'], + 'ipv4_address': '123.234.123.234' + } + } + + def test_network_has_none_value(self): + service_dict = config.merge_service_dicts( + {self.config_name: { + 'default': None + }}, + {self.config_name: { + 'default': { + 'aliases': [] + } + }}, + DEFAULT_VERSION) + + assert service_dict[self.config_name] == { + 'default': { + 'aliases': [] + } + } + + def test_all_properties(self): + service_dict = config.merge_service_dicts( + {self.config_name: { + 'default': { + 'aliases': ['foo.bar', 'foo.baz'], + 'link_local_ips': ['192.168.1.10', '192.168.1.11'], + 'ipv4_address': '111.111.111.111', + 'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-first' + } + }}, + {self.config_name: { + 'default': { + 'aliases': ['foo.baz', 'foo.baz2'], + 'link_local_ips': ['192.168.1.11', '192.168.1.12'], + 'ipv4_address': '123.234.123.234', + 'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-second' + } + }}, + DEFAULT_VERSION) + + assert service_dict[self.config_name] == { + 'default': { + 'aliases': ['foo.bar', 'foo.baz', 'foo.baz2'], + 'link_local_ips': ['192.168.1.10', '192.168.1.11', '192.168.1.12'], + 'ipv4_address': '123.234.123.234', + 'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-second' + } + } + + def test_no_network_name_overrides(self): + service_dict = config.merge_service_dicts( + { + self.config_name: { + 'default': { + 'aliases': ['foo.bar', 'foo.baz'], + 'ipv4_address': '123.234.123.234' + } + } + }, + { + self.config_name: { + 'another_network': { + 'ipv4_address': '123.234.123.234' + } + } + }, + DEFAULT_VERSION) + assert service_dict[self.config_name] == { + 'default': { + 'aliases': ['foo.bar', 'foo.baz'], + 'ipv4_address': '123.234.123.234' + }, + 'another_network': { + 'ipv4_address': '123.234.123.234' + } + } class MergeStringsOrListsTest(unittest.TestCase): @@ -3714,28 +4055,28 @@ class MergeStringsOrListsTest(unittest.TestCase): {'dns': '8.8.8.8'}, {}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8']) + assert set(service_dict['dns']) == {'8.8.8.8'} def test_no_base(self): service_dict = config.merge_service_dicts( {}, {'dns': '8.8.8.8'}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8']) + assert set(service_dict['dns']) == {'8.8.8.8'} def test_add_string(self): service_dict = config.merge_service_dicts( {'dns': ['8.8.8.8']}, {'dns': '9.9.9.9'}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9']) + assert set(service_dict['dns']) == {'8.8.8.8', '9.9.9.9'} def test_add_list(self): service_dict = config.merge_service_dicts( {'dns': '8.8.8.8'}, {'dns': ['9.9.9.9']}, DEFAULT_VERSION) - assert set(service_dict['dns']) == set(['8.8.8.8', '9.9.9.9']) + assert set(service_dict['dns']) == {'8.8.8.8', '9.9.9.9'} class MergeLabelsTest(unittest.TestCase): @@ -3807,7 +4148,7 @@ class MergeBuildTest(unittest.TestCase): assert result['context'] == override['context'] assert result['dockerfile'] == override['dockerfile'] assert result['args'] == {'x': '12', 'y': '2'} - assert set(result['cache_from']) == set(['ubuntu', 'debian']) + assert set(result['cache_from']) == {'ubuntu', 'debian'} assert result['labels'] == override['labels'] def test_empty_override(self): @@ -4011,7 +4352,7 @@ class EnvTest(unittest.TestCase): "tests/fixtures/env", ) ).services[0] - assert set(service_dict['volumes']) == set([VolumeSpec.parse('/tmp:/host/tmp')]) + assert set(service_dict['volumes']) == {VolumeSpec.parse('/tmp:/host/tmp')} service_dict = config.load( build_config_details( @@ -4019,7 +4360,7 @@ class EnvTest(unittest.TestCase): "tests/fixtures/env", ) ).services[0] - assert set(service_dict['volumes']) == set([VolumeSpec.parse('/opt/tmp:/opt/host/tmp')]) + assert set(service_dict['volumes']) == {VolumeSpec.parse('/opt/tmp:/opt/host/tmp')} def load_from_filename(filename, override_dir=None): @@ -4547,6 +4888,11 @@ class ExtendsTest(unittest.TestCase): assert types.SecurityOpt.parse('apparmor:unconfined') in svc['security_opt'] assert types.SecurityOpt.parse('seccomp:unconfined') in svc['security_opt'] + @mock.patch.object(ConfigFile, 'from_filename', wraps=ConfigFile.from_filename) + def test_extends_same_file_optimization(self, from_filename_mock): + load_from_filename('tests/fixtures/extends/no-file-specified.yml') + from_filename_mock.assert_called_once() + @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') class ExpandPathTest(unittest.TestCase): @@ -5026,6 +5372,28 @@ class SerializeTest(unittest.TestCase): assert serialized_service['command'] == 'echo $$FOO' assert serialized_service['entrypoint'][0] == '$$SHELL' + def test_serialize_escape_dont_interpolate(self): + cfg = { + 'version': '2.2', + 'services': { + 'web': { + 'image': 'busybox', + 'command': 'echo $FOO', + 'environment': { + 'CURRENCY': '$' + }, + 'entrypoint': ['$SHELL', '-c'], + } + } + } + config_dict = config.load(build_config_details(cfg), interpolate=False) + + serialized_config = yaml.load(serialize_config(config_dict, escape_dollar=False)) + serialized_service = serialized_config['services']['web'] + assert serialized_service['environment']['CURRENCY'] == '$' + assert serialized_service['command'] == 'echo $FOO' + assert serialized_service['entrypoint'][0] == '$SHELL' + def test_serialize_unicode_values(self): cfg = { 'version': '2.3', @@ -5042,3 +5410,19 @@ class SerializeTest(unittest.TestCase): serialized_config = yaml.load(serialize_config(config_dict)) serialized_service = serialized_config['services']['web'] assert serialized_service['command'] == 'echo 十六夜 咲夜' + + def test_serialize_external_false(self): + cfg = { + 'version': '3.4', + 'volumes': { + 'test': { + 'name': 'test-false', + 'external': False + } + } + } + + config_dict = config.load(build_config_details(cfg)) + serialized_config = yaml.load(serialize_config(config_dict)) + serialized_volume = serialized_config['volumes']['test'] + assert serialized_volume['external'] is False diff --git a/tests/unit/config/environment_test.py b/tests/unit/config/environment_test.py index 854aee5a..88eb0d6e 100644 --- a/tests/unit/config/environment_test.py +++ b/tests/unit/config/environment_test.py @@ -9,6 +9,7 @@ import pytest from compose.config.environment import env_vars_from_file from compose.config.environment import Environment +from compose.config.errors import ConfigurationError from tests import unittest @@ -52,3 +53,12 @@ class EnvironmentTest(unittest.TestCase): assert env_vars_from_file(str(tmpdir.join('bom.env'))) == { 'PARK_BOM': '박봄' } + + def test_env_vars_from_file_whitespace(self): + tmpdir = pytest.ensuretemp('env_file') + self.addCleanup(tmpdir.remove) + with codecs.open('{}/whitespace.env'.format(str(tmpdir)), 'w', encoding='utf-8') as f: + f.write('WHITESPACE =yes\n') + with pytest.raises(ConfigurationError) as exc: + env_vars_from_file(str(tmpdir.join('whitespace.env'))) + assert 'environment variable' in exc.exconly() diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index 0d0e7d28..91fc3e69 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -332,6 +332,37 @@ def test_interpolate_environment_external_resource_convert_types(mock_env): assert value == expected +def test_interpolate_service_name_uses_dot(mock_env): + entry = { + 'service.1': { + 'image': 'busybox', + 'ulimits': { + 'nproc': '${POSINT}', + 'nofile': { + 'soft': '${POSINT}', + 'hard': '${DEFAULT:-40000}' + }, + }, + } + } + + expected = { + 'service.1': { + 'image': 'busybox', + 'ulimits': { + 'nproc': 50, + 'nofile': { + 'soft': 50, + 'hard': 40000 + }, + }, + } + } + + value = interpolate_environment_variables(V3_4, entry, 'service', mock_env) + assert value == expected + + def test_escaped_interpolation(defaults_interpolator): assert defaults_interpolator('$${foo}') == '${foo}' diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index d64263c1..626b466d 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -5,6 +5,9 @@ import docker from .. import mock from .. import unittest +from ..helpers import BUSYBOX_IMAGE_WITH_TAG +from compose.const import LABEL_ONE_OFF +from compose.const import LABEL_SLUG from compose.container import Container from compose.container import get_container_name @@ -15,7 +18,7 @@ class ContainerTest(unittest.TestCase): self.container_id = "abcabcabcbabc12345" self.container_dict = { "Id": self.container_id, - "Image": "busybox:latest", + "Image": BUSYBOX_IMAGE_WITH_TAG, "Command": "top", "Created": 1387384730, "Status": "Up 8 seconds", @@ -30,7 +33,7 @@ class ContainerTest(unittest.TestCase): "Labels": { "com.docker.compose.project": "composetest", "com.docker.compose.service": "web", - "com.docker.compose.container-number": 7, + "com.docker.compose.container-number": "7", }, } } @@ -41,7 +44,7 @@ class ContainerTest(unittest.TestCase): has_been_inspected=True) assert container.dictionary == { "Id": self.container_id, - "Image": "busybox:latest", + "Image": BUSYBOX_IMAGE_WITH_TAG, "Name": "/composetest_db_1", } @@ -56,7 +59,7 @@ class ContainerTest(unittest.TestCase): has_been_inspected=True) assert container.dictionary == { "Id": self.container_id, - "Image": "busybox:latest", + "Image": BUSYBOX_IMAGE_WITH_TAG, "Name": "/composetest_db_1", } @@ -95,6 +98,15 @@ class ContainerTest(unittest.TestCase): container = Container(None, self.container_dict, has_been_inspected=True) assert container.name_without_project == "custom_name_of_container" + def test_name_without_project_one_off(self): + self.container_dict['Name'] = "/composetest_web_092cd63296f" + self.container_dict['Config']['Labels'][LABEL_SLUG] = ( + "092cd63296fdc446ad432d3905dd1fcbe12a2ba6b52" + ) + self.container_dict['Config']['Labels'][LABEL_ONE_OFF] = 'True' + container = Container(None, self.container_dict, has_been_inspected=True) + assert container.name_without_project == 'web_092cd63296fd' + def test_inspect_if_not_inspected(self): mock_client = mock.create_autospec(docker.APIClient) container = Container(mock_client, dict(Id="the_id")) diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py index b27339af..b829de19 100644 --- a/tests/unit/network_test.py +++ b/tests/unit/network_test.py @@ -23,7 +23,10 @@ class NetworkTest(unittest.TestCase): 'aux_addresses': ['11.0.0.1', '24.25.26.27'], 'ip_range': '156.0.0.1-254' } - ] + ], + 'options': { + 'iface': 'eth0', + } } labels = { 'com.project.tests.istest': 'true', @@ -57,6 +60,9 @@ class NetworkTest(unittest.TestCase): 'Subnet': '172.0.0.1/16', 'Gateway': '172.0.0.1' }], + 'Options': { + 'iface': 'eth0', + }, }, 'Labels': remote_labels }, @@ -78,6 +84,7 @@ class NetworkTest(unittest.TestCase): {'Driver': 'overlay', 'Options': remote_options}, net ) + @mock.patch('compose.network.Network.true_name', lambda n: n.full_name) def test_check_remote_network_config_driver_mismatch(self): net = Network(None, 'compose_test', 'net1', 'overlay') with pytest.raises(NetworkConfigChangedError) as e: @@ -87,6 +94,7 @@ class NetworkTest(unittest.TestCase): assert 'driver has changed' in str(e.value) + @mock.patch('compose.network.Network.true_name', lambda n: n.full_name) def test_check_remote_network_config_options_mismatch(self): net = Network(None, 'compose_test', 'net1', 'overlay') with pytest.raises(NetworkConfigChangedError) as e: @@ -140,6 +148,7 @@ class NetworkTest(unittest.TestCase): net ) + @mock.patch('compose.network.Network.true_name', lambda n: n.full_name) def test_check_remote_network_labels_mismatch(self): net = Network(None, 'compose_test', 'net1', 'overlay', labels={ 'com.project.touhou.character': 'sakuya.izayoi' @@ -156,6 +165,11 @@ class NetworkTest(unittest.TestCase): with mock.patch('compose.network.log') as mock_log: check_remote_network_config(remote, net) - mock_log.warn.assert_called_once_with(mock.ANY) - _, args, kwargs = mock_log.warn.mock_calls[0] + mock_log.warning.assert_called_once_with(mock.ANY) + _, args, kwargs = mock_log.warning.mock_calls[0] assert 'label "com.project.touhou.character" has changed' in args[0] + + def test_remote_config_labels_none(self): + remote = {'Labels': None} + local = Network(None, 'test_project', 'test_network') + check_remote_network_config(remote, local) diff --git a/tests/unit/progress_stream_test.py b/tests/unit/progress_stream_test.py index f4a0ab06..6fdb7d92 100644 --- a/tests/unit/progress_stream_test.py +++ b/tests/unit/progress_stream_test.py @@ -21,7 +21,7 @@ class ProgressStreamTestCase(unittest.TestCase): b'31019763, "start": 1413653874, "total": 62763875}, ' b'"progress": "..."}', ] - events = progress_stream.stream_output(output, StringIO()) + events = list(progress_stream.stream_output(output, StringIO())) assert len(events) == 1 def test_stream_output_div_zero(self): @@ -30,7 +30,7 @@ class ProgressStreamTestCase(unittest.TestCase): b'0, "start": 1413653874, "total": 0}, ' b'"progress": "..."}', ] - events = progress_stream.stream_output(output, StringIO()) + events = list(progress_stream.stream_output(output, StringIO())) assert len(events) == 1 def test_stream_output_null_total(self): @@ -39,7 +39,7 @@ class ProgressStreamTestCase(unittest.TestCase): b'0, "start": 1413653874, "total": null}, ' b'"progress": "..."}', ] - events = progress_stream.stream_output(output, StringIO()) + events = list(progress_stream.stream_output(output, StringIO())) assert len(events) == 1 def test_stream_output_progress_event_tty(self): @@ -52,7 +52,7 @@ class ProgressStreamTestCase(unittest.TestCase): return True output = TTYStringIO() - events = progress_stream.stream_output(events, output) + events = list(progress_stream.stream_output(events, output)) assert len(output.getvalue()) > 0 def test_stream_output_progress_event_no_tty(self): @@ -61,7 +61,7 @@ class ProgressStreamTestCase(unittest.TestCase): ] output = StringIO() - events = progress_stream.stream_output(events, output) + events = list(progress_stream.stream_output(events, output)) assert len(output.getvalue()) == 0 def test_stream_output_no_progress_event_no_tty(self): @@ -70,7 +70,7 @@ class ProgressStreamTestCase(unittest.TestCase): ] output = StringIO() - events = progress_stream.stream_output(events, output) + events = list(progress_stream.stream_output(events, output)) assert len(output.getvalue()) > 0 def test_mismatched_encoding_stream_write(self): @@ -97,22 +97,24 @@ class ProgressStreamTestCase(unittest.TestCase): tf.seek(0) assert tf.read() == '???' + def test_get_digest_from_push(self): + digest = "sha256:abcd" + events = [ + {"status": "..."}, + {"status": "..."}, + {"progressDetail": {}, "aux": {"Digest": digest}}, + ] + assert progress_stream.get_digest_from_push(events) == digest + + def test_get_digest_from_pull(self): + events = list() + assert progress_stream.get_digest_from_pull(events) is None -def test_get_digest_from_push(): - digest = "sha256:abcd" - events = [ - {"status": "..."}, - {"status": "..."}, - {"progressDetail": {}, "aux": {"Digest": digest}}, - ] - assert progress_stream.get_digest_from_push(events) == digest - - -def test_get_digest_from_pull(): - digest = "sha256:abcd" - events = [ - {"status": "..."}, - {"status": "..."}, - {"status": "Digest: %s" % digest}, - ] - assert progress_stream.get_digest_from_pull(events) == digest + digest = "sha256:abcd" + events = [ + {"status": "..."}, + {"status": "..."}, + {"status": "Digest: %s" % digest}, + {"status": "..."}, + ] + assert progress_stream.get_digest_from_pull(events) == digest diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 83a01475..6391fac8 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -3,6 +3,8 @@ from __future__ import absolute_import from __future__ import unicode_literals import datetime +import os +import tempfile import docker import pytest @@ -10,14 +12,19 @@ from docker.errors import NotFound from .. import mock from .. import unittest +from ..helpers import BUSYBOX_IMAGE_WITH_TAG +from compose.config import ConfigurationError from compose.config.config import Config from compose.config.types import VolumeFromSpec from compose.const import COMPOSEFILE_V1 as V1 from compose.const import COMPOSEFILE_V2_0 as V2_0 from compose.const import COMPOSEFILE_V2_4 as V2_4 +from compose.const import COMPOSEFILE_V3_7 as V3_7 +from compose.const import DEFAULT_TIMEOUT from compose.const import LABEL_SERVICE from compose.container import Container from compose.errors import OperationFailedError +from compose.project import get_secrets from compose.project import NoSuchService from compose.project import Project from compose.project import ProjectError @@ -29,6 +36,7 @@ class ProjectTest(unittest.TestCase): def setUp(self): self.mock_client = mock.create_autospec(docker.APIClient) self.mock_client._general_configs = {} + self.mock_client.api_version = docker.constants.DEFAULT_DOCKER_API_VERSION def test_from_config_v1(self): config = Config( @@ -36,11 +44,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, { 'name': 'db', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, ], networks=None, @@ -55,22 +63,23 @@ class ProjectTest(unittest.TestCase): ) assert len(project.services) == 2 assert project.get_service('web').name == 'web' - assert project.get_service('web').options['image'] == 'busybox:latest' + assert project.get_service('web').options['image'] == BUSYBOX_IMAGE_WITH_TAG assert project.get_service('db').name == 'db' - assert project.get_service('db').options['image'] == 'busybox:latest' + assert project.get_service('db').options['image'] == BUSYBOX_IMAGE_WITH_TAG assert not project.networks.use_networking + @mock.patch('compose.network.Network.true_name', lambda n: n.full_name) def test_from_config_v2(self): config = Config( version=V2_0, services=[ { 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, { 'name': 'db', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }, ], networks=None, @@ -87,7 +96,7 @@ class ProjectTest(unittest.TestCase): project='composetest', name='web', client=None, - image="busybox:latest", + image=BUSYBOX_IMAGE_WITH_TAG, ) project = Project('test', [web], None) assert project.get_service('web') == web @@ -172,7 +181,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': [VolumeFromSpec('aaa', 'rw', 'container')] }], networks=None, @@ -190,7 +199,7 @@ class ProjectTest(unittest.TestCase): "Name": container_name, "Names": [container_name], "Id": container_name, - "Image": 'busybox:latest' + "Image": BUSYBOX_IMAGE_WITH_TAG } ] project = Project.from_config( @@ -201,11 +210,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'vol', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')] } ], @@ -217,6 +226,7 @@ class ProjectTest(unittest.TestCase): ) assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"] + @mock.patch('compose.network.Network.true_name', lambda n: n.full_name) def test_use_volumes_from_service_container(self): container_ids = ['aabbccddee', '12345'] @@ -228,11 +238,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'vol', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'volumes_from': [VolumeFromSpec('vol', 'rw', 'service')] } ], @@ -251,9 +261,10 @@ class ProjectTest(unittest.TestCase): [container_ids[0] + ':rw'] ) - def test_events(self): + def test_events_legacy(self): services = [Service(name='web'), Service(name='db')] project = Project('test', services, self.mock_client) + self.mock_client.api_version = '1.21' self.mock_client.events.return_value = iter([ { 'status': 'create', @@ -359,6 +370,175 @@ class ProjectTest(unittest.TestCase): }, ] + def test_events(self): + services = [Service(name='web'), Service(name='db')] + project = Project('test', services, self.mock_client) + self.mock_client.api_version = '1.35' + self.mock_client.events.return_value = iter([ + { + 'status': 'create', + 'from': 'example/image', + 'Type': 'container', + 'Actor': { + 'ID': 'abcde', + 'Attributes': { + 'com.docker.compose.project': 'test', + 'com.docker.compose.service': 'web', + 'image': 'example/image', + 'name': 'test_web_1', + } + }, + 'id': 'abcde', + 'time': 1420092061, + 'timeNano': 14200920610000002000, + }, + { + 'status': 'attach', + 'from': 'example/image', + 'Type': 'container', + 'Actor': { + 'ID': 'abcde', + 'Attributes': { + 'com.docker.compose.project': 'test', + 'com.docker.compose.service': 'web', + 'image': 'example/image', + 'name': 'test_web_1', + } + }, + 'id': 'abcde', + 'time': 1420092061, + 'timeNano': 14200920610000003000, + }, + { + 'status': 'create', + 'from': 'example/other', + 'Type': 'container', + 'Actor': { + 'ID': 'bdbdbd', + 'Attributes': { + 'image': 'example/other', + 'name': 'shrewd_einstein', + } + }, + 'id': 'bdbdbd', + 'time': 1420092061, + 'timeNano': 14200920610000005000, + }, + { + 'status': 'create', + 'from': 'example/db', + 'Type': 'container', + 'Actor': { + 'ID': 'ababa', + 'Attributes': { + 'com.docker.compose.project': 'test', + 'com.docker.compose.service': 'db', + 'image': 'example/db', + 'name': 'test_db_1', + } + }, + 'id': 'ababa', + 'time': 1420092061, + 'timeNano': 14200920610000004000, + }, + { + 'status': 'destroy', + 'from': 'example/db', + 'Type': 'container', + 'Actor': { + 'ID': 'eeeee', + 'Attributes': { + 'com.docker.compose.project': 'test', + 'com.docker.compose.service': 'db', + 'image': 'example/db', + 'name': 'test_db_1', + } + }, + 'id': 'eeeee', + 'time': 1420092061, + 'timeNano': 14200920610000004000, + }, + ]) + + def dt_with_microseconds(dt, us): + return datetime.datetime.fromtimestamp(dt).replace(microsecond=us) + + def get_container(cid): + if cid == 'eeeee': + raise NotFound(None, None, "oops") + if cid == 'abcde': + name = 'web' + labels = {LABEL_SERVICE: name} + elif cid == 'ababa': + name = 'db' + labels = {LABEL_SERVICE: name} + else: + labels = {} + name = '' + return { + 'Id': cid, + 'Config': {'Labels': labels}, + 'Name': '/project_%s_1' % name, + } + + self.mock_client.inspect_container.side_effect = get_container + + events = project.events() + + events_list = list(events) + # Assert the return value is a generator + assert not list(events) + assert events_list == [ + { + 'type': 'container', + 'service': 'web', + 'action': 'create', + 'id': 'abcde', + 'attributes': { + 'name': 'test_web_1', + 'image': 'example/image', + }, + 'time': dt_with_microseconds(1420092061, 2), + 'container': Container(None, get_container('abcde')), + }, + { + 'type': 'container', + 'service': 'web', + 'action': 'attach', + 'id': 'abcde', + 'attributes': { + 'name': 'test_web_1', + 'image': 'example/image', + }, + 'time': dt_with_microseconds(1420092061, 3), + 'container': Container(None, get_container('abcde')), + }, + { + 'type': 'container', + 'service': 'db', + 'action': 'create', + 'id': 'ababa', + 'attributes': { + 'name': 'test_db_1', + 'image': 'example/db', + }, + 'time': dt_with_microseconds(1420092061, 4), + 'container': Container(None, get_container('ababa')), + }, + { + 'type': 'container', + 'service': 'db', + 'action': 'destroy', + 'id': 'eeeee', + 'attributes': { + 'name': 'test_db_1', + 'image': 'example/db', + }, + 'time': dt_with_microseconds(1420092061, 4), + 'container': None, + }, + ] + def test_net_unset(self): project = Project.from_config( name='test', @@ -368,7 +548,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, } ], networks=None, @@ -393,7 +573,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'container:aaa' }, ], @@ -413,7 +593,7 @@ class ProjectTest(unittest.TestCase): "Name": container_name, "Names": [container_name], "Id": container_name, - "Image": 'busybox:latest' + "Image": BUSYBOX_IMAGE_WITH_TAG } ] project = Project.from_config( @@ -424,11 +604,11 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'aaa', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, { 'name': 'test', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'network_mode': 'service:aaa' }, ], @@ -451,7 +631,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'foo', - 'image': 'busybox:latest' + 'image': BUSYBOX_IMAGE_WITH_TAG }, ], networks=None, @@ -472,7 +652,7 @@ class ProjectTest(unittest.TestCase): services=[ { 'name': 'foo', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, 'networks': {'custom': None} }, ], @@ -487,9 +667,9 @@ class ProjectTest(unittest.TestCase): def test_container_without_name(self): self.mock_client.containers.return_value = [ - {'Image': 'busybox:latest', 'Id': '1', 'Name': '1'}, - {'Image': 'busybox:latest', 'Id': '2', 'Name': None}, - {'Image': 'busybox:latest', 'Id': '3'}, + {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '1', 'Name': '1'}, + {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '2', 'Name': None}, + {'Image': BUSYBOX_IMAGE_WITH_TAG, 'Id': '3'}, ] self.mock_client.inspect_container.return_value = { 'Id': '1', @@ -506,7 +686,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }], networks=None, volumes=None, @@ -524,7 +704,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }], networks={'default': {}}, volumes={'data': {}}, @@ -536,7 +716,7 @@ class ProjectTest(unittest.TestCase): self.mock_client.remove_volume.side_effect = NotFound(None, None, 'oops') project.down(ImageType.all, True) - self.mock_client.remove_image.assert_called_once_with("busybox:latest") + self.mock_client.remove_image.assert_called_once_with(BUSYBOX_IMAGE_WITH_TAG) def test_no_warning_on_stop(self): self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}} @@ -569,28 +749,56 @@ class ProjectTest(unittest.TestCase): def test_project_platform_value(self): service_config = { 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, } config_data = Config( version=V2_4, services=[service_config], networks={}, volumes={}, secrets=None, configs=None ) project = Project.from_config(name='test', client=self.mock_client, config_data=config_data) - assert project.get_service('web').options.get('platform') is None + assert project.get_service('web').platform is None project = Project.from_config( name='test', client=self.mock_client, config_data=config_data, default_platform='windows' ) - assert project.get_service('web').options.get('platform') == 'windows' + assert project.get_service('web').platform == 'windows' service_config['platform'] = 'linux/s390x' project = Project.from_config(name='test', client=self.mock_client, config_data=config_data) - assert project.get_service('web').options.get('platform') == 'linux/s390x' + assert project.get_service('web').platform == 'linux/s390x' project = Project.from_config( name='test', client=self.mock_client, config_data=config_data, default_platform='windows' ) - assert project.get_service('web').options.get('platform') == 'linux/s390x' + assert project.get_service('web').platform == 'linux/s390x' + + def test_build_container_operation_with_timeout_func_does_not_mutate_options_with_timeout(self): + config_data = Config( + version=V3_7, + services=[ + {'name': 'web', 'image': BUSYBOX_IMAGE_WITH_TAG}, + {'name': 'db', 'image': BUSYBOX_IMAGE_WITH_TAG, 'stop_grace_period': '1s'}, + ], + networks={}, volumes={}, secrets=None, configs=None, + ) + + project = Project.from_config(name='test', client=self.mock_client, config_data=config_data) + + stop_op = project.build_container_operation_with_timeout_func('stop', options={}) + + web_container = mock.create_autospec(Container, service='web') + db_container = mock.create_autospec(Container, service='db') + + # `stop_grace_period` is not set to 'web' service, + # then it is stopped with the default timeout. + stop_op(web_container) + web_container.stop.assert_called_once_with(timeout=DEFAULT_TIMEOUT) + + # `stop_grace_period` is set to 'db' service, + # then it is stopped with the specified timeout and + # the value is not overridden by the previous function call. + stop_op(db_container) + db_container.stop.assert_called_once_with(timeout=1) @mock.patch('compose.parallel.ParallelStreamWriter._write_noansi') def test_error_parallel_pull(self, mock_write): @@ -601,7 +809,7 @@ class ProjectTest(unittest.TestCase): version=V2_0, services=[{ 'name': 'web', - 'image': 'busybox:latest', + 'image': BUSYBOX_IMAGE_WITH_TAG, }], networks=None, volumes=None, @@ -617,3 +825,104 @@ class ProjectTest(unittest.TestCase): self.mock_client.pull.side_effect = OperationFailedError(b'pull error') with pytest.raises(ProjectError): project.pull(parallel_pull=True) + + def test_avoid_multiple_push(self): + service_config_latest = {'image': 'busybox:latest', 'build': '.'} + service_config_default = {'image': 'busybox', 'build': '.'} + service_config_sha = { + 'image': 'busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d', + 'build': '.' + } + svc1 = Service('busy1', **service_config_latest) + svc1_1 = Service('busy11', **service_config_latest) + svc2 = Service('busy2', **service_config_default) + svc2_1 = Service('busy21', **service_config_default) + svc3 = Service('busy3', **service_config_sha) + svc3_1 = Service('busy31', **service_config_sha) + project = Project( + 'composetest', [svc1, svc1_1, svc2, svc2_1, svc3, svc3_1], self.mock_client + ) + with mock.patch('compose.service.Service.push') as fake_push: + project.push() + assert fake_push.call_count == 2 + + def test_get_secrets_no_secret_def(self): + service = 'foo' + secret_source = 'bar' + + secret_defs = mock.Mock() + secret_defs.get.return_value = None + secret = mock.Mock(source=secret_source) + + with self.assertRaises(ConfigurationError): + get_secrets(service, [secret], secret_defs) + + def test_get_secrets_external_warning(self): + service = 'foo' + secret_source = 'bar' + + secret_def = mock.Mock() + secret_def.get.return_value = True + + secret_defs = mock.Mock() + secret_defs.get.side_effect = secret_def + secret = mock.Mock(source=secret_source) + + with mock.patch('compose.project.log') as mock_log: + get_secrets(service, [secret], secret_defs) + + mock_log.warning.assert_called_with("Service \"{service}\" uses secret \"{secret}\" " + "which is external. External secrets are not available" + " to containers created by docker-compose." + .format(service=service, secret=secret_source)) + + def test_get_secrets_uid_gid_mode_warning(self): + service = 'foo' + secret_source = 'bar' + + fd, filename_path = tempfile.mkstemp() + os.close(fd) + self.addCleanup(os.remove, filename_path) + + def mock_get(key): + return {'external': False, 'file': filename_path}[key] + + secret_def = mock.MagicMock() + secret_def.get = mock.MagicMock(side_effect=mock_get) + + secret_defs = mock.Mock() + secret_defs.get.return_value = secret_def + + secret = mock.Mock(uid=True, gid=True, mode=True, source=secret_source) + + with mock.patch('compose.project.log') as mock_log: + get_secrets(service, [secret], secret_defs) + + mock_log.warning.assert_called_with("Service \"{service}\" uses secret \"{secret}\" with uid, " + "gid, or mode. These fields are not supported by this " + "implementation of the Compose file" + .format(service=service, secret=secret_source)) + + def test_get_secrets_secret_file_warning(self): + service = 'foo' + secret_source = 'bar' + not_a_path = 'NOT_A_PATH' + + def mock_get(key): + return {'external': False, 'file': not_a_path}[key] + + secret_def = mock.MagicMock() + secret_def.get = mock.MagicMock(side_effect=mock_get) + + secret_defs = mock.Mock() + secret_defs.get.return_value = secret_def + + secret = mock.Mock(uid=False, gid=False, mode=False, source=secret_source) + + with mock.patch('compose.project.log') as mock_log: + get_secrets(service, [secret], secret_defs) + + mock_log.warning.assert_called_with("Service \"{service}\" uses an undefined secret file " + "\"{secret_file}\", the following file should be created " + "\"{secret_file}\"" + .format(service=service, secret_file=not_a_path)) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 4ccc4865..a6a633db 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -5,11 +5,13 @@ import docker import pytest from docker.constants import DEFAULT_DOCKER_API_VERSION from docker.errors import APIError +from docker.errors import ImageNotFound from docker.errors import NotFound from .. import mock from .. import unittest from compose.config.errors import DependencyError +from compose.config.types import MountSpec from compose.config.types import ServicePort from compose.config.types import ServiceSecret from compose.config.types import VolumeFromSpec @@ -20,6 +22,7 @@ from compose.const import LABEL_ONE_OFF from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.const import SECRETS_PATH +from compose.const import WINDOWS_LONGPATH_PREFIX from compose.container import Container from compose.errors import OperationFailedError from compose.parallel import ParallelStreamWriter @@ -37,6 +40,7 @@ from compose.service import NeedsBuildError from compose.service import NetworkMode from compose.service import NoSuchImageError from compose.service import parse_repository_tag +from compose.service import rewrite_build_path from compose.service import Service from compose.service import ServiceNetworkMode from compose.service import warn_on_masked_volume @@ -316,19 +320,20 @@ class ServiceTest(unittest.TestCase): self.mock_client.inspect_image.return_value = {'Id': 'abcd'} prev_container = mock.Mock( id='ababab', - image_config={'ContainerConfig': {}}) + image_config={'ContainerConfig': {}} + ) + prev_container.full_slug = 'abcdefff1234' prev_container.get.return_value = None opts = service._get_container_create_options( - {}, - 1, - previous_container=prev_container) + {}, 1, previous_container=prev_container + ) assert service.options['labels'] == labels assert service.options['environment'] == environment assert opts['labels'][LABEL_CONFIG_HASH] == \ - '2524a06fcb3d781aa2c981fc40bcfa08013bb318e4273bfa388df22023e6f2aa' + '689149e6041a85f6fb4945a2146a497ed43c8a5cbd8991753d875b165f1b4de4' assert opts['environment'] == ['also=real'] def test_get_container_create_options_sets_affinity_with_binds(self): @@ -354,11 +359,13 @@ class ServiceTest(unittest.TestCase): }.get(key, None) prev_container.get.side_effect = container_get + prev_container.full_slug = 'abcdefff1234' opts = service._get_container_create_options( {}, 1, - previous_container=prev_container) + previous_container=prev_container + ) assert opts['environment'] == ['affinity:container==ababab'] @@ -369,6 +376,7 @@ class ServiceTest(unittest.TestCase): id='ababab', image_config={'ContainerConfig': {}}) prev_container.get.return_value = None + prev_container.full_slug = 'abcdefff1234' opts = service._get_container_create_options( {}, @@ -385,7 +393,7 @@ class ServiceTest(unittest.TestCase): @mock.patch('compose.service.Container', autospec=True) def test_get_container(self, mock_container_class): - container_dict = dict(Name='default_foo_2') + container_dict = dict(Name='default_foo_2_bdfa3ed91e2c') self.mock_client.containers.return_value = [container_dict] service = Service('foo', image='foo', client=self.mock_client) @@ -445,9 +453,24 @@ class ServiceTest(unittest.TestCase): with pytest.raises(OperationFailedError): service.pull() + def test_pull_image_with_default_platform(self): + self.mock_client.api_version = '1.35' + + service = Service( + 'foo', client=self.mock_client, image='someimage:sometag', + default_platform='linux' + ) + assert service.platform == 'linux' + service.pull() + + assert self.mock_client.pull.call_count == 1 + call_args = self.mock_client.pull.call_args + assert call_args[1]['platform'] == 'linux' + @mock.patch('compose.service.Container', autospec=True) def test_recreate_container(self, _): mock_container = mock.create_autospec(Container) + mock_container.full_slug = 'abcdefff1234' service = Service('foo', client=self.mock_client, image='someimage') service.image = lambda: {'Id': 'abc123'} new_container = service.recreate_container(mock_container) @@ -461,6 +484,7 @@ class ServiceTest(unittest.TestCase): @mock.patch('compose.service.Container', autospec=True) def test_recreate_container_with_timeout(self, _): mock_container = mock.create_autospec(Container) + mock_container.full_slug = 'abcdefff1234' self.mock_client.inspect_image.return_value = {'Id': 'abc123'} service = Service('foo', client=self.mock_client, image='someimage') service.recreate_container(mock_container, timeout=1) @@ -492,8 +516,8 @@ class ServiceTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: service.create_container() - assert mock_log.warn.called - _, args, _ = mock_log.warn.mock_calls[0] + assert mock_log.warning.called + _, args, _ = mock_log.warning.mock_calls[0] assert 'was built because it did not already exist' in args[0] assert self.mock_client.build.call_count == 1 @@ -522,7 +546,7 @@ class ServiceTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: service.ensure_image_exists(do_build=BuildAction.force) - assert not mock_log.warn.called + assert not mock_log.warning.called assert self.mock_client.build.call_count == 1 self.mock_client.build.call_args[1]['tag'] == 'default_foo' @@ -537,7 +561,7 @@ class ServiceTest(unittest.TestCase): assert self.mock_client.build.call_count == 1 assert not self.mock_client.build.call_args[1]['pull'] - def test_build_does_with_platform(self): + def test_build_with_platform(self): self.mock_client.api_version = '1.35' self.mock_client.build.return_value = [ b'{"stream": "Successfully built 12345"}', @@ -550,6 +574,47 @@ class ServiceTest(unittest.TestCase): call_args = self.mock_client.build.call_args assert call_args[1]['platform'] == 'linux' + def test_build_with_default_platform(self): + self.mock_client.api_version = '1.35' + self.mock_client.build.return_value = [ + b'{"stream": "Successfully built 12345"}', + ] + + service = Service( + 'foo', client=self.mock_client, build={'context': '.'}, + default_platform='linux' + ) + assert service.platform == 'linux' + service.build() + + assert self.mock_client.build.call_count == 1 + call_args = self.mock_client.build.call_args + assert call_args[1]['platform'] == 'linux' + + def test_service_platform_precedence(self): + self.mock_client.api_version = '1.35' + + service = Service( + 'foo', client=self.mock_client, platform='linux/arm', + default_platform='osx' + ) + assert service.platform == 'linux/arm' + + def test_service_ignore_default_platform_with_unsupported_api(self): + self.mock_client.api_version = '1.32' + self.mock_client.build.return_value = [ + b'{"stream": "Successfully built 12345"}', + ] + + service = Service( + 'foo', client=self.mock_client, default_platform='windows', build={'context': '.'} + ) + assert service.platform is None + service.build() + assert self.mock_client.build.call_count == 1 + call_args = self.mock_client.build.call_args + assert call_args[1]['platform'] is None + def test_build_with_override_build_args(self): self.mock_client.build.return_value = [ b'{"stream": "Successfully built 12345"}', @@ -611,6 +676,7 @@ class ServiceTest(unittest.TestCase): 'options': {'image': 'example.com/foo'}, 'links': [('one', 'one')], 'net': 'other', + 'secrets': [], 'networks': {'default': None}, 'volumes_from': [('two', 'rw')], } @@ -633,6 +699,7 @@ class ServiceTest(unittest.TestCase): 'options': {'image': 'example.com/foo'}, 'links': [], 'networks': {}, + 'secrets': [], 'net': 'aaabbb', 'volumes_from': [], } @@ -645,17 +712,19 @@ class ServiceTest(unittest.TestCase): image='example.com/foo', client=self.mock_client, network_mode=NetworkMode('bridge'), - networks={'bridge': {}}, + networks={'bridge': {}, 'net2': {}}, links=[(Service('one', client=self.mock_client), 'one')], - volumes_from=[VolumeFromSpec(Service('two', client=self.mock_client), 'rw', 'service')] + volumes_from=[VolumeFromSpec(Service('two', client=self.mock_client), 'rw', 'service')], + volumes=[VolumeSpec('/ext', '/int', 'ro')], + build={'context': 'some/random/path'}, ) config_hash = service.config_hash for api_version in set(API_VERSIONS.values()): self.mock_client.api_version = api_version - assert service._get_container_create_options({}, 1)['labels'][LABEL_CONFIG_HASH] == ( - config_hash - ) + assert service._get_container_create_options( + {}, 1 + )['labels'][LABEL_CONFIG_HASH] == config_hash def test_remove_image_none(self): web = Service('web', image='example', client=self.mock_client) @@ -689,6 +758,13 @@ class ServiceTest(unittest.TestCase): mock_log.error.assert_called_once_with( "Failed to remove image for service %s: %s", web.name, error) + def test_remove_non_existing_image(self): + self.mock_client.remove_image.side_effect = ImageNotFound('image not found') + web = Service('web', image='example', client=self.mock_client) + with mock.patch('compose.service.log', autospec=True) as mock_log: + assert not web.remove_image(ImageType.all) + mock_log.warning.assert_called_once_with("Image %s not found.", web.image_name) + def test_specifies_host_port_with_no_ports(self): service = Service( 'foo', @@ -752,7 +828,7 @@ class ServiceTest(unittest.TestCase): assert service.specifies_host_port() def test_image_name_from_config(self): - image_name = 'example/web:latest' + image_name = 'example/web:mytag' service = Service('foo', image=image_name) assert service.image_name == image_name @@ -771,13 +847,13 @@ class ServiceTest(unittest.TestCase): ports=["8080:80"]) service.scale(0) - assert not mock_log.warn.called + assert not mock_log.warning.called service.scale(1) - assert not mock_log.warn.called + assert not mock_log.warning.called service.scale(2) - mock_log.warn.assert_called_once_with( + mock_log.warning.assert_called_once_with( 'The "{}" service specifies a port on the host. If multiple containers ' 'for this service are created on a single host, the port will clash.'.format(name)) @@ -955,6 +1031,41 @@ class ServiceTest(unittest.TestCase): assert service.create_container().id == 'new_cont_id' + def test_build_volume_options_duplicate_binds(self): + self.mock_client.api_version = '1.29' # Trigger 3.2 format workaround + service = Service('foo', client=self.mock_client) + ctnr_opts, override_opts = service._build_container_volume_options( + previous_container=None, + container_options={ + 'volumes': [ + MountSpec.parse({'source': 'vol', 'target': '/data', 'type': 'volume'}), + VolumeSpec.parse('vol:/data:rw'), + ], + 'environment': {}, + }, + override_options={}, + ) + assert 'binds' in override_opts + assert len(override_opts['binds']) == 1 + assert override_opts['binds'][0] == 'vol:/data:rw' + + def test_volumes_order_is_preserved(self): + service = Service('foo', client=self.mock_client) + volumes = [ + VolumeSpec.parse(cfg) for cfg in [ + '/v{0}:/v{0}:rw'.format(i) for i in range(6) + ] + ] + ctnr_opts, override_opts = service._build_container_volume_options( + previous_container=None, + container_options={ + 'volumes': volumes, + 'environment': {}, + }, + override_options={}, + ) + assert override_opts['binds'] == [vol.repr() for vol in volumes] + class TestServiceNetwork(unittest.TestCase): def setUp(self): @@ -1223,10 +1334,8 @@ class ServiceVolumesTest(unittest.TestCase): number=1, ) - assert set(self.mock_client.create_host_config.call_args[1]['binds']) == set([ - '/host/path:/data1:rw', - '/host/path:/data2:rw', - ]) + assert set(self.mock_client.create_host_config.call_args[1]['binds']) == {'/host/path:/data1:rw', + '/host/path:/data2:rw'} def test_get_container_create_options_with_different_host_path_in_container_json(self): service = Service( @@ -1280,7 +1389,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - assert not mock_log.warn.called + assert not mock_log.warning.called def test_warn_on_masked_volume_when_masked(self): volumes_option = [VolumeSpec('/home/user', '/path', 'rw')] @@ -1293,7 +1402,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - mock_log.warn.assert_called_once_with(mock.ANY) + mock_log.warning.assert_called_once_with(mock.ANY) def test_warn_on_masked_no_warning_with_same_path(self): volumes_option = [VolumeSpec('/home/user', '/path', 'rw')] @@ -1303,7 +1412,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - assert not mock_log.warn.called + assert not mock_log.warning.called def test_warn_on_masked_no_warning_with_container_only_option(self): volumes_option = [VolumeSpec(None, '/path', 'rw')] @@ -1315,7 +1424,7 @@ class ServiceVolumesTest(unittest.TestCase): with mock.patch('compose.service.log', autospec=True) as mock_log: warn_on_masked_volume(volumes_option, container_volumes, service) - assert not mock_log.warn.called + assert not mock_log.warning.called def test_create_with_special_volume_mode(self): self.mock_client.inspect_image.return_value = {'Id': 'imageid'} @@ -1387,3 +1496,28 @@ class ServiceSecretTest(unittest.TestCase): assert volumes[0].source == secret1['file'] assert volumes[0].target == '{}/{}'.format(SECRETS_PATH, secret1['secret'].source) + + +class RewriteBuildPathTest(unittest.TestCase): + @mock.patch('compose.service.IS_WINDOWS_PLATFORM', True) + def test_rewrite_url_no_prefix(self): + urls = [ + 'http://test.com', + 'https://test.com', + 'git://test.com', + 'github.com/test/test', + 'git@test.com', + ] + for u in urls: + assert rewrite_build_path(u) == u + + @mock.patch('compose.service.IS_WINDOWS_PLATFORM', True) + def test_rewrite_windows_path(self): + assert rewrite_build_path('C:\\context') == WINDOWS_LONGPATH_PREFIX + 'C:\\context' + assert rewrite_build_path( + rewrite_build_path('C:\\context') + ) == rewrite_build_path('C:\\context') + + @mock.patch('compose.service.IS_WINDOWS_PLATFORM', False) + def test_rewrite_unix_path(self): + assert rewrite_build_path('/context') == '/context' diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 84becb97..21b88d96 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -68,3 +68,11 @@ class TestParseBytes(object): assert utils.parse_bytes(123) == 123 assert utils.parse_bytes('foobar') is None assert utils.parse_bytes('123') == 123 + + +class TestMoreItertools(object): + def test_unique_everseen(self): + unique = utils.unique_everseen + assert list(unique([2, 1, 2, 1])) == [2, 1] + assert list(unique([2, 1, 2, 1], hash)) == [2, 1] + assert list(unique([2, 1, 2, 1], lambda x: 'key_%s' % x)) == [2, 1] |