mirror of
https://github.com/docker/compose.git
synced 2025-07-23 21:54:40 +02:00
commit
6c29830127
41
CHANGELOG.md
41
CHANGELOG.md
@ -1,6 +1,47 @@
|
|||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
1.7.1 (2016-05-04)
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
|
||||||
|
- Fixed a bug where the output of `docker-compose config` for v1 files
|
||||||
|
would be an invalid configuration file.
|
||||||
|
|
||||||
|
- Fixed a bug where `docker-compose config` would not check the validity
|
||||||
|
of links.
|
||||||
|
|
||||||
|
- Fixed an issue where `docker-compose help` would not output a list of
|
||||||
|
available commands and generic options as expected.
|
||||||
|
|
||||||
|
- Fixed an issue where filtering by service when using `docker-compose logs`
|
||||||
|
would not apply for newly created services.
|
||||||
|
|
||||||
|
- Fixed a bug where unchanged services would sometimes be recreated in
|
||||||
|
in the up phase when using Compose with Python 3.
|
||||||
|
|
||||||
|
- Fixed an issue where API errors encountered during the up phase would
|
||||||
|
not be recognized as a failure state by Compose.
|
||||||
|
|
||||||
|
- Fixed a bug where Compose would raise a NameError because of an undefined
|
||||||
|
exception name on non-Windows platforms.
|
||||||
|
|
||||||
|
- Fixed a bug where the wrong version of `docker-py` would sometimes be
|
||||||
|
installed alongside Compose.
|
||||||
|
|
||||||
|
- Fixed a bug where the host value output by `docker-machine config default`
|
||||||
|
would not be recognized as valid options by the `docker-compose`
|
||||||
|
command line.
|
||||||
|
|
||||||
|
- Fixed an issue where Compose would sometimes exit unexpectedly while
|
||||||
|
reading events broadcasted by a Swarm cluster.
|
||||||
|
|
||||||
|
- Corrected a statement in the docs about the location of the `.env` file,
|
||||||
|
which is indeed read from the current directory, instead of in the same
|
||||||
|
location as the Compose file.
|
||||||
|
|
||||||
|
|
||||||
1.7.0 (2016-04-13)
|
1.7.0 (2016-04-13)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
@ -49,11 +49,11 @@ RUN set -ex; \
|
|||||||
|
|
||||||
# Install pip
|
# Install pip
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
curl -L https://pypi.python.org/packages/source/p/pip/pip-7.0.1.tar.gz | tar -xz; \
|
curl -L https://pypi.python.org/packages/source/p/pip/pip-8.1.1.tar.gz | tar -xz; \
|
||||||
cd pip-7.0.1; \
|
cd pip-8.1.1; \
|
||||||
python setup.py install; \
|
python setup.py install; \
|
||||||
cd ..; \
|
cd ..; \
|
||||||
rm -rf pip-7.0.1
|
rm -rf pip-8.1.1
|
||||||
|
|
||||||
# Python3 requires a valid locale
|
# Python3 requires a valid locale
|
||||||
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
|
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
|
||||||
|
@ -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.7.0'
|
__version__ = '1.7.1'
|
||||||
|
@ -21,12 +21,15 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def project_from_options(project_dir, options):
|
def project_from_options(project_dir, options):
|
||||||
environment = Environment.from_env_file(project_dir)
|
environment = Environment.from_env_file(project_dir)
|
||||||
|
host = options.get('--host')
|
||||||
|
if host is not None:
|
||||||
|
host = host.lstrip('=')
|
||||||
return get_project(
|
return get_project(
|
||||||
project_dir,
|
project_dir,
|
||||||
get_config_path_from_options(project_dir, options, environment),
|
get_config_path_from_options(project_dir, options, environment),
|
||||||
project_name=options.get('--project-name'),
|
project_name=options.get('--project-name'),
|
||||||
verbose=options.get('--verbose'),
|
verbose=options.get('--verbose'),
|
||||||
host=options.get('--host'),
|
host=host,
|
||||||
tls_config=tls_config_from_options(options),
|
tls_config=tls_config_from_options(options),
|
||||||
environment=environment
|
environment=environment
|
||||||
)
|
)
|
||||||
|
@ -24,6 +24,7 @@ from ..const import IS_WINDOWS_PLATFORM
|
|||||||
from ..progress_stream import StreamOutputError
|
from ..progress_stream import StreamOutputError
|
||||||
from ..project import NoSuchService
|
from ..project import NoSuchService
|
||||||
from ..project import OneOffFilter
|
from ..project import OneOffFilter
|
||||||
|
from ..project import ProjectError
|
||||||
from ..service import BuildAction
|
from ..service import BuildAction
|
||||||
from ..service import BuildError
|
from ..service import BuildError
|
||||||
from ..service import ConvergenceStrategy
|
from ..service import ConvergenceStrategy
|
||||||
@ -58,7 +59,7 @@ def main():
|
|||||||
except (KeyboardInterrupt, signals.ShutdownException):
|
except (KeyboardInterrupt, signals.ShutdownException):
|
||||||
log.error("Aborting.")
|
log.error("Aborting.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except (UserError, NoSuchService, ConfigurationError) as e:
|
except (UserError, NoSuchService, ConfigurationError, ProjectError) as e:
|
||||||
log.error(e.msg)
|
log.error(e.msg)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except BuildError as e:
|
except BuildError as e:
|
||||||
@ -142,7 +143,7 @@ class TopLevelCommand(object):
|
|||||||
"""Define and run multi-container applications with Docker.
|
"""Define and run multi-container applications with Docker.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]
|
docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
|
||||||
docker-compose -h|--help
|
docker-compose -h|--help
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@ -361,10 +362,14 @@ class TopLevelCommand(object):
|
|||||||
"""
|
"""
|
||||||
Get help on a command.
|
Get help on a command.
|
||||||
|
|
||||||
Usage: help COMMAND
|
Usage: help [COMMAND]
|
||||||
"""
|
"""
|
||||||
handler = get_handler(cls, options['COMMAND'])
|
if options['COMMAND']:
|
||||||
raise SystemExit(getdoc(handler))
|
subject = get_handler(cls, options['COMMAND'])
|
||||||
|
else:
|
||||||
|
subject = cls
|
||||||
|
|
||||||
|
print(getdoc(subject))
|
||||||
|
|
||||||
def kill(self, options):
|
def kill(self, options):
|
||||||
"""
|
"""
|
||||||
@ -411,7 +416,8 @@ class TopLevelCommand(object):
|
|||||||
self.project,
|
self.project,
|
||||||
containers,
|
containers,
|
||||||
options['--no-color'],
|
options['--no-color'],
|
||||||
log_args).run()
|
log_args,
|
||||||
|
event_stream=self.project.events(service_names=options['SERVICE'])).run()
|
||||||
|
|
||||||
def pause(self, options):
|
def pause(self, options):
|
||||||
"""
|
"""
|
||||||
|
@ -12,6 +12,13 @@ from six.moves import input
|
|||||||
|
|
||||||
import compose
|
import compose
|
||||||
|
|
||||||
|
# WindowsError is not defined on non-win32 platforms. Avoid runtime errors by
|
||||||
|
# defining it as OSError (its parent class) if missing.
|
||||||
|
try:
|
||||||
|
WindowsError
|
||||||
|
except NameError:
|
||||||
|
WindowsError = OSError
|
||||||
|
|
||||||
|
|
||||||
def yesno(prompt, default=None):
|
def yesno(prompt, default=None):
|
||||||
"""
|
"""
|
||||||
|
@ -37,6 +37,7 @@ from .validation import validate_against_config_schema
|
|||||||
from .validation import validate_config_section
|
from .validation import validate_config_section
|
||||||
from .validation import validate_depends_on
|
from .validation import validate_depends_on
|
||||||
from .validation import validate_extends_file_path
|
from .validation import validate_extends_file_path
|
||||||
|
from .validation import validate_links
|
||||||
from .validation import validate_network_mode
|
from .validation import validate_network_mode
|
||||||
from .validation import validate_service_constraints
|
from .validation import validate_service_constraints
|
||||||
from .validation import validate_top_level_object
|
from .validation import validate_top_level_object
|
||||||
@ -580,6 +581,7 @@ def validate_service(service_config, service_names, version):
|
|||||||
validate_ulimits(service_config)
|
validate_ulimits(service_config)
|
||||||
validate_network_mode(service_config, service_names)
|
validate_network_mode(service_config, service_names)
|
||||||
validate_depends_on(service_config, service_names)
|
validate_depends_on(service_config, service_names)
|
||||||
|
validate_links(service_config, service_names)
|
||||||
|
|
||||||
if not service_dict.get('image') and has_uppercase(service_name):
|
if not service_dict.get('image') and has_uppercase(service_name):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
@ -726,7 +728,7 @@ class MergeDict(dict):
|
|||||||
|
|
||||||
merged = parse_sequence_func(self.base.get(field, []))
|
merged = parse_sequence_func(self.base.get(field, []))
|
||||||
merged.update(parse_sequence_func(self.override.get(field, [])))
|
merged.update(parse_sequence_func(self.override.get(field, [])))
|
||||||
self[field] = [item.repr() for item in merged.values()]
|
self[field] = [item.repr() for item in sorted(merged.values())]
|
||||||
|
|
||||||
def merge_scalar(self, field):
|
def merge_scalar(self, field):
|
||||||
if self.needs_merge(field):
|
if self.needs_merge(field):
|
||||||
@ -928,7 +930,7 @@ def dict_from_path_mappings(path_mappings):
|
|||||||
|
|
||||||
|
|
||||||
def path_mappings_from_dict(d):
|
def path_mappings_from_dict(d):
|
||||||
return [join_path_mapping(v) for v in d.items()]
|
return [join_path_mapping(v) for v in sorted(d.items())]
|
||||||
|
|
||||||
|
|
||||||
def split_path_mapping(volume_path):
|
def split_path_mapping(volume_path):
|
||||||
|
@ -3,10 +3,11 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
|
|
||||||
VERSION_EXPLANATION = (
|
VERSION_EXPLANATION = (
|
||||||
'Either specify a version of "2" (or "2.0") and place your service '
|
'You might be seeing this error because you\'re using the wrong Compose '
|
||||||
'definitions under the `services` key, or omit the `version` key and place '
|
'file version. Either specify a version of "2" (or "2.0") and place your '
|
||||||
'your service definitions at the root of the file to use version 1.\n'
|
'service definitions under the `services` key, or omit the `version` key '
|
||||||
'For more on the Compose file format versions, see '
|
'and place your service definitions at the root of the file to use '
|
||||||
|
'version 1.\nFor more on the Compose file format versions, see '
|
||||||
'https://docs.docker.com/compose/compose-file/')
|
'https://docs.docker.com/compose/compose-file/')
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import six
|
|||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from compose.config import types
|
from compose.config import types
|
||||||
|
from compose.config.config import V1
|
||||||
|
from compose.config.config import V2_0
|
||||||
|
|
||||||
|
|
||||||
def serialize_config_type(dumper, data):
|
def serialize_config_type(dumper, data):
|
||||||
@ -17,14 +19,36 @@ yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
|
|||||||
|
|
||||||
|
|
||||||
def serialize_config(config):
|
def serialize_config(config):
|
||||||
|
denormalized_services = [
|
||||||
|
denormalize_service_dict(service_dict, config.version)
|
||||||
|
for service_dict in config.services
|
||||||
|
]
|
||||||
|
services = {
|
||||||
|
service_dict.pop('name'): service_dict
|
||||||
|
for service_dict in denormalized_services
|
||||||
|
}
|
||||||
|
|
||||||
output = {
|
output = {
|
||||||
'version': config.version,
|
'version': V2_0,
|
||||||
'services': {service.pop('name'): service for service in config.services},
|
'services': services,
|
||||||
'networks': config.networks,
|
'networks': config.networks,
|
||||||
'volumes': config.volumes,
|
'volumes': config.volumes,
|
||||||
}
|
}
|
||||||
|
|
||||||
return yaml.safe_dump(
|
return yaml.safe_dump(
|
||||||
output,
|
output,
|
||||||
default_flow_style=False,
|
default_flow_style=False,
|
||||||
indent=2,
|
indent=2,
|
||||||
width=80)
|
width=80)
|
||||||
|
|
||||||
|
|
||||||
|
def denormalize_service_dict(service_dict, version):
|
||||||
|
service_dict = service_dict.copy()
|
||||||
|
|
||||||
|
if 'restart' in service_dict:
|
||||||
|
service_dict['restart'] = types.serialize_restart_spec(service_dict['restart'])
|
||||||
|
|
||||||
|
if version == V1 and 'network_mode' not in service_dict:
|
||||||
|
service_dict['network_mode'] = 'bridge'
|
||||||
|
|
||||||
|
return service_dict
|
||||||
|
@ -7,6 +7,8 @@ from __future__ import unicode_literals
|
|||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
from compose.config.config import V1
|
from compose.config.config import V1
|
||||||
from compose.config.errors import ConfigurationError
|
from compose.config.errors import ConfigurationError
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
@ -89,6 +91,13 @@ def parse_restart_spec(restart_config):
|
|||||||
return {'Name': name, 'MaximumRetryCount': int(max_retry_count)}
|
return {'Name': name, 'MaximumRetryCount': int(max_retry_count)}
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_restart_spec(restart_spec):
|
||||||
|
parts = [restart_spec['Name']]
|
||||||
|
if restart_spec['MaximumRetryCount']:
|
||||||
|
parts.append(six.text_type(restart_spec['MaximumRetryCount']))
|
||||||
|
return ':'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
def parse_extra_hosts(extra_hosts_config):
|
def parse_extra_hosts(extra_hosts_config):
|
||||||
if not extra_hosts_config:
|
if not extra_hosts_config:
|
||||||
return {}
|
return {}
|
||||||
|
@ -171,6 +171,14 @@ def validate_network_mode(service_config, service_names):
|
|||||||
"is undefined.".format(s=service_config, dep=dependency))
|
"is undefined.".format(s=service_config, dep=dependency))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_links(service_config, service_names):
|
||||||
|
for link in service_config.config.get('links', []):
|
||||||
|
if link.split(':')[0] not in service_names:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Service '{s.name}' has a link to service '{link}' which is "
|
||||||
|
"undefined.".format(s=service_config, link=link))
|
||||||
|
|
||||||
|
|
||||||
def validate_depends_on(service_config, service_names):
|
def validate_depends_on(service_config, service_names):
|
||||||
for dependency in service_config.config.get('depends_on', []):
|
for dependency in service_config.config.get('depends_on', []):
|
||||||
if dependency not in service_names:
|
if dependency not in service_names:
|
||||||
@ -211,7 +219,7 @@ def handle_error_for_schema_with_id(error, path):
|
|||||||
return get_unsupported_config_msg(path, invalid_config_key)
|
return get_unsupported_config_msg(path, invalid_config_key)
|
||||||
|
|
||||||
if not error.path:
|
if not error.path:
|
||||||
return '{}\n{}'.format(error.message, VERSION_EXPLANATION)
|
return '{}\n\n{}'.format(error.message, VERSION_EXPLANATION)
|
||||||
|
|
||||||
|
|
||||||
def handle_generic_error(error, path):
|
def handle_generic_error(error, path):
|
||||||
@ -408,6 +416,6 @@ def handle_errors(errors, format_error_func, filename):
|
|||||||
|
|
||||||
error_msg = '\n'.join(format_error_func(error) for error in errors)
|
error_msg = '\n'.join(format_error_func(error) for error in errors)
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
"Validation failed{file_msg}, reason(s):\n{error_msg}".format(
|
"The Compose file{file_msg} is invalid because:\n{error_msg}".format(
|
||||||
file_msg=" in file '{}'".format(filename) if filename else "",
|
file_msg=" '{}'".format(filename) if filename else "",
|
||||||
error_msg=error_msg))
|
error_msg=error_msg))
|
||||||
|
@ -59,7 +59,7 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None):
|
|||||||
if error_to_reraise:
|
if error_to_reraise:
|
||||||
raise error_to_reraise
|
raise error_to_reraise
|
||||||
|
|
||||||
return results
|
return results, errors
|
||||||
|
|
||||||
|
|
||||||
def _no_deps(x):
|
def _no_deps(x):
|
||||||
|
@ -342,7 +342,10 @@ class Project(object):
|
|||||||
filters={'label': self.labels()},
|
filters={'label': self.labels()},
|
||||||
decode=True
|
decode=True
|
||||||
):
|
):
|
||||||
if event['status'] in IMAGE_EVENTS:
|
# The first part of this condition is a guard against some events
|
||||||
|
# broadcasted by swarm that don't have a status field.
|
||||||
|
# See https://github.com/docker/compose/issues/3316
|
||||||
|
if 'status' not in event or event['status'] in IMAGE_EVENTS:
|
||||||
# We don't receive any image events because labels aren't applied
|
# We don't receive any image events because labels aren't applied
|
||||||
# to images
|
# to images
|
||||||
continue
|
continue
|
||||||
@ -387,13 +390,18 @@ class Project(object):
|
|||||||
def get_deps(service):
|
def get_deps(service):
|
||||||
return {self.get_service(dep) for dep in service.get_dependency_names()}
|
return {self.get_service(dep) for dep in service.get_dependency_names()}
|
||||||
|
|
||||||
results = parallel.parallel_execute(
|
results, errors = parallel.parallel_execute(
|
||||||
services,
|
services,
|
||||||
do,
|
do,
|
||||||
operator.attrgetter('name'),
|
operator.attrgetter('name'),
|
||||||
None,
|
None,
|
||||||
get_deps
|
get_deps
|
||||||
)
|
)
|
||||||
|
if errors:
|
||||||
|
raise ProjectError(
|
||||||
|
'Encountered errors while bringing up the project.'
|
||||||
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
container
|
container
|
||||||
for svc_containers in results
|
for svc_containers in results
|
||||||
@ -528,3 +536,7 @@ class NoSuchService(Exception):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.msg
|
return self.msg
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectError(Exception):
|
||||||
|
pass
|
||||||
|
@ -453,20 +453,20 @@ class Service(object):
|
|||||||
connected_networks = container.get('NetworkSettings.Networks')
|
connected_networks = container.get('NetworkSettings.Networks')
|
||||||
|
|
||||||
for network, netdefs in self.networks.items():
|
for network, netdefs in self.networks.items():
|
||||||
aliases = netdefs.get('aliases', [])
|
|
||||||
ipv4_address = netdefs.get('ipv4_address', None)
|
|
||||||
ipv6_address = netdefs.get('ipv6_address', None)
|
|
||||||
if network in connected_networks:
|
if network in connected_networks:
|
||||||
|
if short_id_alias_exists(container, network):
|
||||||
|
continue
|
||||||
|
|
||||||
self.client.disconnect_container_from_network(
|
self.client.disconnect_container_from_network(
|
||||||
container.id, network)
|
container.id,
|
||||||
|
network)
|
||||||
|
|
||||||
self.client.connect_container_to_network(
|
self.client.connect_container_to_network(
|
||||||
container.id, network,
|
container.id, network,
|
||||||
aliases=list(self._get_aliases(container).union(aliases)),
|
aliases=self._get_aliases(netdefs, container),
|
||||||
ipv4_address=ipv4_address,
|
ipv4_address=netdefs.get('ipv4_address', None),
|
||||||
ipv6_address=ipv6_address,
|
ipv6_address=netdefs.get('ipv6_address', None),
|
||||||
links=self._get_links(False)
|
links=self._get_links(False))
|
||||||
)
|
|
||||||
|
|
||||||
def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):
|
def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT):
|
||||||
for c in self.duplicate_containers():
|
for c in self.duplicate_containers():
|
||||||
@ -533,11 +533,32 @@ class Service(object):
|
|||||||
numbers = [c.number for c in containers]
|
numbers = [c.number for c in containers]
|
||||||
return 1 if not numbers else max(numbers) + 1
|
return 1 if not numbers else max(numbers) + 1
|
||||||
|
|
||||||
def _get_aliases(self, container):
|
def _get_aliases(self, network, container=None):
|
||||||
if container.labels.get(LABEL_ONE_OFF) == "True":
|
if container and container.labels.get(LABEL_ONE_OFF) == "True":
|
||||||
return set()
|
return []
|
||||||
|
|
||||||
return {self.name, container.short_id}
|
return list(
|
||||||
|
{self.name} |
|
||||||
|
({container.short_id} if container else set()) |
|
||||||
|
set(network.get('aliases', ()))
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_default_networking_config(self):
|
||||||
|
if not self.networks:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
network = self.networks[self.network_mode.id]
|
||||||
|
endpoint = {
|
||||||
|
'Aliases': self._get_aliases(network),
|
||||||
|
'IPAMConfig': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.get('ipv4_address'):
|
||||||
|
endpoint['IPAMConfig']['IPv4Address'] = network.get('ipv4_address')
|
||||||
|
if network.get('ipv6_address'):
|
||||||
|
endpoint['IPAMConfig']['IPv6Address'] = network.get('ipv6_address')
|
||||||
|
|
||||||
|
return {"EndpointsConfig": {self.network_mode.id: endpoint}}
|
||||||
|
|
||||||
def _get_links(self, link_to_self):
|
def _get_links(self, link_to_self):
|
||||||
links = {}
|
links = {}
|
||||||
@ -633,6 +654,10 @@ class Service(object):
|
|||||||
override_options,
|
override_options,
|
||||||
one_off=one_off)
|
one_off=one_off)
|
||||||
|
|
||||||
|
networking_config = self.build_default_networking_config()
|
||||||
|
if networking_config:
|
||||||
|
container_options['networking_config'] = networking_config
|
||||||
|
|
||||||
container_options['environment'] = format_environment(
|
container_options['environment'] = format_environment(
|
||||||
container_options['environment'])
|
container_options['environment'])
|
||||||
return container_options
|
return container_options
|
||||||
@ -796,6 +821,12 @@ class Service(object):
|
|||||||
log.error(six.text_type(e))
|
log.error(six.text_type(e))
|
||||||
|
|
||||||
|
|
||||||
|
def short_id_alias_exists(container, network):
|
||||||
|
aliases = container.get(
|
||||||
|
'NetworkSettings.Networks.{net}.Aliases'.format(net=network)) or ()
|
||||||
|
return container.short_id in aliases
|
||||||
|
|
||||||
|
|
||||||
class NetworkMode(object):
|
class NetworkMode(object):
|
||||||
"""A `standard` network mode (ex: host, bridge)"""
|
"""A `standard` network mode (ex: host, bridge)"""
|
||||||
|
|
||||||
|
@ -13,8 +13,8 @@ weight=10
|
|||||||
# Environment file
|
# Environment file
|
||||||
|
|
||||||
Compose supports declaring default environment variables in an environment
|
Compose supports declaring default environment variables in an environment
|
||||||
file named `.env` and placed in the same folder as your
|
file named `.env` placed in the folder `docker-compose` command is executed from
|
||||||
[compose file](compose-file.md).
|
*(current working directory)*.
|
||||||
|
|
||||||
Compose expects each line in an env file to be in `VAR=VAL` format. Lines
|
Compose expects each line in an env file to be in `VAR=VAL` format. Lines
|
||||||
beginning with `#` (i.e. comments) are ignored, as are blank lines.
|
beginning with `#` (i.e. comments) are ignored, as are blank lines.
|
||||||
|
@ -39,7 +39,7 @@ which the release page specifies, in your terminal.
|
|||||||
|
|
||||||
The following is an example command illustrating the format:
|
The following is an example command illustrating the format:
|
||||||
|
|
||||||
curl -L https://github.com/docker/compose/releases/download/1.7.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
curl -L https://github.com/docker/compose/releases/download/1.7.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
||||||
|
|
||||||
If you have problems installing with `curl`, see
|
If you have problems installing with `curl`, see
|
||||||
[Alternative Install Options](#alternative-install-options).
|
[Alternative Install Options](#alternative-install-options).
|
||||||
@ -54,7 +54,7 @@ which the release page specifies, in your terminal.
|
|||||||
7. Test the installation.
|
7. Test the installation.
|
||||||
|
|
||||||
$ docker-compose --version
|
$ docker-compose --version
|
||||||
docker-compose version: 1.7.0
|
docker-compose version: 1.7.1
|
||||||
|
|
||||||
|
|
||||||
## Alternative install options
|
## Alternative install options
|
||||||
@ -77,7 +77,7 @@ to get started.
|
|||||||
Compose can also be run inside a container, from a small bash script wrapper.
|
Compose can also be run inside a container, from a small bash script wrapper.
|
||||||
To install compose as a container run:
|
To install compose as a container run:
|
||||||
|
|
||||||
$ curl -L https://github.com/docker/compose/releases/download/1.7.0/run.sh > /usr/local/bin/docker-compose
|
$ curl -L https://github.com/docker/compose/releases/download/1.7.1/run.sh > /usr/local/bin/docker-compose
|
||||||
$ chmod +x /usr/local/bin/docker-compose
|
$ chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
## Master builds
|
## Master builds
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
PyYAML==3.11
|
PyYAML==3.11
|
||||||
cached-property==1.2.0
|
cached-property==1.2.0
|
||||||
docker-py==1.8.0
|
docker-py==1.8.1
|
||||||
dockerpty==0.4.1
|
dockerpty==0.4.1
|
||||||
docopt==0.6.1
|
docopt==0.6.1
|
||||||
enum34==1.0.4
|
enum34==1.0.4
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="1.7.0"
|
VERSION="1.7.1"
|
||||||
IMAGE="docker/compose:$VERSION"
|
IMAGE="docker/compose:$VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
2
setup.py
2
setup.py
@ -34,7 +34,7 @@ install_requires = [
|
|||||||
'requests >= 2.6.1, < 2.8',
|
'requests >= 2.6.1, < 2.8',
|
||||||
'texttable >= 0.8.1, < 0.9',
|
'texttable >= 0.8.1, < 0.9',
|
||||||
'websocket-client >= 0.32.0, < 1.0',
|
'websocket-client >= 0.32.0, < 1.0',
|
||||||
'docker-py > 1.7.2, < 2',
|
'docker-py >= 1.8.1, < 2',
|
||||||
'dockerpty >= 0.4.1, < 0.5',
|
'dockerpty >= 0.4.1, < 0.5',
|
||||||
'six >= 1.3.0, < 2',
|
'six >= 1.3.0, < 2',
|
||||||
'jsonschema >= 2.5.1, < 3',
|
'jsonschema >= 2.5.1, < 3',
|
||||||
|
@ -140,20 +140,23 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
|
|
||||||
def test_help(self):
|
def test_help(self):
|
||||||
self.base_dir = 'tests/fixtures/no-composefile'
|
self.base_dir = 'tests/fixtures/no-composefile'
|
||||||
result = self.dispatch(['help', 'up'], returncode=1)
|
result = self.dispatch(['help', 'up'], returncode=0)
|
||||||
assert 'Usage: up [options] [SERVICE...]' in result.stderr
|
assert 'Usage: up [options] [SERVICE...]' in result.stdout
|
||||||
# Prevent tearDown from trying to create a project
|
# Prevent tearDown from trying to create a project
|
||||||
self.base_dir = None
|
self.base_dir = None
|
||||||
|
|
||||||
# TODO: this shouldn't be v2-dependent
|
def test_shorthand_host_opt(self):
|
||||||
@v2_only()
|
self.dispatch(
|
||||||
|
['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')),
|
||||||
|
'up', '-d'],
|
||||||
|
returncode=0
|
||||||
|
)
|
||||||
|
|
||||||
def test_config_list_services(self):
|
def test_config_list_services(self):
|
||||||
self.base_dir = 'tests/fixtures/v2-full'
|
self.base_dir = 'tests/fixtures/v2-full'
|
||||||
result = self.dispatch(['config', '--services'])
|
result = self.dispatch(['config', '--services'])
|
||||||
assert set(result.stdout.rstrip().split('\n')) == {'web', 'other'}
|
assert set(result.stdout.rstrip().split('\n')) == {'web', 'other'}
|
||||||
|
|
||||||
# TODO: this shouldn't be v2-dependent
|
|
||||||
@v2_only()
|
|
||||||
def test_config_quiet_with_error(self):
|
def test_config_quiet_with_error(self):
|
||||||
self.base_dir = None
|
self.base_dir = None
|
||||||
result = self.dispatch([
|
result = self.dispatch([
|
||||||
@ -162,14 +165,10 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
], returncode=1)
|
], returncode=1)
|
||||||
assert "'notaservice' must be a mapping" in result.stderr
|
assert "'notaservice' must be a mapping" in result.stderr
|
||||||
|
|
||||||
# TODO: this shouldn't be v2-dependent
|
|
||||||
@v2_only()
|
|
||||||
def test_config_quiet(self):
|
def test_config_quiet(self):
|
||||||
self.base_dir = 'tests/fixtures/v2-full'
|
self.base_dir = 'tests/fixtures/v2-full'
|
||||||
assert self.dispatch(['config', '-q']).stdout == ''
|
assert self.dispatch(['config', '-q']).stdout == ''
|
||||||
|
|
||||||
# TODO: this shouldn't be v2-dependent
|
|
||||||
@v2_only()
|
|
||||||
def test_config_default(self):
|
def test_config_default(self):
|
||||||
self.base_dir = 'tests/fixtures/v2-full'
|
self.base_dir = 'tests/fixtures/v2-full'
|
||||||
result = self.dispatch(['config'])
|
result = self.dispatch(['config'])
|
||||||
@ -198,6 +197,58 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
}
|
}
|
||||||
assert output == expected
|
assert output == expected
|
||||||
|
|
||||||
|
def test_config_restart(self):
|
||||||
|
self.base_dir = 'tests/fixtures/restart'
|
||||||
|
result = self.dispatch(['config'])
|
||||||
|
assert yaml.load(result.stdout) == {
|
||||||
|
'version': '2.0',
|
||||||
|
'services': {
|
||||||
|
'never': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'restart': 'no',
|
||||||
|
},
|
||||||
|
'always': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'restart': 'always',
|
||||||
|
},
|
||||||
|
'on-failure': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'restart': 'on-failure',
|
||||||
|
},
|
||||||
|
'on-failure-5': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'restart': 'on-failure:5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'networks': {},
|
||||||
|
'volumes': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_config_v1(self):
|
||||||
|
self.base_dir = 'tests/fixtures/v1-config'
|
||||||
|
result = self.dispatch(['config'])
|
||||||
|
assert yaml.load(result.stdout) == {
|
||||||
|
'version': '2.0',
|
||||||
|
'services': {
|
||||||
|
'net': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'network_mode': 'bridge',
|
||||||
|
},
|
||||||
|
'volume': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'volumes': ['/data:rw'],
|
||||||
|
'network_mode': 'bridge',
|
||||||
|
},
|
||||||
|
'app': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'volumes_from': ['service:volume:rw'],
|
||||||
|
'network_mode': 'service:net',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'networks': {},
|
||||||
|
'volumes': {},
|
||||||
|
}
|
||||||
|
|
||||||
def test_ps(self):
|
def test_ps(self):
|
||||||
self.project.get_service('simple').create_container()
|
self.project.get_service('simple').create_container()
|
||||||
result = self.dispatch(['ps'])
|
result = self.dispatch(['ps'])
|
||||||
@ -683,9 +734,7 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
['-f', 'v2-invalid.yml', 'up', '-d'],
|
['-f', 'v2-invalid.yml', 'up', '-d'],
|
||||||
returncode=1)
|
returncode=1)
|
||||||
|
|
||||||
# TODO: fix validation error messages for v2 files
|
assert "Unsupported config option for services.bar: 'net'" in result.stderr
|
||||||
# assert "Unsupported config option for service 'web': 'net'" in exc.exconly()
|
|
||||||
assert "Unsupported config option" in result.stderr
|
|
||||||
|
|
||||||
def test_up_with_net_v1(self):
|
def test_up_with_net_v1(self):
|
||||||
self.base_dir = 'tests/fixtures/net-container'
|
self.base_dir = 'tests/fixtures/net-container'
|
||||||
|
2
tests/fixtures/extends/invalid-links.yml
vendored
2
tests/fixtures/extends/invalid-links.yml
vendored
@ -1,3 +1,5 @@
|
|||||||
|
mydb:
|
||||||
|
build: '.'
|
||||||
myweb:
|
myweb:
|
||||||
build: '.'
|
build: '.'
|
||||||
extends:
|
extends:
|
||||||
|
14
tests/fixtures/restart/docker-compose.yml
vendored
Normal file
14
tests/fixtures/restart/docker-compose.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: "2"
|
||||||
|
services:
|
||||||
|
never:
|
||||||
|
image: busybox
|
||||||
|
restart: "no"
|
||||||
|
always:
|
||||||
|
image: busybox
|
||||||
|
restart: always
|
||||||
|
on-failure:
|
||||||
|
image: busybox
|
||||||
|
restart: on-failure
|
||||||
|
on-failure-5:
|
||||||
|
image: busybox
|
||||||
|
restart: "on-failure:5"
|
10
tests/fixtures/v1-config/docker-compose.yml
vendored
Normal file
10
tests/fixtures/v1-config/docker-compose.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
net:
|
||||||
|
image: busybox
|
||||||
|
volume:
|
||||||
|
image: busybox
|
||||||
|
volumes:
|
||||||
|
- /data
|
||||||
|
app:
|
||||||
|
image: busybox
|
||||||
|
net: "container:net"
|
||||||
|
volumes_from: ["volume"]
|
@ -19,6 +19,7 @@ from compose.const import LABEL_PROJECT
|
|||||||
from compose.const import LABEL_SERVICE
|
from compose.const import LABEL_SERVICE
|
||||||
from compose.container import Container
|
from compose.container import Container
|
||||||
from compose.project import Project
|
from compose.project import Project
|
||||||
|
from compose.project import ProjectError
|
||||||
from compose.service import ConvergenceStrategy
|
from compose.service import ConvergenceStrategy
|
||||||
from tests.integration.testcases import v2_only
|
from tests.integration.testcases import v2_only
|
||||||
|
|
||||||
@ -565,7 +566,11 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'busybox:latest',
|
'image': 'busybox:latest',
|
||||||
'command': 'top',
|
'command': 'top',
|
||||||
'networks': {'foo': None, 'bar': None, 'baz': None},
|
'networks': {
|
||||||
|
'foo': None,
|
||||||
|
'bar': None,
|
||||||
|
'baz': {'aliases': ['extra']},
|
||||||
|
},
|
||||||
}],
|
}],
|
||||||
volumes={},
|
volumes={},
|
||||||
networks={
|
networks={
|
||||||
@ -581,15 +586,23 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
config_data=config_data,
|
config_data=config_data,
|
||||||
)
|
)
|
||||||
project.up()
|
project.up()
|
||||||
self.assertEqual(len(project.containers()), 1)
|
|
||||||
|
containers = project.containers()
|
||||||
|
assert len(containers) == 1
|
||||||
|
container, = containers
|
||||||
|
|
||||||
for net_name in ['foo', 'bar', 'baz']:
|
for net_name in ['foo', 'bar', 'baz']:
|
||||||
full_net_name = 'composetest_{}'.format(net_name)
|
full_net_name = 'composetest_{}'.format(net_name)
|
||||||
network_data = self.client.inspect_network(full_net_name)
|
network_data = self.client.inspect_network(full_net_name)
|
||||||
self.assertEqual(network_data['Name'], full_net_name)
|
assert network_data['Name'] == full_net_name
|
||||||
|
|
||||||
|
aliases_key = 'NetworkSettings.Networks.{net}.Aliases'
|
||||||
|
assert 'web' in container.get(aliases_key.format(net='composetest_foo'))
|
||||||
|
assert 'web' in container.get(aliases_key.format(net='composetest_baz'))
|
||||||
|
assert 'extra' in container.get(aliases_key.format(net='composetest_baz'))
|
||||||
|
|
||||||
foo_data = self.client.inspect_network('composetest_foo')
|
foo_data = self.client.inspect_network('composetest_foo')
|
||||||
self.assertEqual(foo_data['Driver'], 'bridge')
|
assert foo_data['Driver'] == 'bridge'
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_up_with_ipam_config(self):
|
def test_up_with_ipam_config(self):
|
||||||
@ -740,7 +753,8 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
config_data=config_data,
|
config_data=config_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(project.up()) == 0
|
with self.assertRaises(ProjectError):
|
||||||
|
project.up()
|
||||||
|
|
||||||
@v2_only()
|
@v2_only()
|
||||||
def test_project_up_volumes(self):
|
def test_project_up_volumes(self):
|
||||||
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import py
|
import py
|
||||||
@ -83,10 +84,10 @@ class CLITestCase(unittest.TestCase):
|
|||||||
self.assertTrue(project.services)
|
self.assertTrue(project.services)
|
||||||
|
|
||||||
def test_command_help(self):
|
def test_command_help(self):
|
||||||
with pytest.raises(SystemExit) as exc:
|
with mock.patch('sys.stdout', new=StringIO()) as fake_stdout:
|
||||||
TopLevelCommand.help({'COMMAND': 'up'})
|
TopLevelCommand.help({'COMMAND': 'up'})
|
||||||
|
|
||||||
assert 'Usage: up' in exc.exconly()
|
assert "Usage: up" in fake_stdout.getvalue()
|
||||||
|
|
||||||
def test_command_help_nonexistent(self):
|
def test_command_help_nonexistent(self):
|
||||||
with pytest.raises(NoSuchCommand):
|
with pytest.raises(NoSuchCommand):
|
||||||
|
@ -1360,6 +1360,17 @@ class ConfigTest(unittest.TestCase):
|
|||||||
config.load(config_details)
|
config.load(config_details)
|
||||||
assert "Service 'one' depends on service 'three'" in exc.exconly()
|
assert "Service 'one' depends on service 'three'" in exc.exconly()
|
||||||
|
|
||||||
|
def test_linked_service_is_undefined(self):
|
||||||
|
with self.assertRaises(ConfigurationError):
|
||||||
|
config.load(
|
||||||
|
build_config_details({
|
||||||
|
'version': '2',
|
||||||
|
'services': {
|
||||||
|
'web': {'image': 'busybox', 'links': ['db:db']},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
def test_load_dockerfile_without_context(self):
|
def test_load_dockerfile_without_context(self):
|
||||||
config_details = build_config_details({
|
config_details = build_config_details({
|
||||||
'version': '2',
|
'version': '2',
|
||||||
|
@ -29,7 +29,7 @@ def get_deps(obj):
|
|||||||
|
|
||||||
|
|
||||||
def test_parallel_execute():
|
def test_parallel_execute():
|
||||||
results = parallel_execute(
|
results, errors = parallel_execute(
|
||||||
objects=[1, 2, 3, 4, 5],
|
objects=[1, 2, 3, 4, 5],
|
||||||
func=lambda x: x * 2,
|
func=lambda x: x * 2,
|
||||||
get_name=six.text_type,
|
get_name=six.text_type,
|
||||||
@ -37,6 +37,7 @@ def test_parallel_execute():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert sorted(results) == [2, 4, 6, 8, 10]
|
assert sorted(results) == [2, 4, 6, 8, 10]
|
||||||
|
assert errors == {}
|
||||||
|
|
||||||
|
|
||||||
def test_parallel_execute_with_deps():
|
def test_parallel_execute_with_deps():
|
||||||
|
@ -643,6 +643,35 @@ class ServiceTest(unittest.TestCase):
|
|||||||
assert service.image_name == 'testing_foo'
|
assert service.image_name == 'testing_foo'
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceNetwork(object):
|
||||||
|
|
||||||
|
def test_connect_container_to_networks_short_aliase_exists(self):
|
||||||
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
|
service = Service(
|
||||||
|
'db',
|
||||||
|
mock_client,
|
||||||
|
'myproject',
|
||||||
|
image='foo',
|
||||||
|
networks={'project_default': {}})
|
||||||
|
container = Container(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
'Id': 'abcdef',
|
||||||
|
'NetworkSettings': {
|
||||||
|
'Networks': {
|
||||||
|
'project_default': {
|
||||||
|
'Aliases': ['analias', 'abcdef'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
True)
|
||||||
|
service.connect_container_to_networks(container)
|
||||||
|
|
||||||
|
assert not mock_client.disconnect_container_from_network.call_count
|
||||||
|
assert not mock_client.connect_container_to_network.call_count
|
||||||
|
|
||||||
|
|
||||||
def sort_by_name(dictionary_list):
|
def sort_by_name(dictionary_list):
|
||||||
return sorted(dictionary_list, key=lambda k: k['name'])
|
return sorted(dictionary_list, key=lambda k: k['name'])
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user