fix: prevent tag deletion when storage.delete.enabled is false

Signed-off-by: Joonas Bergius <joonas@defenseunicorns.com>
This commit is contained in:
Joonas Bergius
2026-04-25 17:42:15 -05:00
parent 0f3e627937
commit 8baf3e0826
6 changed files with 71 additions and 3 deletions

View File

@@ -1304,7 +1304,7 @@ func TestManifestAPI(t *testing.T) {
}
func TestManifestAPI_DeleteTag(t *testing.T) {
env := newTestEnv(t, false)
env := newTestEnv(t, true)
defer env.Shutdown()
imageName, err := reference.WithName("foo/bar")
@@ -1349,7 +1349,7 @@ func TestManifestAPI_DeleteTag(t *testing.T) {
}
func TestManifestAPI_DeleteTag_Unknown(t *testing.T) {
env := newTestEnv(t, false)
env := newTestEnv(t, true)
defer env.Shutdown()
imageName, err := reference.WithName("foo/bar")
@@ -1393,6 +1393,38 @@ func TestManifestAPI_DeleteTag_ReadOnly(t *testing.T) {
checkResponse(t, msg, resp, http.StatusMethodNotAllowed)
}
func TestManifestAPI_DeleteTag_DeleteDisabled(t *testing.T) {
env := newTestEnv(t, false)
defer env.Shutdown()
imageName, err := reference.WithName("foo/bar")
checkErr(t, err, "building named object")
tag := "latest"
createRepository(env, t, imageName.Name(), tag)
ref, err := reference.WithTag(imageName, tag)
checkErr(t, err, "building tag reference")
u, err := env.builder.BuildManifestURL(ref)
checkErr(t, err, "building tag URL")
resp, err := httpDelete(u)
msg := "deleting tag with delete disabled"
checkErr(t, err, msg)
defer resp.Body.Close()
checkResponse(t, msg, resp, http.StatusMethodNotAllowed)
// nolint:errcheck
checkBodyHasErrorCodes(t, msg, resp, errcode.ErrorCodeUnsupported)
msg = "checking tag exists after rejected delete"
resp, err = http.Get(u)
checkErr(t, err, msg)
defer resp.Body.Close()
checkResponse(t, msg, resp, http.StatusOK)
}
// storageManifestErrDriverFactory implements the factory.StorageDriverFactory interface.
type storageManifestErrDriverFactory struct{}

View File

@@ -86,6 +86,9 @@ type App struct {
// readOnly is true if the registry is in a read-only maintenance mode
readOnly bool
// deleteEnabled is true if the registry is configured to enable deletions.
deleteEnabled bool
}
// NewApp takes a configuration and returns a configured app, ready to serve
@@ -185,6 +188,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
e, ok := d["enabled"]
if ok {
if deleteEnabled, ok := e.(bool); ok && deleteEnabled {
app.deleteEnabled = deleteEnabled
options = append(options, storage.EnableDelete)
}
}

View File

@@ -2,6 +2,7 @@ package handlers
import (
"bytes"
"errors"
"fmt"
"mime"
"net/http"
@@ -436,10 +437,19 @@ func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Reques
return
}
if !imh.App.deleteEnabled {
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
return
}
if imh.Tag != "" {
dcontext.GetLogger(imh).Debug("DeleteImageTag")
tagService := imh.Repository.Tags(imh.Context)
if err := tagService.Untag(imh.Context, imh.Tag); err != nil {
if errors.Is(err, distribution.ErrUnsupported) {
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported.WithDetail(err))
return
}
switch err.(type) {
case distribution.ErrTagUnknown, driver.PathNotFoundError:
imh.Errors = append(imh.Errors, errcode.ErrorCodeManifestUnknown.WithDetail(err))

View File

@@ -244,6 +244,7 @@ func (repo *repository) Tags(ctx context.Context) distribution.TagService {
repository: repo,
blobStore: repo.registry.blobStore,
concurrencyLimit: limit,
deleteEnabled: repo.registry.deleteEnabled,
}
return tags

View File

@@ -28,6 +28,7 @@ type tagStore struct {
repository *repository
blobStore *blobStore
concurrencyLimit int
deleteEnabled bool
}
// All returns all tags
@@ -109,6 +110,9 @@ func (ts *tagStore) Get(ctx context.Context, tag string) (v1.Descriptor, error)
// Untag removes the tag association
func (ts *tagStore) Untag(ctx context.Context, tag string) error {
if !ts.deleteEnabled {
return distribution.ErrUnsupported
}
tagPath, err := pathFor(manifestTagPathSpec{
name: ts.repository.Named().Name(),
tag: tag,

View File

@@ -25,7 +25,7 @@ type tagsTestEnv struct {
func testTagStore(t *testing.T) *tagsTestEnv {
ctx := context.Background()
d := inmemory.New()
reg, err := NewRegistry(ctx, d)
reg, err := NewRegistry(ctx, d, EnableDelete)
if err != nil {
t.Fatal(err)
}
@@ -120,6 +120,23 @@ func TestTagStoreUnTag(t *testing.T) {
}
}
func TestTagStoreUnTag_DeleteDisabled(t *testing.T) {
ctx := context.Background()
d := inmemory.New()
reg, err := NewRegistry(ctx, d)
if err != nil {
t.Fatal(err)
}
repoRef, _ := reference.WithName("a/b")
repo, err := reg.Repository(ctx, repoRef)
if err != nil {
t.Fatal(err)
}
if err := repo.Tags(ctx).Untag(ctx, "latest"); err != distribution.ErrUnsupported {
t.Errorf("expected distribution.ErrUnsupported, got %v", err)
}
}
func TestTagStoreAll(t *testing.T) {
env := testTagStore(t)
tagStore := env.ts