Add support for multiple platforms in image export and load#50166
Add support for multiple platforms in image export and load#50166thaJeztah merged 2 commits intomoby:masterfrom
Conversation
6fb6efe to
377f4d7
Compare
|
Looks like the description and title may not match; it's probably either For a quick TL;DR (as they are confusing, and have confused people many times);
|
|
Hi @thaJeztah,
From a Docker CLI perspective, I mean But the naming I used in the title reflects the naming in the Moby code itself (exportImage for And to make the naming even more confusing, So some renaming would be good here for consistency (can do that in a follow-up commit). |
daemon/images/image_exporter.go
Outdated
| if len(platformList) > 0 { | ||
| platform = platformList[len(platformList)-1] | ||
| } |
There was a problem hiding this comment.
Considering if we should make this check for the overlap between the given list of platforms and the image that's specified.
Basically thinking;
docker pull --platform linux/s390x alpine- I now have
alpineon my machine, but only aslinux/s390x docker save --platform linux/s390x,linux/amd64 alpine(savealpine, but only thes390xandamd64variants, skip other variants- ^^ we only have
s390x, and "skip"amd64(which ... isn't present, so nothing to skip)
I guess that depends a bit on how we describe / consider the purpose of --platform in this case;
- Is it a requirement? (export if present, fail if not)
- Is it a filter? (export the given platforms, and skip other ones)
There was a problem hiding this comment.
Is it a requirement? (export if present, fail if not)
Yes, I view it as such.
Maybe the better approach is to modify the code you mentioned to error out if more than one platform is passed when using the docker image store? E.g.,
$ docker save --platform linux/s390x,linux/amd64 alpine
<error: --platform only takes a single platform when using the Docker image store.">
Then the user does:
$ docker save --platform linux/amd64 alpine
<error indicating failure because image does not contain specified platform>
$ docker save --platform linux/s390x alpine
<success>
WDYT?
vvoland
left a comment
There was a problem hiding this comment.
Thanks, left some comment! Also needs an entry in version-history.md
api/server/router/image/backend.go
Outdated
|
|
||
| type importExportBackend interface { | ||
| LoadImage(ctx context.Context, inTar io.ReadCloser, platform *ocispec.Platform, outStream io.Writer, quiet bool) error | ||
| LoadImage(ctx context.Context, inTar io.ReadCloser, platformList []*ocispec.Platform, outStream io.Writer, quiet bool) error |
There was a problem hiding this comment.
| LoadImage(ctx context.Context, inTar io.ReadCloser, platformList []*ocispec.Platform, outStream io.Writer, quiet bool) error | |
| LoadImage(ctx context.Context, inTar io.ReadCloser, platforms []ocispec.Platform, outStream io.Writer, quiet bool) error |
We can now drop the pointer. Before, the pointer was necessary to describe if explicit platform is specified or not.
There was a problem hiding this comment.
Makes sense, done.
| if formPlatforms := r.Form["platform"]; len(formPlatforms) > 1 { | ||
| // TODO(thaJeztah): remove once we support multiple platforms: see https://github.com/moby/moby/issues/48759 | ||
| return errdefs.InvalidParameter(errors.New("multiple platform parameters not supported")) | ||
| formPlatforms := r.Form["platform"] |
There was a problem hiding this comment.
This (and the other endpoint) could use
moby/api/server/httputils/form.go
Line 169 in 84f5e53
daemon/images/image_exporter.go
Outdated
| // The docker image store only supports single platform images. If multiple platforms are given, | ||
| // use the last one (for compatibility with previous behavior). | ||
| if len(platformList) > 0 { | ||
| platform = platformList[len(platformList)-1] | ||
| } | ||
|
|
There was a problem hiding this comment.
We should error out if there are more than 1 platforms passed.
daemon/containerd/image_exporter.go
Outdated
| if len(platformList) == 0 { | ||
| pm = matchAllWithPreference(i.hostPlatformMatcher()) | ||
| } else { | ||
| pl := make([]ocispec.Platform, 0, len(platformList)) | ||
| for _, p := range platformList { | ||
| pl = append(pl, *p) | ||
| } | ||
| pm = platforms.Any(pl...) | ||
| } |
There was a problem hiding this comment.
This matcher is ONLY used for the backwards compatible manifest.json and should still "prefer" the host platform to be used for it.
There was a problem hiding this comment.
Yes good catch; I've fixed it. Had to create a new matcher called matchAnyWithPreference which matches any of the given platforms but orders the result so that the host platform comes first (if present in the image manifests).
| // matchAnyWithPreference will return a platform matcher that matches any of the | ||
| // given platforms but will order platforms matching the preferred matcher first. |
There was a problem hiding this comment.
Nit; trying to avoid future tense;
| // matchAnyWithPreference will return a platform matcher that matches any of the | |
| // given platforms but will order platforms matching the preferred matcher first. | |
| // matchAnyWithPreference returns platform matcher that matches any of the | |
| // given platforms but orders platforms to match the preferred matcher first. |
There was a problem hiding this comment.
Make sense, done.
|
|
||
| // matchAnyWithPreference will return a platform matcher that matches any of the | ||
| // given platforms but will order platforms matching the preferred matcher first. | ||
| func matchAnyWithPreference(preferred platforms.MatchComparer, platformList []ocispec.Platform) platforms.MatchComparer { |
There was a problem hiding this comment.
Could use input on this, but changing the signature to return the concrete type may be more convenient;
func matchAnyWithPreference(preferred platforms.MatchComparer, platformList []ocispec.Platform) platformsWithPreferenceMatcher {At least; locations where it's used should already assert it satisfies the interface, but it can make it more convenient to "jump" to the implementation (i.e., in TestPlatformsWithPreferenceMatcher, clicking matcher.Match in my IDE will directly take me to the implementation, not to the interface.
| func (c platformsWithPreferenceMatcher) Match(p ocispec.Platform) bool { | ||
| return platforms.Any(c.platformList...).Match(p) | ||
| } |
There was a problem hiding this comment.
Probably not for this PR, but we could even consider merging with the existing matchAllWithPreference, and consider the platform-list to be an optional argument, which would make it something like;
func (c platformsWithPreferenceMatcher) Match(p ocispec.Platform) bool {
if len(c.platformList) == 0 {
return true
}
return platforms.Any(c.platformList...).Match(p)
}Mostly looking if we can reduce the number of matcher variations we have; for example, we also have ImageService. matchRequestedOrDefault, which currently accepts a single platform as argument, but (MAYBE?) could take a variadic list or slice, so that we can use the same logic in more places;
moby/daemon/containerd/platform_matchers.go
Lines 39 to 54 in 9e985bd
There was a problem hiding this comment.
Good idea; I added a follow-up commit to the PR, PTAL.
Thanks!
integration/image/save_test.go
Outdated
| cerrdefs "github.com/containerd/errdefs" | ||
| "github.com/cpuguy83/tar2go" | ||
| containertypes "github.com/docker/docker/api/types/container" | ||
| "github.com/docker/docker/api/types/image" |
There was a problem hiding this comment.
This needs a rebase, and needs to use the new import paths for the api
After rebasing and updating the import paths, it's also needed to run hack/vendor.sh because we now vendor the API module in the main module (don't ask 😆)
Currently the image export and load APIs can be used to export or load all platforms for the image, or a single specified platform. This commit updates the API so that it accepts a list of platforms to export or load, thereby giving clients the ability to export only selected platforms of an image into a tar file, or load selected platforms from a tar file. Unit and integration tests were updated accordingly. As this requires a daemon API change, the API version was bumped. Signed-off-by: Cesar Talledo <cesar.talledo@docker.com>
Merge the matchAllWithPreference matcher into the matchAnyWithPreference matcher to reduce code duplication and simplify the code. Signed-off-by: Cesar Talledo <cesar.talledo@docker.com>
|
Did a quick rebase and vendor |
Thanks @thaJeztah! |
|
CI looks good, the one integration test failure is spurious and unrelated to this PR. |
thaJeztah
left a comment
There was a problem hiding this comment.
LGTM
I can fix-up the API version and version-history in a follow-up.
| type: "array" | ||
| items: | ||
| type: "string" |
There was a problem hiding this comment.
I'll open a follow-up to update the docs/api/version-history.md - those docs probably still need to be moved to the API module as well.
| for _, p := range platformList { | ||
| platformNames = append(platformNames, platforms.FormatAll(p)) | ||
| } | ||
| log.G(ctx).WithFields(log.Fields{"error": err, "platform(s)": platformNames}).Debug("failed to import image to containerd") |
There was a problem hiding this comment.
Nit (can be updated in a follow-up); platform(s) as name for the structured logs feels a bit awkward; probably either platforms or platform would be better.
| err = verifyImagePlatforms(ctx, imgSvc, imgRef, platforms) | ||
| assert.NilError(t, err) | ||
| }) | ||
| cleanup(ctx, t) |
There was a problem hiding this comment.
No need to change; I initially considered this could be a t.Cleanupin each test; like https://go.dev/play/p/fyK3a91yn5g, but not sure ift.Cleanup` is guaranteed to have finished between each test 🤔 (that way the cleanup - including any failure - was associated with each test).
package main
import "testing"
func TestMultiCleanup(t *testing.T) {
cleanup := func(t *testing.T) func() {
return func() {
t.Helper()
t.Log("cleaning up", t.Name())
}
}
t.Run("test one", func(t *testing.T) {
t.Cleanup(cleanup(t))
})
t.Run("test two", func(t *testing.T) {
t.Cleanup(cleanup(t))
t.Fatal("test two failed")
})
t.Run("test three", func(t *testing.T) {
t.Cleanup(cleanup(t))
})
}Alternatively,, we could could setup a fake store as part of a setup() for each test, so that no state is shared between tests which would allow t.Parallel() to work as well.
|
- What I did
Add support for multiple platforms when loading or exporting images from/to tar archives.
This way clients (e.g., docker cli) can do things like:
Or
Fixes #48759 .
NOTE: See the corresponding PR in the Docker CLI so that
docker image loadanddocker image saveaccept multiple platforms via the--platformoption (as opposed to a single platform as they currently do).- How I did it
Changed the image export and load APIs so that they accept an array of platforms, as opposed to a single platform. Also modified the code that handles those APIs accordingly.
- How to verify it
Updated unit and integration tests accordingly.
- Human readable description for the release notes
- A picture of a cute animal (not mandatory but encouraged)