summaryrefslogtreecommitdiff
path: root/compose/cli/errors.py
blob: 1506aa66078d6c312a84d88f30f298233ca58b99 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
from __future__ import absolute_import
from __future__ import unicode_literals

import contextlib
import logging
import socket
from distutils.spawn import find_executable
from textwrap import dedent

import six
from docker.errors import APIError
from requests.exceptions import ConnectionError as RequestsConnectionError
from requests.exceptions import ReadTimeout
from requests.exceptions import SSLError
from requests.packages.urllib3.exceptions import ReadTimeoutError

from ..const import API_VERSION_TO_ENGINE_VERSION
from .utils import is_docker_for_mac_installed
from .utils import is_mac
from .utils import is_ubuntu
from .utils import is_windows


log = logging.getLogger(__name__)


class UserError(Exception):

    def __init__(self, msg):
        self.msg = dedent(msg).strip()

    def __unicode__(self):
        return self.msg

    __str__ = __unicode__


class ConnectionError(Exception):
    pass


@contextlib.contextmanager
def handle_connection_errors(client):
    try:
        yield
    except SSLError as e:
        log.error('SSL error: %s' % e)
        raise ConnectionError()
    except RequestsConnectionError as e:
        if e.args and isinstance(e.args[0], ReadTimeoutError):
            log_timeout_error(client.timeout)
            raise ConnectionError()
        exit_with_error(get_conn_error_message(client.base_url))
    except APIError as e:
        log_api_error(e, client.api_version)
        raise ConnectionError()
    except (ReadTimeout, socket.timeout) as e:
        log_timeout_error(client.timeout)
        raise ConnectionError()
    except Exception as e:
        if is_windows():
            import pywintypes
            if isinstance(e, pywintypes.error):
                log_windows_pipe_error(e)
                raise ConnectionError()
        raise


def log_windows_pipe_error(exc):
    if exc.winerror == 232:  # https://github.com/docker/compose/issues/5005
        log.error(
            "The current Compose file version is not compatible with your engine version. "
            "Please upgrade your Compose file to a more recent version, or set "
            "a COMPOSE_API_VERSION in your environment."
        )
    else:
        log.error(
            "Windows named pipe error: {} (code: {})".format(exc.strerror, exc.winerror)
        )


def log_timeout_error(timeout):
    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)." % timeout)


def log_api_error(e, client_version):
    explanation = e.explanation
    if isinstance(explanation, six.binary_type):
        explanation = explanation.decode('utf-8')

    if 'client is newer than server' not in explanation:
        log.error(explanation)
        return

    version = API_VERSION_TO_ENGINE_VERSION.get(client_version)
    if not version:
        # They've set a custom API version
        log.error(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()


def get_conn_error_message(url):
    if find_executable('docker') is None:
        return docker_not_found_msg("Couldn't connect to Docker daemon.")
    if is_docker_for_mac_installed():
        return conn_error_docker_for_mac
    if find_executable('docker-machine') is not None:
        return conn_error_docker_machine
    return conn_error_generic.format(url=url)


def docker_not_found_msg(problem):
    return "{} You might need to install Docker:\n\n{}".format(
        problem, docker_install_url())


def docker_install_url():
    if is_mac():
        return docker_install_url_mac
    elif is_ubuntu():
        return docker_install_url_ubuntu
    elif is_windows():
        return docker_install_url_windows
    else:
        return docker_install_url_generic


docker_install_url_mac = "https://docs.docker.com/engine/installation/mac/"
docker_install_url_ubuntu = "https://docs.docker.com/engine/installation/ubuntulinux/"
docker_install_url_windows = "https://docs.docker.com/engine/installation/windows/"
docker_install_url_generic = "https://docs.docker.com/engine/installation/"


conn_error_docker_machine = """
    Couldn't connect to Docker daemon - you might need to run `docker-machine start default`.
"""

conn_error_docker_for_mac = """
    Couldn't connect to Docker daemon. You might need to start Docker for Mac.
"""


conn_error_generic = """
    Couldn't connect to Docker daemon at {url} - is it running?

    If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
"""