Skip to content

Commit 20b5575

Browse files
committed
Check presence of errors in server response to image push
Fixes: docker#3277 Signed-off-by: Francesco Zardi <frazar0@hotmail.it>
1 parent bcf3e11 commit 20b5575

File tree

4 files changed

+93
-1
lines changed

4 files changed

+93
-1
lines changed

docker/api/image.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import itertools
2+
import json
13
import logging
24
import os
35

46
from .. import auth, errors, utils
57
from ..constants import DEFAULT_DATA_CHUNK_SIZE
8+
from ..utils.json_stream import json_stream
69

710
log = logging.getLogger(__name__)
811

@@ -433,6 +436,16 @@ def pull(self, repository, tag=None, stream=False, auth_config=None,
433436

434437
return self._result(response)
435438

439+
@staticmethod
440+
def _raise_if_error(chunk, response):
441+
"""
442+
Raise an exception if the specified chunk of a JSON response contains an
443+
error message.
444+
"""
445+
if isinstance(chunk, dict) and 'error' in chunk:
446+
raise errors.APIError(chunk['error'], response=response)
447+
return chunk
448+
436449
def push(self, repository, tag=None, stream=False, auth_config=None,
437450
decode=False):
438451
"""
@@ -495,7 +508,20 @@ def push(self, repository, tag=None, stream=False, auth_config=None,
495508
self._raise_for_status(response)
496509

497510
if stream:
498-
return self._stream_helper(response, decode=decode)
511+
if decode:
512+
return (self._raise_if_error(chunk, response) for chunk in
513+
self._stream_helper(response, decode=True))
514+
else:
515+
result_stream, internal_stream = itertools.tee(
516+
self._stream_helper(response, decode=False))
517+
for chunk_json in json_stream(internal_stream):
518+
self._raise_if_error(chunk_json, response)
519+
return result_stream
520+
521+
for chunk_str in response.text.splitlines():
522+
chunk_json = json.loads(chunk_str)
523+
if 'error' in chunk_json:
524+
raise errors.APIError(chunk_json['error'], response=response)
499525

500526
return self._result(response)
501527

tests/unit/api_image_test.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,33 @@ def test_push_image_with_auth(self):
271271
timeout=DEFAULT_TIMEOUT_SECONDS
272272
)
273273

274+
275+
def test_push_image_with_auth_error(self):
276+
auth_config = {
277+
'username': "test_user",
278+
'password': "test_password",
279+
'serveraddress': "test_server",
280+
}
281+
encoded_auth = auth.encode_header(auth_config)
282+
with pytest.raises(docker.errors.APIError, match='bad auth'):
283+
self.client.push(
284+
fake_api.FAKE_IMAGE_NAME_ERROR, tag=fake_api.FAKE_TAG_NAME,
285+
auth_config=auth_config
286+
)
287+
288+
fake_request.assert_called_with(
289+
'POST',
290+
f"{url_prefix}images/test_image_error/push",
291+
params={
292+
'tag': fake_api.FAKE_TAG_NAME,
293+
},
294+
data='{}',
295+
headers={'Content-Type': 'application/json',
296+
'X-Registry-Auth': encoded_auth},
297+
stream=False,
298+
timeout=DEFAULT_TIMEOUT_SECONDS
299+
)
300+
274301
def test_push_image_stream(self):
275302
with mock.patch('docker.auth.resolve_authconfig',
276303
fake_resolve_authconfig):
@@ -315,6 +342,32 @@ def test_push_image_stream_with_auth(self):
315342
)
316343

317344

345+
def test_push_image_stream_with_auth_error(self):
346+
auth_config = {
347+
'username': "test_user",
348+
'password': "test_password",
349+
'serveraddress': "test_server",
350+
}
351+
encoded_auth = auth.encode_header(auth_config)
352+
with pytest.raises(docker.errors.APIError, match='bad auth'):
353+
self.client.push(
354+
fake_api.FAKE_IMAGE_NAME_ERROR, tag=fake_api.FAKE_TAG_NAME,
355+
auth_config=auth_config, stream=True
356+
)
357+
358+
fake_request.assert_called_with(
359+
'POST',
360+
f"{url_prefix}images/test_image_error/push",
361+
params={
362+
'tag': fake_api.FAKE_TAG_NAME,
363+
},
364+
data='{}',
365+
headers={'Content-Type': 'application/json',
366+
'X-Registry-Auth': encoded_auth},
367+
stream=True,
368+
timeout=DEFAULT_TIMEOUT_SECONDS
369+
)
370+
318371
def test_tag_image(self):
319372
self.client.tag(fake_api.FAKE_IMAGE_ID, fake_api.FAKE_REPO_NAME)
320373

tests/unit/api_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def response(status_code=200, content='', headers=None, reason=None, elapsed=0,
3131
request=None, raw=None):
3232
res = requests.Response()
3333
res.status_code = status_code
34+
if isinstance(content, str):
35+
content = content.encode('ascii')
3436
if not isinstance(content, bytes):
3537
content = json.dumps(content).encode('ascii')
3638
res._content = content

tests/unit/fake_api.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
from docker import constants
24

35
from . import fake_stat
@@ -9,6 +11,7 @@
911
FAKE_EXEC_ID = 'b098ec855f10434b5c7c973c78484208223a83f663ddaefb0f02a242840cb1c7'
1012
FAKE_NETWORK_ID = '1999cfb42e414483841a125ade3c276c3cb80cb3269b14e339354ac63a31b02c'
1113
FAKE_IMAGE_NAME = 'test_image'
14+
FAKE_IMAGE_NAME_ERROR = 'test_image_error'
1215
FAKE_TARBALL_PATH = '/path/to/tarball'
1316
FAKE_REPO_NAME = 'repo'
1417
FAKE_TAG_NAME = 'tag'
@@ -358,6 +361,12 @@ def post_fake_push():
358361
response = {'Id': FAKE_IMAGE_ID}
359362
return status_code, response
360363

364+
def post_fake_push_error():
365+
status_code = 200
366+
response = "\r\n".join(
367+
json.dumps(c) for c in [{'Id': FAKE_IMAGE_ID}, {'error': 'bad auth'}])
368+
return status_code, response
369+
361370

362371
def post_fake_build_container():
363372
status_code = 200
@@ -603,6 +612,8 @@ def post_fake_config():
603612
get_fake_insert_image,
604613
f'{prefix}/{CURRENT_VERSION}/images/test_image/push':
605614
post_fake_push,
615+
f'{prefix}/{CURRENT_VERSION}/images/test_image_error/push':
616+
post_fake_push_error,
606617
f'{prefix}/{CURRENT_VERSION}/commit':
607618
post_fake_commit,
608619
f'{prefix}/{CURRENT_VERSION}/containers/create':

0 commit comments

Comments
 (0)