Beispiel #1
0
    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()})
Beispiel #2
0
    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)
Beispiel #3
0
    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,
        )
Beispiel #4
0
    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)
Beispiel #5
0
    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)
Beispiel #6
0
    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)
Beispiel #7
0
    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)
Beispiel #8
0
    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)
Beispiel #9
0
    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)
Beispiel #10
0
    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)
Beispiel #11
0
    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,
        )