New tests and small fixes while comparing against the V2 spec

Fixes #391
This commit is contained in:
Joseph Schorr 2015-09-29 15:02:03 -04:00
parent 41bfe2ffde
commit decdaa4c79
6 changed files with 232 additions and 33 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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!

View file

@ -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