Skip to content

Commit 6e7de43

Browse files
committed
bug: Correct Docker compat REST API image delete endpoint
The Docker `-XDELETE image/$name?force=true` endpoint only removes containers using an image if they are in a non running state. In Podman, when forcefully removing images we also forcefully delete containers using the image including running containers. This patch changes the Docker image force delete compat API to act like the Docker API while maintaining commands like `podman rmi -f $imagename` It also corrects the API return code returned when an image is requested to be deleted with running containers using it. Fixes: #25871 Signed-off-by: Lewis Roy <[email protected]>
1 parent 5c5ecde commit 6e7de43

File tree

8 files changed

+72
-20
lines changed

8 files changed

+72
-20
lines changed

libpod/reset.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func (r *Runtime) Reset(ctx context.Context) error {
178178
rmiOptions := &libimage.RemoveImagesOptions{
179179
Force: true,
180180
Ignore: true,
181-
RemoveContainerFunc: r.RemoveContainersForImageCallback(ctx),
181+
RemoveContainerFunc: r.RemoveContainersForImageCallback(ctx, true),
182182
Filters: []string{"readonly=false"},
183183
}
184184
if _, rmiErrors := r.LibimageRuntime().RemoveImages(ctx, nil, rmiOptions); rmiErrors != nil {

libpod/runtime_img.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import (
2323

2424
// RemoveContainersForImageCallback returns a callback that can be used in
2525
// `libimage`. When forcefully removing images, containers using the image
26-
// should be removed as well. The callback allows for more graceful removal as
27-
// we can use the libpod-internal removal logic.
28-
func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context) libimage.RemoveContainerFunc {
26+
// should be removed as well unless the request comes from the Docker compat API.
27+
// The callback allows for more graceful removal as we can use the libpod-internal
28+
// removal logic.
29+
func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context, force bool) libimage.RemoveContainerFunc {
2930
return func(imageID string) error {
3031
if !r.valid {
3132
return define.ErrRuntimeStopped
@@ -44,12 +45,12 @@ func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context) libimage
4445
if err != nil {
4546
return fmt.Errorf("container %s is in pod %s, but pod cannot be retrieved: %w", ctr.ID(), ctr.config.Pod, err)
4647
}
47-
if _, err := r.removePod(ctx, pod, true, true, timeout); err != nil {
48+
if _, err := r.removePod(ctx, pod, true, force, timeout); err != nil {
4849
return fmt.Errorf("removing image %s: container %s using image could not be removed: %w", imageID, ctr.ID(), err)
4950
}
5051
} else {
5152
opts := ctrRmOpts{
52-
Force: true,
53+
Force: force,
5354
Timeout: timeout,
5455
}
5556
if _, _, err := r.removeContainer(ctx, ctr, opts); err != nil {

pkg/api/handlers/compat/images_remove.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99

1010
"github.com/containers/podman/v5/libpod"
11+
"github.com/containers/podman/v5/libpod/define"
1112
"github.com/containers/podman/v5/pkg/api/handlers/utils"
1213
api "github.com/containers/podman/v5/pkg/api/types"
1314
"github.com/containers/podman/v5/pkg/domain/entities"
@@ -41,9 +42,10 @@ func RemoveImage(w http.ResponseWriter, r *http.Request) {
4142
imageEngine := abi.ImageEngine{Libpod: runtime}
4243

4344
options := entities.ImageRemoveOptions{
44-
Force: query.Force,
45-
NoPrune: query.NoPrune,
46-
Ignore: query.Ignore,
45+
Force: query.Force,
46+
NoPrune: query.NoPrune,
47+
Ignore: query.Ignore,
48+
DisableForceRemoveContainers: true,
4749
}
4850
report, rmerrors := imageEngine.Remove(r.Context(), []string{possiblyNormalizedName}, options)
4951
if len(rmerrors) > 0 && rmerrors[0] != nil {
@@ -56,6 +58,10 @@ func RemoveImage(w http.ResponseWriter, r *http.Request) {
5658
utils.Error(w, http.StatusConflict, fmt.Errorf("image %s is in use: %w", name, err))
5759
return
5860
}
61+
if errors.Is(err, define.ErrCtrStateInvalid) {
62+
utils.Error(w, http.StatusConflict, fmt.Errorf("image %s is in an invalid state: %w", name, err))
63+
return
64+
}
5965
utils.Error(w, http.StatusInternalServerError, err)
6066
return
6167
}

pkg/api/server/register_images.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
227227
// - in: query
228228
// name: force
229229
// type: boolean
230-
// description: remove the image even if used by containers or has other tags
230+
// description: Remove the image even if it is being used by stopped containers or has other tags
231231
// - in: query
232232
// name: noprune
233233
// type: boolean

pkg/domain/entities/images.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ type ImageRemoveOptions struct {
6464
// Confirms if given name is a manifest list and removes it, otherwise returns error.
6565
LookupManifest bool
6666
// NoPrune will not remove dangling images
67-
NoPrune bool
67+
NoPrune bool
68+
DisableForceRemoveContainers bool
6869
}
6970

7071
// ImageRemoveReport is the response for removing one or more image(s) from storage
@@ -73,8 +74,10 @@ type ImageRemoveReport = entitiesTypes.ImageRemoveReport
7374

7475
type ImageHistoryOptions struct{}
7576

76-
type ImageHistoryLayer = entitiesTypes.ImageHistoryLayer
77-
type ImageHistoryReport = entitiesTypes.ImageHistoryReport
77+
type (
78+
ImageHistoryLayer = entitiesTypes.ImageHistoryLayer
79+
ImageHistoryReport = entitiesTypes.ImageHistoryReport
80+
)
7881

7982
// ImagePullOptions are the arguments for pulling images.
8083
type ImagePullOptions struct {
@@ -255,8 +258,10 @@ type ImagePruneOptions struct {
255258
Filter []string `json:"filter" schema:"filter"`
256259
}
257260

258-
type ImageTagOptions struct{}
259-
type ImageUntagOptions struct{}
261+
type (
262+
ImageTagOptions struct{}
263+
ImageUntagOptions struct{}
264+
)
260265

261266
// ImageInspectReport is the data when inspecting an image.
262267
type ImageInspectReport = entitiesTypes.ImageInspectReport

pkg/domain/infra/abi/images.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrID string) (*entities.Boo
5858

5959
func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) ([]*reports.PruneReport, error) {
6060
pruneOptions := &libimage.RemoveImagesOptions{
61-
RemoveContainerFunc: ir.Libpod.RemoveContainersForImageCallback(ctx),
61+
RemoveContainerFunc: ir.Libpod.RemoveContainersForImageCallback(ctx, true),
6262
IsExternalContainerFunc: ir.Libpod.IsExternalContainerCallback(ctx),
6363
ExternalContainers: opts.External,
6464
Filters: append(opts.Filter, "readonly=false"),
@@ -666,7 +666,7 @@ func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entitie
666666
if !opts.All {
667667
libimageOptions.Filters = append(libimageOptions.Filters, "intermediate=false")
668668
}
669-
libimageOptions.RemoveContainerFunc = ir.Libpod.RemoveContainersForImageCallback(ctx)
669+
libimageOptions.RemoveContainerFunc = ir.Libpod.RemoveContainersForImageCallback(ctx, !opts.DisableForceRemoveContainers)
670670

671671
libimageReport, libimageErrors := ir.Libpod.LibimageRuntime().RemoveImages(ctx, images, libimageOptions)
672672

test/apiv2/10-images.at

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,47 @@ t DELETE images/test1:latest 200
231231

232232
t GET "images/get?names=alpine" 200 '[POSIX tar archive]'
233233

234+
# START: Testing variance between Docker API and Podman API
235+
# regarding force deleting images.
236+
# Podman: Force deleting an image will force remove any
237+
# container using the image.
238+
# Docker: Force deleting an image will only remove non
239+
# running containers using the image.
240+
241+
# Create new image
242+
podman image build -t docker.io/library/test1:latest - <<EOF
243+
from alpine
244+
RUN >file4
245+
EOF
246+
247+
# Create running container
248+
podman run --rm -d --name test_container docker.io/library/test1:latest top
249+
250+
# When using the Docker Compat API, force deleting an image
251+
# shouldn't force delete any container using the image, only
252+
# containers in a non running state should be removed.
253+
# https://github.com/containers/podman/issues/25871
254+
t DELETE images/test1:latest?force=true 409
255+
256+
# When using the Podman Libpod API, deleting an image
257+
# with a running container will fail.
258+
t DELETE libpod/images/test1:latest 409
259+
260+
# When using the Podman Libpod API, force deleting an
261+
# image will also force delete all containers using the image.
262+
263+
# Verify container exists.
264+
t GET libpod/containers/test_container/exists 204
265+
266+
# Delete image with force.
267+
t DELETE libpod/images/test1:latest?force=true 200
268+
269+
# Verify container also removed.
270+
t GET libpod/containers/test_container/exists 404
271+
272+
# END: Testing variance between Docker API and Podman API
273+
# regarding force deleting images.
274+
234275
podman pull busybox
235276
t GET "images/get?names=alpine&names=busybox" 200 '[POSIX tar archive]'
236277
img_cnt=$(tar xf "$WORKDIR/curl.result.out" manifest.json -O | jq "length")

test/apiv2/python/rest_api/test_v2_0_0_image.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,8 @@ def test_inspect(self):
6666
self.assertIn("sha256:",image['Id'])
6767

6868
def test_delete(self):
69-
r = requests.delete(self.podman_url + "/v1.40/images/alpine?force=true")
70-
self.assertEqual(r.status_code, 200, r.text)
71-
self.assertIsInstance(r.json(), list)
69+
r = requests.delete(self.compat_uri("images/alpine?force=true"))
70+
self.assertEqual(r.status_code, 409, r.text)
7271

7372
def test_pull(self):
7473
r = requests.post(self.uri("/images/pull?reference=alpine"), timeout=15)

0 commit comments

Comments
 (0)