Skip to content

Commit c485bd8

Browse files
committed
Merge pull request #752 from docker/697-stricter-url-construction
Stricter url construction
2 parents 33acb9d + 3d884f9 commit c485bd8

File tree

3 files changed

+79
-46
lines changed

3 files changed

+79
-46
lines changed

docker/client.py

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def attach(self, container, stdout=True, stderr=True,
4141
'stderr': stderr and 1 or 0,
4242
'stream': stream and 1 or 0,
4343
}
44-
u = self._url("/containers/{0}/attach".format(container))
44+
u = self._url("/containers/{0}/attach", container)
4545
response = self._post(u, params=params, stream=stream)
4646

4747
return self._get_result(container, stream, response)
@@ -58,7 +58,7 @@ def attach_socket(self, container, params=None, ws=False):
5858
if ws:
5959
return self._attach_websocket(container, params)
6060

61-
u = self._url("/containers/{0}/attach".format(container))
61+
u = self._url("/containers/{0}/attach", container)
6262
return self._get_raw_response_socket(self.post(
6363
u, None, params=self._attach_params(params), stream=True))
6464

@@ -275,8 +275,9 @@ def create_host_config(self, *args, **kwargs):
275275

276276
@check_resource
277277
def diff(self, container):
278-
return self._result(self._get(self._url("/containers/{0}/changes".
279-
format(container))), True)
278+
return self._result(
279+
self._get(self._url("/containers/{0}/changes", container)), True
280+
)
280281

281282
def events(self, since=None, until=None, filters=None, decode=None):
282283
if isinstance(since, datetime):
@@ -326,7 +327,7 @@ def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
326327
'Cmd': cmd
327328
}
328329

329-
url = self._url('/containers/{0}/exec'.format(container))
330+
url = self._url('/containers/{0}/exec', container)
330331
res = self._post_json(url, data=data)
331332
return self._result(res, True)
332333

@@ -337,7 +338,7 @@ def exec_inspect(self, exec_id):
337338
)
338339
if isinstance(exec_id, dict):
339340
exec_id = exec_id.get('Id')
340-
res = self._get(self._url("/exec/{0}/json".format(exec_id)))
341+
res = self._get(self._url("/exec/{0}/json", exec_id))
341342
return self._result(res, True)
342343

343344
def exec_resize(self, exec_id, height=None, width=None):
@@ -347,7 +348,7 @@ def exec_resize(self, exec_id, height=None, width=None):
347348
exec_id = exec_id.get('Id')
348349

349350
params = {'h': height, 'w': width}
350-
url = self._url("/exec/{0}/resize".format(exec_id))
351+
url = self._url("/exec/{0}/resize", exec_id)
351352
res = self._post(url, params=params)
352353
self._raise_for_status(res)
353354

@@ -362,27 +363,28 @@ def exec_start(self, exec_id, detach=False, tty=False, stream=False):
362363
'Detach': detach
363364
}
364365

365-
res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)),
366-
data=data, stream=stream)
366+
res = self._post_json(
367+
self._url('/exec/{0}/start', exec_id), data=data, stream=stream
368+
)
367369
return self._get_result_tty(stream, res, tty)
368370

369371
@check_resource
370372
def export(self, container):
371-
res = self._get(self._url("/containers/{0}/export".format(container)),
372-
stream=True)
373+
res = self._get(
374+
self._url("/containers/{0}/export", container), stream=True
375+
)
373376
self._raise_for_status(res)
374377
return res.raw
375378

376379
@check_resource
377380
def get_image(self, image):
378-
res = self._get(self._url("/images/{0}/get".format(image)),
379-
stream=True)
381+
res = self._get(self._url("/images/{0}/get", image), stream=True)
380382
self._raise_for_status(res)
381383
return res.raw
382384

383385
@check_resource
384386
def history(self, image):
385-
res = self._get(self._url("/images/{0}/history".format(image)))
387+
res = self._get(self._url("/images/{0}/history", image))
386388
return self._result(res, True)
387389

388390
def images(self, name=None, quiet=False, all=False, viz=False,
@@ -496,7 +498,7 @@ def insert(self, image, url, path):
496498
raise errors.DeprecatedMethod(
497499
'insert is not available for API version >=1.12'
498500
)
499-
api_url = self._url("/images/{0}/insert".format(image))
501+
api_url = self._url("/images/{0}/insert", image)
500502
params = {
501503
'url': url,
502504
'path': path
@@ -506,21 +508,18 @@ def insert(self, image, url, path):
506508
@check_resource
507509
def inspect_container(self, container):
508510
return self._result(
509-
self._get(self._url("/containers/{0}/json".format(container))),
510-
True)
511+
self._get(self._url("/containers/{0}/json", container)), True
512+
)
511513

512514
@check_resource
513515
def inspect_image(self, image):
514516
return self._result(
515-
self._get(
516-
self._url("/images/{0}/json".format(image.replace('/', '%2F')))
517-
),
518-
True
517+
self._get(self._url("/images/{0}/json", image)), True
519518
)
520519

521520
@check_resource
522521
def kill(self, container, signal=None):
523-
url = self._url("/containers/{0}/kill".format(container))
522+
url = self._url("/containers/{0}/kill", container)
524523
params = {}
525524
if signal is not None:
526525
params['signal'] = signal
@@ -583,7 +582,7 @@ def logs(self, container, stdout=True, stderr=True, stream=False,
583582
if tail != 'all' and (not isinstance(tail, int) or tail <= 0):
584583
tail = 'all'
585584
params['tail'] = tail
586-
url = self._url("/containers/{0}/logs".format(container))
585+
url = self._url("/containers/{0}/logs", container)
587586
res = self._get(url, params=params, stream=stream)
588587
return self._get_result(container, stream, res)
589588
return self.attach(
@@ -596,7 +595,7 @@ def logs(self, container, stdout=True, stderr=True, stream=False,
596595

597596
@check_resource
598597
def pause(self, container):
599-
url = self._url('/containers/{0}/pause'.format(container))
598+
url = self._url('/containers/{0}/pause', container)
600599
res = self._post(url)
601600
self._raise_for_status(res)
602601

@@ -605,7 +604,7 @@ def ping(self):
605604

606605
@check_resource
607606
def port(self, container, private_port):
608-
res = self._get(self._url("/containers/{0}/json".format(container)))
607+
res = self._get(self._url("/containers/{0}/json", container))
609608
self._raise_for_status(res)
610609
json_ = res.json()
611610
s_port = str(private_port)
@@ -692,7 +691,7 @@ def push(self, repository, tag=None, stream=False,
692691
if not tag:
693692
repository, tag = utils.parse_repository_tag(repository)
694693
registry, repo_name = auth.resolve_repository_name(repository)
695-
u = self._url("/images/{0}/push".format(repository))
694+
u = self._url("/images/{0}/push", repository)
696695
params = {
697696
'tag': tag
698697
}
@@ -725,14 +724,15 @@ def push(self, repository, tag=None, stream=False,
725724
@check_resource
726725
def remove_container(self, container, v=False, link=False, force=False):
727726
params = {'v': v, 'link': link, 'force': force}
728-
res = self._delete(self._url("/containers/" + container),
729-
params=params)
727+
res = self._delete(
728+
self._url("/containers/{0}", container), params=params
729+
)
730730
self._raise_for_status(res)
731731

732732
@check_resource
733733
def remove_image(self, image, force=False, noprune=False):
734734
params = {'force': force, 'noprune': noprune}
735-
res = self._delete(self._url("/images/" + image), params=params)
735+
res = self._delete(self._url("/images/{0}", image), params=params)
736736
self._raise_for_status(res)
737737

738738
@check_resource
@@ -741,29 +741,30 @@ def rename(self, container, name):
741741
raise errors.InvalidVersion(
742742
'rename was only introduced in API version 1.17'
743743
)
744-
url = self._url("/containers/{0}/rename".format(container))
744+
url = self._url("/containers/{0}/rename", container)
745745
params = {'name': name}
746746
res = self._post(url, params=params)
747747
self._raise_for_status(res)
748748

749749
@check_resource
750750
def resize(self, container, height, width):
751751
params = {'h': height, 'w': width}
752-
url = self._url("/containers/{0}/resize".format(container))
752+
url = self._url("/containers/{0}/resize", container)
753753
res = self._post(url, params=params)
754754
self._raise_for_status(res)
755755

756756
@check_resource
757757
def restart(self, container, timeout=10):
758758
params = {'t': timeout}
759-
url = self._url("/containers/{0}/restart".format(container))
759+
url = self._url("/containers/{0}/restart", container)
760760
res = self._post(url, params=params)
761761
self._raise_for_status(res)
762762

763763
def search(self, term):
764-
return self._result(self._get(self._url("/images/search"),
765-
params={'term': term}),
766-
True)
764+
return self._result(
765+
self._get(self._url("/images/search"), params={'term': term}),
766+
True
767+
)
767768

768769
@check_resource
769770
def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
@@ -829,7 +830,7 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
829830
)
830831
start_config = self.create_host_config(**start_config_kwargs)
831832

832-
url = self._url("/containers/{0}/start".format(container))
833+
url = self._url("/containers/{0}/start", container)
833834
res = self._post_json(url, data=start_config)
834835
self._raise_for_status(res)
835836

@@ -839,13 +840,13 @@ def stats(self, container, decode=None):
839840
raise errors.InvalidVersion(
840841
'Stats retrieval is not supported in API < 1.17!')
841842

842-
url = self._url("/containers/{0}/stats".format(container))
843+
url = self._url("/containers/{0}/stats", container)
843844
return self._stream_helper(self._get(url, stream=True), decode=decode)
844845

845846
@check_resource
846847
def stop(self, container, timeout=10):
847848
params = {'t': timeout}
848-
url = self._url("/containers/{0}/stop".format(container))
849+
url = self._url("/containers/{0}/stop", container)
849850

850851
res = self._post(url, params=params,
851852
timeout=(timeout + (self.timeout or 0)))
@@ -858,14 +859,14 @@ def tag(self, image, repository, tag=None, force=False):
858859
'repo': repository,
859860
'force': 1 if force else 0
860861
}
861-
url = self._url("/images/{0}/tag".format(image))
862+
url = self._url("/images/{0}/tag", image)
862863
res = self._post(url, params=params)
863864
self._raise_for_status(res)
864865
return res.status_code == 201
865866

866867
@check_resource
867868
def top(self, container):
868-
u = self._url("/containers/{0}/top".format(container))
869+
u = self._url("/containers/{0}/top", container)
869870
return self._result(self._get(u), True)
870871

871872
def version(self, api_version=True):
@@ -874,13 +875,13 @@ def version(self, api_version=True):
874875

875876
@check_resource
876877
def unpause(self, container):
877-
url = self._url('/containers/{0}/unpause'.format(container))
878+
url = self._url('/containers/{0}/unpause', container)
878879
res = self._post(url)
879880
self._raise_for_status(res)
880881

881882
@check_resource
882883
def wait(self, container, timeout=None):
883-
url = self._url("/containers/{0}/wait".format(container))
884+
url = self._url("/containers/{0}/wait", container)
884885
res = self._post(url, timeout=timeout)
885886
self._raise_for_status(res)
886887
json_ = res.json()

docker/clientbase.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,21 @@ def _get(self, url, **kwargs):
8888
def _delete(self, url, **kwargs):
8989
return self.delete(url, **self._set_request_timeout(kwargs))
9090

91-
def _url(self, path, versioned_api=True):
91+
def _url(self, pathfmt, resource_id=None, versioned_api=True):
92+
if resource_id and not isinstance(resource_id, six.string_types):
93+
raise ValueError(
94+
'Expected a resource ID string but found {0} ({1}) '
95+
'instead'.format(resource_id, type(resource_id))
96+
)
97+
elif resource_id:
98+
resource_id = six.moves.urllib.parse.quote_plus(resource_id)
99+
92100
if versioned_api:
93-
return '{0}/v{1}{2}'.format(self.base_url, self._version, path)
101+
return '{0}/v{1}{2}'.format(
102+
self.base_url, self._version, pathfmt.format(resource_id)
103+
)
94104
else:
95-
return '{0}{1}'.format(self.base_url, path)
105+
return '{0}{1}'.format(self.base_url, pathfmt.format(resource_id))
96106

97107
def _raise_for_status(self, response, explanation=None):
98108
"""Raises stored :class:`APIError`, if one occurred."""
@@ -136,7 +146,7 @@ def _attach_params(self, override=None):
136146

137147
@check_resource
138148
def _attach_websocket(self, container, params=None):
139-
url = self._url("/containers/{0}/attach/ws".format(container))
149+
url = self._url("/containers/{0}/attach/ws", container)
140150
req = requests.Request("POST", url, params=self._attach_params(params))
141151
full_url = req.prepare().url
142152
full_url = full_url.replace("http://", "ws://", 1)

tests/test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ def test_ctor(self):
144144
'Version parameter must be a string or None. Found float'
145145
)
146146

147+
def test_url_valid_resource(self):
148+
url = self.client._url('/hello/{0}/world', 'somename')
149+
self.assertEqual(
150+
url, '{0}{1}'.format(url_prefix, 'hello/somename/world')
151+
)
152+
153+
url = self.client._url('/hello/{0}/world', '/some?name')
154+
self.assertEqual(
155+
url, '{0}{1}'.format(url_prefix, 'hello/%2Fsome%3Fname/world')
156+
)
157+
158+
def test_url_invalid_resource(self):
159+
with pytest.raises(ValueError):
160+
self.client._url('/hello/{0}/world', ['sakuya', 'izayoi'])
161+
162+
def test_url_no_resource(self):
163+
url = self.client._url('/simple')
164+
self.assertEqual(url, '{0}{1}'.format(url_prefix, 'simple'))
165+
166+
url = self.client._url('/simple', None)
167+
self.assertEqual(url, '{0}{1}'.format(url_prefix, 'simple'))
168+
147169
#########################
148170
# INFORMATION TESTS #
149171
#########################

0 commit comments

Comments
 (0)