Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/480.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enable DELETE on the Docker v2 manifest endpoint so users can delete manifests by digest.
36 changes: 35 additions & 1 deletion pulp_container/app/registry_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
FileStorageRedirects,
S3StorageRedirects,
)
from pulp_container.app.tasks import aadd_and_remove, download_image_data
from pulp_container.app.tasks import aadd_and_remove, download_image_data, recursive_remove_content
from pulp_container.app.token_verification import (
RegistryAuthentication,
RegistryPermission,
Expand Down Expand Up @@ -1329,6 +1329,40 @@ def handle_safe_method(self, request, path, pk):
# Fallthrough catchall, no manifest or tag found
raise ManifestNotFound(reference=pk)

def destroy(self, request, path, pk=None):
"""
Delete a manifest identified by digest.
"""
if not pk.startswith("sha256:"):
raise InvalidRequest(message="A manifest can only be deleted by digest.")

_, repository = self.get_dr_push(request, path)
latest_version = repository.latest_version()

tags = models.Tag.objects.filter(tagged_manifest__digest=pk, pk__in=latest_version.content)
manifest = models.Manifest.objects.filter(digest=pk, pk__in=latest_version.content).first()
if not manifest and tags.exists():
manifest = tags.first().tagged_manifest
if not manifest:
pending_manifest = repository.pending_manifests.filter(digest=pk).first()
if pending_manifest:
repository.pending_manifests.remove(pending_manifest)
return Response(status=202)
raise ManifestNotFound(reference=pk)

tags = models.Tag.objects.filter(tagged_manifest=manifest, pk__in=latest_version.content)
content_units = [str(manifest.pk)] + [str(tag.pk) for tag in tags]

dispatch(
recursive_remove_content,
exclusive_resources=[repository],
kwargs={
"repository_pk": str(repository.pk),
"content_units": content_units,
},
)
return Response(status=202)

def get_content_units_to_add(self, manifest, tag=None):
add_content_units = [str(manifest.pk)]
if tag:
Expand Down
131 changes: 131 additions & 0 deletions pulp_container/tests/functional/api/test_delete_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Tests for deleting manifests via the Docker v2 API."""

import time

import pytest

from pulp_container.tests.functional.constants import REGISTRY_V2_REPO_PULP


def _wait_for_tag(container_bindings, repository_href, tag_name, present, timeout=60):
for _ in range(timeout):
repository = container_bindings.RepositoriesContainerPushApi.read(repository_href)
tags = container_bindings.ContentTagsApi.list(
name=tag_name, repository_version=repository.latest_version_href
)
if bool(tags.results) == present:
if present:
return tags.results[0].tagged_manifest
return None
time.sleep(1)
if present:
pytest.fail(f"Tag '{tag_name}' was not available in the repository")
pytest.fail(f"Tag '{tag_name}' was not removed from the repository")


def test_delete_manifest_by_digest(
add_to_cleanup,
local_registry,
registry_client,
container_bindings,
full_path,
):
"""Delete a manifest by digest via DELETE /v2/<name>/manifests/<digest>."""
repo_name = "delete/manifest"
local_url = full_path(f"{repo_name}:manifest_a")
image_path = f"{REGISTRY_V2_REPO_PULP}:manifest_a"
registry_client.pull(image_path)
local_registry.tag_and_push(image_path, local_url)

namespace = container_bindings.PulpContainerNamespacesApi.list(name="delete").results[0]
add_to_cleanup(container_bindings.PulpContainerNamespacesApi, namespace.pulp_href)

repository = container_bindings.RepositoriesContainerPushApi.list(name=repo_name).results[0]
manifest_href = _wait_for_tag(
container_bindings, repository.pulp_href, "manifest_a", present=True
)
digest = container_bindings.ContentManifestsApi.read(manifest_href).digest

delete_path = f"/v2/{full_path(repo_name)}/manifests/{digest}"
response, _ = local_registry.get_response("DELETE", delete_path)
assert response.status_code == 202

_wait_for_tag(container_bindings, repository.pulp_href, "manifest_a", present=False)


def test_delete_manifest_by_tag_rejected(
add_to_cleanup,
local_registry,
registry_client,
container_bindings,
full_path,
):
"""Delete by tag name is not allowed."""
repo_name = "delete/by-tag"
local_url = full_path(f"{repo_name}:manifest_a")
image_path = f"{REGISTRY_V2_REPO_PULP}:manifest_a"
registry_client.pull(image_path)
local_registry.tag_and_push(image_path, local_url)

namespace = container_bindings.PulpContainerNamespacesApi.list(name="delete").results[0]
add_to_cleanup(container_bindings.PulpContainerNamespacesApi, namespace.pulp_href)

delete_path = f"/v2/{full_path(repo_name)}/manifests/manifest_a"
response, _ = local_registry.get_response("DELETE", delete_path)
assert response.status_code == 400
assert response.json()["errors"][0]["code"] == "INVALID_REQUEST"


def test_delete_manifest_not_found(
add_to_cleanup,
local_registry,
registry_client,
container_bindings,
full_path,
):
"""Deleting a non-existent manifest returns 404."""
repo_name = "delete/not-found"
local_url = full_path(f"{repo_name}:manifest_a")
image_path = f"{REGISTRY_V2_REPO_PULP}:manifest_a"
registry_client.pull(image_path)
local_registry.tag_and_push(image_path, local_url)

namespace = container_bindings.PulpContainerNamespacesApi.list(name="delete").results[0]
add_to_cleanup(container_bindings.PulpContainerNamespacesApi, namespace.pulp_href)

digest = f"sha256:{'0' * 64}"
delete_path = f"/v2/{full_path(repo_name)}/manifests/{digest}"
response, _ = local_registry.get_response("DELETE", delete_path)
assert response.status_code == 404
assert response.json()["errors"][0]["code"] == "MANIFEST_UNKNOWN"


def test_delete_manifest_without_login(
add_to_cleanup,
gen_user,
container_bindings,
full_path,
local_registry,
registry_client,
):
"""Delete requires push permissions on the namespace."""
repo_name = "delete/unauth"
local_url = full_path(f"{repo_name}:manifest_a")
image_path = f"{REGISTRY_V2_REPO_PULP}:manifest_a"
registry_client.pull(image_path)
local_registry.tag_and_push(image_path, local_url)

namespace = container_bindings.PulpContainerNamespacesApi.list(name="delete").results[0]
add_to_cleanup(container_bindings.PulpContainerNamespacesApi, namespace.pulp_href)

repository = container_bindings.RepositoriesContainerPushApi.list(name=repo_name).results[0]
manifest_href = _wait_for_tag(
container_bindings, repository.pulp_href, "manifest_a", present=True
)
digest = container_bindings.ContentManifestsApi.read(manifest_href).digest

delete_path = f"/v2/{full_path(repo_name)}/manifests/{digest}"
user_helpless = gen_user()
with user_helpless:
response, _ = local_registry.get_response("DELETE", delete_path)
assert response.status_code in (401, 403)
Loading