Skip to content

Commit

Permalink
c8d/push: Support --platform switch
Browse files Browse the repository at this point in the history
Add a `platform` parameter to the `POST /images/{id}/push` that
allows to specify a single-platform manifest to be pushed instead of the
whole image index.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
  • Loading branch information
vvoland committed Apr 4, 2024
1 parent ed44da7 commit 489ca74
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 18 deletions.
2 changes: 1 addition & 1 deletion api/server/router/image/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 11 additions & 1 deletion api/server/router/image/image_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -205,7 +206,16 @@ 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 != "" {
p, err := platforms.Parse(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
}
Expand Down
4 changes: 4 additions & 0 deletions client/image_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu
}
}

if options.Platform != "" {
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()
Expand Down
72 changes: 58 additions & 14 deletions daemon/containerd/image_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -85,10 +85,58 @@ 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
}

platform, err := im.ImagePlatform(ctx)
if err != nil {
return err
}

if !pm.Match(platform) {
return nil
}

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
Expand All @@ -99,17 +147,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()
Expand All @@ -121,7 +164,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)
}
}
}()
Expand Down Expand Up @@ -169,8 +212,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
Expand Down
2 changes: 1 addition & 1 deletion daemon/image_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion daemon/images/image_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package images // import "github.com/docker/docker/daemon/images"

import (
"context"
"errors"
"io"
"time"

Expand All @@ -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.
Expand Down

0 comments on commit 489ca74

Please sign in to comment.