浏览代码

Merge dfe5a6c6f70c3093be68f7552caaa8bc6b2e9b03 into ee8b788538ea2c6d46d65f17be156de65bc21bb9

Paweł Gronowski 1 年之前
父节点
当前提交
f4abb248d1

+ 1 - 1
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 {

+ 21 - 1
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)
+}

+ 9 - 0
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()

+ 81 - 7
daemon/containerd/image_manifest.go

@@ -6,6 +6,7 @@ import (
 
 	"github.com/containerd/containerd"
 	"github.com/containerd/containerd/content"
+	cerrdefs "github.com/containerd/containerd/errdefs"
 	"github.com/containerd/containerd/images"
 	containerdimages "github.com/containerd/containerd/images"
 	"github.com/containerd/containerd/platforms"
@@ -21,8 +22,7 @@ var (
 )
 
 // walkImageManifests calls the handler for each locally present manifest in
-// the image. The image implements the containerd.Image interface, but all
-// operations act on the specific manifest instead of the index.
+// the image.
 func (i *ImageService) walkImageManifests(ctx context.Context, img containerdimages.Image, handler func(img *ImageManifest) error) error {
 	desc := img.Target
 
@@ -48,6 +48,52 @@ func (i *ImageService) walkImageManifests(ctx context.Context, img containerdima
 	return errNotManifestOrIndex
 }
 
+// walkReachableImageManifests calls the handler for each manifest in the
+// multiplatform image that can be reached from the given image.
+// The image might not be present locally, but its descriptor is known.
+func (i *ImageService) walkReachableImageManifests(ctx context.Context, img containerdimages.Image, handler func(img *ImageManifest) error) error {
+	desc := img.Target
+
+	handleManifest := func(ctx context.Context, d ocispec.Descriptor) error {
+		platformImg, err := i.NewImageManifest(ctx, img, d)
+		if err != nil {
+			if err == errNotManifest {
+				return nil
+			}
+			return err
+		}
+		return handler(platformImg)
+	}
+
+	if containerdimages.IsManifestType(desc.MediaType) {
+		return handleManifest(ctx, desc)
+	}
+
+	if containerdimages.IsIndexType(desc.MediaType) {
+		return containerdimages.Walk(ctx, containerdimages.HandlerFunc(
+			func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
+				err := handleManifest(ctx, desc)
+				if err != nil {
+					return nil, err
+				}
+
+				descs, err := containerdimages.Children(ctx, i.content, desc)
+				if err != nil {
+					if cerrdefs.IsNotFound(err) {
+						return nil, nil
+					}
+					return nil, err
+				}
+				return descs, nil
+			}), desc)
+	}
+
+	return errNotManifestOrIndex
+}
+
+// ImageManifest implements the containerd.Image interface, but all operations
+// act on the specific manifest instead of the index as opposed to the struct
+// returned by containerd.NewImageWithPlatform.
 type ImageManifest struct {
 	containerd.Image
 
@@ -78,21 +124,31 @@ func (im *ImageManifest) Metadata() containerdimages.Image {
 	return md
 }
 
+func (im *ImageManifest) IsAttestation() bool {
+	// Quick check for buildkit attestation manifests
+	// https://github.com/moby/buildkit/blob/v0.11.4/docs/attestations/attestation-storage.md
+	// This would have also been caught by the layer check below, but it requires
+	// an additional content read and deserialization of Manifest.
+	if _, has := im.Target().Annotations[attestation.DockerAnnotationReferenceType]; has {
+		return true
+	}
+	return false
+}
+
 // IsPseudoImage returns false if the manifest has no layers or any of its layers is a known image layer.
 // Some manifests use the image media type for compatibility, even if they are not a real image.
 func (im *ImageManifest) IsPseudoImage(ctx context.Context) (bool, error) {
 	desc := im.Target()
 
-	// Quick check for buildkit attestation manifests
-	// https://github.com/moby/buildkit/blob/v0.11.4/docs/attestations/attestation-storage.md
-	// This would have also been caught by the layer check below, but it requires
-	// an additional content read and deserialization of Manifest.
-	if _, has := desc.Annotations[attestation.DockerAnnotationReferenceType]; has {
+	if im.IsAttestation() {
 		return true, nil
 	}
 
 	mfst, err := im.Manifest(ctx)
 	if err != nil {
+		if cerrdefs.IsNotFound(err) {
+			return false, errdefs.NotFound(errors.Wrapf(err, "failed to read manifest %v", desc.Digest))
+		}
 		return true, err
 	}
 	if len(mfst.Layers) == 0 {
@@ -149,3 +205,21 @@ func readManifest(ctx context.Context, store content.Provider, desc ocispec.Desc
 
 	return mfst, nil
 }
+
+// ImagePlatform returns the platform of the image manifest.
+// If the manifest list doesn't have a platform filled, it will be read from the config.
+func (m *ImageManifest) ImagePlatform(ctx context.Context) (ocispec.Platform, error) {
+	target := m.Target()
+	if target.Platform != nil {
+		return *target.Platform, nil
+	}
+
+	configDesc, err := m.Config(ctx)
+	if err != nil {
+		return ocispec.Platform{}, err
+	}
+
+	var out ocispec.Platform
+	err = readConfig(ctx, m.ContentStore(), configDesc, &out)
+	return out, err
+}

+ 62 - 14
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

+ 1 - 1
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

+ 7 - 1
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.