diff --git a/api/server/router/image/backend.go b/api/server/router/image/backend.go index ecac8b3225..b6fa9a2f13 100644 --- a/api/server/router/image/backend.go +++ b/api/server/router/image/backend.go @@ -39,7 +39,7 @@ type importExportBackend interface { type registryBackend interface { PullImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error - PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error + PushImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error } type Searcher interface { diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index b941cab69f..56d185a682 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -187,6 +187,7 @@ func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter img := vars["name"] tag := r.Form.Get("tag") + platformStr := r.Form.Get("platform") var ref reference.Named @@ -205,7 +206,19 @@ func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter ref = r } - if err := ir.backend.PushImage(ctx, ref, metaHeaders, authConfig, output); err != nil { + var platform *ocispec.Platform + if platformStr != "" { + if versions.LessThan(httputils.VersionFromContext(ctx), "1.46") { + return errdefs.InvalidParameter(errors.New("platform parameter is not supported in API version < 1.46")) + } + p, err := parsePlatform(platformStr) + if err != nil { + return errdefs.InvalidParameter(err) + } + platform = &p + } + + if err := ir.backend.PushImage(ctx, ref, platform, metaHeaders, authConfig, output); err != nil { if !output.Flushed() { return err } @@ -537,3 +550,10 @@ func validateRepoName(name reference.Named) error { } return nil } + +func parsePlatform(platformStr string) (ocispec.Platform, error) { + if platformStr == "remote" { + return platforms.DefaultSpec(), nil + } + return platforms.Parse(platformStr) +} diff --git a/client/image_push.go b/client/image_push.go index e6a6b11eea..bfa50b12ef 100644 --- a/client/image_push.go +++ b/client/image_push.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" + "github.com/containerd/containerd/platforms" "github.com/distribution/reference" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" @@ -36,6 +37,14 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu } } + if options.Platform != "" { + if options.Platform == "local" { + query.Set("platform", platforms.DefaultString()) + } else { + query.Set("platform", options.Platform) + } + } + resp, err := cli.tryImagePush(ctx, name, query, options.RegistryAuth) if errdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { newAuthHeader, privilegeErr := options.PrivilegeFunc() diff --git a/daemon/containerd/image_push.go b/daemon/containerd/image_push.go index 150e3c61c5..a778abac8d 100644 --- a/daemon/containerd/image_push.go +++ b/daemon/containerd/image_push.go @@ -41,7 +41,7 @@ import ( // pointing to the new target repository. This will allow subsequent pushes // to perform cross-repo mounts of the shared content when pushing to a different // repository on the same registry. -func (i *ImageService) PushImage(ctx context.Context, sourceRef reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) (retErr error) { +func (i *ImageService) PushImage(ctx context.Context, sourceRef reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) (retErr error) { start := time.Now() defer func() { if retErr == nil { @@ -76,7 +76,7 @@ func (i *ImageService) PushImage(ctx context.Context, sourceRef reference.Named, continue } - if err := i.pushRef(ctx, named, metaHeaders, authConfig, out); err != nil { + if err := i.pushRef(ctx, named, platform, metaHeaders, authConfig, out); err != nil { return err } } @@ -85,10 +85,62 @@ func (i *ImageService) PushImage(ctx context.Context, sourceRef reference.Named, } } - return i.pushRef(ctx, sourceRef, metaHeaders, authConfig, out) + return i.pushRef(ctx, sourceRef, platform, metaHeaders, authConfig, out) } -func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, out progress.Output) (retErr error) { +func (i *ImageService) getPushTarget(ctx context.Context, targetRef reference.Named, platform *ocispec.Platform) (ocispec.Descriptor, error) { + img, err := i.images.Get(ctx, targetRef.String()) + if cerrdefs.IsNotFound(err) { + return ocispec.Descriptor{}, errdefs.NotFound(fmt.Errorf("tag does not exist: %s", reference.FamiliarString(targetRef))) + } + + im, err := i.getImageManifestForPlatform(ctx, img, platform) + if err != nil { + return ocispec.Descriptor{}, err + } + + return im.Target(), nil +} + +func (i *ImageService) getImageManifestForPlatform(ctx context.Context, img containerdimages.Image, platform *ocispec.Platform) (*ImageManifest, error) { + if platform == nil { + return i.NewImageManifest(ctx, img, img.Target) + } + + pm := platforms.OnlyStrict(*platform) + var result *ImageManifest + err := i.walkReachableImageManifests(ctx, img, func(im *ImageManifest) error { + if im.IsAttestation() { + return nil + } + + imgPlatform, err := im.ImagePlatform(ctx) + if err != nil { + return err + } + + if !pm.Match(imgPlatform) { + return nil + } + + if result != nil { + return errdefs.Conflict(errors.Errorf("multiple manifests found for platform %s", *platform)) + } + + result = im + return nil + }) + if result == nil || cerrdefs.IsNotFound(err) { + return nil, errdefs.NotFound(fmt.Errorf("manifest not found for platform %s", *platform)) + } + if err != nil { + return nil, err + } + + return result, nil +} + +func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, out progress.Output) (retErr error) { leasedCtx, release, err := i.client.WithLease(ctx) if err != nil { return err @@ -99,17 +151,12 @@ func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, m } }() - img, err := i.images.Get(ctx, targetRef.String()) + target, err := i.getPushTarget(ctx, targetRef, platform) if err != nil { - if cerrdefs.IsNotFound(err) { - return errdefs.NotFound(fmt.Errorf("tag does not exist: %s", reference.FamiliarString(targetRef))) - } - return errdefs.NotFound(err) + return err } - target := img.Target store := i.content - resolver, tracker := i.newResolverFromAuthConfig(ctx, authConfig, targetRef) pp := pushProgress{Tracker: tracker} jobsQueue := newJobs() @@ -121,7 +168,7 @@ func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, m finishProgress() if retErr == nil { if tagged, ok := targetRef.(reference.Tagged); ok { - progress.Messagef(out, "", "%s: digest: %s size: %d", tagged.Tag(), target.Digest, img.Target.Size) + progress.Messagef(out, "", "%s: digest: %s size: %d", tagged.Tag(), target.Digest, target.Size) } } }() @@ -169,8 +216,9 @@ func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, m "missing content: %w\n"+ "Note: You're trying to push a manifest list/index which "+ "references multiple platform specific manifests, but not all of them are available locally "+ - "or available to the remote repository.\n"+ - "Make sure you have all the referenced content and try again.", + "or available to the remote repository.\n\n"+ + "Make sure you have all the referenced content and try again.\n"+ + "You can also push only a single platform specific manifest directly by specifying the platform you want to push.", err)) } return err diff --git a/daemon/image_service.go b/daemon/image_service.go index 12a61b4022..eed6f43d6a 100644 --- a/daemon/image_service.go +++ b/daemon/image_service.go @@ -28,7 +28,7 @@ type ImageService interface { // Images PullImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error - PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error + PushImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error CreateImage(ctx context.Context, config []byte, parent string, contentStoreDigest digest.Digest) (builder.Image, error) ImageDelete(ctx context.Context, imageRef string, force, prune bool) ([]imagetype.DeleteResponse, error) ExportImage(ctx context.Context, names []string, outStream io.Writer) error diff --git a/daemon/images/image_push.go b/daemon/images/image_push.go index 30a4cb9ba3..af2e64b598 100644 --- a/daemon/images/image_push.go +++ b/daemon/images/image_push.go @@ -2,6 +2,7 @@ package images // import "github.com/docker/docker/daemon/images" import ( "context" + "errors" "io" "time" @@ -10,11 +11,16 @@ import ( "github.com/docker/docker/api/types/registry" "github.com/docker/docker/distribution" progressutils "github.com/docker/docker/distribution/utils" + "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/progress" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // PushImage initiates a push operation on the repository named localName. -func (i *ImageService) PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error { +func (i *ImageService) PushImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error { + if platform != nil { + return errdefs.NotImplemented(errors.New("selecting platform is not supported with graphdriver backed image store")) + } start := time.Now() // Include a buffer so that slow client connections don't affect // transfer performance.