Skip to content

c8d: implement missing image delete logic#45427

Merged
thaJeztah merged 1 commit intomoby:masterfrom
laurazard:c8d-fix-incomplete-images
May 10, 2023
Merged

c8d: implement missing image delete logic#45427
thaJeztah merged 1 commit intomoby:masterfrom
laurazard:c8d-fix-incomplete-images

Conversation

@laurazard
Copy link
Member

@laurazard laurazard commented Apr 28, 2023

- Related

- What I did

Ports over all the previous image delete logic, such as:

  • Implement prune and force flags
  • Implement hard and soft image delete conflics, which represent:
    • image referenced in multiple tags (soft conflict)
    • image being used by a stopped container (soft conflict)
    • image being used by a running container (hard conflict)
  • Implement delete logic such as:
    • if deleting by reference, and there are other references to the same image, just
      delete the passed reference
    • if deleting by reference, and there is only 1 reference and the image is being used
      by a running container, throw an error if !force, or delete the reference and create
      a dangling reference otherwise
    • if deleting by imageID, and force is true, remove all tags (otherwise soft conflict)
    • if imageID, check if stopped container is using the image (soft conflict), and
      delete anyway if force
    • if imageID was passed in, check if running container is using the image (hard conflict)
    • if prune is true, and the image being deleted has dangling parents, remove them

Also implements logic to get image parents in c8d by comparing shared layers, like what @vvoland did in #45265, so that we can prune dangling parents on image delete.

- How I did it

With neovim! 👀

- How to verify it

Pull and delete a ton of different images, tag them and use them and try to delete them 😅
Compare the results, and see if they make sense.

Some examples from me:

root@d7ded31152c0:/go/src/github.com/docker/docker# docker pull bash
Using default tag: latest
e0acf0b8fb59: Download complete
13bc69df1e93: Download complete
0e841167f105: Download complete
c41833b44d91: Download complete
dc061c8e5140: Download complete
c8c412e0d7ad: Download complete
docker.io/library/bash:latest
root@d7ded31152c0:/go/src/github.com/docker/docker# docker run -dit --name foo bash
847ac95823ef2efb8839345cca85ca28f4b6cefcd32af4d37b5ebe4d16f0b624
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rmi bash
Error response from daemon: conflict: unable to remove repository reference "bash:latest" (must force) - container 847ac95823ef is using its referenced image e0acf0b8fb59
root@d7ded31152c0:/go/src/github.com/docker/docker# docker tag bash bar
root@d7ded31152c0:/go/src/github.com/docker/docker# docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
bar          latest    e0acf0b8fb59   2 seconds ago    24.8MB
bash         latest    e0acf0b8fb59   19 seconds ago   24.8MB
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rmi e0ac
Error response from daemon: conflict: unable to delete e0acf0b8fb59 (cannot be forced) - image is being used by running container 847ac95823ef
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rmi bash
Untagged: bash:latest
root@d7ded31152c0:/go/src/github.com/docker/docker# docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
bar          latest    e0acf0b8fb59   35 seconds ago   24.8MB
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rmi -f bar
Untagged: bar:latest
root@d7ded31152c0:/go/src/github.com/docker/docker# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
<none>       <none>    e0acf0b8fb59   4 seconds ago   24.8MB
root@d7ded31152c0:/go/src/github.com/docker/docker# docker kill foo
foo
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rm foo
foo
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rmi e0ac
Untagged: moby-dangling@sha256:e0acf0b8fb59c01b6a2b66de360c86bcad5c3cd114db325155970e6bab9663a0
Deleted: sha256:e0acf0b8fb59c01b6a2b66de360c86bcad5c3cd114db325155970e6bab9663a0
root@d7ded31152c0:/go/src/github.com/docker/docker#

