mirror of
https://github.com/docker/compose.git
synced 2025-07-27 15:44:08 +02:00
commit
10267a83dc
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,7 +1,7 @@
|
|||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
1.13.0 (2017-05-01)
|
1.13.0 (2017-05-02)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
### Breaking changes
|
### Breaking changes
|
||||||
@ -47,6 +47,15 @@ Change log
|
|||||||
- Fixed an issue where paths containing unicode characters passed via the `-f`
|
- Fixed an issue where paths containing unicode characters passed via the `-f`
|
||||||
flag were causing Compose to crash
|
flag were causing Compose to crash
|
||||||
|
|
||||||
|
- Fixed an issue where the output of `docker-compose config` would be invalid
|
||||||
|
if the Compose file contained external secrets
|
||||||
|
|
||||||
|
- Fixed a bug where using `--exit-code-from` with `up` would fail if Compose
|
||||||
|
was installed in a Python 3 environment
|
||||||
|
|
||||||
|
- Fixed a bug where recreating containers using a combination of `tmpfs` and
|
||||||
|
`volumes` would result in an invalid config state
|
||||||
|
|
||||||
|
|
||||||
1.12.0 (2017-04-04)
|
1.12.0 (2017-04-04)
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -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.13.0-rc1'
|
__version__ = '1.13.0'
|
||||||
|
@ -944,9 +944,9 @@ class TopLevelCommand(object):
|
|||||||
|
|
||||||
exit_code = 0
|
exit_code = 0
|
||||||
if exit_value_from:
|
if exit_value_from:
|
||||||
candidates = filter(
|
candidates = list(filter(
|
||||||
lambda c: c.service == exit_value_from,
|
lambda c: c.service == exit_value_from,
|
||||||
attached_containers)
|
attached_containers))
|
||||||
if not candidates:
|
if not candidates:
|
||||||
log.error(
|
log.error(
|
||||||
'No containers matching the spec "{0}" '
|
'No containers matching the spec "{0}" '
|
||||||
|
@ -300,6 +300,13 @@
|
|||||||
"driver": {"type": "string"},
|
"driver": {"type": "string"},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^.+$": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -51,7 +51,10 @@ def denormalize_config(config, image_digests=None):
|
|||||||
del vol_conf['external_name']
|
del vol_conf['external_name']
|
||||||
|
|
||||||
if config.version in (V3_1, V3_2):
|
if config.version in (V3_1, V3_2):
|
||||||
result['secrets'] = config.secrets
|
result['secrets'] = config.secrets.copy()
|
||||||
|
for secret_name, secret_conf in result['secrets'].items():
|
||||||
|
if 'external_name' in secret_conf:
|
||||||
|
del secret_conf['external_name']
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,8 +158,8 @@ def check_remote_ipam_config(remote, local):
|
|||||||
if sorted(lc.get('AuxiliaryAddresses')) != sorted(rc.get('AuxiliaryAddresses')):
|
if sorted(lc.get('AuxiliaryAddresses')) != sorted(rc.get('AuxiliaryAddresses')):
|
||||||
raise NetworkConfigChangedError(local.full_name, 'IPAM config aux_addresses')
|
raise NetworkConfigChangedError(local.full_name, 'IPAM config aux_addresses')
|
||||||
|
|
||||||
remote_opts = remote_ipam.get('Options', {})
|
remote_opts = remote_ipam.get('Options') or {}
|
||||||
local_opts = local.ipam.get('options', {})
|
local_opts = local.ipam.get('options') or {}
|
||||||
for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
|
for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
|
||||||
if remote_opts.get(k) != local_opts.get(k):
|
if remote_opts.get(k) != local_opts.get(k):
|
||||||
raise NetworkConfigChangedError(local.full_name, 'IPAM option "{}"'.format(k))
|
raise NetworkConfigChangedError(local.full_name, 'IPAM option "{}"'.format(k))
|
||||||
|
@ -16,6 +16,7 @@ from docker.errors import NotFound
|
|||||||
from docker.types import LogConfig
|
from docker.types import LogConfig
|
||||||
from docker.utils.ports import build_port_bindings
|
from docker.utils.ports import build_port_bindings
|
||||||
from docker.utils.ports import split_port
|
from docker.utils.ports import split_port
|
||||||
|
from docker.utils.utils import convert_tmpfs_mounts
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from . import const
|
from . import const
|
||||||
@ -744,6 +745,7 @@ class Service(object):
|
|||||||
|
|
||||||
binds, affinity = merge_volume_bindings(
|
binds, affinity = merge_volume_bindings(
|
||||||
container_options.get('volumes') or [],
|
container_options.get('volumes') or [],
|
||||||
|
self.options.get('tmpfs') or [],
|
||||||
previous_container)
|
previous_container)
|
||||||
override_options['binds'] = binds
|
override_options['binds'] = binds
|
||||||
container_options['environment'].update(affinity)
|
container_options['environment'].update(affinity)
|
||||||
@ -1126,7 +1128,7 @@ def parse_repository_tag(repo_path):
|
|||||||
# Volumes
|
# Volumes
|
||||||
|
|
||||||
|
|
||||||
def merge_volume_bindings(volumes, previous_container):
|
def merge_volume_bindings(volumes, tmpfs, previous_container):
|
||||||
"""Return a list of volume bindings for a container. Container data volumes
|
"""Return a list of volume bindings for a container. Container data volumes
|
||||||
are replaced by those from the previous container.
|
are replaced by those from the previous container.
|
||||||
"""
|
"""
|
||||||
@ -1138,7 +1140,7 @@ def merge_volume_bindings(volumes, previous_container):
|
|||||||
if volume.external)
|
if volume.external)
|
||||||
|
|
||||||
if previous_container:
|
if previous_container:
|
||||||
old_volumes = get_container_data_volumes(previous_container, volumes)
|
old_volumes = get_container_data_volumes(previous_container, volumes, tmpfs)
|
||||||
warn_on_masked_volume(volumes, old_volumes, previous_container.service)
|
warn_on_masked_volume(volumes, old_volumes, previous_container.service)
|
||||||
volume_bindings.update(
|
volume_bindings.update(
|
||||||
build_volume_binding(volume) for volume in old_volumes)
|
build_volume_binding(volume) for volume in old_volumes)
|
||||||
@ -1149,7 +1151,7 @@ def merge_volume_bindings(volumes, previous_container):
|
|||||||
return list(volume_bindings.values()), affinity
|
return list(volume_bindings.values()), affinity
|
||||||
|
|
||||||
|
|
||||||
def get_container_data_volumes(container, volumes_option):
|
def get_container_data_volumes(container, volumes_option, tmpfs_option):
|
||||||
"""Find the container data volumes that are in `volumes_option`, and return
|
"""Find the container data volumes that are in `volumes_option`, and return
|
||||||
a mapping of volume bindings for those volumes.
|
a mapping of volume bindings for those volumes.
|
||||||
"""
|
"""
|
||||||
@ -1172,6 +1174,10 @@ def get_container_data_volumes(container, volumes_option):
|
|||||||
if volume.external:
|
if volume.external:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Attempting to rebind tmpfs volumes breaks: https://github.com/docker/compose/issues/4751
|
||||||
|
if volume.internal in convert_tmpfs_mounts(tmpfs_option).keys():
|
||||||
|
continue
|
||||||
|
|
||||||
mount = container_mounts.get(volume.internal)
|
mount = container_mounts.get(volume.internal)
|
||||||
|
|
||||||
# New volume, doesn't exist in the old container
|
# New volume, doesn't exist in the old container
|
||||||
|
@ -498,10 +498,19 @@ _docker_compose_unpause() {
|
|||||||
|
|
||||||
_docker_compose_up() {
|
_docker_compose_up() {
|
||||||
case "$prev" in
|
case "$prev" in
|
||||||
|
=)
|
||||||
|
COMPREPLY=("$cur")
|
||||||
|
return
|
||||||
|
;;
|
||||||
--exit-code-from)
|
--exit-code-from)
|
||||||
__docker_compose_services_all
|
__docker_compose_services_all
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
|
--scale)
|
||||||
|
COMPREPLY=( $(compgen -S "=" -W "$(___docker_compose_all_services_in_compose_file)" -- "$cur") )
|
||||||
|
__docker_compose_nospace
|
||||||
|
return
|
||||||
|
;;
|
||||||
--timeout|-t)
|
--timeout|-t)
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
@ -509,7 +518,7 @@ _docker_compose_up() {
|
|||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--abort-on-container-exit --build -d --exit-code-from --force-recreate --help --no-build --no-color --no-deps --no-recreate --timeout -t --remove-orphans" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--abort-on-container-exit --build -d --exit-code-from --force-recreate --help --no-build --no-color --no-deps --no-recreate --remove-orphans --scale --timeout -t" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker_compose_services_all
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="1.13.0-rc1"
|
VERSION="1.13.0"
|
||||||
IMAGE="docker/compose:$VERSION"
|
IMAGE="docker/compose:$VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
@ -552,6 +552,24 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
self.assertEqual(len(project.get_service('data').containers(stopped=True)), 1)
|
self.assertEqual(len(project.get_service('data').containers(stopped=True)), 1)
|
||||||
self.assertEqual(len(project.get_service('console').containers()), 0)
|
self.assertEqual(len(project.get_service('console').containers()), 0)
|
||||||
|
|
||||||
|
def test_project_up_recreate_with_tmpfs_volume(self):
|
||||||
|
# https://github.com/docker/compose/issues/4751
|
||||||
|
project = Project.from_config(
|
||||||
|
name='composetest',
|
||||||
|
config_data=load_config({
|
||||||
|
'version': '2.1',
|
||||||
|
'services': {
|
||||||
|
'foo': {
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'tmpfs': ['/dev/shm'],
|
||||||
|
'volumes': ['/dev/shm']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), client=self.client
|
||||||
|
)
|
||||||
|
project.up()
|
||||||
|
project.up(strategy=ConvergenceStrategy.always)
|
||||||
|
|
||||||
def test_unscale_after_restart(self):
|
def test_unscale_after_restart(self):
|
||||||
web = self.create_service('web')
|
web = self.create_service('web')
|
||||||
project = Project('composetest', [web], self.client)
|
project = Project('composetest', [web], self.client)
|
||||||
|
@ -3825,7 +3825,8 @@ class SerializeTest(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
secrets_dict = {
|
secrets_dict = {
|
||||||
'one': {'file': '/one.txt'},
|
'one': {'file': '/one.txt'},
|
||||||
'source': {'file': '/source.pem'}
|
'source': {'file': '/source.pem'},
|
||||||
|
'two': {'external': True},
|
||||||
}
|
}
|
||||||
config_dict = config.load(build_config_details({
|
config_dict = config.load(build_config_details({
|
||||||
'version': '3.1',
|
'version': '3.1',
|
||||||
@ -3837,6 +3838,7 @@ class SerializeTest(unittest.TestCase):
|
|||||||
serialized_service = serialized_config['services']['web']
|
serialized_service = serialized_config['services']['web']
|
||||||
assert secret_sort(serialized_service['secrets']) == secret_sort(service_dict['secrets'])
|
assert secret_sort(serialized_service['secrets']) == secret_sort(service_dict['secrets'])
|
||||||
assert 'secrets' in serialized_config
|
assert 'secrets' in serialized_config
|
||||||
|
assert serialized_config['secrets']['two'] == secrets_dict['two']
|
||||||
|
|
||||||
def test_serialize_ports(self):
|
def test_serialize_ports(self):
|
||||||
config_dict = config.Config(version='2.0', services=[
|
config_dict = config.Config(version='2.0', services=[
|
||||||
|
@ -100,6 +100,44 @@ class NetworkTest(unittest.TestCase):
|
|||||||
{'Driver': 'overlay', 'Options': None}, net
|
{'Driver': 'overlay', 'Options': None}, net
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_check_remote_network_config_null_remote_ipam_options(self):
|
||||||
|
ipam_config = {
|
||||||
|
'driver': 'default',
|
||||||
|
'config': [
|
||||||
|
{'subnet': '172.0.0.1/16', },
|
||||||
|
{
|
||||||
|
'subnet': '156.0.0.1/25',
|
||||||
|
'gateway': '156.0.0.1',
|
||||||
|
'aux_addresses': ['11.0.0.1', '24.25.26.27'],
|
||||||
|
'ip_range': '156.0.0.1-254'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
net = Network(
|
||||||
|
None, 'compose_test', 'net1', 'bridge', ipam=ipam_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
check_remote_network_config(
|
||||||
|
{
|
||||||
|
'Driver': 'bridge',
|
||||||
|
'Attachable': True,
|
||||||
|
'IPAM': {
|
||||||
|
'Driver': 'default',
|
||||||
|
'Config': [{
|
||||||
|
'Subnet': '156.0.0.1/25',
|
||||||
|
'Gateway': '156.0.0.1',
|
||||||
|
'AuxiliaryAddresses': ['24.25.26.27', '11.0.0.1'],
|
||||||
|
'IPRange': '156.0.0.1-254'
|
||||||
|
}, {
|
||||||
|
'Subnet': '172.0.0.1/16',
|
||||||
|
'Gateway': '172.0.0.1'
|
||||||
|
}],
|
||||||
|
'Options': None
|
||||||
|
},
|
||||||
|
},
|
||||||
|
net
|
||||||
|
)
|
||||||
|
|
||||||
def test_check_remote_network_labels_mismatch(self):
|
def test_check_remote_network_labels_mismatch(self):
|
||||||
net = Network(None, 'compose_test', 'net1', 'overlay', labels={
|
net = Network(None, 'compose_test', 'net1', 'overlay', labels={
|
||||||
'com.project.touhou.character': 'sakuya.izayoi'
|
'com.project.touhou.character': 'sakuya.izayoi'
|
||||||
|
@ -858,6 +858,7 @@ class ServiceVolumesTest(unittest.TestCase):
|
|||||||
'/new/volume',
|
'/new/volume',
|
||||||
'/existing/volume',
|
'/existing/volume',
|
||||||
'named:/named/vol',
|
'named:/named/vol',
|
||||||
|
'/dev/tmpfs'
|
||||||
]]
|
]]
|
||||||
|
|
||||||
self.mock_client.inspect_image.return_value = {
|
self.mock_client.inspect_image.return_value = {
|
||||||
@ -903,15 +904,18 @@ class ServiceVolumesTest(unittest.TestCase):
|
|||||||
VolumeSpec.parse('imagedata:/mnt/image/data:rw'),
|
VolumeSpec.parse('imagedata:/mnt/image/data:rw'),
|
||||||
]
|
]
|
||||||
|
|
||||||
volumes = get_container_data_volumes(container, options)
|
volumes = get_container_data_volumes(container, options, ['/dev/tmpfs'])
|
||||||
assert sorted(volumes) == sorted(expected)
|
assert sorted(volumes) == sorted(expected)
|
||||||
|
|
||||||
def test_merge_volume_bindings(self):
|
def test_merge_volume_bindings(self):
|
||||||
options = [
|
options = [
|
||||||
VolumeSpec.parse('/host/volume:/host/volume:ro', True),
|
VolumeSpec.parse(v, True) for v in [
|
||||||
VolumeSpec.parse('/host/rw/volume:/host/rw/volume', True),
|
'/host/volume:/host/volume:ro',
|
||||||
VolumeSpec.parse('/new/volume', True),
|
'/host/rw/volume:/host/rw/volume',
|
||||||
VolumeSpec.parse('/existing/volume', True),
|
'/new/volume',
|
||||||
|
'/existing/volume',
|
||||||
|
'/dev/tmpfs'
|
||||||
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
self.mock_client.inspect_image.return_value = {
|
self.mock_client.inspect_image.return_value = {
|
||||||
@ -936,7 +940,7 @@ class ServiceVolumesTest(unittest.TestCase):
|
|||||||
'existingvolume:/existing/volume:rw',
|
'existingvolume:/existing/volume:rw',
|
||||||
]
|
]
|
||||||
|
|
||||||
binds, affinity = merge_volume_bindings(options, previous_container)
|
binds, affinity = merge_volume_bindings(options, ['/dev/tmpfs'], previous_container)
|
||||||
assert sorted(binds) == sorted(expected)
|
assert sorted(binds) == sorted(expected)
|
||||||
assert affinity == {'affinity:container': '=cdefab'}
|
assert affinity == {'affinity:container': '=cdefab'}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user