def put(self, request, *args, **kwargs): """PUT /v2/<name>/manifests/<reference> https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-manifests """ # We likely can default to the v1 manifest, unless otherwise specified # This isn't used or checked for the time being # application/vnd.oci.image.manifest.v1+json _ = request.META.get("CONTENT_TYPE", settings.IMAGE_MANIFEST_CONTENT_TYPE) name = kwargs.get("name") reference = kwargs.get("reference") tag = kwargs.get("tag") # If allow_continue False, return response allow_continue, response, _ = is_authenticated( request, name, must_be_owner=True ) if not allow_continue: return response # Reference must be sha256 digest if reference and not reference.startswith("sha256:"): return Response(status=400) # Also provide the body in case we have a tag image = get_image_by_tag(name, reference, tag, create=True, body=request.body) # If allow_continue False, return response allow_continue, response, _ = is_authenticated( request, image.repository, must_be_owner=True ) if not allow_continue: return response return Response(status=201, headers={"Location": image.get_manifest_url()})
def delete(self, request, *args, **kwargs): """DELETE /v2/<name>/manifests/<tag>""" # A registry must globally disable or enable both if settings.DISABLE_TAG_MANIFEST_DELETE: return Response(status=405) name = kwargs.get("name") reference = kwargs.get("reference") tag = kwargs.get("tag") # If allow_continue False, return response allow_continue, response, _ = is_authenticated( request, name, must_be_owner=True ) if not allow_continue: return response # Retrieve the image, return of None indicates not found image = get_image_by_tag(name, reference=reference, tag=tag, create=False) if not image: raise Http404 # Delete the image tag if tag: tag = image.tag_set.filter(name=tag) tag.delete() # Delete a manifest elif reference: image.delete() # Upon success, the registry MUST respond with a 202 Accepted code. return Response(status=202)
def patch(self, request, *args, **kwargs): """a patch request is done after a POST with content-length 0 to indicate a chunked upload request. """ session_id = kwargs.get("session_id") content_length = int(request.META.get("CONTENT_LENGTH")) content_range = request.META.get("HTTP_CONTENT_RANGE") content_type = request.META.get("CONTENT_TYPE") if not session_id or not content_length or not content_type: return Response(status=400) # If a content range is not defined, assume start to end if not content_range: content_start = 0 content_end = content_length - 1 else: # Parse content range into start and end (int) try: content_start, content_end = parse_content_range(content_range) except ValueError: return Response(status=400) # Confirm that content length (body) == header value, otherwise bad request if len(request.body) != content_length: return Response(status=400) # Get the session id, if it has not expired, keep open for next filecache = cache.caches["django_oci_upload"] if not filecache.get(session_id): return Response(status=400) # Break apart into blob id and session uuid _, blob_id, version = session_id.split("/", 3) blob = get_object_or_404(Blob, id=blob_id, digest=version) allow_continue, response, _ = is_authenticated(request, blob.repository, must_be_owner=True) if not allow_continue: return response # Update the blob content_type TODO: There should be some check # to ensure that a next chunk content type is not different from that # already defined blob.content_type = content_type # Now process the PATCH request to upload the chunk return storage.upload_blob_chunk( blob=blob, body=request.body, content_start=content_start, content_end=content_end, content_length=content_length, )
def delete(self, request, *args, **kwargs): """DELETE /v2/<name>/blobs/<digest>""" name = kwargs.get("name") digest = kwargs.get("digest") # If allow_continue False, return response allow_continue, response, _ = is_authenticated(request, name, must_be_owner=True) if not allow_continue: return response return storage.delete_blob(name, digest)
def head(self, request, *args, **kwargs): """HEAD /v2/<name>/manifests/<reference>""" name = kwargs.get("name") reference = kwargs.get("reference") tag = kwargs.get("tag") allow_continue, response, _ = is_authenticated(request, name) if not allow_continue: return response image = get_image_by_tag(name, tag=tag, reference=reference) if not image: raise Http404 return Response(status=200)
def head(self, request, *args, **kwargs): """HEAD /v2/<name>/blobs/<digest>""" name = kwargs.get("name") digest = kwargs.get("digest") # If allow_continue False, return response allow_continue, response, _ = is_authenticated(request, name, must_be_owner=True) if not allow_continue: return response # A HEAD request to an existing blob or manifest URL MUST return 200 OK. return storage.blob_exists(name, digest)
def get(self, request, *args, **kwargs): """POST /v2/<name>/blobs/<digest>""" # the name is only used to validate the user has permission to upload name = kwargs.get("name") digest = kwargs.get("digest") # If allow_continue False, return response allow_continue, response, _ = is_authenticated(request, name, scopes=["pull"]) if not allow_continue: return response return storage.download_blob(name, digest)
def get(self, request, *args, **kwargs): """GET /v2/<name>/tags/list. We don't require authentication to list tags, unless the repository is private. """ name = kwargs.get("name") number = request.GET.get("n") last = request.GET.get("last") try: repository = Repository.objects.get(name=name) except Repository.DoesNotExist: raise Http404 # If allow_continue False, return response allow_continue, response, _ = is_authenticated( request, repository, scopes=["pull"] ) if not allow_continue: return response tags = [ x for x in list(repository.image_set.values_list("tag__name", flat=True)) if x ] # Tags must be sorted in lexical order tags.sort() # Number must be an integer if defined if number: number = int(number) # if last, <tagname> not included in the results, but up to <int> tags after <tagname> will be returned. if last and number: try: start = tags.index(last) except IndexError: start = 0 tags = tags[start:number] elif number: tags = tags[:number] # Ensure tags sorted in lexical order data = {"name": repository.name, "tags": sorted(tags)} return Response(status=200, data=data)
def get(self, request, *args, **kwargs): """GET /v2/<name>/manifests/<reference>""" name = kwargs.get("name") reference = kwargs.get("reference") tag = kwargs.get("tag") # If allow_continue False, return response allow_continue, response, _ = is_authenticated(request, name, scopes=["pull"]) if not allow_continue: return response image = get_image_by_tag(name, tag=tag, reference=reference) # If the manifest is not found in the registry, the response code MUST be 404 Not Found. if not image: raise Http404 return Response(image.manifest, status=200)
def post(self, request, *args, **kwargs): """POST /v2/<name>/blobs/uploads/""" # the name is only used to validate the user has permission to upload name = kwargs.get("name") # Look if we want to mount (get) a blob from another repository mount = request.GET.get("mount") from_repo = request.GET.get("from") # Validate user having a token, no repository required allow_continue, response, user = is_authenticated( request, name, repository_exists=False) if not allow_continue: return response # For check media type and content length (if needed) content_length = int(request.META.get("CONTENT_LENGTH", 0)) content_type = request.META.get("CONTENT_TYPE", settings.DEFAULT_CONTENT_TYPE) # If no content length, tell the user it's required if content_length in [None, ""]: return Response(status=411) # Get or create the requested repository repository, created = Repository.objects.get_or_create(name=name) # If created, add user to owners if created: repository.owners.add(user) repository.save() # Otherwise, must be an owner else: allow_continue, response, _ = is_authenticated(request, repository, must_be_owner=True) if not allow_continue: return response # Case 1: POST provided with digest == single monolithic upload # /v2/<name>/blobs/uploads/?digest=<digest> if "digest" in request.GET: # Unsupported media type, only needed for digest if content_type not in settings.CONTENT_TYPES: return Response(status=415) digest = request.GET["digest"] # Confirm that content length (body) == header value, otherwise bad request if len(request.body) != content_length: return Response(status=400) # The storage.create_blob handles creation of blob with body (no second request required) # We only pass the name to return it with the blob's download url, there is no association return storage.create_blob( body=request.body, digest=digest, content_type=content_type, repository=repository, ) # Case 2: Mount a blob from a different repository # /v2/<name>/blobs/uploads/?mount=<digest>&from=<other_name> elif mount and from_repo: # Get the existing repository from_repository = get_object_or_404(Repository, name=from_repo) # Mount is the digest of the blob we need. We use the same datafile try: blob = Blob.objects.get(digest=mount, repository=from_repository) except Blob.DoesNotExist: # Cross-mounting of nonexistent blob should yield session id return storage.create_blob_request(repository) # Unset the pk and id, and add a new repository blob.pk = None blob.id = None blob.repository = repository blob.save() # Successful mount MUST be 201 Created, and MUST contain Location: <blob-location> return Response(status=201, headers={"Location": blob.get_download_url()}) # Case 3; Content type length 0 indicates chunked upload elif content_length != 0: return storage.create_blob_request(repository) # Case 3: POST without digest == single monolithic with POST then PUT # returns a session location to upload to with PUT return storage.create_blob_request(repository)
def put(self, request, *args, **kwargs): """PUT /v2/<name>/blobs/uploads/ A put request can happen in two scenarios. 1. after a POST request, and must include a session_id. The session id is created via the file system cache, and each one includes the request type, image id (associated with a tag), repository, and a randomly generated uuid. The upload will not continue if any required metadata is missing or the identifier is already expired. The second case is after several PATCH requests to upload chunks of a blob. The final chunk might be provided this case. """ # These header attributes are shared by both scenarios session_id = kwargs.get("session_id") digest = request.GET.get("digest") content_length = int(request.META.get("CONTENT_LENGTH", 0)) content_type = request.META.get("CONTENT_TYPE", settings.DEFAULT_CONTENT_TYPE) # Presence of content range distinguishes chunked upload from single PUT # A final PUT request may not have a content_range if no chunk to upload content_range = request.META.get("HTTP_CONTENT_RANGE") if not session_id or not digest or not content_type: return Response(status=400) # Confirm that content length (body) == header value, otherwise bad request if len(request.body) != content_length: return Response(status=400) # Get the session id, if it has not expired filecache = cache.caches["django_oci_upload"] if not filecache.get(session_id): return Response(status=400) # Ensure it cannot be used again filecache.set(session_id, None, timeout=0) # Break apart into blob id, and session uuid (version) _, blob_id, version = session_id.split("/") blob = get_object_or_404(Blob, id=blob_id, digest=version) # If allow_continue False, return response allow_continue, response, _ = is_authenticated(request, blob.repository, must_be_owner=True) if not allow_continue: return response if not content_range and request.body: # Now process the PUT request to the file! Provide the blob to update return storage.create_blob( blob=blob, body=request.body, digest=digest, content_type=content_type, ) # Scenario 2: a PUT to end a chunked upload session, no final chunk elif not request.body: return storage.finish_blob( blob=blob, digest=digest, ) # Scenario 3: a PUT to end a chunked upload session with a final chunk # First ensure upload session cannot be used again filecache.set(session_id, None, timeout=0) # Parse the start and end for the chunk to write try: content_start, content_end = parse_content_range(content_range) except ValueError: return Response(status=400) # Write the final chunk and finish the session status_code = storage.write_chunk( blob=blob, content_start=content_start, content_end=content_end, body=request.body, ) # If it's already existing, return Accepted header, otherwise alert created if status_code != 202: return Response(status=status_code) return storage.finish_blob( blob=blob, digest=digest, )