(complains if it's the only reference and a container is using it, allows deleting if there is another reference, forcing leaves dangling image)

root@d7ded31152c0:/go/src/github.com/docker/docker# docker pull bash
Using default tag: latest
e0acf0b8fb59: Download complete
13bc69df1e93: Download complete
0e841167f105: Download complete
c8c412e0d7ad: Download complete
c41833b44d91: Download complete
dc061c8e5140: Download complete
docker.io/library/bash:latest
root@d7ded31152c0:/go/src/github.com/docker/docker# docker tag bash bar
root@d7ded31152c0:/go/src/github.com/docker/docker# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
bar          latest    e0acf0b8fb59   1 second ago    24.8MB
bash         latest    e0acf0b8fb59   6 seconds ago   24.8MB
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rmi e0ac
Error response from daemon: conflict: unable to delete e0acf0b8fb59 (must be forced) - image is referenced in multiple repositories
root@d7ded31152c0:/go/src/github.com/docker/docker# docker rmi -f e0ac
Untagged: bar:latest
Untagged: bash:latest
Deleted: sha256:e0acf0b8fb59c01b6a2b66de360c86bcad5c3cd114db325155970e6bab9663a0
root@d7ded31152c0:/go/src/github.com/docker/docker# docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE
root@d7ded31152c0:/go/src/github.com/docker/docker#

(complains if image referenced in multiple repos, removes all and deletes with force)

- Description for the changelog

- A picture of a cute animal (not mandatory but encouraged)

image

@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from ad3a94d to 0b78470 Compare April 28, 2023 09:28
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from 0b78470 to b8113bb Compare April 28, 2023 09:35
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch 2 times, most recently from d775d72 to d8f4e5b Compare April 28, 2023 10:07
@laurazard
Copy link
Member Author

laurazard commented Apr 28, 2023

Failures are unrelated:

=== FAIL: github.com/docker/docker/integration-cli TestDockerCLIPushSuite/TestPushToCentralRegistryUnauthorized (51.36s)
    docker_cli_push_test.go:229: assertion failed: strings.Contains(out, "Retrying") is true

see: #45415

@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from d8f4e5b to 9d68cc3 Compare April 28, 2023 11:22
@thaJeztah thaJeztah added status/2-code-review area/images Image Distribution containerd-integration Issues and PRs related to containerd integration labels Apr 28, 2023
@laurazard laurazard requested a review from tianon April 28, 2023 16:45
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch 2 times, most recently from 3b3b1af to c8c76b1 Compare May 2, 2023 17:00
@laurazard laurazard requested a review from corhere May 2, 2023 17:03
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from c8c76b1 to 79a74fb Compare May 2, 2023 17:04
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from 79a74fb to b482a09 Compare May 3, 2023 09:36
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch 5 times, most recently from 20cbe62 to 6c29ecb Compare May 4, 2023 10:48
// are divided into two categories grouped by their severity:
//
// Hard Conflict:
// - a pull or build using the image.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly unrelated, but I did not find any check for this in the old implementation either 😅 We should probably remove it, unless I missed it somewhere (not even sure how we would check for this, or how a pull would be using the image – for build, I guess with the classic builder this would fall under the "check if a running container is using it"?).
cc @thaJeztah

@laurazard laurazard requested review from corhere, rumpl and vvoland May 4, 2023 11:01
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from 6c29ecb to 5200291 Compare May 4, 2023 11:12
Copy link
Contributor

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's time to dig into the nuances of error handling

@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch 4 times, most recently from 03945c2 to 0678c9d Compare May 5, 2023 01:34
@laurazard laurazard requested review from corhere and neersighted May 5, 2023 01:34
Copy link
Contributor

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm not too thrilled about the number of ImageService().List calls (containerd does a full table scan for every list call, even with filters) but we can always iterate on the implementation later if it is found to be a performance issue in practice.

@laurazard
Copy link
Member Author

I agree re: performance! I can think of a few changes we can make to reduce the no. of ImageService.List() calls (by reordering some things, and leaving the code-paths that imply the most calls for last) – but some of them will always be necessary with the current model/implementation.

I've addressed the last nits for now, and I have another branch I'll open a PR for soon with some of those refactors (although that still needs a good amount of work for readability – lmk if you have any good ideas!).

@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from 0678c9d to 95c3f29 Compare May 8, 2023 01:19
Ports over all the previous image delete logic, such as:
- Introduce `prune` and `force` flags
- Introduce the concept of hard and soft image delete conflics, which represent:
  - image referenced in multiple tags (soft conflict)
  - image being used by a stopped container (soft conflict)
  - image being used by a running container (hard conflict)
- Implement delete logic such as:
  - if deleting by reference, and there are other references to the same image, just
    delete the passed reference
  - if deleting by reference, and there is only 1 reference and the image is being used
    by a running container, throw an error if !force, or delete the reference and create
    a dangling reference otherwise
  - if deleting by imageID, and force is true, remove all tags (otherwise soft conflict)
  - if imageID, check if stopped container is using the image (soft conflict), and
    delete anyway if force
  - if imageID was passed in, check if running container is using the image (hard conflict)
  - if `prune` is true, and the image being deleted has dangling parents, remove them

This commit also implements logic to get image parents in c8d by comparing shared layers.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
@laurazard laurazard force-pushed the c8d-fix-incomplete-images branch from 95c3f29 to cad9713 Compare May 8, 2023 01:32
@laurazard laurazard requested review from corhere and cpuguy83 May 8, 2023 01:32
@laurazard
Copy link
Member Author

@thaJeztah @rumpl @vvoland @corhere @cpuguy83 @tianon PTAL (sorry for the mass ping 😅)

Copy link
Contributor

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@vvoland vvoland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; left some thoughts/nits, but I think they can be looked at in a follow-up

return false, err
}
for _, ref := range refs {
if !isDanglingImage(ref) && ref.Name != img.Name {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-nit/optimizatino (no blocker);

Suggested change
if !isDanglingImage(ref) && ref.Name != img.Name {
if ref.Name != img.Name && !isDanglingImage(ref) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isDanglingImage() is a straightforward string compare, so either order should have about the performance, no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it'd be very minimal 😂 isDanglingImage() does slightly more but definitely no issue

func (i *ImageService) isSingleReference(ctx context.Context, img images.Image) (bool, error) {
refs, err := i.client.ImageService().List(ctx, "target.digest=="+img.Target.Digest.String())
if err != nil {
return false, err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, because this is something we need to look at in a wider scope; this would return a containerd error; we may have to translate those to one of our errdefs definitions.

if err != nil {
return nil, err
}
i.LogImageEvent(imgID.String(), imgID.String(), "untag")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should one of these be the image name? https://github.com/moby/moby/blob/3f7fde76c2e4e7c234e85c9a8634e2d7639f4f6d/daemon/containerd/image_events.go#L10C1-L11

Suggested change
i.LogImageEvent(imgID.String(), imgID.String(), "untag")
i.LogImageEvent(imgID.String(), img.Name, "untag")

(or the familiar string from below)?

edit: ❓ looks like the non-containerd implementation does the same, so no need to change (but we should look "why")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah! I wondered the same (but did this over a while so I forgot to call it out when opening the PR) but yes, I left it like this since this was the non-containerd implementation as well. I'll leave it for a follow up to change both/discuss any potential impacts of doing so.

return nil, err
}
i.LogImageEvent(imgID.String(), imgID.String(), "untag")
records := []types.ImageDeleteResponseItem{{Untagged: reference.FamiliarString(reference.TagNameOnly(parsedRef))}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me; I was looking into some of these conversions; we have some very silly code-paths where we convert "friendly" name to "fully-qualified" and back (and normalise again).

We could consider defining refString once, as most "happy" code-paths need it (only in case of an error we discard it);

refString := reference.FamiliarString(reference.TagNameOnly(parsedRef))

Comment on lines +84 to +87
using := func(c *container.Container) bool {
return c.ImageID == imgID
}
ctr := i.containers.First(using)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could inline the function here, which makes it clearer it's only used once.

Suggested change
using := func(c *container.Container) bool {
return c.ImageID == imgID
}
ctr := i.containers.First(using)
ctr := i.containers.First( func(c *container.Container) bool {
return c.ImageID == imgID
})

type imageDeleteConflict struct {
hard bool
used bool
reference string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should look at synchronising this code with the old implementation (I see that one uses imgID here instead of a string).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially had it like that, and then changed it so we could reuse it here:
https://github.com/moby/moby/pull/45427/files#diff-5d5a46c403a4d9666b17b889c12655b3c1539ab462cc38f3fd2e43cf99749b43R95
re @corhere's comment: #45427 (comment)

Would probably be good to make the same change in the old implementation

return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", idc.reference, forceMsg, idc.message)
}

func (imageDeleteConflict) Conflict() {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a pointer as well? I noticed my IDE is complaining about mixed pointer and non-pointer receivers;

Suggested change
func (imageDeleteConflict) Conflict() {}
func (*imageDeleteConflict) Conflict() {}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sounds good!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really matter in practice especially since this method is never called. It would technically make a difference for whether or not (*imageDeleteConflict)(nil).Conflict() nil-deref panics, but that's about it.

Why is your IDE complaining about mixing pointer and non-pointer receivers? Mixing is a perfectly reasonable thing to do to distinguish methods which mutate the receiver from "const" methods.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's why I removed it initially. I don't use an IDE (just gopls and golang-ci-langserver) but maybe it complains not because there are different methods with mixed pointer/non-pointer receivers but because there are other implementators of that interface with pointer/non-pointer receivers.

// Implements the error interface.
type imageDeleteConflict struct {
hard bool
used bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like used is not used anywhere in the containerd implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/images Image Distribution containerd-integration Issues and PRs related to containerd integration process/cherry-picked status/2-code-review

Projects

Development

Successfully merging this pull request may close these issues.

containerd integration: implement "force" option for image delete containerd integration: implement "prune" (--no-prune) option for image delete

6 participants