Merge dfe5a6c6f7
into ee8b788538
This commit is contained in:
commit
f4abb248d1
7 changed files with 182 additions and 25 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Reference in a new issue