summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorFelipe Sateler <fsateler@debian.org>2019-11-22 21:15:41 -0300
committerFelipe Sateler <fsateler@debian.org>2019-11-22 21:15:41 -0300
commit97b16e5404375cc6cca4469045984cac0eabd335 (patch)
treeb9cfdfec00f4a6afceed718cbb155651d23a51fc /tests
parent813ff34b5328e530d94c95cd8235431cde391e4c (diff)
parentd66f980dd002ce94c3196b1a74dc8c1a0788be06 (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')
-rw-r--r--tests/acceptance/cli_test.py324
-rw-r--r--tests/fixtures/UpperCaseDir/docker-compose.yml4
-rw-r--r--tests/fixtures/abort-on-container-exit-0/docker-compose.yml4
-rw-r--r--tests/fixtures/abort-on-container-exit-1/docker-compose.yml4
-rw-r--r--tests/fixtures/build-args/Dockerfile2
-rw-r--r--tests/fixtures/build-ctx/Dockerfile2
-rw-r--r--tests/fixtures/build-memory/Dockerfile2
-rw-r--r--tests/fixtures/build-multiple-composefile/a/Dockerfile4
-rw-r--r--tests/fixtures/build-multiple-composefile/b/Dockerfile4
-rw-r--r--tests/fixtures/build-multiple-composefile/docker-compose.yml8
-rw-r--r--tests/fixtures/compatibility-mode/docker-compose.yml8
-rw-r--r--tests/fixtures/default-env-file/.env24
-rw-r--r--tests/fixtures/default-env-file/alt/.env4
-rw-r--r--tests/fixtures/default-env-file/docker-compose.yml4
-rw-r--r--tests/fixtures/dockerfile-with-volume/Dockerfile2
-rw-r--r--tests/fixtures/duplicate-override-yaml-files/docker-compose.yml4
-rw-r--r--tests/fixtures/echo-services/docker-compose.yml4
-rw-r--r--tests/fixtures/entrypoint-dockerfile/Dockerfile2
-rw-r--r--tests/fixtures/env-file-override/.env.conf2
-rw-r--r--tests/fixtures/env-file-override/.env.override1
-rw-r--r--tests/fixtures/env-file-override/docker-compose.yml6
-rw-r--r--tests/fixtures/environment-composefile/docker-compose.yml2
-rw-r--r--tests/fixtures/environment-exec/docker-compose.yml2
-rw-r--r--tests/fixtures/exit-code-from/docker-compose.yml4
-rw-r--r--tests/fixtures/expose-composefile/docker-compose.yml2
-rw-r--r--tests/fixtures/images-service-tag/Dockerfile2
-rw-r--r--tests/fixtures/images-service-tag/docker-compose.yml11
-rw-r--r--tests/fixtures/links-composefile/docker-compose.yml6
-rw-r--r--tests/fixtures/logging-composefile-legacy/docker-compose.yml4
-rw-r--r--tests/fixtures/logging-composefile/docker-compose.yml4
-rw-r--r--tests/fixtures/logs-composefile/docker-compose.yml8
-rw-r--r--tests/fixtures/logs-restart-composefile/docker-compose.yml7
-rw-r--r--tests/fixtures/logs-tail-composefile/docker-compose.yml4
-rw-r--r--tests/fixtures/longer-filename-composefile/docker-compose.yaml2
-rw-r--r--tests/fixtures/multiple-composefiles/compose2.yml2
-rw-r--r--tests/fixtures/multiple-composefiles/docker-compose.yml4
-rw-r--r--tests/fixtures/networks/default-network-config.yml4
-rw-r--r--tests/fixtures/networks/docker-compose.yml6
-rw-r--r--tests/fixtures/networks/external-default.yml4
-rw-r--r--tests/fixtures/no-links-composefile/docker-compose.yml6
-rw-r--r--tests/fixtures/override-files/docker-compose.yml4
-rw-r--r--tests/fixtures/override-files/extra.yml2
-rw-r--r--tests/fixtures/override-yaml-files/docker-compose.yml4
-rw-r--r--tests/fixtures/ports-composefile-scale/docker-compose.yml2
-rw-r--r--tests/fixtures/ports-composefile/docker-compose.yml2
-rw-r--r--tests/fixtures/ports-composefile/expanded-notation.yml2
-rw-r--r--tests/fixtures/ps-services-filter/docker-compose.yml2
-rw-r--r--tests/fixtures/run-labels/docker-compose.yml2
-rw-r--r--tests/fixtures/run-workdir/docker-compose.yml2
-rw-r--r--tests/fixtures/scale/docker-compose.yml8
-rw-r--r--tests/fixtures/simple-composefile-volume-ready/docker-compose.merge.yml2
-rw-r--r--tests/fixtures/simple-composefile-volume-ready/docker-compose.yml2
-rw-r--r--tests/fixtures/simple-composefile/digest.yml2
-rw-r--r--tests/fixtures/simple-composefile/docker-compose.yml4
-rw-r--r--tests/fixtures/simple-composefile/ignore-pull-failures.yml2
-rw-r--r--tests/fixtures/simple-composefile/pull-with-build.yml11
-rw-r--r--tests/fixtures/simple-dockerfile/Dockerfile2
-rw-r--r--tests/fixtures/simple-failing-dockerfile/Dockerfile2
-rw-r--r--tests/fixtures/sleeps-composefile/docker-compose.yml4
-rw-r--r--tests/fixtures/stop-signal-composefile/docker-compose.yml2
-rw-r--r--tests/fixtures/tagless-image/Dockerfile2
-rw-r--r--tests/fixtures/top/docker-compose.yml4
-rw-r--r--tests/fixtures/unicode-environment/docker-compose.yml2
-rw-r--r--tests/fixtures/user-composefile/docker-compose.yml2
-rw-r--r--tests/fixtures/v2-dependencies/docker-compose.yml6
-rw-r--r--tests/fixtures/v2-full/Dockerfile2
-rw-r--r--tests/fixtures/v2-full/docker-compose.yml2
-rw-r--r--tests/fixtures/v2-simple/docker-compose.yml4
-rw-r--r--tests/fixtures/v2-simple/links-invalid.yml4
-rw-r--r--tests/fixtures/v2-simple/one-container.yml5
-rw-r--r--tests/helpers.py6
-rw-r--r--tests/integration/environment_test.py70
-rw-r--r--tests/integration/project_test.py296
-rw-r--r--tests/integration/service_test.py164
-rw-r--r--tests/integration/state_test.py189
-rw-r--r--tests/integration/testcases.py9
-rw-r--r--tests/unit/bundle_test.py19
-rw-r--r--tests/unit/cli/docker_client_test.py2
-rw-r--r--tests/unit/cli/log_printer_test.py13
-rw-r--r--tests/unit/cli/main_test.py89
-rw-r--r--tests/unit/cli/utils_test.py25
-rw-r--r--tests/unit/cli_test.py5
-rw-r--r--tests/unit/config/config_test.py464
-rw-r--r--tests/unit/config/environment_test.py10
-rw-r--r--tests/unit/config/interpolation_test.py31
-rw-r--r--tests/unit/container_test.py20
-rw-r--r--tests/unit/network_test.py20
-rw-r--r--tests/unit/progress_stream_test.py50
-rw-r--r--tests/unit/project_test.py375
-rw-r--r--tests/unit/service_test.py190
-rw-r--r--tests/unit/utils_test.py8
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]