Skip to content

Commit 70728c4

Browse files
author
Matthew Stoltenberg
committed
add support to BaseClient for blob upload
* change BaseClientV2._http_response to accept bindata which is passed directly to requests * add _Manifest.from_file to support loading a manifest from a file then calling put_manifest * add BaseClientV2.put_blob based on implementation in python-dxf
1 parent 8abf6b0 commit 70728c4

File tree

4 files changed

+76
-3
lines changed

4 files changed

+76
-3
lines changed

docker_registry_client/_BaseClient.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import logging
2-
from requests import get, put, delete
2+
from requests import codes, get, head, post, put, delete
33
from requests.exceptions import HTTPError
44
import json
55
from .AuthorizationService import AuthorizationService
6+
from .digest import docker_digest
67
from .manifest import sign as sign_manifest
78

9+
try:
10+
import urllib.parse as urlparse
11+
from urllib.parse import urlencode
12+
except ImportError:
13+
from urllib import urlencode
14+
import urlparse
15+
816
# urllib3 throws some ssl warnings with older versions of python
917
# they're probably ok for the registry client to ignore
1018
import warnings
@@ -139,6 +147,14 @@ def __init__(self, content, type, digest):
139147
self._type = type
140148
self._digest = digest
141149

150+
@classmethod
151+
def from_file(cls, fpath=None, fobj=None):
152+
digest = docker_digest(fpath, fobj)
153+
if fobj is None:
154+
fobj = open(fpath, 'rb')
155+
return cls(json.loads(fobj.read().decode()),
156+
'application/json', digest)
157+
142158

143159
BASE_CONTENT_TYPE = 'application/vnd.docker.distribution.manifest'
144160

@@ -147,6 +163,7 @@ class BaseClientV2(CommonBaseClient):
147163
LIST_TAGS = '/v2/{name}/tags/list'
148164
MANIFEST = '/v2/{name}/manifests/{reference}'
149165
BLOB = '/v2/{name}/blobs/{digest}'
166+
BLOB_UPLOAD = '/v2/{name}/blobs/uploads/'
150167
schema_1_signed = BASE_CONTENT_TYPE + '.v1+prettyjws'
151168
schema_1 = BASE_CONTENT_TYPE + '.v1+json'
152169
schema_2 = BASE_CONTENT_TYPE + '.v2+json'
@@ -208,6 +225,31 @@ def put_manifest(self, name, reference, manifest):
208225
name=name, reference=reference,
209226
)
210227

228+
def put_blob(self, name, fpath=None, fobj=None):
229+
self.auth.desired_scope = 'repository:%s:*' % name
230+
digest = docker_digest(fpath, fobj)
231+
try:
232+
self._http_call(self.BLOB, head,
233+
name=name, digest=digest)
234+
return digest
235+
except HTTPError as exc:
236+
if exc.response.status_code != codes.not_found:
237+
raise
238+
if fobj is None:
239+
fobj = open(fpath, 'rb')
240+
resp = self._http_response(self.BLOB_UPLOAD, post,
241+
name=name)
242+
parts = list(urlparse.urlparse(resp.headers['Location']))
243+
query = urlparse.parse_qs(parts[4])
244+
query.update({'digest': digest})
245+
parts[0] = '' # scheme
246+
parts[1] = '' # netloc
247+
parts[4] = urlencode(query, True)
248+
self._http_call(urlparse.urlunparse(parts),
249+
put, bindata=fobj,
250+
name=name, digest=digest,
251+
content_type='application/octet-stream')
252+
211253
def delete_manifest(self, name, digest):
212254
self.auth.desired_scope = 'repository:%s:*' % name
213255
return self._http_call(self.MANIFEST, delete,
@@ -227,7 +269,7 @@ def _cache_manifest_digest(self, name, reference, response=None):
227269
self._manifest_digests[(name, reference)] = untrusted_digest
228270

229271
def _http_response(self, url, method, data=None, content_type=None,
230-
schema=None, **kwargs):
272+
bindata=None, schema=None, **kwargs):
231273
"""url -> full target url
232274
method -> method from requests
233275
data -> request body
@@ -258,6 +300,8 @@ def _http_response(self, url, method, data=None, content_type=None,
258300

259301
if data and not content_type:
260302
data = json.dumps(data)
303+
if bindata:
304+
data = bindata
261305

262306
path = url.format(**kwargs)
263307
logger.debug("%s %s", method.__name__.upper(), path)

docker_registry_client/digest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import hashlib
2+
3+
4+
def docker_digest(fpath=None, fobj=None, prepend=True):
5+
if fobj is None:
6+
fobj = open(fpath, 'rb')
7+
hasher = hashlib.sha256()
8+
for chunk in iter(lambda: fobj.read(8192), b''):
9+
hasher.update(chunk)
10+
fobj.seek(0)
11+
retval = hasher.hexdigest()
12+
if prepend:
13+
retval = 'sha256:' + retval
14+
return retval

tests/test_base_client.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import absolute_import
2+
from io import BytesIO
23

3-
from docker_registry_client._BaseClient import BaseClientV1, BaseClientV2
4+
from docker_registry_client._BaseClient import (
5+
BaseClientV1, BaseClientV2, _Manifest
6+
)
47
from drc_test_utils.mock_registry import (
58
mock_v1_registry, mock_v2_registry, TEST_NAME, TEST_TAG,
69
)
@@ -21,3 +24,14 @@ def test_get_manifest_and_digest(self):
2124
url = mock_v2_registry()
2225
manifest, digest = BaseClientV2(url).get_manifest_and_digest(TEST_NAME,
2326
TEST_TAG)
27+
28+
29+
class TestManifest(object):
30+
FAKE_MANIFEST = b'{ "schemaVersion": 2 }'
31+
FAKE_DIGEST = ('sha256:0467ad45d6957ca671e3d219aa5965d4'
32+
'7f8621823a80c8e76174bcc1b9a225fd')
33+
34+
def test_fromfile(self):
35+
fobj = BytesIO(self.FAKE_MANIFEST)
36+
manifest = _Manifest.from_file(fobj=fobj)
37+
assert manifest._digest == self.FAKE_DIGEST

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ deps =
77
docker-py==1.10.6
88
flexmock==0.10.2
99
pytest==3.0.5
10+
ecdsa==0.13
1011

1112
[testenv:lint]
1213
deps =

0 commit comments

Comments
 (0)