mirror of
https://github.com/containers/podman.git
synced 2025-05-20 16:47:39 +08:00
Merge pull request #25946 from ninja-quokka/docker_compat_force_image_remove
bug: Correct Docker compat REST API image delete endpoint
This commit is contained in:
@ -178,7 +178,7 @@ func (r *Runtime) Reset(ctx context.Context) error {
|
|||||||
rmiOptions := &libimage.RemoveImagesOptions{
|
rmiOptions := &libimage.RemoveImagesOptions{
|
||||||
Force: true,
|
Force: true,
|
||||||
Ignore: true,
|
Ignore: true,
|
||||||
RemoveContainerFunc: r.RemoveContainersForImageCallback(ctx),
|
RemoveContainerFunc: r.RemoveContainersForImageCallback(ctx, true),
|
||||||
Filters: []string{"readonly=false"},
|
Filters: []string{"readonly=false"},
|
||||||
}
|
}
|
||||||
if _, rmiErrors := r.LibimageRuntime().RemoveImages(ctx, nil, rmiOptions); rmiErrors != nil {
|
if _, rmiErrors := r.LibimageRuntime().RemoveImages(ctx, nil, rmiOptions); rmiErrors != nil {
|
||||||
|
@ -23,9 +23,10 @@ import (
|
|||||||
|
|
||||||
// RemoveContainersForImageCallback returns a callback that can be used in
|
// RemoveContainersForImageCallback returns a callback that can be used in
|
||||||
// `libimage`. When forcefully removing images, containers using the image
|
// `libimage`. When forcefully removing images, containers using the image
|
||||||
// should be removed as well. The callback allows for more graceful removal as
|
// should be removed as well unless the request comes from the Docker compat API.
|
||||||
// we can use the libpod-internal removal logic.
|
// The callback allows for more graceful removal as we can use the libpod-internal
|
||||||
func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context) libimage.RemoveContainerFunc {
|
// removal logic.
|
||||||
|
func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context, force bool) libimage.RemoveContainerFunc {
|
||||||
return func(imageID string) error {
|
return func(imageID string) error {
|
||||||
if !r.valid {
|
if !r.valid {
|
||||||
return define.ErrRuntimeStopped
|
return define.ErrRuntimeStopped
|
||||||
@ -44,12 +45,12 @@ func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context) libimage
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("container %s is in pod %s, but pod cannot be retrieved: %w", ctr.ID(), ctr.config.Pod, err)
|
return fmt.Errorf("container %s is in pod %s, but pod cannot be retrieved: %w", ctr.ID(), ctr.config.Pod, err)
|
||||||
}
|
}
|
||||||
if _, err := r.removePod(ctx, pod, true, true, timeout); err != nil {
|
if _, err := r.removePod(ctx, pod, true, force, timeout); err != nil {
|
||||||
return fmt.Errorf("removing image %s: container %s using image could not be removed: %w", imageID, ctr.ID(), err)
|
return fmt.Errorf("removing image %s: container %s using image could not be removed: %w", imageID, ctr.ID(), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
opts := ctrRmOpts{
|
opts := ctrRmOpts{
|
||||||
Force: true,
|
Force: force,
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
if _, _, err := r.removeContainer(ctx, ctr, opts); err != nil {
|
if _, _, err := r.removeContainer(ctx, ctr, opts); err != nil {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/containers/podman/v5/libpod"
|
"github.com/containers/podman/v5/libpod"
|
||||||
|
"github.com/containers/podman/v5/libpod/define"
|
||||||
"github.com/containers/podman/v5/pkg/api/handlers/utils"
|
"github.com/containers/podman/v5/pkg/api/handlers/utils"
|
||||||
api "github.com/containers/podman/v5/pkg/api/types"
|
api "github.com/containers/podman/v5/pkg/api/types"
|
||||||
"github.com/containers/podman/v5/pkg/domain/entities"
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
@ -44,6 +45,7 @@ func RemoveImage(w http.ResponseWriter, r *http.Request) {
|
|||||||
Force: query.Force,
|
Force: query.Force,
|
||||||
NoPrune: query.NoPrune,
|
NoPrune: query.NoPrune,
|
||||||
Ignore: query.Ignore,
|
Ignore: query.Ignore,
|
||||||
|
DisableForceRemoveContainers: true,
|
||||||
}
|
}
|
||||||
report, rmerrors := imageEngine.Remove(r.Context(), []string{possiblyNormalizedName}, options)
|
report, rmerrors := imageEngine.Remove(r.Context(), []string{possiblyNormalizedName}, options)
|
||||||
if len(rmerrors) > 0 && rmerrors[0] != nil {
|
if len(rmerrors) > 0 && rmerrors[0] != nil {
|
||||||
@ -56,6 +58,10 @@ func RemoveImage(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.Error(w, http.StatusConflict, fmt.Errorf("image %s is in use: %w", name, err))
|
utils.Error(w, http.StatusConflict, fmt.Errorf("image %s is in use: %w", name, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, define.ErrCtrStateInvalid) {
|
||||||
|
utils.Error(w, http.StatusConflict, fmt.Errorf("image %s is in an invalid state: %w", name, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
utils.Error(w, http.StatusInternalServerError, err)
|
utils.Error(w, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
|
|||||||
// - in: query
|
// - in: query
|
||||||
// name: force
|
// name: force
|
||||||
// type: boolean
|
// type: boolean
|
||||||
// description: remove the image even if used by containers or has other tags
|
// description: Remove the image even if it is being used by stopped containers or has other tags
|
||||||
// - in: query
|
// - in: query
|
||||||
// name: noprune
|
// name: noprune
|
||||||
// type: boolean
|
// type: boolean
|
||||||
|
@ -65,6 +65,7 @@ type ImageRemoveOptions struct {
|
|||||||
LookupManifest bool
|
LookupManifest bool
|
||||||
// NoPrune will not remove dangling images
|
// NoPrune will not remove dangling images
|
||||||
NoPrune bool
|
NoPrune bool
|
||||||
|
DisableForceRemoveContainers bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageRemoveReport is the response for removing one or more image(s) from storage
|
// ImageRemoveReport is the response for removing one or more image(s) from storage
|
||||||
@ -73,8 +74,10 @@ type ImageRemoveReport = entitiesTypes.ImageRemoveReport
|
|||||||
|
|
||||||
type ImageHistoryOptions struct{}
|
type ImageHistoryOptions struct{}
|
||||||
|
|
||||||
type ImageHistoryLayer = entitiesTypes.ImageHistoryLayer
|
type (
|
||||||
type ImageHistoryReport = entitiesTypes.ImageHistoryReport
|
ImageHistoryLayer = entitiesTypes.ImageHistoryLayer
|
||||||
|
ImageHistoryReport = entitiesTypes.ImageHistoryReport
|
||||||
|
)
|
||||||
|
|
||||||
// ImagePullOptions are the arguments for pulling images.
|
// ImagePullOptions are the arguments for pulling images.
|
||||||
type ImagePullOptions struct {
|
type ImagePullOptions struct {
|
||||||
@ -255,8 +258,10 @@ type ImagePruneOptions struct {
|
|||||||
Filter []string `json:"filter" schema:"filter"`
|
Filter []string `json:"filter" schema:"filter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageTagOptions struct{}
|
type (
|
||||||
type ImageUntagOptions struct{}
|
ImageTagOptions struct{}
|
||||||
|
ImageUntagOptions struct{}
|
||||||
|
)
|
||||||
|
|
||||||
// ImageInspectReport is the data when inspecting an image.
|
// ImageInspectReport is the data when inspecting an image.
|
||||||
type ImageInspectReport = entitiesTypes.ImageInspectReport
|
type ImageInspectReport = entitiesTypes.ImageInspectReport
|
||||||
|
@ -58,7 +58,7 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrID string) (*entities.Boo
|
|||||||
|
|
||||||
func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) ([]*reports.PruneReport, error) {
|
func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) ([]*reports.PruneReport, error) {
|
||||||
pruneOptions := &libimage.RemoveImagesOptions{
|
pruneOptions := &libimage.RemoveImagesOptions{
|
||||||
RemoveContainerFunc: ir.Libpod.RemoveContainersForImageCallback(ctx),
|
RemoveContainerFunc: ir.Libpod.RemoveContainersForImageCallback(ctx, true),
|
||||||
IsExternalContainerFunc: ir.Libpod.IsExternalContainerCallback(ctx),
|
IsExternalContainerFunc: ir.Libpod.IsExternalContainerCallback(ctx),
|
||||||
ExternalContainers: opts.External,
|
ExternalContainers: opts.External,
|
||||||
Filters: append(opts.Filter, "readonly=false"),
|
Filters: append(opts.Filter, "readonly=false"),
|
||||||
@ -666,7 +666,7 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie
|
|||||||
if !opts.All {
|
if !opts.All {
|
||||||
libimageOptions.Filters = append(libimageOptions.Filters, "intermediate=false")
|
libimageOptions.Filters = append(libimageOptions.Filters, "intermediate=false")
|
||||||
}
|
}
|
||||||
libimageOptions.RemoveContainerFunc = ir.Libpod.RemoveContainersForImageCallback(ctx)
|
libimageOptions.RemoveContainerFunc = ir.Libpod.RemoveContainersForImageCallback(ctx, !opts.DisableForceRemoveContainers)
|
||||||
|
|
||||||
libimageReport, libimageErrors := ir.Libpod.LibimageRuntime().RemoveImages(ctx, images, libimageOptions)
|
libimageReport, libimageErrors := ir.Libpod.LibimageRuntime().RemoveImages(ctx, images, libimageOptions)
|
||||||
|
|
||||||
|
@ -231,6 +231,47 @@ t DELETE images/test1:latest 200
|
|||||||
|
|
||||||
t GET "images/get?names=alpine" 200 '[POSIX tar archive]'
|
t GET "images/get?names=alpine" 200 '[POSIX tar archive]'
|
||||||
|
|
||||||
|
# START: Testing variance between Docker API and Podman API
|
||||||
|
# regarding force deleting images.
|
||||||
|
# Podman: Force deleting an image will force remove any
|
||||||
|
# container using the image.
|
||||||
|
# Docker: Force deleting an image will only remove non
|
||||||
|
# running containers using the image.
|
||||||
|
|
||||||
|
# Create new image
|
||||||
|
podman image build -t docker.io/library/test1:latest - <<EOF
|
||||||
|
from alpine
|
||||||
|
RUN >file4
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create running container
|
||||||
|
podman run --rm -d --name test_container docker.io/library/test1:latest top
|
||||||
|
|
||||||
|
# When using the Docker Compat API, force deleting an image
|
||||||
|
# shouldn't force delete any container using the image, only
|
||||||
|
# containers in a non running state should be removed.
|
||||||
|
# https://github.com/containers/podman/issues/25871
|
||||||
|
t DELETE images/test1:latest?force=true 409
|
||||||
|
|
||||||
|
# When using the Podman Libpod API, deleting an image
|
||||||
|
# with a running container will fail.
|
||||||
|
t DELETE libpod/images/test1:latest 409
|
||||||
|
|
||||||
|
# When using the Podman Libpod API, force deleting an
|
||||||
|
# image will also force delete all containers using the image.
|
||||||
|
|
||||||
|
# Verify container exists.
|
||||||
|
t GET libpod/containers/test_container/exists 204
|
||||||
|
|
||||||
|
# Delete image with force.
|
||||||
|
t DELETE libpod/images/test1:latest?force=true 200
|
||||||
|
|
||||||
|
# Verify container also removed.
|
||||||
|
t GET libpod/containers/test_container/exists 404
|
||||||
|
|
||||||
|
# END: Testing variance between Docker API and Podman API
|
||||||
|
# regarding force deleting images.
|
||||||
|
|
||||||
podman pull busybox
|
podman pull busybox
|
||||||
t GET "images/get?names=alpine&names=busybox" 200 '[POSIX tar archive]'
|
t GET "images/get?names=alpine&names=busybox" 200 '[POSIX tar archive]'
|
||||||
img_cnt=$(tar xf "$WORKDIR/curl.result.out" manifest.json -O | jq "length")
|
img_cnt=$(tar xf "$WORKDIR/curl.result.out" manifest.json -O | jq "length")
|
||||||
|
@ -66,9 +66,8 @@ class ImageTestCase(APITestCase):
|
|||||||
self.assertIn("sha256:",image['Id'])
|
self.assertIn("sha256:",image['Id'])
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
r = requests.delete(self.podman_url + "/v1.40/images/alpine?force=true")
|
r = requests.delete(self.compat_uri("images/alpine?force=true"))
|
||||||
self.assertEqual(r.status_code, 200, r.text)
|
self.assertEqual(r.status_code, 409, r.text)
|
||||||
self.assertIsInstance(r.json(), list)
|
|
||||||
|
|
||||||
def test_pull(self):
|
def test_pull(self):
|
||||||
r = requests.post(self.uri("/images/pull?reference=alpine"), timeout=15)
|
r = requests.post(self.uri("/images/pull?reference=alpine"), timeout=15)
|
||||||
|
Reference in New Issue
Block a user