mirror of
https://github.com/distribution/distribution.git
synced 2026-05-04 10:13:25 +00:00
fix: prevent tag deletion when storage.delete.enabled is false
Signed-off-by: Joonas Bergius <joonas@defenseunicorns.com>
This commit is contained in:
@@ -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{}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user