mirror of https://github.com/docker/compose.git
commit
9a46e62873
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,6 +1,27 @@
|
|||
Change log
|
||||
==========
|
||||
|
||||
1.11.2 (2017-02-17)
|
||||
-------------------
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Fixed a bug that was preventing secrets configuration from being
|
||||
loaded properly
|
||||
|
||||
- Fixed a bug where the `docker-compose config` command would fail
|
||||
if the config file contained secrets definitions
|
||||
|
||||
- Fixed an issue where Compose on some linux distributions would
|
||||
pick up and load an outdated version of the requests library
|
||||
|
||||
- Fixed an issue where socket-type files inside a build folder
|
||||
would cause `docker-compose` to crash when trying to build that
|
||||
service
|
||||
|
||||
- Fixed an issue where recursive wildcard patterns `**` were not being
|
||||
recognized in `.dockerignore` files.
|
||||
|
||||
1.11.1 (2017-02-09)
|
||||
-------------------
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '1.11.1'
|
||||
__version__ = '1.11.2'
|
||||
|
|
|
@ -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 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 signals
|
||||
from .. import __version__
|
||||
|
|
|
@ -763,6 +763,11 @@ def finalize_service(service_config, service_names, version, environment):
|
|||
if 'restart' in service_dict:
|
||||
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)
|
||||
|
||||
service_dict['name'] = service_config.name
|
||||
|
|
|
@ -102,4 +102,7 @@ def denormalize_service_dict(service_dict, version):
|
|||
service_dict['healthcheck']['timeout']
|
||||
)
|
||||
|
||||
if 'secrets' in service_dict:
|
||||
service_dict['secrets'] = map(lambda s: s.repr(), service_dict['secrets'])
|
||||
|
||||
return service_dict
|
||||
|
|
|
@ -253,3 +253,8 @@ class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')):
|
|||
@property
|
||||
def merge_field(self):
|
||||
return self.source
|
||||
|
||||
def repr(self):
|
||||
return dict(
|
||||
[(k, v) for k, v in self._asdict().items() if v is not None]
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ PyYAML==3.11
|
|||
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
|
||||
cached-property==1.2.0
|
||||
colorama==0.3.7
|
||||
docker==2.0.2
|
||||
docker==2.1.0
|
||||
dockerpty==0.4.1
|
||||
docopt==0.6.1
|
||||
enum34==1.0.4; python_version < '3.4'
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
set -e
|
||||
|
||||
VERSION="1.11.1"
|
||||
VERSION="1.11.2"
|
||||
IMAGE="docker/compose:$VERSION"
|
||||
|
||||
|
||||
|
|
12
setup.py
12
setup.py
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import codecs
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
@ -37,7 +37,7 @@ install_requires = [
|
|||
'requests >= 2.6.1, != 2.11.0, < 2.12',
|
||||
'texttable >= 0.8.1, < 0.9',
|
||||
'websocket-client >= 0.32.0, < 1.0',
|
||||
'docker >= 2.0.2, < 3.0',
|
||||
'docker >= 2.1.0, < 3.0',
|
||||
'dockerpty >= 0.4.1, < 0.5',
|
||||
'six >= 1.3.0, < 2',
|
||||
'jsonschema >= 2.5.1, < 3',
|
||||
|
@ -64,11 +64,9 @@ try:
|
|||
for key, value in extras_require.items():
|
||||
if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]):
|
||||
install_requires.extend(value)
|
||||
except Exception:
|
||||
logging.getLogger(__name__).exception(
|
||||
'Failed to compute platform dependencies. All dependencies will be '
|
||||
'installed as a result.'
|
||||
)
|
||||
except Exception as e:
|
||||
print("Failed to compute platform dependencies: {}. ".format(e) +
|
||||
"All dependencies will be installed as a result.", file=sys.stderr)
|
||||
for key, value in extras_require.items():
|
||||
if key.startswith(':'):
|
||||
install_requires.extend(value)
|
||||
|
|
|
@ -13,6 +13,7 @@ import pytest
|
|||
|
||||
from ...helpers import build_config_details
|
||||
from compose.config import config
|
||||
from compose.config import types
|
||||
from compose.config.config import resolve_build_args
|
||||
from compose.config.config import resolve_environment
|
||||
from compose.config.config import V1
|
||||
|
@ -53,6 +54,10 @@ def service_sort(services):
|
|||
return sorted(services, key=itemgetter('name'))
|
||||
|
||||
|
||||
def secret_sort(secrets):
|
||||
return sorted(secrets, key=itemgetter('source'))
|
||||
|
||||
|
||||
class ConfigTest(unittest.TestCase):
|
||||
def test_load(self):
|
||||
service_dicts = config.load(
|
||||
|
@ -1770,6 +1775,38 @@ class ConfigTest(unittest.TestCase):
|
|||
'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):
|
||||
config_details = build_config_details({
|
||||
'version': '2',
|
||||
|
@ -1849,6 +1886,91 @@ class ConfigTest(unittest.TestCase):
|
|||
config.load(config_details)
|
||||
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):
|
||||
def test_network_mode_standard(self):
|
||||
|
@ -3405,3 +3527,24 @@ class SerializeTest(unittest.TestCase):
|
|||
denormalized_service = denormalize_service_dict(processed_service, V2_1)
|
||||
assert denormalized_service['healthcheck']['interval'] == '100s'
|
||||
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…
Reference in New Issue