From 475cba8c5bdf7cc4436575c8813ee9f65f2f74d1 Mon Sep 17 00:00:00 2001 From: Ivan Bazulic Date: Wed, 21 Aug 2024 15:20:37 -0400 Subject: [PATCH] api: Add tag deletion endpoint for v2 api (PROJQUAY-7599) (#3128) * api: Add ability to delete tags via v2 call (PROJQUAY-7599) The deletion of tags was previously not supported by the Docker v2 API. Current versions of both the OCI spec and Docker v2 API provide this ability, hence adding it to Quay as well. See [OCI spec](https://github.com/opencontainers/distribution-spec/blob/main/spec.md) for more details. * Fix test call * Add missing argument to test * Add security tests * Enable conformance tests * Switch to v1.1.0 instead of release candidate for conformance tests * Revert changes to conformance tests --- endpoints/v2/manifest.py | 32 ++++++++++++++++++++++++++ test/registry/registry_tests.py | 40 +++++++++++++++++++++++++++++++++ test/specs.py | 13 +++++++++++ 3 files changed, 85 insertions(+) diff --git a/endpoints/v2/manifest.py b/endpoints/v2/manifest.py index c080a2a4e6..9f2c448bb5 100644 --- a/endpoints/v2/manifest.py +++ b/endpoints/v2/manifest.py @@ -405,6 +405,38 @@ def delete_manifest_by_digest(namespace_name, repo_name, manifest_ref): return Response(status=202) +@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=["DELETE"]) +@disallow_for_account_recovery_mode +@parse_repository_name() +@process_registry_jwt_auth(scopes=["pull", "push"]) +@log_unauthorized_delete +@require_repo_write(allow_for_superuser=True, disallow_for_restricted_users=True) +@anon_protect +@check_readonly +@check_pushes_disabled +def delete_manifest_by_tag(namespace_name, repo_name, manifest_ref): + """ + Deletes the manifest specified by the tag. + """ + with db_disallow_replica_use(): + repository_ref = registry_model.lookup_repository( + namespace_name, repo_name, model_cache=model_cache + ) + if repository_ref is None: + raise NameUnknown("repository not found") + + tag = registry_model.get_repo_tag(repository_ref, manifest_ref) + if tag is None: + raise ManifestUnknown() + + deleted_tag = registry_model.delete_tag(model_cache, repository_ref, manifest_ref) + if not deleted_tag: + raise ManifestUnknown() + + track_and_log("delete_tag", repository_ref, tag=deleted_tag.name, digest=manifest_ref) + return Response(status=202) + + def _write_manifest_and_log(namespace_name, repo_name, tag_name, manifest_impl): _validate_schema1_manifest(namespace_name, repo_name, manifest_impl) with db_disallow_replica_use(): diff --git a/test/registry/registry_tests.py b/test/registry/registry_tests.py index 5021708862..a48ba7b55e 100644 --- a/test/registry/registry_tests.py +++ b/test/registry/registry_tests.py @@ -3103,3 +3103,43 @@ def test_conftest_policy_push(manifest_protocol, openpolicyagent_policy, liveser credentials=credentials, expected_failure=None, ) + + +def test_attempt_pull_by_tag_reference_for_deleted_tag( + manifest_protocol, basic_images, liveserver_session +): + """Test: Delete a tag by specifying the reference in a v2 call""" + credentials = ("devtable", "password") + + # Push the new repository + result = manifest_protocol.push( + liveserver_session, "devtable", "newrepo", "latest", basic_images, credentials=credentials + ) + digests = [str(manifest.digest) for manifest in list(result.manifests.values())] + assert len(digests) == 1 + + # Ensure we can pull by digest + manifest_protocol.pull( + liveserver_session, "devtable", "newrepo", digests[0], basic_images, credentials=credentials + ) + + # Ensure we can pull by tag + manifest_protocol.pull( + liveserver_session, "devtable", "newrepo", "latest", basic_images, credentials=credentials + ) + + # Delete the tag by reference + manifest_protocol.delete( + liveserver_session, "devtable", "newrepo", "latest", credentials=credentials + ) + + # Attempt to pull from the repository by tag to verify it's not accessible + manifest_protocol.pull( + liveserver_session, + "devtable", + "newrepo", + "latest", + basic_images, + credentials=credentials, + expected_failure=Failures.UNKNOWN_TAG, + ) diff --git a/test/specs.py b/test/specs.py index 03683dde79..ac7fd0eb2b 100644 --- a/test/specs.py +++ b/test/specs.py @@ -670,6 +670,19 @@ def build_v2_index_specs(): IndexV2TestSpec( "v2.write_manifest_by_digest", "PUT", ANOTHER_ORG_REPO, manifest_ref=FAKE_DIGEST ).request_status(401, 401, 401, 401, 400), + # v2.delete_manifest_by_tag + IndexV2TestSpec( + "v2.delete_manifest_by_tag", "DELETE", PUBLIC_REPO, manifest_ref=FAKE_MANIFEST + ).request_status(401, 401, 401, 401, 401), + IndexV2TestSpec( + "v2.delete_manifest_by_tag", "DELETE", PRIVATE_REPO, manifest_ref=FAKE_MANIFEST + ).request_status(401, 401, 401, 401, 404), + IndexV2TestSpec( + "v2.delete_manifest_by_tag", "DELETE", ORG_REPO, manifest_ref=FAKE_MANIFEST + ).request_status(401, 401, 401, 401, 404), + IndexV2TestSpec( + "v2.delete_manifest_by_tag", "DELETE", ANOTHER_ORG_REPO, manifest_ref=FAKE_MANIFEST + ).request_status(401, 401, 401, 401, 404), # v2.delete_manifest_by_digest IndexV2TestSpec( "v2.delete_manifest_by_digest", "DELETE", PUBLIC_REPO, manifest_ref=FAKE_DIGEST