Manifest delete (DELETE /v2/<name>/manifests/<digest>): rejects tag references with 405 UNSUPPORTED per OCI spec, cascades to tags and manifest_blobs via ON DELETE CASCADE, returns 202 Accepted. Blob delete (DELETE /v2/<name>/blobs/<digest>): removes manifest_blobs associations only — blob row and file are preserved for GC to handle, since other repos may reference the same content-addressed blob. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
194 lines
5.4 KiB
Go
194 lines
5.4 KiB
Go
package oci
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestManifestDeleteByDigest(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
content := []byte(`{"schemaVersion":2}`)
|
|
fdb.addManifest(1, "latest", "sha256:aaaa", "application/vnd.oci.image.manifest.v1+json", content)
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/myrepo/manifests/sha256:aaaa", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusAccepted {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusAccepted)
|
|
}
|
|
|
|
// Verify manifest was deleted from fakeDB.
|
|
_, err := fdb.GetManifestByDigest(1, "sha256:aaaa")
|
|
if err == nil {
|
|
t.Fatal("manifest should have been deleted")
|
|
}
|
|
}
|
|
|
|
func TestManifestDeleteByTagUnsupported(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
content := []byte(`{"schemaVersion":2}`)
|
|
fdb.addManifest(1, "latest", "sha256:aaaa", "application/vnd.oci.image.manifest.v1+json", content)
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/myrepo/manifests/latest", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
|
|
var body ociErrorResponse
|
|
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode error: %v", err)
|
|
}
|
|
if len(body.Errors) != 1 || body.Errors[0].Code != "UNSUPPORTED" {
|
|
t.Fatalf("error code: got %+v, want UNSUPPORTED", body.Errors)
|
|
}
|
|
}
|
|
|
|
func TestManifestDeleteNotFound(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
// No manifests added.
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/myrepo/manifests/sha256:nonexistent", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusNotFound)
|
|
}
|
|
|
|
var body ociErrorResponse
|
|
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode error: %v", err)
|
|
}
|
|
if len(body.Errors) != 1 || body.Errors[0].Code != "MANIFEST_UNKNOWN" {
|
|
t.Fatalf("error code: got %+v, want MANIFEST_UNKNOWN", body.Errors)
|
|
}
|
|
}
|
|
|
|
func TestManifestDeleteRepoNotFound(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/nosuchrepo/manifests/sha256:aaaa", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusNotFound)
|
|
}
|
|
|
|
var body ociErrorResponse
|
|
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode error: %v", err)
|
|
}
|
|
if len(body.Errors) != 1 || body.Errors[0].Code != "NAME_UNKNOWN" {
|
|
t.Fatalf("error code: got %+v, want NAME_UNKNOWN", body.Errors)
|
|
}
|
|
}
|
|
|
|
func TestManifestDeleteCascadesTag(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
content := []byte(`{"schemaVersion":2}`)
|
|
fdb.addManifest(1, "latest", "sha256:aaaa", "application/vnd.oci.image.manifest.v1+json", content)
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/myrepo/manifests/sha256:aaaa", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusAccepted {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusAccepted)
|
|
}
|
|
|
|
// The tag "latest" should also be gone (cascading delete in fakeDB).
|
|
_, err := fdb.GetManifestByTag(1, "latest")
|
|
if err == nil {
|
|
t.Fatal("tag 'latest' should have been cascaded on manifest delete")
|
|
}
|
|
}
|
|
|
|
func TestBlobDelete(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
fdb.addBlob(1, "sha256:b1")
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/myrepo/blobs/sha256:b1", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusAccepted {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusAccepted)
|
|
}
|
|
|
|
// Blob should be removed from repo in fakeDB.
|
|
exists, _ := fdb.BlobExistsInRepo(1, "sha256:b1")
|
|
if exists {
|
|
t.Fatal("blob should have been removed from repo")
|
|
}
|
|
}
|
|
|
|
func TestBlobDeleteNotInRepo(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
fdb.addRepo("myrepo", 1)
|
|
// Blob not added to this repo.
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/myrepo/blobs/sha256:nonexistent", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusNotFound)
|
|
}
|
|
|
|
var body ociErrorResponse
|
|
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode error: %v", err)
|
|
}
|
|
if len(body.Errors) != 1 || body.Errors[0].Code != "BLOB_UNKNOWN" {
|
|
t.Fatalf("error code: got %+v, want BLOB_UNKNOWN", body.Errors)
|
|
}
|
|
}
|
|
|
|
func TestBlobDeleteRepoNotFound(t *testing.T) {
|
|
fdb := newFakeDB()
|
|
|
|
h := NewHandler(fdb, newFakeBlobs(), allowAll(), nil)
|
|
router := testRouter(h)
|
|
|
|
req := authedRequest("DELETE", "/v2/nosuchrepo/blobs/sha256:b1", nil)
|
|
rr := httptest.NewRecorder()
|
|
router.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("status: got %d, want %d", rr.Code, http.StatusNotFound)
|
|
}
|
|
}
|