Skip to content

Commit 42cee9d

Browse files
Merge pull request #25946 from ninja-quokka/docker_compat_force_image_remove
bug: Correct Docker compat REST API image delete endpoint
2 parents b75a0f5 + 6e7de43 commit 42cee9d

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)