From 5166b2c1a87aa4f09fecbe48d33bffc93b478134 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 23 Apr 2014 18:16:35 +0100 Subject: [PATCH] Update docker-py Using commit: https://github.com/aanand/docker-py/commit/b31bb4d879c8ecc37491edb9f56369c513577918 --- fig/packages/docker/__init__.py | 2 +- fig/packages/docker/auth/auth.py | 15 +++++--- fig/packages/docker/client.py | 66 ++++++++++++-------------------- fig/packages/docker/errors.py | 61 +++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 fig/packages/docker/errors.py diff --git a/fig/packages/docker/__init__.py b/fig/packages/docker/__init__.py index 5388e7286..e10a57610 100644 --- a/fig/packages/docker/__init__.py +++ b/fig/packages/docker/__init__.py @@ -15,4 +15,4 @@ __title__ = 'docker-py' __version__ = '0.3.0' -from .client import Client, APIError # flake8: noqa +from .client import Client # flake8: noqa diff --git a/fig/packages/docker/auth/auth.py b/fig/packages/docker/auth/auth.py index 69cfa89d3..36f3e4c92 100644 --- a/fig/packages/docker/auth/auth.py +++ b/fig/packages/docker/auth/auth.py @@ -20,6 +20,7 @@ import os from fig.packages import six from ..utils import utils +from .. import errors INDEX_URL = 'https://index.docker.io/v1/' DOCKER_CONFIG_FILENAME = '.dockercfg' @@ -45,18 +46,19 @@ def expand_registry_url(hostname): def resolve_repository_name(repo_name): if '://' in repo_name: - raise ValueError('Repository name cannot contain a ' - 'scheme ({0})'.format(repo_name)) + raise errors.InvalidRepository( + 'Repository name cannot contain a scheme ({0})'.format(repo_name)) parts = repo_name.split('/', 1) if '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost': # This is a docker index repo (ex: foo/bar or ubuntu) return INDEX_URL, repo_name if len(parts) < 2: - raise ValueError('Invalid repository name ({0})'.format(repo_name)) + raise errors.InvalidRepository( + 'Invalid repository name ({0})'.format(repo_name)) if 'index.docker.io' in parts[0]: - raise ValueError('Invalid repository name,' - 'try "{0}" instead'.format(parts[1])) + raise errors.InvalidRepository( + 'Invalid repository name, try "{0}" instead'.format(parts[1])) return expand_registry_url(parts[0]), parts[1] @@ -147,7 +149,8 @@ def load_config(root=None): data.append(line.strip().split(' = ')[1]) if len(data) < 2: # Not enough data - raise Exception('Invalid or empty configuration file!') + raise errors.InvalidConfigFile( + 'Invalid or empty configuration file!') username, password = decode_auth(data[0]) conf[INDEX_URL] = { diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 8b447d785..7f00b4c45 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -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): diff --git a/fig/packages/docker/errors.py b/fig/packages/docker/errors.py new file mode 100644 index 000000000..9aad700d9 --- /dev/null +++ b/fig/packages/docker/errors.py @@ -0,0 +1,61 @@ +# Copyright 2014 dotCloud inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + + +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 DockerException(Exception): + pass + + +class InvalidRepository(DockerException): + pass + + +class InvalidConfigFile(DockerException): + pass