Skip to content

Commit 4f89ca7

Browse files
committed
ClientBase class to extract utility methods and constructor and sanitize Client class
1 parent d66369c commit 4f89ca7

File tree

3 files changed

+241
-227
lines changed

3 files changed

+241
-227
lines changed

docker/client.py

Lines changed: 5 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -12,246 +12,22 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import json
1615
import os
1716
import re
1817
import shlex
19-
import struct
2018
import warnings
2119
from datetime import datetime
2220

23-
import requests
24-
import requests.exceptions
2521
import six
26-
import websocket
27-
2822

23+
from . import clientbase
2924
from . import constants
3025
from . import errors
3126
from .auth import auth
32-
from .unixconn import unixconn
33-
from .ssladapter import ssladapter
3427
from .utils import utils, check_resource
35-
from .tls import TLSConfig
36-
37-
38-
class Client(requests.Session):
39-
def __init__(self, base_url=None, version=None,
40-
timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False):
41-
super(Client, self).__init__()
42-
43-
if tls and not base_url.startswith('https://'):
44-
raise errors.TLSParameterError(
45-
'If using TLS, the base_url argument must begin with '
46-
'"https://".')
47-
48-
self.base_url = base_url
49-
self.timeout = timeout
50-
51-
self._auth_configs = auth.load_config()
52-
53-
base_url = utils.parse_host(base_url)
54-
if base_url.startswith('http+unix://'):
55-
self._adapter = unixconn.UnixAdapter(base_url, timeout)
56-
self.mount('http+docker://', self._adapter)
57-
self.base_url = 'http+docker://localunixsocket'
58-
else:
59-
# Use SSLAdapter for the ability to specify SSL version
60-
if isinstance(tls, TLSConfig):
61-
tls.configure_client(self)
62-
elif tls:
63-
self._adapter = ssladapter.SSLAdapter()
64-
self.mount('https://', self._adapter)
65-
self.base_url = base_url
66-
67-
# version detection needs to be after unix adapter mounting
68-
if version is None:
69-
self._version = constants.DEFAULT_DOCKER_API_VERSION
70-
elif isinstance(version, six.string_types):
71-
if version.lower() == 'auto':
72-
self._version = self._retrieve_server_version()
73-
else:
74-
self._version = version
75-
else:
76-
raise errors.DockerException(
77-
'Version parameter must be a string or None. Found {0}'.format(
78-
type(version).__name__
79-
)
80-
)
81-
82-
def _retrieve_server_version(self):
83-
try:
84-
return self.version(api_version=False)["ApiVersion"]
85-
except KeyError:
86-
raise errors.DockerException(
87-
'Invalid response from docker daemon: key "ApiVersion"'
88-
' is missing.'
89-
)
90-
except Exception as e:
91-
raise errors.DockerException(
92-
'Error while fetching server API version: {0}'.format(e)
93-
)
94-
95-
def _set_request_timeout(self, kwargs):
96-
"""Prepare the kwargs for an HTTP request by inserting the timeout
97-
parameter, if not already present."""
98-
kwargs.setdefault('timeout', self.timeout)
99-
return kwargs
100-
101-
def _post(self, url, **kwargs):
102-
return self.post(url, **self._set_request_timeout(kwargs))
103-
104-
def _get(self, url, **kwargs):
105-
return self.get(url, **self._set_request_timeout(kwargs))
106-
107-
def _delete(self, url, **kwargs):
108-
return self.delete(url, **self._set_request_timeout(kwargs))
10928

