Compare commits
30 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa8fb8f708 | ||
|
|
b49f42f9b7 | ||
|
|
afd3bcfbf4 | ||
|
|
d20e3f3342 | ||
|
|
27297fd1af | ||
|
|
66f4a795a2 | ||
|
|
40c26ca676 | ||
|
|
abce83ef25 | ||
|
|
bb5d7b2433 | ||
|
|
252699c1d1 | ||
|
|
ad0e6d219b | ||
|
|
dc5b3f3b3e | ||
|
|
1fe2443735 | ||
|
|
27e0f31275 | ||
|
|
2f13201b9e | ||
|
|
276db7231a | ||
|
|
c092fa37de | ||
|
|
47e4442722 | ||
|
|
b306e843d3 | ||
|
|
fc7b74d7f9 | ||
|
|
2cd6cb9a47 | ||
|
|
01d1895a35 | ||
|
|
979a0d53f7 | ||
|
|
f083526829 | ||
|
|
b392b6e12e | ||
|
|
165eb9c91a | ||
|
|
1636985a7a | ||
|
|
a3a9d8944a | ||
|
|
84774cacd2 | ||
|
|
22249add84 |
17 changed files with 223 additions and 50 deletions
|
|
@ -620,7 +620,7 @@ Bug Fixes:
|
||||||
if at least one container is using the network.
|
if at least one container is using the network.
|
||||||
|
|
||||||
- When printings logs during `up` or `logs`, flush the output buffer after
|
- When printings logs during `up` or `logs`, flush the output buffer after
|
||||||
each line to prevent buffering issues from hideing logs.
|
each line to prevent buffering issues from hiding logs.
|
||||||
|
|
||||||
- Recreate a container if one of its dependencies is being created.
|
- Recreate a container if one of its dependencies is being created.
|
||||||
Previously a container was only recreated if it's dependencies already
|
Previously a container was only recreated if it's dependencies already
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ A `docker-compose.yml` looks like this:
|
||||||
image: redis
|
image: redis
|
||||||
|
|
||||||
For more information about the Compose file, see the
|
For more information about the Compose file, see the
|
||||||
[Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file.md)
|
[Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file/compose-versioning.md)
|
||||||
|
|
||||||
Compose has commands for managing the whole lifecycle of your application:
|
Compose has commands for managing the whole lifecycle of your application:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__version__ = '1.11.0'
|
__version__ = '1.12.0dev'
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ def convert_service_to_bundle(name, service_dict, image_digest):
|
||||||
return container_config
|
return container_config
|
||||||
|
|
||||||
|
|
||||||
# See https://github.com/docker/swarmkit/blob//agent/exec/container/container.go#L95
|
# See https://github.com/docker/swarmkit/blob/agent/exec/container/container.go#L95
|
||||||
def set_command_and_args(config, entrypoint, command):
|
def set_command_and_args(config, entrypoint, command):
|
||||||
if isinstance(entrypoint, six.string_types):
|
if isinstance(entrypoint, six.string_types):
|
||||||
entrypoint = split_command(entrypoint)
|
entrypoint = split_command(entrypoint)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import print_function
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Attempt to detect https://github.com/docker/compose/issues/4344
|
||||||
|
try:
|
||||||
|
# We don't try importing pip because it messes with package imports
|
||||||
|
# on some Linux distros (Ubuntu, Fedora)
|
||||||
|
# https://github.com/docker/compose/issues/4425
|
||||||
|
# https://github.com/docker/compose/issues/4481
|
||||||
|
# https://github.com/pypa/pip/blob/master/pip/_vendor/__init__.py
|
||||||
|
s_cmd = subprocess.Popen(
|
||||||
|
['pip', 'freeze'], stderr=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
packages = s_cmd.communicate()[0].splitlines()
|
||||||
|
dockerpy_installed = len(
|
||||||
|
list(filter(lambda p: p.startswith(b'docker-py=='), packages))
|
||||||
|
) > 0
|
||||||
|
if dockerpy_installed:
|
||||||
|
from .colors import red
|
||||||
|
print(
|
||||||
|
red('ERROR:'),
|
||||||
|
"Dependency conflict: an older version of the 'docker-py' package "
|
||||||
|
"is polluting the namespace. "
|
||||||
|
"Run the following command to remedy the issue:\n"
|
||||||
|
"pip uninstall docker docker-py; pip install docker",
|
||||||
|
file=sys.stderr
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
# pip command is not available, which indicates it's probably the binary
|
||||||
|
# distribution of Compose which is not affected
|
||||||
|
pass
|
||||||
|
|
@ -14,30 +14,6 @@ from distutils.spawn import find_executable
|
||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
|
|
||||||
# Attempt to detect https://github.com/docker/compose/issues/4344
|
|
||||||
try:
|
|
||||||
# A regular import statement causes PyInstaller to freak out while
|
|
||||||
# trying to load pip. This way it is simply ignored.
|
|
||||||
pip = __import__('pip')
|
|
||||||
pip_packages = pip.get_installed_distributions()
|
|
||||||
if 'docker-py' in [pkg.project_name for pkg in pip_packages]:
|
|
||||||
from .colors import red
|
|
||||||
print(
|
|
||||||
red('ERROR:'),
|
|
||||||
"Dependency conflict: an older version of the 'docker-py' package "
|
|
||||||
"is polluting the namespace. "
|
|
||||||
"Run the following command to remedy the issue:\n"
|
|
||||||
"pip uninstall docker docker-py; pip install docker",
|
|
||||||
file=sys.stderr
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
except ImportError:
|
|
||||||
# pip is not available, which indicates it's probably the binary
|
|
||||||
# distribution of Compose which is not affected
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
from . import signals
|
from . import signals
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
|
|
||||||
|
|
@ -186,11 +186,6 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
|
||||||
if version == '3':
|
if version == '3':
|
||||||
version = V3_0
|
version = V3_0
|
||||||
|
|
||||||
if version not in (V2_0, V2_1, V3_0):
|
|
||||||
raise ConfigurationError(
|
|
||||||
'Version in "{}" is unsupported. {}'
|
|
||||||
.format(self.filename, VERSION_EXPLANATION))
|
|
||||||
|
|
||||||
return version
|
return version
|
||||||
|
|
||||||
def get_service(self, name):
|
def get_service(self, name):
|
||||||
|
|
@ -479,7 +474,7 @@ def process_config_file(config_file, environment, service_name=None):
|
||||||
'service',
|
'service',
|
||||||
environment)
|
environment)
|
||||||
|
|
||||||
if config_file.version in (V2_0, V2_1, V3_0):
|
if config_file.version in (V2_0, V2_1, V3_0, V3_1):
|
||||||
processed_config = dict(config_file.config)
|
processed_config = dict(config_file.config)
|
||||||
processed_config['services'] = services
|
processed_config['services'] = services
|
||||||
processed_config['volumes'] = interpolate_config_section(
|
processed_config['volumes'] = interpolate_config_section(
|
||||||
|
|
@ -495,7 +490,9 @@ def process_config_file(config_file, environment, service_name=None):
|
||||||
elif config_file.version == V1:
|
elif config_file.version == V1:
|
||||||
processed_config = services
|
processed_config = services
|
||||||
else:
|
else:
|
||||||
raise Exception("Unsupported version: {}".format(repr(config_file.version)))
|
raise ConfigurationError(
|
||||||
|
'Version in "{}" is unsupported. {}'
|
||||||
|
.format(config_file.filename, VERSION_EXPLANATION))
|
||||||
|
|
||||||
config_file = config_file._replace(config=processed_config)
|
config_file = config_file._replace(config=processed_config)
|
||||||
validate_against_config_schema(config_file)
|
validate_against_config_schema(config_file)
|
||||||
|
|
@ -766,6 +763,11 @@ def finalize_service(service_config, service_names, version, environment):
|
||||||
if 'restart' in service_dict:
|
if 'restart' in service_dict:
|
||||||
service_dict['restart'] = parse_restart_spec(service_dict['restart'])
|
service_dict['restart'] = parse_restart_spec(service_dict['restart'])
|
||||||
|
|
||||||
|
if 'secrets' in service_dict:
|
||||||
|
service_dict['secrets'] = [
|
||||||
|
types.ServiceSecret.parse(s) for s in service_dict['secrets']
|
||||||
|
]
|
||||||
|
|
||||||
normalize_build(service_dict, service_config.working_dir, environment)
|
normalize_build(service_dict, service_config.working_dir, environment)
|
||||||
|
|
||||||
service_dict['name'] = service_config.name
|
service_dict['name'] = service_config.name
|
||||||
|
|
|
||||||
|
|
@ -102,4 +102,7 @@ def denormalize_service_dict(service_dict, version):
|
||||||
service_dict['healthcheck']['timeout']
|
service_dict['healthcheck']['timeout']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if 'secrets' in service_dict:
|
||||||
|
service_dict['secrets'] = map(lambda s: s.repr(), service_dict['secrets'])
|
||||||
|
|
||||||
return service_dict
|
return service_dict
|
||||||
|
|
|
||||||
|
|
@ -253,3 +253,8 @@ class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')):
|
||||||
@property
|
@property
|
||||||
def merge_field(self):
|
def merge_field(self):
|
||||||
return self.source
|
return self.source
|
||||||
|
|
||||||
|
def repr(self):
|
||||||
|
return dict(
|
||||||
|
[(k, v) for k, v in self._asdict().items() if v is not None]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,7 @@ class Project(object):
|
||||||
|
|
||||||
# TODO: get labels from the API v1.22 , see github issue 2618
|
# TODO: get labels from the API v1.22 , see github issue 2618
|
||||||
try:
|
try:
|
||||||
# this can fail if the conatiner has been removed
|
# this can fail if the container has been removed
|
||||||
container = Container.from_id(self.client, event['id'])
|
container = Container.from_id(self.client, event['id'])
|
||||||
except APIError:
|
except APIError:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ exe = EXE(pyz,
|
||||||
'compose/config/config_schema_v3.0.json',
|
'compose/config/config_schema_v3.0.json',
|
||||||
'DATA'
|
'DATA'
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'compose/config/config_schema_v3.1.json',
|
||||||
|
'compose/config/config_schema_v3.1.json',
|
||||||
|
'DATA'
|
||||||
|
),
|
||||||
(
|
(
|
||||||
'compose/GITSHA',
|
'compose/GITSHA',
|
||||||
'compose/GITSHA',
|
'compose/GITSHA',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# Util functions for release scritps
|
# Util functions for release scripts
|
||||||
#
|
#
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="1.11.0"
|
VERSION="1.12.0dev"
|
||||||
IMAGE="docker/compose:$VERSION"
|
IMAGE="docker/compose:$VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
10
setup.py
10
setup.py
|
|
@ -1,10 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
from __future__ import print_function
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import codecs
|
import codecs
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -64,11 +64,9 @@ try:
|
||||||
for key, value in extras_require.items():
|
for key, value in extras_require.items():
|
||||||
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
|
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
|
||||||
install_requires.extend(value)
|
install_requires.extend(value)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logging.getLogger(__name__).exception(
|
print("Failed to compute platform dependencies: {}. ".format(e) +
|
||||||
'Failed to compute platform dependencies. All dependencies will be '
|
"All dependencies will be installed as a result.", file=sys.stderr)
|
||||||
'installed as a result.'
|
|
||||||
)
|
|
||||||
for key, value in extras_require.items():
|
for key, value in extras_require.items():
|
||||||
if key.startswith(':'):
|
if key.startswith(':'):
|
||||||
install_requires.extend(value)
|
install_requires.extend(value)
|
||||||
|
|
|
||||||
|
|
@ -1234,7 +1234,7 @@ class CLITestCase(DockerClientTestCase):
|
||||||
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
|
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
|
||||||
self.assertEqual(user, container.get('Config.User'))
|
self.assertEqual(user, container.get('Config.User'))
|
||||||
|
|
||||||
def test_run_service_with_environement_overridden(self):
|
def test_run_service_with_environment_overridden(self):
|
||||||
name = 'service'
|
name = 'service'
|
||||||
self.base_dir = 'tests/fixtures/environment-composefile'
|
self.base_dir = 'tests/fixtures/environment-composefile'
|
||||||
self.dispatch([
|
self.dispatch([
|
||||||
|
|
@ -1246,9 +1246,9 @@ class CLITestCase(DockerClientTestCase):
|
||||||
])
|
])
|
||||||
service = self.project.get_service(name)
|
service = self.project.get_service(name)
|
||||||
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
|
container = service.containers(stopped=True, one_off=OneOffFilter.only)[0]
|
||||||
# env overriden
|
# env overridden
|
||||||
self.assertEqual('notbar', container.environment['foo'])
|
self.assertEqual('notbar', container.environment['foo'])
|
||||||
# keep environement from yaml
|
# keep environment from yaml
|
||||||
self.assertEqual('world', container.environment['hello'])
|
self.assertEqual('world', container.environment['hello'])
|
||||||
# added option from command line
|
# added option from command line
|
||||||
self.assertEqual('beta', container.environment['alpha'])
|
self.assertEqual('beta', container.environment['alpha'])
|
||||||
|
|
@ -1293,7 +1293,7 @@ class CLITestCase(DockerClientTestCase):
|
||||||
self.assertEqual(port_range[0], "0.0.0.0:49153")
|
self.assertEqual(port_range[0], "0.0.0.0:49153")
|
||||||
self.assertEqual(port_range[1], "0.0.0.0:49154")
|
self.assertEqual(port_range[1], "0.0.0.0:49154")
|
||||||
|
|
||||||
def test_run_service_with_explicitly_maped_ports(self):
|
def test_run_service_with_explicitly_mapped_ports(self):
|
||||||
# create one off container
|
# create one off container
|
||||||
self.base_dir = 'tests/fixtures/ports-composefile'
|
self.base_dir = 'tests/fixtures/ports-composefile'
|
||||||
self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple'])
|
self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple'])
|
||||||
|
|
@ -1310,7 +1310,7 @@ class CLITestCase(DockerClientTestCase):
|
||||||
self.assertEqual(port_short, "0.0.0.0:30000")
|
self.assertEqual(port_short, "0.0.0.0:30000")
|
||||||
self.assertEqual(port_full, "0.0.0.0:30001")
|
self.assertEqual(port_full, "0.0.0.0:30001")
|
||||||
|
|
||||||
def test_run_service_with_explicitly_maped_ip_ports(self):
|
def test_run_service_with_explicitly_mapped_ip_ports(self):
|
||||||
# create one off container
|
# create one off container
|
||||||
self.base_dir = 'tests/fixtures/ports-composefile'
|
self.base_dir = 'tests/fixtures/ports-composefile'
|
||||||
self.dispatch([
|
self.dispatch([
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ class CLITestCase(unittest.TestCase):
|
||||||
mock_client.create_host_config.call_args[1].get('restart_policy')
|
mock_client.create_host_config.call_args[1].get('restart_policy')
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_command_manula_and_service_ports_together(self):
|
def test_command_manual_and_service_ports_together(self):
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
client=None,
|
client=None,
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ import pytest
|
||||||
|
|
||||||
from ...helpers import build_config_details
|
from ...helpers import build_config_details
|
||||||
from compose.config import config
|
from compose.config import config
|
||||||
|
from compose.config import types
|
||||||
from compose.config.config import resolve_build_args
|
from compose.config.config import resolve_build_args
|
||||||
from compose.config.config import resolve_environment
|
from compose.config.config import resolve_environment
|
||||||
from compose.config.config import V1
|
from compose.config.config import V1
|
||||||
from compose.config.config import V2_0
|
from compose.config.config import V2_0
|
||||||
from compose.config.config import V2_1
|
from compose.config.config import V2_1
|
||||||
from compose.config.config import V3_0
|
from compose.config.config import V3_0
|
||||||
|
from compose.config.config import V3_1
|
||||||
from compose.config.environment import Environment
|
from compose.config.environment import Environment
|
||||||
from compose.config.errors import ConfigurationError
|
from compose.config.errors import ConfigurationError
|
||||||
from compose.config.errors import VERSION_EXPLANATION
|
from compose.config.errors import VERSION_EXPLANATION
|
||||||
|
|
@ -52,6 +54,10 @@ def service_sort(services):
|
||||||
return sorted(services, key=itemgetter('name'))
|
return sorted(services, key=itemgetter('name'))
|
||||||
|
|
||||||
|
|
||||||
|
def secret_sort(secrets):
|
||||||
|
return sorted(secrets, key=itemgetter('source'))
|
||||||
|
|
||||||
|
|
||||||
class ConfigTest(unittest.TestCase):
|
class ConfigTest(unittest.TestCase):
|
||||||
def test_load(self):
|
def test_load(self):
|
||||||
service_dicts = config.load(
|
service_dicts = config.load(
|
||||||
|
|
@ -168,6 +174,9 @@ class ConfigTest(unittest.TestCase):
|
||||||
cfg = config.load(build_config_details({'version': version}))
|
cfg = config.load(build_config_details({'version': version}))
|
||||||
assert cfg.version == V3_0
|
assert cfg.version == V3_0
|
||||||
|
|
||||||
|
cfg = config.load(build_config_details({'version': '3.1'}))
|
||||||
|
assert cfg.version == V3_1
|
||||||
|
|
||||||
def test_v1_file_version(self):
|
def test_v1_file_version(self):
|
||||||
cfg = config.load(build_config_details({'web': {'image': 'busybox'}}))
|
cfg = config.load(build_config_details({'web': {'image': 'busybox'}}))
|
||||||
assert cfg.version == V1
|
assert cfg.version == V1
|
||||||
|
|
@ -1766,6 +1775,38 @@ class ConfigTest(unittest.TestCase):
|
||||||
'labels': {'com.docker.compose.test': 'yes'}
|
'labels': {'com.docker.compose.test': 'yes'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_merge_different_secrets(self):
|
||||||
|
base = {
|
||||||
|
'image': 'busybox',
|
||||||
|
'secrets': [
|
||||||
|
{'source': 'src.txt'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
override = {'secrets': ['other-src.txt']}
|
||||||
|
|
||||||
|
actual = config.merge_service_dicts(base, override, V3_1)
|
||||||
|
assert secret_sort(actual['secrets']) == secret_sort([
|
||||||
|
{'source': 'src.txt'},
|
||||||
|
{'source': 'other-src.txt'}
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_merge_secrets_override(self):
|
||||||
|
base = {
|
||||||
|
'image': 'busybox',
|
||||||
|
'secrets': ['src.txt'],
|
||||||
|
}
|
||||||
|
override = {
|
||||||
|
'secrets': [
|
||||||
|
{
|
||||||
|
'source': 'src.txt',
|
||||||
|
'target': 'data.txt',
|
||||||
|
'mode': 0o400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
actual = config.merge_service_dicts(base, override, V3_1)
|
||||||
|
assert actual['secrets'] == override['secrets']
|
||||||
|
|
||||||
def test_external_volume_config(self):
|
def test_external_volume_config(self):
|
||||||
config_details = build_config_details({
|
config_details = build_config_details({
|
||||||
'version': '2',
|
'version': '2',
|
||||||
|
|
@ -1845,6 +1886,91 @@ class ConfigTest(unittest.TestCase):
|
||||||
config.load(config_details)
|
config.load(config_details)
|
||||||
assert 'has neither an image nor a build context' in exc.exconly()
|
assert 'has neither an image nor a build context' in exc.exconly()
|
||||||
|
|
||||||
|
def test_load_secrets(self):
|
||||||
|
base_file = config.ConfigFile(
|
||||||
|
'base.yaml',
|
||||||
|
{
|
||||||
|
'version': '3.1',
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'image': 'example/web',
|
||||||
|
'secrets': [
|
||||||
|
'one',
|
||||||
|
{
|
||||||
|
'source': 'source',
|
||||||
|
'target': 'target',
|
||||||
|
'uid': '100',
|
||||||
|
'gid': '200',
|
||||||
|
'mode': 0o777,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'secrets': {
|
||||||
|
'one': {'file': 'secret.txt'},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
details = config.ConfigDetails('.', [base_file])
|
||||||
|
service_dicts = config.load(details).services
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
'name': 'web',
|
||||||
|
'image': 'example/web',
|
||||||
|
'secrets': [
|
||||||
|
types.ServiceSecret('one', None, None, None, None),
|
||||||
|
types.ServiceSecret('source', 'target', '100', '200', 0o777),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
assert service_sort(service_dicts) == service_sort(expected)
|
||||||
|
|
||||||
|
def test_load_secrets_multi_file(self):
|
||||||
|
base_file = config.ConfigFile(
|
||||||
|
'base.yaml',
|
||||||
|
{
|
||||||
|
'version': '3.1',
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'image': 'example/web',
|
||||||
|
'secrets': ['one'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'secrets': {
|
||||||
|
'one': {'file': 'secret.txt'},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
override_file = config.ConfigFile(
|
||||||
|
'base.yaml',
|
||||||
|
{
|
||||||
|
'version': '3.1',
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'secrets': [
|
||||||
|
{
|
||||||
|
'source': 'source',
|
||||||
|
'target': 'target',
|
||||||
|
'uid': '100',
|
||||||
|
'gid': '200',
|
||||||
|
'mode': 0o777,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
details = config.ConfigDetails('.', [base_file, override_file])
|
||||||
|
service_dicts = config.load(details).services
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
'name': 'web',
|
||||||
|
'image': 'example/web',
|
||||||
|
'secrets': [
|
||||||
|
types.ServiceSecret('one', None, None, None, None),
|
||||||
|
types.ServiceSecret('source', 'target', '100', '200', 0o777),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
assert service_sort(service_dicts) == service_sort(expected)
|
||||||
|
|
||||||
|
|
||||||
class NetworkModeTest(unittest.TestCase):
|
class NetworkModeTest(unittest.TestCase):
|
||||||
def test_network_mode_standard(self):
|
def test_network_mode_standard(self):
|
||||||
|
|
@ -3401,3 +3527,24 @@ class SerializeTest(unittest.TestCase):
|
||||||
denormalized_service = denormalize_service_dict(processed_service, V2_1)
|
denormalized_service = denormalize_service_dict(processed_service, V2_1)
|
||||||
assert denormalized_service['healthcheck']['interval'] == '100s'
|
assert denormalized_service['healthcheck']['interval'] == '100s'
|
||||||
assert denormalized_service['healthcheck']['timeout'] == '30s'
|
assert denormalized_service['healthcheck']['timeout'] == '30s'
|
||||||
|
|
||||||
|
def test_denormalize_secrets(self):
|
||||||
|
service_dict = {
|
||||||
|
'name': 'web',
|
||||||
|
'image': 'example/web',
|
||||||
|
'secrets': [
|
||||||
|
types.ServiceSecret('one', None, None, None, None),
|
||||||
|
types.ServiceSecret('source', 'target', '100', '200', 0o777),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
denormalized_service = denormalize_service_dict(service_dict, V3_1)
|
||||||
|
assert secret_sort(denormalized_service['secrets']) == secret_sort([
|
||||||
|
{'source': 'one'},
|
||||||
|
{
|
||||||
|
'source': 'source',
|
||||||
|
'target': 'target',
|
||||||
|
'uid': '100',
|
||||||
|
'gid': '200',
|
||||||
|
'mode': 0o777,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue