mirror of
https://github.com/docker/compose.git
synced 2025-07-21 20:54:32 +02:00
Merge pull request #3095 from dnephin/refactor_command_dispatch
Refactor command dispatch and fix api version mismatch error
This commit is contained in:
commit
e5cd869c61
@ -1,52 +1,25 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from requests.exceptions import ConnectionError
|
|
||||||
from requests.exceptions import SSLError
|
|
||||||
|
|
||||||
from . import errors
|
|
||||||
from . import verbose_proxy
|
from . import verbose_proxy
|
||||||
from .. import config
|
from .. import config
|
||||||
from ..const import API_VERSIONS
|
from ..const import API_VERSIONS
|
||||||
from ..project import Project
|
from ..project import Project
|
||||||
from .docker_client import docker_client
|
from .docker_client import docker_client
|
||||||
from .utils import call_silently
|
|
||||||
from .utils import get_version_info
|
from .utils import get_version_info
|
||||||
from .utils import is_mac
|
|
||||||
from .utils import is_ubuntu
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
def project_from_options(project_dir, options):
|
||||||
def friendly_error_message():
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
except SSLError as e:
|
|
||||||
raise errors.UserError('SSL error: %s' % e)
|
|
||||||
except ConnectionError:
|
|
||||||
if call_silently(['which', 'docker']) != 0:
|
|
||||||
if is_mac():
|
|
||||||
raise errors.DockerNotFoundMac()
|
|
||||||
elif is_ubuntu():
|
|
||||||
raise errors.DockerNotFoundUbuntu()
|
|
||||||
else:
|
|
||||||
raise errors.DockerNotFoundGeneric()
|
|
||||||
elif call_silently(['which', 'docker-machine']) == 0:
|
|
||||||
raise errors.ConnectionErrorDockerMachine()
|
|
||||||
else:
|
|
||||||
raise errors.ConnectionErrorGeneric(get_client().base_url)
|
|
||||||
|
|
||||||
|
|
||||||
def project_from_options(base_dir, options):
|
|
||||||
return get_project(
|
return get_project(
|
||||||
base_dir,
|
project_dir,
|
||||||
get_config_path_from_options(options),
|
get_config_path_from_options(options),
|
||||||
project_name=options.get('--project-name'),
|
project_name=options.get('--project-name'),
|
||||||
verbose=options.get('--verbose'),
|
verbose=options.get('--verbose'),
|
||||||
@ -76,8 +49,8 @@ def get_client(verbose=False, version=None):
|
|||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
def get_project(base_dir, config_path=None, project_name=None, verbose=False):
|
def get_project(project_dir, config_path=None, project_name=None, verbose=False):
|
||||||
config_details = config.find(base_dir, config_path)
|
config_details = config.find(project_dir, config_path)
|
||||||
project_name = get_project_name(config_details.working_dir, project_name)
|
project_name = get_project_name(config_details.working_dir, project_name)
|
||||||
config_data = config.load(config_details)
|
config_data = config.load(config_details)
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import sys
|
|
||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
|
|
||||||
from docopt import docopt
|
from docopt import docopt
|
||||||
@ -15,24 +14,21 @@ def docopt_full_help(docstring, *args, **kwargs):
|
|||||||
raise SystemExit(docstring)
|
raise SystemExit(docstring)
|
||||||
|
|
||||||
|
|
||||||
class DocoptCommand(object):
|
class DocoptDispatcher(object):
|
||||||
def docopt_options(self):
|
|
||||||
return {'options_first': True}
|
|
||||||
|
|
||||||
def sys_dispatch(self):
|
def __init__(self, command_class, options):
|
||||||
self.dispatch(sys.argv[1:])
|
self.command_class = command_class
|
||||||
|
self.options = options
|
||||||
def dispatch(self, argv):
|
|
||||||
self.perform_command(*self.parse(argv))
|
|
||||||
|
|
||||||
def parse(self, argv):
|
def parse(self, argv):
|
||||||
options = docopt_full_help(getdoc(self), argv, **self.docopt_options())
|
command_help = getdoc(self.command_class)
|
||||||
|
options = docopt_full_help(command_help, argv, **self.options)
|
||||||
command = options['COMMAND']
|
command = options['COMMAND']
|
||||||
|
|
||||||
if command is None:
|
if command is None:
|
||||||
raise SystemExit(getdoc(self))
|
raise SystemExit(command_help)
|
||||||
|
|
||||||
handler = self.get_handler(command)
|
handler = get_handler(self.command_class, command)
|
||||||
docstring = getdoc(handler)
|
docstring = getdoc(handler)
|
||||||
|
|
||||||
if docstring is None:
|
if docstring is None:
|
||||||
@ -41,17 +37,18 @@ class DocoptCommand(object):
|
|||||||
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
||||||
return options, handler, command_options
|
return options, handler, command_options
|
||||||
|
|
||||||
def get_handler(self, command):
|
|
||||||
|
def get_handler(command_class, command):
|
||||||
command = command.replace('-', '_')
|
command = command.replace('-', '_')
|
||||||
# we certainly want to have "exec" command, since that's what docker client has
|
# we certainly want to have "exec" command, since that's what docker client has
|
||||||
# but in python exec is a keyword
|
# but in python exec is a keyword
|
||||||
if command == "exec":
|
if command == "exec":
|
||||||
command = "exec_command"
|
command = "exec_command"
|
||||||
|
|
||||||
if not hasattr(self, command):
|
if not hasattr(command_class, command):
|
||||||
raise NoSuchCommand(command, self)
|
raise NoSuchCommand(command, command_class)
|
||||||
|
|
||||||
return getattr(self, command)
|
return getattr(command_class, command)
|
||||||
|
|
||||||
|
|
||||||
class NoSuchCommand(Exception):
|
class NoSuchCommand(Exception):
|
||||||
|
@ -1,10 +1,27 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import logging
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from docker.errors import APIError
|
||||||
|
from requests.exceptions import ConnectionError as RequestsConnectionError
|
||||||
|
from requests.exceptions import ReadTimeout
|
||||||
|
from requests.exceptions import SSLError
|
||||||
|
|
||||||
|
from ..const import API_VERSION_TO_ENGINE_VERSION
|
||||||
|
from ..const import HTTP_TIMEOUT
|
||||||
|
from .utils import call_silently
|
||||||
|
from .utils import is_mac
|
||||||
|
from .utils import is_ubuntu
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UserError(Exception):
|
class UserError(Exception):
|
||||||
|
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
self.msg = dedent(msg).strip()
|
self.msg = dedent(msg).strip()
|
||||||
|
|
||||||
@ -14,44 +31,90 @@ class UserError(Exception):
|
|||||||
__str__ = __unicode__
|
__str__ = __unicode__
|
||||||
|
|
||||||
|
|
||||||
class DockerNotFoundMac(UserError):
|
class ConnectionError(Exception):
|
||||||
def __init__(self):
|
pass
|
||||||
super(DockerNotFoundMac, self).__init__("""
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def handle_connection_errors(client):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except SSLError as e:
|
||||||
|
log.error('SSL error: %s' % e)
|
||||||
|
raise ConnectionError()
|
||||||
|
except RequestsConnectionError:
|
||||||
|
if call_silently(['which', 'docker']) != 0:
|
||||||
|
if is_mac():
|
||||||
|
exit_with_error(docker_not_found_mac)
|
||||||
|
if is_ubuntu():
|
||||||
|
exit_with_error(docker_not_found_ubuntu)
|
||||||
|
exit_with_error(docker_not_found_generic)
|
||||||
|
if call_silently(['which', 'docker-machine']) == 0:
|
||||||
|
exit_with_error(conn_error_docker_machine)
|
||||||
|
exit_with_error(conn_error_generic.format(url=client.base_url))
|
||||||
|
except APIError as e:
|
||||||
|
log_api_error(e, client.api_version)
|
||||||
|
raise ConnectionError()
|
||||||
|
except ReadTimeout as e:
|
||||||
|
log.error(
|
||||||
|
"An HTTP request took too long to complete. Retry with --verbose to "
|
||||||
|
"obtain debug information.\n"
|
||||||
|
"If you encounter this issue regularly because of slow network "
|
||||||
|
"conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher "
|
||||||
|
"value (current value: %s)." % HTTP_TIMEOUT)
|
||||||
|
raise ConnectionError()
|
||||||
|
|
||||||
|
|
||||||
|
def log_api_error(e, client_version):
|
||||||
|
if 'client is newer than server' not in e.explanation:
|
||||||
|
log.error(e.explanation)
|
||||||
|
return
|
||||||
|
|
||||||
|
version = API_VERSION_TO_ENGINE_VERSION.get(client_version)
|
||||||
|
if not version:
|
||||||
|
# They've set a custom API version
|
||||||
|
log.error(e.explanation)
|
||||||
|
return
|
||||||
|
|
||||||
|
log.error(
|
||||||
|
"The Docker Engine version is less than the minimum required by "
|
||||||
|
"Compose. Your current project requires a Docker Engine of "
|
||||||
|
"version {version} or greater.".format(version=version))
|
||||||
|
|
||||||
|
|
||||||
|
def exit_with_error(msg):
|
||||||
|
log.error(dedent(msg).strip())
|
||||||
|
raise ConnectionError()
|
||||||
|
|
||||||
|
|
||||||
|
docker_not_found_mac = """
|
||||||
Couldn't connect to Docker daemon. You might need to install docker-osx:
|
Couldn't connect to Docker daemon. You might need to install docker-osx:
|
||||||
|
|
||||||
https://github.com/noplay/docker-osx
|
https://github.com/noplay/docker-osx
|
||||||
""")
|
"""
|
||||||
|
|
||||||
|
|
||||||
class DockerNotFoundUbuntu(UserError):
|
docker_not_found_ubuntu = """
|
||||||
def __init__(self):
|
|
||||||
super(DockerNotFoundUbuntu, self).__init__("""
|
|
||||||
Couldn't connect to Docker daemon. You might need to install Docker:
|
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||||
|
|
||||||
https://docs.docker.com/engine/installation/ubuntulinux/
|
https://docs.docker.com/engine/installation/ubuntulinux/
|
||||||
""")
|
"""
|
||||||
|
|
||||||
|
|
||||||
class DockerNotFoundGeneric(UserError):
|
docker_not_found_generic = """
|
||||||
def __init__(self):
|
|
||||||
super(DockerNotFoundGeneric, self).__init__("""
|
|
||||||
Couldn't connect to Docker daemon. You might need to install Docker:
|
Couldn't connect to Docker daemon. You might need to install Docker:
|
||||||
|
|
||||||
https://docs.docker.com/engine/installation/
|
https://docs.docker.com/engine/installation/
|
||||||
""")
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ConnectionErrorDockerMachine(UserError):
|
conn_error_docker_machine = """
|
||||||
def __init__(self):
|
|
||||||
super(ConnectionErrorDockerMachine, self).__init__("""
|
|
||||||
Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
|
Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
|
||||||
""")
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ConnectionErrorGeneric(UserError):
|
conn_error_generic = """
|
||||||
def __init__(self, url):
|
Couldn't connect to Docker daemon at {url} - is it running?
|
||||||
super(ConnectionErrorGeneric, self).__init__("""
|
|
||||||
Couldn't connect to Docker daemon at %s - is it running?
|
|
||||||
|
|
||||||
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
|
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
|
||||||
""" % url)
|
"""
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import print_function
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@ -10,18 +11,14 @@ import sys
|
|||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
from docker.errors import APIError
|
from . import errors
|
||||||
from requests.exceptions import ReadTimeout
|
|
||||||
|
|
||||||
from . import signals
|
from . import signals
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..config import config
|
from ..config import config
|
||||||
from ..config import ConfigurationError
|
from ..config import ConfigurationError
|
||||||
from ..config import parse_environment
|
from ..config import parse_environment
|
||||||
from ..config.serialize import serialize_config
|
from ..config.serialize import serialize_config
|
||||||
from ..const import API_VERSION_TO_ENGINE_VERSION
|
|
||||||
from ..const import DEFAULT_TIMEOUT
|
from ..const import DEFAULT_TIMEOUT
|
||||||
from ..const import HTTP_TIMEOUT
|
|
||||||
from ..const import IS_WINDOWS_PLATFORM
|
from ..const import IS_WINDOWS_PLATFORM
|
||||||
from ..progress_stream import StreamOutputError
|
from ..progress_stream import StreamOutputError
|
||||||
from ..project import NoSuchService
|
from ..project import NoSuchService
|
||||||
@ -30,10 +27,10 @@ from ..service import BuildError
|
|||||||
from ..service import ConvergenceStrategy
|
from ..service import ConvergenceStrategy
|
||||||
from ..service import ImageType
|
from ..service import ImageType
|
||||||
from ..service import NeedsBuildError
|
from ..service import NeedsBuildError
|
||||||
from .command import friendly_error_message
|
|
||||||
from .command import get_config_path_from_options
|
from .command import get_config_path_from_options
|
||||||
from .command import project_from_options
|
from .command import project_from_options
|
||||||
from .docopt_command import DocoptCommand
|
from .docopt_command import DocoptDispatcher
|
||||||
|
from .docopt_command import get_handler
|
||||||
from .docopt_command import NoSuchCommand
|
from .docopt_command import NoSuchCommand
|
||||||
from .errors import UserError
|
from .errors import UserError
|
||||||
from .formatter import ConsoleWarningFormatter
|
from .formatter import ConsoleWarningFormatter
|
||||||
@ -51,23 +48,16 @@ console_handler = logging.StreamHandler(sys.stderr)
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
setup_logging()
|
command = dispatch()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
command = TopLevelCommand()
|
command()
|
||||||
command.sys_dispatch()
|
|
||||||
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) as e:
|
||||||
log.error(e.msg)
|
log.error(e.msg)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except NoSuchCommand as e:
|
|
||||||
commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))
|
|
||||||
log.error("No such command: %s\n\n%s", e.command, commands)
|
|
||||||
sys.exit(1)
|
|
||||||
except APIError as e:
|
|
||||||
log_api_error(e)
|
|
||||||
sys.exit(1)
|
|
||||||
except BuildError as e:
|
except BuildError as e:
|
||||||
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
|
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -77,31 +67,42 @@ def main():
|
|||||||
except NeedsBuildError as e:
|
except NeedsBuildError as e:
|
||||||
log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name)
|
log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except ReadTimeout as e:
|
except errors.ConnectionError:
|
||||||
log.error(
|
|
||||||
"An HTTP request took too long to complete. Retry with --verbose to "
|
|
||||||
"obtain debug information.\n"
|
|
||||||
"If you encounter this issue regularly because of slow network "
|
|
||||||
"conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher "
|
|
||||||
"value (current value: %s)." % HTTP_TIMEOUT
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def log_api_error(e):
|
def dispatch():
|
||||||
if 'client is newer than server' in e.explanation:
|
setup_logging()
|
||||||
# we need JSON formatted errors. In the meantime...
|
dispatcher = DocoptDispatcher(
|
||||||
# TODO: fix this by refactoring project dispatch
|
TopLevelCommand,
|
||||||
# http://github.com/docker/compose/pull/2832#commitcomment-15923800
|
{'options_first': True, 'version': get_version_info('compose')})
|
||||||
client_version = e.explanation.split('client API version: ')[1].split(',')[0]
|
|
||||||
log.error(
|
try:
|
||||||
"The engine version is lesser than the minimum required by "
|
options, handler, command_options = dispatcher.parse(sys.argv[1:])
|
||||||
"compose. Your current project requires a Docker Engine of "
|
except NoSuchCommand as e:
|
||||||
"version {version} or superior.".format(
|
commands = "\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))
|
||||||
version=API_VERSION_TO_ENGINE_VERSION[client_version]
|
log.error("No such command: %s\n\n%s", e.command, commands)
|
||||||
))
|
sys.exit(1)
|
||||||
else:
|
|
||||||
log.error(e.explanation)
|
setup_console_handler(console_handler, options.get('--verbose'))
|
||||||
|
return functools.partial(perform_command, options, handler, command_options)
|
||||||
|
|
||||||
|
|
||||||
|
def perform_command(options, handler, command_options):
|
||||||
|
if options['COMMAND'] in ('help', 'version'):
|
||||||
|
# Skip looking up the compose file.
|
||||||
|
handler(command_options)
|
||||||
|
return
|
||||||
|
|
||||||
|
if options['COMMAND'] == 'config':
|
||||||
|
command = TopLevelCommand(None)
|
||||||
|
handler(command, options, command_options)
|
||||||
|
return
|
||||||
|
|
||||||
|
project = project_from_options('.', options)
|
||||||
|
command = TopLevelCommand(project)
|
||||||
|
with errors.handle_connection_errors(project.client):
|
||||||
|
handler(command, command_options)
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
@ -134,7 +135,7 @@ def parse_doc_section(name, source):
|
|||||||
return [s.strip() for s in pattern.findall(source)]
|
return [s.strip() for s in pattern.findall(source)]
|
||||||
|
|
||||||
|
|
||||||
class TopLevelCommand(DocoptCommand):
|
class TopLevelCommand(object):
|
||||||
"""Define and run multi-container applications with Docker.
|
"""Define and run multi-container applications with Docker.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@ -171,30 +172,12 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
up Create and start containers
|
up Create and start containers
|
||||||
version Show the Docker-Compose version information
|
version Show the Docker-Compose version information
|
||||||
"""
|
"""
|
||||||
base_dir = '.'
|
|
||||||
|
|
||||||
def docopt_options(self):
|
def __init__(self, project, project_dir='.'):
|
||||||
options = super(TopLevelCommand, self).docopt_options()
|
self.project = project
|
||||||
options['version'] = get_version_info('compose')
|
self.project_dir = '.'
|
||||||
return options
|
|
||||||
|
|
||||||
def perform_command(self, options, handler, command_options):
|
def build(self, options):
|
||||||
setup_console_handler(console_handler, options.get('--verbose'))
|
|
||||||
|
|
||||||
if options['COMMAND'] in ('help', 'version'):
|
|
||||||
# Skip looking up the compose file.
|
|
||||||
handler(None, command_options)
|
|
||||||
return
|
|
||||||
|
|
||||||
if options['COMMAND'] == 'config':
|
|
||||||
handler(options, command_options)
|
|
||||||
return
|
|
||||||
|
|
||||||
project = project_from_options(self.base_dir, options)
|
|
||||||
with friendly_error_message():
|
|
||||||
handler(project, command_options)
|
|
||||||
|
|
||||||
def build(self, project, options):
|
|
||||||
"""
|
"""
|
||||||
Build or rebuild services.
|
Build or rebuild services.
|
||||||
|
|
||||||
@ -209,7 +192,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
--no-cache Do not use cache when building the image.
|
--no-cache Do not use cache when building the image.
|
||||||
--pull Always attempt to pull a newer version of the image.
|
--pull Always attempt to pull a newer version of the image.
|
||||||
"""
|
"""
|
||||||
project.build(
|
self.project.build(
|
||||||
service_names=options['SERVICE'],
|
service_names=options['SERVICE'],
|
||||||
no_cache=bool(options.get('--no-cache', False)),
|
no_cache=bool(options.get('--no-cache', False)),
|
||||||
pull=bool(options.get('--pull', False)),
|
pull=bool(options.get('--pull', False)),
|
||||||
@ -228,7 +211,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
config_path = get_config_path_from_options(config_options)
|
config_path = get_config_path_from_options(config_options)
|
||||||
compose_config = config.load(config.find(self.base_dir, config_path))
|
compose_config = config.load(config.find(self.project_dir, config_path))
|
||||||
|
|
||||||
if options['--quiet']:
|
if options['--quiet']:
|
||||||
return
|
return
|
||||||
@ -239,7 +222,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
|
|
||||||
print(serialize_config(compose_config))
|
print(serialize_config(compose_config))
|
||||||
|
|
||||||
def create(self, project, options):
|
def create(self, options):
|
||||||
"""
|
"""
|
||||||
Creates containers for a service.
|
Creates containers for a service.
|
||||||
|
|
||||||
@ -255,13 +238,13 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
"""
|
"""
|
||||||
service_names = options['SERVICE']
|
service_names = options['SERVICE']
|
||||||
|
|
||||||
project.create(
|
self.project.create(
|
||||||
service_names=service_names,
|
service_names=service_names,
|
||||||
strategy=convergence_strategy_from_opts(options),
|
strategy=convergence_strategy_from_opts(options),
|
||||||
do_build=build_action_from_opts(options),
|
do_build=build_action_from_opts(options),
|
||||||
)
|
)
|
||||||
|
|
||||||
def down(self, project, options):
|
def down(self, options):
|
||||||
"""
|
"""
|
||||||
Stop containers and remove containers, networks, volumes, and images
|
Stop containers and remove containers, networks, volumes, and images
|
||||||
created by `up`. Only containers and networks are removed by default.
|
created by `up`. Only containers and networks are removed by default.
|
||||||
@ -275,9 +258,9 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
-v, --volumes Remove data volumes
|
-v, --volumes Remove data volumes
|
||||||
"""
|
"""
|
||||||
image_type = image_type_from_opt('--rmi', options['--rmi'])
|
image_type = image_type_from_opt('--rmi', options['--rmi'])
|
||||||
project.down(image_type, options['--volumes'])
|
self.project.down(image_type, options['--volumes'])
|
||||||
|
|
||||||
def events(self, project, options):
|
def events(self, options):
|
||||||
"""
|
"""
|
||||||
Receive real time events from containers.
|
Receive real time events from containers.
|
||||||
|
|
||||||
@ -296,12 +279,12 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
event['time'] = event['time'].isoformat()
|
event['time'] = event['time'].isoformat()
|
||||||
return json.dumps(event)
|
return json.dumps(event)
|
||||||
|
|
||||||
for event in project.events():
|
for event in self.project.events():
|
||||||
formatter = json_format_event if options['--json'] else format_event
|
formatter = json_format_event if options['--json'] else format_event
|
||||||
print(formatter(event))
|
print(formatter(event))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
def exec_command(self, project, options):
|
def exec_command(self, options):
|
||||||
"""
|
"""
|
||||||
Execute a command in a running container
|
Execute a command in a running container
|
||||||
|
|
||||||
@ -317,7 +300,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
instances of a service [default: 1]
|
instances of a service [default: 1]
|
||||||
"""
|
"""
|
||||||
index = int(options.get('--index'))
|
index = int(options.get('--index'))
|
||||||
service = project.get_service(options['SERVICE'])
|
service = self.project.get_service(options['SERVICE'])
|
||||||
try:
|
try:
|
||||||
container = service.get_container(number=index)
|
container = service.get_container(number=index)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -341,27 +324,28 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
signals.set_signal_handler_to_shutdown()
|
signals.set_signal_handler_to_shutdown()
|
||||||
try:
|
try:
|
||||||
operation = ExecOperation(
|
operation = ExecOperation(
|
||||||
project.client,
|
self.project.client,
|
||||||
exec_id,
|
exec_id,
|
||||||
interactive=tty,
|
interactive=tty,
|
||||||
)
|
)
|
||||||
pty = PseudoTerminal(project.client, operation)
|
pty = PseudoTerminal(self.project.client, operation)
|
||||||
pty.start()
|
pty.start()
|
||||||
except signals.ShutdownException:
|
except signals.ShutdownException:
|
||||||
log.info("received shutdown exception: closing")
|
log.info("received shutdown exception: closing")
|
||||||
exit_code = project.client.exec_inspect(exec_id).get("ExitCode")
|
exit_code = self.project.client.exec_inspect(exec_id).get("ExitCode")
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
def help(self, project, options):
|
@classmethod
|
||||||
|
def help(cls, options):
|
||||||
"""
|
"""
|
||||||
Get help on a command.
|
Get help on a command.
|
||||||
|
|
||||||
Usage: help COMMAND
|
Usage: help COMMAND
|
||||||
"""
|
"""
|
||||||
handler = self.get_handler(options['COMMAND'])
|
handler = get_handler(cls, options['COMMAND'])
|
||||||
raise SystemExit(getdoc(handler))
|
raise SystemExit(getdoc(handler))
|
||||||
|
|
||||||
def kill(self, project, options):
|
def kill(self, options):
|
||||||
"""
|
"""
|
||||||
Force stop service containers.
|
Force stop service containers.
|
||||||
|
|
||||||
@ -373,9 +357,9 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
"""
|
"""
|
||||||
signal = options.get('-s', 'SIGKILL')
|
signal = options.get('-s', 'SIGKILL')
|
||||||
|
|
||||||
project.kill(service_names=options['SERVICE'], signal=signal)
|
self.project.kill(service_names=options['SERVICE'], signal=signal)
|
||||||
|
|
||||||
def logs(self, project, options):
|
def logs(self, options):
|
||||||
"""
|
"""
|
||||||
View output from containers.
|
View output from containers.
|
||||||
|
|
||||||
@ -388,7 +372,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
--tail="all" Number of lines to show from the end of the logs
|
--tail="all" Number of lines to show from the end of the logs
|
||||||
for each container.
|
for each container.
|
||||||
"""
|
"""
|
||||||
containers = project.containers(service_names=options['SERVICE'], stopped=True)
|
containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||||
|
|
||||||
monochrome = options['--no-color']
|
monochrome = options['--no-color']
|
||||||
tail = options['--tail']
|
tail = options['--tail']
|
||||||
@ -405,16 +389,16 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
print("Attaching to", list_containers(containers))
|
print("Attaching to", list_containers(containers))
|
||||||
LogPrinter(containers, monochrome=monochrome, log_args=log_args).run()
|
LogPrinter(containers, monochrome=monochrome, log_args=log_args).run()
|
||||||
|
|
||||||
def pause(self, project, options):
|
def pause(self, options):
|
||||||
"""
|
"""
|
||||||
Pause services.
|
Pause services.
|
||||||
|
|
||||||
Usage: pause [SERVICE...]
|
Usage: pause [SERVICE...]
|
||||||
"""
|
"""
|
||||||
containers = project.pause(service_names=options['SERVICE'])
|
containers = self.project.pause(service_names=options['SERVICE'])
|
||||||
exit_if(not containers, 'No containers to pause', 1)
|
exit_if(not containers, 'No containers to pause', 1)
|
||||||
|
|
||||||
def port(self, project, options):
|
def port(self, options):
|
||||||
"""
|
"""
|
||||||
Print the public port for a port binding.
|
Print the public port for a port binding.
|
||||||
|
|
||||||
@ -426,7 +410,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
instances of a service [default: 1]
|
instances of a service [default: 1]
|
||||||
"""
|
"""
|
||||||
index = int(options.get('--index'))
|
index = int(options.get('--index'))
|
||||||
service = project.get_service(options['SERVICE'])
|
service = self.project.get_service(options['SERVICE'])
|
||||||
try:
|
try:
|
||||||
container = service.get_container(number=index)
|
container = service.get_container(number=index)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -435,7 +419,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
options['PRIVATE_PORT'],
|
options['PRIVATE_PORT'],
|
||||||
protocol=options.get('--protocol') or 'tcp') or '')
|
protocol=options.get('--protocol') or 'tcp') or '')
|
||||||
|
|
||||||
def ps(self, project, options):
|
def ps(self, options):
|
||||||
"""
|
"""
|
||||||
List containers.
|
List containers.
|
||||||
|
|
||||||
@ -445,8 +429,8 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
-q Only display IDs
|
-q Only display IDs
|
||||||
"""
|
"""
|
||||||
containers = sorted(
|
containers = sorted(
|
||||||
project.containers(service_names=options['SERVICE'], stopped=True) +
|
self.project.containers(service_names=options['SERVICE'], stopped=True) +
|
||||||
project.containers(service_names=options['SERVICE'], one_off=True),
|
self.project.containers(service_names=options['SERVICE'], one_off=True),
|
||||||
key=attrgetter('name'))
|
key=attrgetter('name'))
|
||||||
|
|
||||||
if options['-q']:
|
if options['-q']:
|
||||||
@ -472,7 +456,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
])
|
])
|
||||||
print(Formatter().table(headers, rows))
|
print(Formatter().table(headers, rows))
|
||||||
|
|
||||||
def pull(self, project, options):
|
def pull(self, options):
|
||||||
"""
|
"""
|
||||||
Pulls images for services.
|
Pulls images for services.
|
||||||
|
|
||||||
@ -481,12 +465,12 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
Options:
|
Options:
|
||||||
--ignore-pull-failures Pull what it can and ignores images with pull failures.
|
--ignore-pull-failures Pull what it can and ignores images with pull failures.
|
||||||
"""
|
"""
|
||||||
project.pull(
|
self.project.pull(
|
||||||
service_names=options['SERVICE'],
|
service_names=options['SERVICE'],
|
||||||
ignore_pull_failures=options.get('--ignore-pull-failures')
|
ignore_pull_failures=options.get('--ignore-pull-failures')
|
||||||
)
|
)
|
||||||
|
|
||||||
def rm(self, project, options):
|
def rm(self, options):
|
||||||
"""
|
"""
|
||||||
Remove stopped service containers.
|
Remove stopped service containers.
|
||||||
|
|
||||||
@ -501,21 +485,21 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
-f, --force Don't ask to confirm removal
|
-f, --force Don't ask to confirm removal
|
||||||
-v Remove volumes associated with containers
|
-v Remove volumes associated with containers
|
||||||
"""
|
"""
|
||||||
all_containers = project.containers(service_names=options['SERVICE'], stopped=True)
|
all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
|
||||||
stopped_containers = [c for c in all_containers if not c.is_running]
|
stopped_containers = [c for c in all_containers if not c.is_running]
|
||||||
|
|
||||||
if len(stopped_containers) > 0:
|
if len(stopped_containers) > 0:
|
||||||
print("Going to remove", list_containers(stopped_containers))
|
print("Going to remove", list_containers(stopped_containers))
|
||||||
if options.get('--force') \
|
if options.get('--force') \
|
||||||
or yesno("Are you sure? [yN] ", default=False):
|
or yesno("Are you sure? [yN] ", default=False):
|
||||||
project.remove_stopped(
|
self.project.remove_stopped(
|
||||||
service_names=options['SERVICE'],
|
service_names=options['SERVICE'],
|
||||||
v=options.get('-v', False)
|
v=options.get('-v', False)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print("No stopped containers")
|
print("No stopped containers")
|
||||||
|
|
||||||
def run(self, project, options):
|
def run(self, options):
|
||||||
"""
|
"""
|
||||||
Run a one-off command on a service.
|
Run a one-off command on a service.
|
||||||
|
|
||||||
@ -544,7 +528,7 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
-T Disable pseudo-tty allocation. By default `docker-compose run`
|
-T Disable pseudo-tty allocation. By default `docker-compose run`
|
||||||
allocates a TTY.
|
allocates a TTY.
|
||||||
"""
|
"""
|
||||||
service = project.get_service(options['SERVICE'])
|
service = self.project.get_service(options['SERVICE'])
|
||||||
detach = options['-d']
|
detach = options['-d']
|
||||||
|
|
||||||
if IS_WINDOWS_PLATFORM and not detach:
|
if IS_WINDOWS_PLATFORM and not detach:
|
||||||
@ -592,9 +576,9 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
if options['--name']:
|
if options['--name']:
|
||||||
container_options['name'] = options['--name']
|
container_options['name'] = options['--name']
|
||||||
|
|
||||||
run_one_off_container(container_options, project, service, options)
|
run_one_off_container(container_options, self.project, service, options)
|
||||||
|
|
||||||
def scale(self, project, options):
|
def scale(self, options):
|
||||||
"""
|
"""
|
||||||
Set number of containers to run for a service.
|
Set number of containers to run for a service.
|
||||||
|
|
||||||
@ -620,18 +604,18 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise UserError('Number of containers for service "%s" is not a '
|
raise UserError('Number of containers for service "%s" is not a '
|
||||||
'number' % service_name)
|
'number' % service_name)
|
||||||
project.get_service(service_name).scale(num, timeout=timeout)
|
self.project.get_service(service_name).scale(num, timeout=timeout)
|
||||||
|
|
||||||
def start(self, project, options):
|
def start(self, options):
|
||||||
"""
|
"""
|
||||||
Start existing containers.
|
Start existing containers.
|
||||||
|
|
||||||
Usage: start [SERVICE...]
|
Usage: start [SERVICE...]
|
||||||
"""
|
"""
|
||||||
containers = project.start(service_names=options['SERVICE'])
|
containers = self.project.start(service_names=options['SERVICE'])
|
||||||
exit_if(not containers, 'No containers to start', 1)
|
exit_if(not containers, 'No containers to start', 1)
|
||||||
|
|
||||||
def stop(self, project, options):
|
def stop(self, options):
|
||||||
"""
|
"""
|
||||||
Stop running containers without removing them.
|
Stop running containers without removing them.
|
||||||
|
|
||||||
@ -644,9 +628,9 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
(default: 10)
|
(default: 10)
|
||||||
"""
|
"""
|
||||||
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
|
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
|
||||||
project.stop(service_names=options['SERVICE'], timeout=timeout)
|
self.project.stop(service_names=options['SERVICE'], timeout=timeout)
|
||||||
|
|
||||||
def restart(self, project, options):
|
def restart(self, options):
|
||||||
"""
|
"""
|
||||||
Restart running containers.
|
Restart running containers.
|
||||||
|
|
||||||
@ -657,19 +641,19 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
(default: 10)
|
(default: 10)
|
||||||
"""
|
"""
|
||||||
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
|
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
|
||||||
containers = project.restart(service_names=options['SERVICE'], timeout=timeout)
|
containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout)
|
||||||
exit_if(not containers, 'No containers to restart', 1)
|
exit_if(not containers, 'No containers to restart', 1)
|
||||||
|
|
||||||
def unpause(self, project, options):
|
def unpause(self, options):
|
||||||
"""
|
"""
|
||||||
Unpause services.
|
Unpause services.
|
||||||
|
|
||||||
Usage: unpause [SERVICE...]
|
Usage: unpause [SERVICE...]
|
||||||
"""
|
"""
|
||||||
containers = project.unpause(service_names=options['SERVICE'])
|
containers = self.project.unpause(service_names=options['SERVICE'])
|
||||||
exit_if(not containers, 'No containers to unpause', 1)
|
exit_if(not containers, 'No containers to unpause', 1)
|
||||||
|
|
||||||
def up(self, project, options):
|
def up(self, options):
|
||||||
"""
|
"""
|
||||||
Builds, (re)creates, starts, and attaches to containers for a service.
|
Builds, (re)creates, starts, and attaches to containers for a service.
|
||||||
|
|
||||||
@ -719,8 +703,8 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
if detached and cascade_stop:
|
if detached and cascade_stop:
|
||||||
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
||||||
|
|
||||||
with up_shutdown_context(project, service_names, timeout, detached):
|
with up_shutdown_context(self.project, service_names, timeout, detached):
|
||||||
to_attach = project.up(
|
to_attach = self.project.up(
|
||||||
service_names=service_names,
|
service_names=service_names,
|
||||||
start_deps=start_deps,
|
start_deps=start_deps,
|
||||||
strategy=convergence_strategy_from_opts(options),
|
strategy=convergence_strategy_from_opts(options),
|
||||||
@ -737,9 +721,10 @@ class TopLevelCommand(DocoptCommand):
|
|||||||
|
|
||||||
if cascade_stop:
|
if cascade_stop:
|
||||||
print("Aborting on container exit...")
|
print("Aborting on container exit...")
|
||||||
project.stop(service_names=service_names, timeout=timeout)
|
self.project.stop(service_names=service_names, timeout=timeout)
|
||||||
|
|
||||||
def version(self, project, options):
|
@classmethod
|
||||||
|
def version(cls, options):
|
||||||
"""
|
"""
|
||||||
Show version informations
|
Show version informations
|
||||||
|
|
||||||
|
@ -4,28 +4,12 @@ from __future__ import unicode_literals
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from requests.exceptions import ConnectionError
|
|
||||||
|
|
||||||
from compose.cli import errors
|
|
||||||
from compose.cli.command import friendly_error_message
|
|
||||||
from compose.cli.command import get_config_path_from_options
|
from compose.cli.command import get_config_path_from_options
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
from tests import mock
|
from tests import mock
|
||||||
|
|
||||||
|
|
||||||
class TestFriendlyErrorMessage(object):
|
|
||||||
|
|
||||||
def test_dispatch_generic_connection_error(self):
|
|
||||||
with pytest.raises(errors.ConnectionErrorGeneric):
|
|
||||||
with mock.patch(
|
|
||||||
'compose.cli.command.call_silently',
|
|
||||||
autospec=True,
|
|
||||||
side_effect=[0, 1]
|
|
||||||
):
|
|
||||||
with friendly_error_message():
|
|
||||||
raise ConnectionError()
|
|
||||||
|
|
||||||
|
|
||||||
class TestGetConfigPathFromOptions(object):
|
class TestGetConfigPathFromOptions(object):
|
||||||
|
|
||||||
def test_path_from_options(self):
|
def test_path_from_options(self):
|
||||||
|
51
tests/unit/cli/errors_test.py
Normal file
51
tests/unit/cli/errors_test.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from docker.errors import APIError
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
from compose.cli import errors
|
||||||
|
from compose.cli.errors import handle_connection_errors
|
||||||
|
from tests import mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture
|
||||||
|
def mock_logging():
|
||||||
|
with mock.patch('compose.cli.errors.log', autospec=True) as mock_log:
|
||||||
|
yield mock_log
|
||||||
|
|
||||||
|
|
||||||
|
def patch_call_silently(side_effect):
|
||||||
|
return mock.patch(
|
||||||
|
'compose.cli.errors.call_silently',
|
||||||
|
autospec=True,
|
||||||
|
side_effect=side_effect)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHandleConnectionErrors(object):
|
||||||
|
|
||||||
|
def test_generic_connection_error(self, mock_logging):
|
||||||
|
with pytest.raises(errors.ConnectionError):
|
||||||
|
with patch_call_silently([0, 1]):
|
||||||
|
with handle_connection_errors(mock.Mock()):
|
||||||
|
raise ConnectionError()
|
||||||
|
|
||||||
|
_, args, _ = mock_logging.error.mock_calls[0]
|
||||||
|
assert "Couldn't connect to Docker daemon at" in args[0]
|
||||||
|
|
||||||
|
def test_api_error_version_mismatch(self, mock_logging):
|
||||||
|
with pytest.raises(errors.ConnectionError):
|
||||||
|
with handle_connection_errors(mock.Mock(api_version='1.22')):
|
||||||
|
raise APIError(None, None, "client is newer than server")
|
||||||
|
|
||||||
|
_, args, _ = mock_logging.error.mock_calls[0]
|
||||||
|
assert "Docker Engine of version 1.10.0 or greater" in args[0]
|
||||||
|
|
||||||
|
def test_api_error_version_other(self, mock_logging):
|
||||||
|
msg = "Something broke!"
|
||||||
|
with pytest.raises(errors.ConnectionError):
|
||||||
|
with handle_connection_errors(mock.Mock(api_version='1.22')):
|
||||||
|
raise APIError(None, None, msg)
|
||||||
|
|
||||||
|
mock_logging.error.assert_called_once_with(msg)
|
@ -3,6 +3,8 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from compose import container
|
from compose import container
|
||||||
from compose.cli.errors import UserError
|
from compose.cli.errors import UserError
|
||||||
from compose.cli.formatter import ConsoleWarningFormatter
|
from compose.cli.formatter import ConsoleWarningFormatter
|
||||||
@ -11,7 +13,6 @@ from compose.cli.main import convergence_strategy_from_opts
|
|||||||
from compose.cli.main import setup_console_handler
|
from compose.cli.main import setup_console_handler
|
||||||
from compose.service import ConvergenceStrategy
|
from compose.service import ConvergenceStrategy
|
||||||
from tests import mock
|
from tests import mock
|
||||||
from tests import unittest
|
|
||||||
|
|
||||||
|
|
||||||
def mock_container(service, number):
|
def mock_container(service, number):
|
||||||
@ -22,7 +23,14 @@ def mock_container(service, number):
|
|||||||
name_without_project='{0}_{1}'.format(service, number))
|
name_without_project='{0}_{1}'.format(service, number))
|
||||||
|
|
||||||
|
|
||||||
class CLIMainTestCase(unittest.TestCase):
|
@pytest.fixture
|
||||||
|
def logging_handler():
|
||||||
|
stream = mock.Mock()
|
||||||
|
stream.isatty.return_value = True
|
||||||
|
return logging.StreamHandler(stream=stream)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLIMainTestCase(object):
|
||||||
|
|
||||||
def test_build_log_printer(self):
|
def test_build_log_printer(self):
|
||||||
containers = [
|
containers = [
|
||||||
@ -34,7 +42,7 @@ class CLIMainTestCase(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
service_names = ['web', 'db']
|
service_names = ['web', 'db']
|
||||||
log_printer = build_log_printer(containers, service_names, True, False, {'follow': True})
|
log_printer = build_log_printer(containers, service_names, True, False, {'follow': True})
|
||||||
self.assertEqual(log_printer.containers, containers[:3])
|
assert log_printer.containers == containers[:3]
|
||||||
|
|
||||||
def test_build_log_printer_all_services(self):
|
def test_build_log_printer_all_services(self):
|
||||||
containers = [
|
containers = [
|
||||||
@ -44,58 +52,53 @@ class CLIMainTestCase(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
service_names = []
|
service_names = []
|
||||||
log_printer = build_log_printer(containers, service_names, True, False, {'follow': True})
|
log_printer = build_log_printer(containers, service_names, True, False, {'follow': True})
|
||||||
self.assertEqual(log_printer.containers, containers)
|
assert log_printer.containers == containers
|
||||||
|
|
||||||
|
|
||||||
class SetupConsoleHandlerTestCase(unittest.TestCase):
|
class TestSetupConsoleHandlerTestCase(object):
|
||||||
|
|
||||||
def setUp(self):
|
def test_with_tty_verbose(self, logging_handler):
|
||||||
self.stream = mock.Mock()
|
setup_console_handler(logging_handler, True)
|
||||||
self.stream.isatty.return_value = True
|
assert type(logging_handler.formatter) == ConsoleWarningFormatter
|
||||||
self.handler = logging.StreamHandler(stream=self.stream)
|
assert '%(name)s' in logging_handler.formatter._fmt
|
||||||
|
assert '%(funcName)s' in logging_handler.formatter._fmt
|
||||||
|
|
||||||
def test_with_tty_verbose(self):
|
def test_with_tty_not_verbose(self, logging_handler):
|
||||||
setup_console_handler(self.handler, True)
|
setup_console_handler(logging_handler, False)
|
||||||
assert type(self.handler.formatter) == ConsoleWarningFormatter
|
assert type(logging_handler.formatter) == ConsoleWarningFormatter
|
||||||
assert '%(name)s' in self.handler.formatter._fmt
|
assert '%(name)s' not in logging_handler.formatter._fmt
|
||||||
assert '%(funcName)s' in self.handler.formatter._fmt
|
assert '%(funcName)s' not in logging_handler.formatter._fmt
|
||||||
|
|
||||||
def test_with_tty_not_verbose(self):
|
def test_with_not_a_tty(self, logging_handler):
|
||||||
setup_console_handler(self.handler, False)
|
logging_handler.stream.isatty.return_value = False
|
||||||
assert type(self.handler.formatter) == ConsoleWarningFormatter
|
setup_console_handler(logging_handler, False)
|
||||||
assert '%(name)s' not in self.handler.formatter._fmt
|
assert type(logging_handler.formatter) == logging.Formatter
|
||||||
assert '%(funcName)s' not in self.handler.formatter._fmt
|
|
||||||
|
|
||||||
def test_with_not_a_tty(self):
|
|
||||||
self.stream.isatty.return_value = False
|
|
||||||
setup_console_handler(self.handler, False)
|
|
||||||
assert type(self.handler.formatter) == logging.Formatter
|
|
||||||
|
|
||||||
|
|
||||||
class ConvergeStrategyFromOptsTestCase(unittest.TestCase):
|
class TestConvergeStrategyFromOptsTestCase(object):
|
||||||
|
|
||||||
def test_invalid_opts(self):
|
def test_invalid_opts(self):
|
||||||
options = {'--force-recreate': True, '--no-recreate': True}
|
options = {'--force-recreate': True, '--no-recreate': True}
|
||||||
with self.assertRaises(UserError):
|
with pytest.raises(UserError):
|
||||||
convergence_strategy_from_opts(options)
|
convergence_strategy_from_opts(options)
|
||||||
|
|
||||||
def test_always(self):
|
def test_always(self):
|
||||||
options = {'--force-recreate': True, '--no-recreate': False}
|
options = {'--force-recreate': True, '--no-recreate': False}
|
||||||
self.assertEqual(
|
assert (
|
||||||
convergence_strategy_from_opts(options),
|
convergence_strategy_from_opts(options) ==
|
||||||
ConvergenceStrategy.always
|
ConvergenceStrategy.always
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_never(self):
|
def test_never(self):
|
||||||
options = {'--force-recreate': False, '--no-recreate': True}
|
options = {'--force-recreate': False, '--no-recreate': True}
|
||||||
self.assertEqual(
|
assert (
|
||||||
convergence_strategy_from_opts(options),
|
convergence_strategy_from_opts(options) ==
|
||||||
ConvergenceStrategy.never
|
ConvergenceStrategy.never
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_changed(self):
|
def test_changed(self):
|
||||||
options = {'--force-recreate': False, '--no-recreate': False}
|
options = {'--force-recreate': False, '--no-recreate': False}
|
||||||
self.assertEqual(
|
assert (
|
||||||
convergence_strategy_from_opts(options),
|
convergence_strategy_from_opts(options) ==
|
||||||
ConvergenceStrategy.changed
|
ConvergenceStrategy.changed
|
||||||
)
|
)
|
||||||
|
@ -64,26 +64,20 @@ class CLITestCase(unittest.TestCase):
|
|||||||
self.assertTrue(project.client)
|
self.assertTrue(project.client)
|
||||||
self.assertTrue(project.services)
|
self.assertTrue(project.services)
|
||||||
|
|
||||||
def test_help(self):
|
|
||||||
command = TopLevelCommand()
|
|
||||||
with self.assertRaises(SystemExit):
|
|
||||||
command.dispatch(['-h'])
|
|
||||||
|
|
||||||
def test_command_help(self):
|
def test_command_help(self):
|
||||||
with self.assertRaises(SystemExit) as ctx:
|
with pytest.raises(SystemExit) as exc:
|
||||||
TopLevelCommand().dispatch(['help', 'up'])
|
TopLevelCommand.help({'COMMAND': 'up'})
|
||||||
|
|
||||||
self.assertIn('Usage: up', str(ctx.exception))
|
assert 'Usage: up' in exc.exconly()
|
||||||
|
|
||||||
def test_command_help_nonexistent(self):
|
def test_command_help_nonexistent(self):
|
||||||
with self.assertRaises(NoSuchCommand):
|
with pytest.raises(NoSuchCommand):
|
||||||
TopLevelCommand().dispatch(['help', 'nonexistent'])
|
TopLevelCommand.help({'COMMAND': 'nonexistent'})
|
||||||
|
|
||||||
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason="requires dockerpty")
|
||||||
@mock.patch('compose.cli.main.RunOperation', autospec=True)
|
@mock.patch('compose.cli.main.RunOperation', autospec=True)
|
||||||
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
||||||
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
|
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
|
||||||
command = TopLevelCommand()
|
|
||||||
mock_client = mock.create_autospec(docker.Client)
|
mock_client = mock.create_autospec(docker.Client)
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
@ -92,9 +86,10 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'service': {'image': 'busybox'}
|
'service': {'image': 'busybox'}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
command = TopLevelCommand(project)
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
command.run(project, {
|
command.run({
|
||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
@ -126,8 +121,8 @@ class CLITestCase(unittest.TestCase):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
command = TopLevelCommand()
|
command = TopLevelCommand(project)
|
||||||
command.run(project, {
|
command.run({
|
||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
@ -147,8 +142,8 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'always'
|
'always'
|
||||||
)
|
)
|
||||||
|
|
||||||
command = TopLevelCommand()
|
command = TopLevelCommand(project)
|
||||||
command.run(project, {
|
command.run({
|
||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
@ -168,7 +163,6 @@ class CLITestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_command_manula_and_service_ports_together(self):
|
def test_command_manula_and_service_ports_together(self):
|
||||||
command = TopLevelCommand()
|
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
client=None,
|
client=None,
|
||||||
@ -176,9 +170,10 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'service': {'image': 'busybox'},
|
'service': {'image': 'busybox'},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
command = TopLevelCommand(project)
|
||||||
|
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
command.run(project, {
|
command.run({
|
||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user