110-
def _url(self, path, versioned_api=True):
111-
if versioned_api:
112-
return '{0}/v{1}{2}'.format(self.base_url, self._version, path)
113-
else:
114-
return '{0}{1}'.format(self.base_url, path)
115-
116-
def _raise_for_status(self, response, explanation=None):
117-
"""Raises stored :class:`APIError`, if one occurred."""
118-
try:
119-
response.raise_for_status()
120-
except requests.exceptions.HTTPError as e:
121-
raise errors.APIError(e, response, explanation=explanation)
122-
123-
def _result(self, response, json=False, binary=False):
124-
assert not (json and binary)
125-
self._raise_for_status(response)
126-
127-
if json:
128-
return response.json()
129-
if binary:
130-
return response.content
131-
return response.text
132-
133-
def _post_json(self, url, data, **kwargs):
134-
# Go <1.1 can't unserialize null to a string
135-
# so we do this disgusting thing here.
136-
data2 = {}
137-
if data is not None:
138-
for k, v in six.iteritems(data):
139-
if v is not None:
140-
data2[k] = v
141-
142-
if 'headers' not in kwargs:
143-
kwargs['headers'] = {}
144-
kwargs['headers']['Content-Type'] = 'application/json'
145-
return self._post(url, data=json.dumps(data2), **kwargs)
146-
147-
def _attach_params(self, override=None):
148-
return override or {
149-
'stdout': 1,
150-
'stderr': 1,
151-
'stream': 1
152-
}
153-
154-
@check_resource
155-
def _attach_websocket(self, container, params=None):
156-
url = self._url("/containers/{0}/attach/ws".format(container))
157-
req = requests.Request("POST", url, params=self._attach_params(params))
158-
full_url = req.prepare().url
159-
full_url = full_url.replace("http://", "ws://", 1)
160-
full_url = full_url.replace("https://", "wss://", 1)
161-
return self._create_websocket_connection(full_url)
162-
163-
def _create_websocket_connection(self, url):
164-
return websocket.create_connection(url)
165-
166-
def _get_raw_response_socket(self, response):
167-
self._raise_for_status(response)
168-
if six.PY3:
169-
sock = response.raw._fp.fp.raw
170-
else:
171-
sock = response.raw._fp.fp._sock
172-
try:
173-
# Keep a reference to the response to stop it being garbage
174-
# collected. If the response is garbage collected, it will
175-
# close TLS sockets.
176-
sock._response = response
177-
except AttributeError:
178-
# UNIX sockets can't have attributes set on them, but that's
179-
# fine because we won't be doing TLS over them
180-
pass
181-
182-
return sock
183-
184-
def _stream_helper(self, response, decode=False):
185-
"""Generator for data coming from a chunked-encoded HTTP response."""
186-
if response.raw._fp.chunked:
187-
reader = response.raw
188-
while not reader.closed:
189-
# this read call will block until we get a chunk
190-
data = reader.read(1)
191-
if not data:
192-
break
193-
if reader._fp.chunk_left:
194-
data += reader.read(reader._fp.chunk_left)
195-
if decode:
196-
if six.PY3:
197-
data = data.decode('utf-8')
198-
data = json.loads(data)
199-
yield data
200-
else:
201-
# Response isn't chunked, meaning we probably
202-
# encountered an error immediately
203-
yield self._result(response)
204-
205-
def _multiplexed_buffer_helper(self, response):
206-
"""A generator of multiplexed data blocks read from a buffered
207-
response."""
208-
buf = self._result(response, binary=True)
209-
walker = 0
210-
while True:
211-
if len(buf[walker:]) < 8:
212-
break
213-
_, length = struct.unpack_from('>BxxxL', buf[walker:])
214-
start = walker + constants.STREAM_HEADER_SIZE_BYTES
215-
end = start + length
216-
walker = end
217-
yield buf[start:end]
218-
219-
def _multiplexed_response_stream_helper(self, response):
220-
"""A generator of multiplexed data blocks coming from a response
221-
stream."""
222-
223-
# Disable timeout on the underlying socket to prevent
224-
# Read timed out(s) for long running processes
225-
socket = self._get_raw_response_socket(response)
226-
if six.PY3:
227-
socket._sock.settimeout(None)
228-
else:
229-
socket.settimeout(None)
230-
231-
while True:
232-
header = response.raw.read(constants.STREAM_HEADER_SIZE_BYTES)
233-
if not header:
234-
break
235-
_, length = struct.unpack('>BxxxL', header)
236-
if not length:
237-
break
238-
data = response.raw.read(length)
239-
if not data:
240-
break
241-
yield data
242-
243-
@property
244-
def api_version(self):
245-
return self._version
246-
247-
def get_adapter(self, url):
248-
try:
249-
return super(Client, self).get_adapter(url)
250-
except requests.exceptions.InvalidSchema as e:
251-
if self._adapter:
252-
return self._adapter
253-
raise e
25429

30+
class Client(clientbase.ClientBase):
25531
@check_resource
25632
def attach(self, container, stdout=True, stderr=True,
25733
stream=False, logs=False):
@@ -745,7 +521,9 @@ def inspect_container(self, container):
745521
@check_resource
746522
def inspect_image(self, image):
747523
return self._result(
748-
self._get(self._url("/images/{0}/json".format(image))),
524+
self._get(
525+
self._url("/images/{0}/json".format(image.replace('/', '%2F')))
526+
),
749527
True
750528
)
751529

0 commit comments

Comments
 (0)