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
|
@ -40,7 +40,11 @@ def get_tag(namespace, repository, tag):
|
|||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
|
||||
if permission.can() or model.repository.repository_is_public(namespace, repository):
|
||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
||||
try:
|
||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
||||
except model.DataModelException:
|
||||
abort(404)
|
||||
|
||||
resp = make_response('"%s"' % tag_image.docker_image_id)
|
||||
resp.headers['Content-Type'] = 'application/json'
|
||||
return resp
|
||||
|
|
|
@ -30,20 +30,6 @@ class _InvalidRangeHeader(Exception):
|
|||
pass
|
||||
|
||||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
||||
@process_jwt_auth
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def check_blob_existence(namespace, repo_name, digest):
|
||||
try:
|
||||
model.image.get_repo_image_by_storage_checksum(namespace, repo_name, digest)
|
||||
|
||||
# The response body must be empty for a successful HEAD request
|
||||
return make_response('')
|
||||
except model.InvalidImageException:
|
||||
raise BlobUnknown()
|
||||
|
||||
|
||||
def _base_blob_fetch(namespace, repo_name, digest):
|
||||
""" Some work that is common to both GET and HEAD requests. Callers MUST check for proper
|
||||
authorization before calling this method.
|
||||
|
@ -55,6 +41,7 @@ def _base_blob_fetch(namespace, repo_name, digest):
|
|||
|
||||
headers = {
|
||||
'Docker-Content-Digest': digest,
|
||||
'Content-Length': found.image_size,
|
||||
}
|
||||
|
||||
# Add the Accept-Ranges header if the storage engine supports resumable
|
||||
|
@ -76,6 +63,7 @@ def check_blob_exists(namespace, repo_name, digest):
|
|||
|
||||
response = make_response('')
|
||||
response.headers.extend(headers)
|
||||
response.headers['Content-Length'] = headers['Content-Length']
|
||||
return response
|
||||
|
||||
|
||||
|
@ -149,8 +137,9 @@ def fetch_existing_upload(namespace, repo_name, upload_uuid):
|
|||
except model.InvalidBlobUpload:
|
||||
raise BlobUploadUnknown()
|
||||
|
||||
# Note: Docker byte ranges are exclusive so we have to add one to the byte count.
|
||||
accepted = make_response('', 204)
|
||||
accepted.headers['Range'] = _render_range(found.byte_count)
|
||||
accepted.headers['Range'] = _render_range(found.byte_count + 1)
|
||||
accepted.headers['Docker-Upload-UUID'] = upload_uuid
|
||||
return accepted
|
||||
|
||||
|
@ -305,3 +294,15 @@ def cancel_upload(namespace, repo_name, upload_uuid):
|
|||
storage.cancel_chunked_upload({found.location.name}, found.uuid, found.storage_metadata)
|
||||
|
||||
return make_response('', 204)
|
||||
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/<digest>', methods=['DELETE'])
|
||||
@process_jwt_auth
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def delete_digest(namespace, repo_name, upload_uuid):
|
||||
# We do not support deleting arbitrary digests, as they break repo images.
|
||||
return make_response('', 501)
|
||||
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ class DigestInvalid(V2RegistryException):
|
|||
class ManifestBlobUnknown(V2RegistryException):
|
||||
def __init__(self, detail=None):
|
||||
super(ManifestBlobUnknown, self).__init__('MANIFEST_BLOB_UNKNOWN',
|
||||
'blob unknown to registry',
|
||||
'manifest blob unknown to registry',
|
||||
detail)
|
||||
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ from app import storage, docker_v2_signing_key
|
|||
from auth.jwt_auth import process_jwt_auth
|
||||
from endpoints.decorators import anon_protect
|
||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
|
||||
from endpoints.v2.errors import (ManifestBlobUnknown, ManifestInvalid, ManifestUnverified,
|
||||
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnverified,
|
||||
ManifestUnknown, TagInvalid, NameInvalid)
|
||||
from endpoints.trackhelper import track_and_log
|
||||
from endpoints.notificationhelper import spawn_notification
|
||||
|
@ -102,7 +102,12 @@ class SignedManifest(object):
|
|||
"""
|
||||
for blob_sum_obj, history_obj in reversed(zip(self._parsed[_FS_LAYERS_KEY],
|
||||
self._parsed[_HISTORY_KEY])):
|
||||
image_digest = digest_tools.Digest.parse_digest(blob_sum_obj[_BLOB_SUM_KEY])
|
||||
|
||||
try:
|
||||
image_digest = digest_tools.Digest.parse_digest(blob_sum_obj[_BLOB_SUM_KEY])
|
||||
except digest_tools.InvalidDigestException:
|
||||
raise ManifestInvalid()
|
||||
|
||||
metadata_string = history_obj[_V1_COMPAT_KEY]
|
||||
|
||||
v1_metadata = json.loads(metadata_string)
|
||||
|
@ -271,7 +276,7 @@ def _write_manifest(namespace, repo_name, manifest):
|
|||
leaf_layer = mdata
|
||||
|
||||
except model.InvalidImageException:
|
||||
raise ManifestBlobUnknown(detail={'missing': digest_str})
|
||||
raise BlobUnknown(detail={'digest': digest_str})
|
||||
|
||||
if leaf_layer is None:
|
||||
# The manifest doesn't actually reference any layers!
|
||||
|
|
|
@ -1,19 +1,48 @@
|
|||
# XXX This code is not yet ready to be run in production, and should remain disabled until such
|
||||
# XXX time as this notice is removed.
|
||||
|
||||
from flask import jsonify
|
||||
from flask import jsonify, request, url_for
|
||||
|
||||
from app import get_app_url
|
||||
from endpoints.v2 import v2_bp, require_repo_read
|
||||
from endpoints.v2.errors import NameUnknown
|
||||
from auth.jwt_auth import process_jwt_auth
|
||||
from endpoints.decorators import anon_protect
|
||||
from data import model
|
||||
|
||||
def _add_pagination(query, url):
|
||||
limit = request.args.get('n', None)
|
||||
page = request.args.get('page', 1)
|
||||
|
||||
if limit is None:
|
||||
return None, query
|
||||
|
||||
url = get_app_url() + url
|
||||
query = query.paginate(page, limit)
|
||||
link = url + '?n=%s&last=%s; rel="next"' % (limit, page + 1)
|
||||
return link, query
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/tags/list', methods=['GET'])
|
||||
@process_jwt_auth
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def list_all_tags(namespace, repo_name):
|
||||
return jsonify({
|
||||
repository = model.repository.get_repository(namespace, repo_name)
|
||||
if repository is None:
|
||||
raise NameUnknown()
|
||||
|
||||
query = model.tag.list_repository_tags(namespace, repo_name)
|
||||
|
||||
url = url_for('v2.list_all_tags', namespace=namespace, repo_name=repo_name)
|
||||
link, query = _add_pagination(query, url)
|
||||
|
||||
response = jsonify({
|
||||
'name': '{0}/{1}'.format(namespace, repo_name),
|
||||
'tags': [tag.name for tag in model.tag.list_repository_tags(namespace, repo_name)],
|
||||
'tags': [tag.name for tag in query],
|
||||
})
|
||||
|
||||
if link is not None:
|
||||
response.headers['Link'] = link
|
||||
|
||||
return response
|
||||
|
|
Reference in a new issue