Skip to content

Comments

Handle multi-platform images when listing them#113

Merged
rumpl merged 1 commit intoc8dfrom
feat-multi-platform-images
Nov 22, 2022
Merged

Handle multi-platform images when listing them#113
rumpl merged 1 commit intoc8dfrom
feat-multi-platform-images

Conversation

@rumpl
Copy link
Owner

@rumpl rumpl commented Nov 16, 2022

The images list will return an entry for each platform of an image

Copy link
Collaborator

@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.

Left one DRY comment and noticed an issue with assigning the SharedSize.

Overall, with the current CLI UX (each image entry differs only with the size, there's no indication that they are actually different platforms) I think it would make more sense to show just one entry, but with combined size of all present platforms.
It would also be more true to reality - it's one multi-platform image and not different images. What does the user gain by having multiple separate entries with the same ID? He can't even reference them on their own, for example if he saw one entry with big size, took its ID and did docker rm then all other entries would get deleted too.

Ideally (this would require CLI changes too though), I think it would make sense to have root entry representing the manifest list (which would show combined size) and children entry for each present platform that directly represents the manifest for that platform (which shows the size only for that platform). Root entry would have the manifest list hash and each platform entry would have the platform's manifest hash. Problem with this approach is that we would have to support manifest-hash as image id in all commands, which would be a bit difficulty due to being forced to have a separate image in the ImageStore for each manifest.

platforms, err := images.Platforms(ctx, i.client.ContentStore(), img.Target)
if err != nil {
return nil, err
image, c, err := i.singlePlatformImage(ctx, img, c8dplatforms.DefaultSpec(), opts)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we need to duplicate the same logic for case where images.Platforms returns error. This can only happen if the img.Target itself is missing, so the default platform won't be there neither.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed.

}
chainIDs := identity.ChainIDs(diffIDs)
if opts.SharedSize {
root[n] = &chainIDs
Copy link
Collaborator

Choose a reason for hiding this comment

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

... which is used here to increment usages of each layer.

} else {
for _, platform := range platforms {
image, c, err := i.singlePlatformImage(ctx, img, platform, opts)
chainIDs = c
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will save the chainIDs only for the last iterated platforms (see below) ...

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed

@rumpl
Copy link
Owner Author

rumpl commented Nov 16, 2022

Overall, with the current CLI UX (each image entry differs only with the size, there's no indication that they are actually different platforms) I think it would make more sense to show just one entry, but with combined size of all present platforms. It would also be more true to reality - it's one multi-platform image and not different images. What does the user gain by having multiple separate entries with the same ID? He can't even reference them on their own, for example if he saw one entry with big size, took its ID and did docker rm then all other entries would get deleted too.

Ideally (this would require CLI changes too though), I think it would make sense to have root entry representing the manifest list (which would show combined size) and children entry for each present platform that directly represents the manifest for that platform (which shows the size only for that platform). Root entry would have the manifest list hash and each platform entry would have the platform's manifest hash. Problem with this approach is that we would have to support manifest-hash as image id in all commands, which would be a bit difficulty due to being forced to have a separate image in the ImageStore for each manifest.

I agree, this is a first step, we can make the changes to the API/cli later...

@rumpl
Copy link
Owner Author

rumpl commented Nov 16, 2022

Overall, with the current CLI UX (each image entry differs only with the size, there's no indication that they are actually different platforms) I think it would make more sense to show just one entry, but with combined size of all present platforms.

I don't think this is the best way to represent it, I think it should be one per platform but with a PLATFROM column

@rumpl rumpl force-pushed the feat-multi-platform-images branch 2 times, most recently from 24ca13b to e186129 Compare November 17, 2022 12:47
Copy link
Collaborator

@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.

A few more thing related to SharedSize and checking platform presence 😅

I don't think this is the best way to represent it, I think it should be one per platform but with a PLATFROM column

Yes, I meant that until we have that column in the CLI. But maybe we can just ignore this for now and let it be confusing for now 😄 At least it's better than having no output at all.

@@ -66,63 +70,32 @@ func (i *ImageService) Images(ctx context.Context, opts types.ImageListOptions)
root = make([]*[]digest.Digest, len(imgs))
Copy link
Collaborator

@vvoland vvoland Nov 17, 2022

Choose a reason for hiding this comment

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

The slice size is too small for the root[n+m] = &chainIDs (will panic). Perhaps we should just use append and reserve capacity instead of setting the length upfront.

Suggested change
root = make([]*[]digest.Digest, len(imgs))
root = make([]*[]digest.Digest, 0, len(imgs))

(see next comment)

repoTags = []string{familiarName}
repoDigests = []string{familiarName + "@" + img.Target().Digest.String()} // "hello-world@sha256:bfea6278a0a267fad2634554f4f0c6f31981eea41c553fdf5a83e95a41d40c38"
if opts.SharedSize {
root[n+m] = &chainIDs
Copy link
Collaborator

Choose a reason for hiding this comment

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

(follow-up to 1 comment)

Suggested change
root[n+m] = &chainIDs
root = append(root, &chainIDs)

type imageFilterFunc func(image containerd.Image) bool
func (i *ImageService) singlePlatformImage(ctx context.Context, img images.Image, platform v1.Platform, opts types.ImageListOptions) (*types.ImageSummary, []digest.Digest, error) {
platMC := c8dplatforms.OnlyStrict(platform)
if avail, _, _, _, err := images.Check(ctx, i.client.ContentStore(), img.Target, platMC); !avail {
Copy link
Collaborator

Choose a reason for hiding this comment

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

avail can be true in case the manifest is present in the content store, but some of its children is missing.
In this case, and if the Check returns an error, we probably want to do an early return:

Suggested change
if avail, _, _, _, err := images.Check(ctx, i.client.ContentStore(), img.Target, platMC); !avail {
avail, _, _, missing, err := images.Check(ctx, i.client.ContentStore(), img.Target, platMC)
if !avail || err != nil || len(missing) > 0 {

Copy link
Owner Author

Choose a reason for hiding this comment

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

Are you sure about that, the docs says If available is true, the caller can assume that required represents the complete set of content required for the image.

Copy link
Collaborator

@vvoland vvoland Nov 21, 2022

Choose a reason for hiding this comment

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

Yes. The available name is a bit misleading, one has to read doc multiple times to understand it correctly 😄

If available is true, the caller can assume that required (one of return values) represents the complete set of content required for the image.

And the latter fragment:
missing will have the components that are part of required but not available in the provider.

The code also confirms:
https://github.com/containerd/containerd/blob/main/images/image.go#L323

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yeah ok looks like the documentation is not right

Copy link
Owner Author

Choose a reason for hiding this comment

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

So I just tested this (on an ARM machine):

$ docker pull --platform linux/amd64 hello-world
$ docker images

If I add the check for missing I can't see the image, if I remove it everything is fine, I can even see the size of the image. So I don't think we should check for missing.

Copy link
Owner Author

Choose a reason for hiding this comment

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

missing will contain the descriptor of the rootfs but I can run the image just fine (with docker run --platform linux/amd64 hello-world)...

Copy link
Collaborator

@vvoland vvoland Nov 21, 2022

Choose a reason for hiding this comment

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

That's strange, I'm getting different results. Pulling amd64 starting from fresh state, yields these results for me:

# docker pull --platform linux/amd64 hello-world
available=true error="<nil>" missing="[]" platform="{amd64 linux  [] }"
available=false error="<nil>" missing="[{application/vnd.docker.distribution.manifest.list.v2+json sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af 2561 [] map[] [] <nil>}]" platform="{arm linux  [] v5}"
available=false error="<nil>" missing="[{application/vnd.docker.distribution.manifest.list.v2+json sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af 2561 [] map[] [] <nil>}]" platform="{arm linux  [] v7}"
available=false error="<nil>" missing="[{application/vnd.docker.distribution.manifest.list.v2+json sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af 2561 [] map[] [] <nil>}]" platform="{arm64 linux  [] v8}"
...

Then doing:

# echo 'FROM hello-world' | docker buildx build -t asdf -

will produce:

available=true error="<nil>" missing="[]" platform="{amd64 linux  [] }"
available=true error="<nil>" missing="[{application/vnd.docker.image.rootfs.diff.tar.gzip sha256:b921b04d0447ddcd82a9220d887cd146f6ef39e20a938ee5e19a90fc3323e030 3684 [] map[] [] <nil>}]" platform="{arm linux  [] v5}"
available=true error="<nil>" missing="[{application/vnd.docker.image.rootfs.diff.tar.gzip sha256:9b157615502ddff86482f7fe2fa7a074db74a62fce12b4e8507827ac8f08d0ce 2993 [] map[] [] <nil>}]" platform="{arm linux  [] v7}"
available=true error="<nil>" missing="[]" platform="{arm64 linux  [] v8}"
available=false error="<nil>" missing="[{application/vnd.docker.distribution.manifest.list.v2+json sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af 2561 [] map[] [] <nil>}]" platform="{386 linux  [] }"

...

so images ls will show multiple images:

# docker image ls
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
asdf          latest    75f42e558e3d   7 seconds ago   4.63kB
hello-world   latest    faa03e786c97   3 minutes ago   7.03kB
hello-world   latest    faa03e786c97   3 minutes ago   8.25kB
hello-world   latest    faa03e786c97   3 minutes ago   7.56kB
hello-world   latest    faa03e786c97   3 minutes ago   7.78kB

.. which are not available locally:

# docker run --rm --platform linux/arm/v5 hello-world
Unable to find image 'hello-world:latest' locally
^C

Copy link
Owner Author

Choose a reason for hiding this comment

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

Okay my c8d was in a weird state, once I reset everything it works, I updated the code.

@rumpl rumpl force-pushed the feat-multi-platform-images branch 2 times, most recently from ac75cea to 0789fc5 Compare November 21, 2022 17:38
The images list will return an entry for each platform of an image

Signed-off-by: Djordje Lukic <[email protected]>
@rumpl rumpl force-pushed the feat-multi-platform-images branch from 0789fc5 to 9ac5d76 Compare November 21, 2022 17:41
@rumpl rumpl requested a review from vvoland November 22, 2022 09:25
@rumpl rumpl merged commit 4284892 into c8d Nov 22, 2022
@rumpl rumpl deleted the feat-multi-platform-images branch November 22, 2022 09:37
@thaJeztah
Copy link
Collaborator

Looks like this introduced some issues with docker desktop (and the UX on the cli); see moby#44573

Comment on lines -101 to -108
var repoDigests, repoTags []string
if isDanglingImage(img) {
repoTags = []string{"<none>:<none>"}
repoDigests = []string{"<none>@<none>"}
} else {
familiarName := reference.FamiliarString(ref)
repoTags = []string{familiarName}
repoDigests = []string{familiarName + "@" + img.Target().Digest.String()} // "hello-world@sha256:bfea6278a0a267fad2634554f4f0c6f31981eea41c553fdf5a83e95a41d40c38"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I missed this one. Was this removed on purpose or just accidentally?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Created a PR to restore this: #126

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants