|
|
@@ -24,6 +24,7 @@ from fig.packages import six
|
|
|
from .auth import auth
|
|
|
from .unixconn import unixconn
|
|
|
from .utils import utils
|
|
|
+from . import errors
|
|
|
|
|
|
if not six.PY3:
|
|
|
import websocket
|
|
|
@@ -33,41 +34,6 @@ DEFAULT_TIMEOUT_SECONDS = 60
|
|
|
STREAM_HEADER_SIZE_BYTES = 8
|
|
|
|
|
|
|
|
|
-class APIError(requests.exceptions.HTTPError):
|
|
|
- def __init__(self, message, response, explanation=None):
|
|
|
- # requests 1.2 supports response as a keyword argument, but
|
|
|
- # requests 1.1 doesn't
|
|
|
- super(APIError, self).__init__(message)
|
|
|
- self.response = response
|
|
|
-
|
|
|
- self.explanation = explanation
|
|
|
-
|
|
|
- if self.explanation is None and response.content:
|
|
|
- self.explanation = response.content.strip()
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- message = super(APIError, self).__str__()
|
|
|
-
|
|
|
- if self.is_client_error():
|
|
|
- message = '%s Client Error: %s' % (
|
|
|
- self.response.status_code, self.response.reason)
|
|
|
-
|
|
|
- elif self.is_server_error():
|
|
|
- message = '%s Server Error: %s' % (
|
|
|
- self.response.status_code, self.response.reason)
|
|
|
-
|
|
|
- if self.explanation:
|
|
|
- message = '%s ("%s")' % (message, self.explanation)
|
|
|
-
|
|
|
- return message
|
|
|
-
|
|
|
- def is_client_error(self):
|
|
|
- return 400 <= self.response.status_code < 500
|
|
|
-
|
|
|
- def is_server_error(self):
|
|
|
- return 500 <= self.response.status_code < 600
|
|
|
-
|
|
|
-
|
|
|
class Client(requests.Session):
|
|
|
def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION,
|
|
|
timeout=DEFAULT_TIMEOUT_SECONDS):
|
|
|
@@ -112,7 +78,7 @@ class Client(requests.Session):
|
|
|
try:
|
|
|
response.raise_for_status()
|
|
|
except requests.exceptions.HTTPError as e:
|
|
|
- raise APIError(e, response, explanation=explanation)
|
|
|
+ raise errors.APIError(e, response, explanation=explanation)
|
|
|
|
|
|
def _result(self, response, json=False, binary=False):
|
|
|
assert not (json and binary)
|
|
|
@@ -239,9 +205,23 @@ class Client(requests.Session):
|
|
|
|
|
|
def _stream_helper(self, response):
|
|
|
"""Generator for data coming from a chunked-encoded HTTP response."""
|
|
|
- for line in response.iter_lines(chunk_size=32):
|
|
|
- if line:
|
|
|
- yield line
|
|
|
+ socket_fp = self._get_raw_response_socket(response)
|
|
|
+ socket_fp.setblocking(1)
|
|
|
+ socket = socket_fp.makefile()
|
|
|
+ while True:
|
|
|
+ # Because Docker introduced newlines at the end of chunks in v0.9,
|
|
|
+ # and only on some API endpoints, we have to cater for both cases.
|
|
|
+ size_line = socket.readline()
|
|
|
+ if size_line == '\r\n':
|
|
|
+ size_line = socket.readline()
|
|
|
+
|
|
|
+ size = int(size_line, 16)
|
|
|
+ if size <= 0:
|
|
|
+ break
|
|
|
+ data = socket.readline()
|
|
|
+ if not data:
|
|
|
+ break
|
|
|
+ yield data
|
|
|
|
|
|
def _multiplexed_buffer_helper(self, response):
|
|
|
"""A generator of multiplexed data blocks read from a buffered
|
|
|
@@ -341,7 +321,7 @@ class Client(requests.Session):
|
|
|
nocache=False, rm=False, stream=False, timeout=None):
|
|
|
remote = context = headers = None
|
|
|
if path is None and fileobj is None:
|
|
|
- raise Exception("Either path or fileobj needs to be provided.")
|
|
|
+ raise TypeError("Either path or fileobj needs to be provided.")
|
|
|
|
|
|
if fileobj is not None:
|
|
|
context = utils.mkbuildcontext(fileobj)
|
|
|
@@ -714,8 +694,12 @@ class Client(requests.Session):
|
|
|
}
|
|
|
if binds:
|
|
|
bind_pairs = [
|
|
|
- '{0}:{1}'.format(host, dest) for host, dest in binds.items()
|
|
|
+ '%s:%s:%s' % (
|
|
|
+ h, d['bind'],
|
|
|
+ 'ro' if 'ro' in d and d['ro'] else 'rw'
|
|
|
+ ) for h, d in binds.items()
|
|
|
]
|
|
|
+
|
|
|
start_config['Binds'] = bind_pairs
|
|
|
|
|
|
if volumes_from and not isinstance(volumes_from, six.string_types):
|