New tests and small fixes while comparing against the V2 spec
Fixes #391
This commit is contained in:
parent
41bfe2ffde
commit
decdaa4c79
6 changed files with 232 additions and 33 deletions
|
@ -19,6 +19,7 @@ from endpoints.api import api_bp
|
|||
from initdb import wipe_database, initialize_database, populate_database
|
||||
from endpoints.csrf import generate_csrf_token
|
||||
from tempfile import NamedTemporaryFile
|
||||
from jsonschema import validate as validate_schema
|
||||
|
||||
import endpoints.decorated
|
||||
import json
|
||||
|
@ -295,8 +296,52 @@ class V1RegistryPullMixin(V1RegistryMixin):
|
|||
|
||||
|
||||
class V2RegistryMixin(BaseRegistryMixin):
|
||||
MANIFEST_SCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
},
|
||||
'tag': {
|
||||
'type': 'string',
|
||||
},
|
||||
'signatures': {
|
||||
'type': 'array',
|
||||
'itemType': {
|
||||
'type': 'object',
|
||||
},
|
||||
},
|
||||
'fsLayers': {
|
||||
'type': 'array',
|
||||
'itemType': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'blobSum': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
'required': 'blobSum',
|
||||
},
|
||||
},
|
||||
'history': {
|
||||
'type': 'array',
|
||||
'itemType': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'v1Compatibility': {
|
||||
'type': 'object',
|
||||
},
|
||||
},
|
||||
'required': ['v1Compatibility'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'required': ['name', 'tag', 'fsLayers', 'history', 'signatures'],
|
||||
}
|
||||
|
||||
def v2_ping(self):
|
||||
self.conduct('GET', '/v2/', expected_code=200 if self.jwt else 401, auth='jwt')
|
||||
response = self.conduct('GET', '/v2/', expected_code=200 if self.jwt else 401, auth='jwt')
|
||||
self.assertEquals(response.headers['Docker-Distribution-API-Version'], 'registry/2.0')
|
||||
|
||||
|
||||
def do_auth(self, username, password, namespace, repository, expected_code=200, scopes=[]):
|
||||
|
@ -315,9 +360,12 @@ class V2RegistryMixin(BaseRegistryMixin):
|
|||
self.assertIsNotNone(response_json.get('token'))
|
||||
self.jwt = response_json['token']
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class V2RegistryPushMixin(V2RegistryMixin):
|
||||
def do_push(self, namespace, repository, username, password, images):
|
||||
def do_push(self, namespace, repository, username, password, images, tag_name=None,
|
||||
cancel=False, invalid=False):
|
||||
# Ping!
|
||||
self.v2_ping()
|
||||
|
||||
|
@ -325,7 +373,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
self.do_auth(username, password, namespace, repository, scopes=['push', 'pull'])
|
||||
|
||||
# Build a fake manifest.
|
||||
tag_name = 'latest'
|
||||
tag_name = tag_name or 'latest'
|
||||
builder = SignedManifestBuilder(namespace, repository, tag_name)
|
||||
for image_id, contents in images.iteritems():
|
||||
if isinstance(contents, dict):
|
||||
|
@ -334,12 +382,16 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
full_contents = contents
|
||||
|
||||
checksum = 'sha256:' + hashlib.sha256(full_contents).hexdigest()
|
||||
if invalid:
|
||||
checksum = 'sha256:' + hashlib.sha256('foobarbaz').hexdigest()
|
||||
|
||||
builder.add_layer(checksum, json.dumps({'id': image_id, 'data': contents}))
|
||||
|
||||
# Build the manifest.
|
||||
manifest = builder.build(_JWK)
|
||||
|
||||
# Push the image's layers.
|
||||
checksums = {}
|
||||
for image_id, contents in images.iteritems():
|
||||
chunks = None
|
||||
if isinstance(contents, dict):
|
||||
|
@ -357,6 +409,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
response = self.conduct('POST', '/v2/%s/%s/blobs/uploads/' % (namespace, repository),
|
||||
expected_code=202, auth='jwt')
|
||||
|
||||
upload_uuid = response.headers['Docker-Upload-UUID']
|
||||
location = response.headers['Location'][len(self.get_server_url()):]
|
||||
|
||||
# PATCH the image data into the layer.
|
||||
|
@ -377,17 +430,48 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
if expected_code != 204:
|
||||
return
|
||||
|
||||
# Retrieve the upload status at each point.
|
||||
status_url = '/v2/%s/%s/blobs/uploads/%s' % (namespace, repository, upload_uuid)
|
||||
response = self.conduct('GET', status_url, expected_code=204, auth='jwt',
|
||||
headers=dict(host=self.get_server_url()))
|
||||
self.assertEquals(response.headers['Docker-Upload-UUID'], upload_uuid)
|
||||
self.assertEquals(response.headers['Range'], "bytes=0-%s" % end_byte)
|
||||
|
||||
if cancel:
|
||||
self.conduct('DELETE', location, params=dict(digest=checksum), expected_code=204,
|
||||
auth='jwt')
|
||||
|
||||
# Ensure the upload was canceled.
|
||||
status_url = '/v2/%s/%s/blobs/uploads/%s' % (namespace, repository, upload_uuid)
|
||||
self.conduct('GET', status_url, expected_code=404, auth='jwt',
|
||||
headers=dict(host=self.get_server_url()))
|
||||
return
|
||||
|
||||
# Finish the layer upload with a PUT.
|
||||
self.conduct('PUT', location, params=dict(digest=checksum), expected_code=201, auth='jwt')
|
||||
response = self.conduct('PUT', location, params=dict(digest=checksum), expected_code=201,
|
||||
auth='jwt')
|
||||
|
||||
self.assertEquals(response.headers['Docker-Content-Digest'], checksum)
|
||||
checksums[image_id] = checksum
|
||||
|
||||
# Ensure the layer exists now.
|
||||
response = self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum),
|
||||
expected_code=200, auth='jwt')
|
||||
self.assertEquals(response.headers['Docker-Content-Digest'], checksum)
|
||||
self.assertEquals(response.headers['Content-Length'], str(len(full_contents)))
|
||||
|
||||
# Write the manifest.
|
||||
put_code = 404 if invalid else 202
|
||||
self.conduct('PUT', '/v2/%s/%s/manifests/%s' % (namespace, repository, tag_name),
|
||||
data=manifest.bytes, expected_code=202,
|
||||
data=manifest.bytes, expected_code=put_code,
|
||||
headers={'Content-Type': 'application/json'}, auth='jwt')
|
||||
|
||||
return checksums, manifest.digest
|
||||
|
||||
|
||||
class V2RegistryPullMixin(V2RegistryMixin):
|
||||
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200):
|
||||
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200,
|
||||
manifest_id=None, expected_manifest_code=200):
|
||||
# Ping!
|
||||
self.v2_ping()
|
||||
|
||||
|
@ -397,11 +481,18 @@ class V2RegistryPullMixin(V2RegistryMixin):
|
|||
if expected_code != 200:
|
||||
return
|
||||
|
||||
# Retrieve the manifest for the tag.
|
||||
tag_name = 'latest'
|
||||
response = self.conduct('GET', '/v2/%s/%s/manifests/%s' % (namespace, repository, tag_name),
|
||||
auth='jwt')
|
||||
# Retrieve the manifest for the tag or digest.
|
||||
manifest_id = manifest_id or 'latest'
|
||||
response = self.conduct('GET', '/v2/%s/%s/manifests/%s' % (namespace, repository, manifest_id),
|
||||
auth='jwt', expected_code=expected_manifest_code)
|
||||
if expected_manifest_code != 200:
|
||||
return
|
||||
|
||||
manifest_data = json.loads(response.text)
|
||||
|
||||
# Ensure the manifest returned by us is valid.
|
||||
validate_schema(manifest_data, V2RegistryMixin.MANIFEST_SCHEMA)
|
||||
|
||||
blobs = {}
|
||||
|
||||
for layer in manifest_data['fsLayers']:
|
||||
|
@ -605,6 +696,47 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
|||
RegistryTestCaseMixin, LiveServerTestCase):
|
||||
""" Tests for V2 registry. """
|
||||
|
||||
def test_invalid_push(self):
|
||||
images = {
|
||||
'someid': 'onlyimagehere'
|
||||
}
|
||||
|
||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images, invalid=True)
|
||||
|
||||
def test_cancel_push(self):
|
||||
images = {
|
||||
'someid': 'onlyimagehere'
|
||||
}
|
||||
|
||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images, cancel=True)
|
||||
|
||||
|
||||
def test_pull_by_checksum(self):
|
||||
# Add a new repository under the user, so we have a real repository to pull.
|
||||
images = {
|
||||
'someid': 'onlyimagehere'
|
||||
}
|
||||
|
||||
_, digest = self.do_push('devtable', 'newrepo', 'devtable', 'password', images)
|
||||
|
||||
# Attempt to pull by digest.
|
||||
self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id=digest)
|
||||
|
||||
|
||||
def test_pull_invalid_image_tag(self):
|
||||
# Add a new repository under the user, so we have a real repository to pull.
|
||||
images = {
|
||||
'someid': 'onlyimagehere'
|
||||
}
|
||||
|
||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images)
|
||||
self.clearSession()
|
||||
|
||||
# Attempt to pull the invalid tag.
|
||||
self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id='invalid',
|
||||
expected_manifest_code=404)
|
||||
|
||||
|
||||
def test_partial_upload_below_5mb(self):
|
||||
chunksize = 1024 * 1024 * 2
|
||||
size = chunksize * 3
|
||||
|
@ -685,6 +817,34 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
|||
# Attempt to push the chunked upload, which should fail.
|
||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images)
|
||||
|
||||
def test_multiple_tags(self):
|
||||
# Create the repo.
|
||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', {'someid': 'onlyimagehere'},
|
||||
tag_name='latest')
|
||||
|
||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', {'anotherid': 'anotherimage'},
|
||||
tag_name='foobar')
|
||||
|
||||
# Retrieve the tags.
|
||||
response = self.conduct('GET', '/v2/devtable/newrepo/tags/list', auth='jwt', expected_code=200)
|
||||
data = json.loads(response.text)
|
||||
self.assertEquals(data['name'], "devtable/newrepo")
|
||||
self.assertIn('latest', data['tags'])
|
||||
self.assertIn('foobar', data['tags'])
|
||||
|
||||
# Retrieve the tags with pageniation.
|
||||
response = self.conduct('GET', '/v2/devtable/newrepo/tags/list', auth='jwt',
|
||||
params=dict(n=1), expected_code=200)
|
||||
|
||||
data = json.loads(response.text)
|
||||
self.assertEquals(data['name'], "devtable/newrepo")
|
||||
self.assertEquals(len(data['tags']), 1)
|
||||
self.assertIn('latest', data['tags'])
|
||||
self.assertTrue(response.headers['Link'].find('n=1&last=2') > 0)
|
||||
|
||||
# Try to get tags before a repo exists.
|
||||
self.conduct('GET', '/v2/devtable/doesnotexist/tags/list', auth='jwt', expected_code=401)
|
||||
|
||||
|
||||
class V1PushV2PullRegistryTests(V2RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMixin,
|
||||
RegistryTestCaseMixin, LiveServerTestCase):
|
||||
|
|
Reference in a new issue