github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_push.go (about)

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/containerd/containerd/content"
    12  	cerrdefs "github.com/containerd/containerd/errdefs"
    13  	"github.com/containerd/containerd/images"
    14  	containerdimages "github.com/containerd/containerd/images"
    15  	containerdlabels "github.com/containerd/containerd/labels"
    16  	"github.com/containerd/containerd/platforms"
    17  	"github.com/containerd/containerd/remotes"
    18  	"github.com/containerd/containerd/remotes/docker"
    19  	"github.com/containerd/log"
    20  	"github.com/distribution/reference"
    21  	"github.com/docker/docker/api/types/events"
    22  	"github.com/docker/docker/api/types/registry"
    23  	dimages "github.com/docker/docker/daemon/images"
    24  	"github.com/docker/docker/errdefs"
    25  	"github.com/docker/docker/internal/compatcontext"
    26  	"github.com/docker/docker/pkg/progress"
    27  	"github.com/docker/docker/pkg/streamformatter"
    28  	"github.com/opencontainers/go-digest"
    29  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    30  	"github.com/pkg/errors"
    31  	"golang.org/x/sync/semaphore"
    32  )
    33  
    34  // PushImage initiates a push operation of the image pointed to by sourceRef.
    35  // If reference is untagged, all tags from the reference repository are pushed.
    36  // Image manifest (or index) is pushed as is, which will probably fail if you
    37  // don't have all content referenced by the index.
    38  // Cross-repo mounts will be attempted for non-existing blobs.
    39  //
    40  // It will also add distribution source labels to the pushed content
    41  // pointing to the new target repository. This will allow subsequent pushes
    42  // to perform cross-repo mounts of the shared content when pushing to a different
    43  // repository on the same registry.
    44  func (i *ImageService) PushImage(ctx context.Context, sourceRef reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) (retErr error) {
    45  	start := time.Now()
    46  	defer func() {
    47  		if retErr == nil {
    48  			dimages.ImageActions.WithValues("push").UpdateSince(start)
    49  		}
    50  	}()
    51  	out := streamformatter.NewJSONProgressOutput(outStream, false)
    52  	progress.Messagef(out, "", "The push refers to repository [%s]", sourceRef.Name())
    53  
    54  	if _, tagged := sourceRef.(reference.Tagged); !tagged {
    55  		if _, digested := sourceRef.(reference.Digested); !digested {
    56  			// Image is not tagged nor digested, that means all tags push was requested.
    57  
    58  			// Find all images with the same repository.
    59  			imgs, err := i.getAllImagesWithRepository(ctx, sourceRef)
    60  			if err != nil {
    61  				return err
    62  			}
    63  
    64  			if len(imgs) == 0 {
    65  				return fmt.Errorf("An image does not exist locally with the tag: %s", reference.FamiliarName(sourceRef))
    66  			}
    67  
    68  			for _, img := range imgs {
    69  				named, err := reference.ParseNamed(img.Name)
    70  				if err != nil {
    71  					// This shouldn't happen, but log a warning just in case.
    72  					log.G(ctx).WithFields(log.Fields{
    73  						"image":     img.Name,
    74  						"sourceRef": sourceRef,
    75  					}).Warn("refusing to push an invalid tag")
    76  					continue
    77  				}
    78  
    79  				if err := i.pushRef(ctx, named, metaHeaders, authConfig, out); err != nil {
    80  					return err
    81  				}
    82  			}
    83  
    84  			return nil
    85  		}
    86  	}
    87  
    88  	return i.pushRef(ctx, sourceRef, metaHeaders, authConfig, out)
    89  }
    90  
    91  func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, metaHeaders map[string][]string, authConfig *registry.AuthConfig, out progress.Output) (retErr error) {
    92  	leasedCtx, release, err := i.client.WithLease(ctx)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	defer func() {
    97  		if err := release(compatcontext.WithoutCancel(leasedCtx)); err != nil {
    98  			log.G(ctx).WithField("image", targetRef).WithError(err).Warn("failed to release lease created for push")
    99  		}
   100  	}()
   101  
   102  	img, err := i.images.Get(ctx, targetRef.String())
   103  	if err != nil {
   104  		if cerrdefs.IsNotFound(err) {
   105  			return errdefs.NotFound(fmt.Errorf("tag does not exist: %s", reference.FamiliarString(targetRef)))
   106  		}
   107  		return errdefs.NotFound(err)
   108  	}
   109  
   110  	target := img.Target
   111  	store := i.content
   112  
   113  	resolver, tracker := i.newResolverFromAuthConfig(ctx, authConfig, targetRef)
   114  	pp := pushProgress{Tracker: tracker}
   115  	jobsQueue := newJobs()
   116  	finishProgress := jobsQueue.showProgress(ctx, out, combinedProgress([]progressUpdater{
   117  		&pp,
   118  		pullProgress{showExists: false, store: store},
   119  	}))
   120  	defer func() {
   121  		finishProgress()
   122  		if retErr == nil {
   123  			if tagged, ok := targetRef.(reference.Tagged); ok {
   124  				progress.Messagef(out, "", "%s: digest: %s size: %d", tagged.Tag(), target.Digest, img.Target.Size)
   125  			}
   126  		}
   127  	}()
   128  
   129  	var limiter *semaphore.Weighted = nil // TODO: Respect max concurrent downloads/uploads
   130  
   131  	mountableBlobs, err := findMissingMountable(ctx, store, jobsQueue, target, targetRef, limiter)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	// Create a store which fakes the local existence of possibly mountable blobs.
   137  	// Otherwise they can't be pushed at all.
   138  	realStore := store
   139  	wrapped := wrapWithFakeMountableBlobs(store, mountableBlobs)
   140  	store = wrapped
   141  
   142  	pusher, err := resolver.Pusher(ctx, targetRef.String())
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	addLayerJobs := containerdimages.HandlerFunc(
   148  		func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   149  			switch {
   150  			case containerdimages.IsIndexType(desc.MediaType),
   151  				containerdimages.IsManifestType(desc.MediaType),
   152  				containerdimages.IsConfigType(desc.MediaType):
   153  			default:
   154  				jobsQueue.Add(desc)
   155  			}
   156  
   157  			return nil, nil
   158  		},
   159  	)
   160  
   161  	handlerWrapper := func(h images.Handler) images.Handler {
   162  		return containerdimages.Handlers(addLayerJobs, h)
   163  	}
   164  
   165  	err = remotes.PushContent(ctx, pusher, target, store, limiter, platforms.All, handlerWrapper)
   166  	if err != nil {
   167  		if containerdimages.IsIndexType(target.MediaType) && cerrdefs.IsNotFound(err) {
   168  			return errdefs.NotFound(fmt.Errorf(
   169  				"missing content: %w\n"+
   170  					"Note: You're trying to push a manifest list/index which "+
   171  					"references multiple platform specific manifests, but not all of them are available locally "+
   172  					"or available to the remote repository.\n"+
   173  					"Make sure you have all the referenced content and try again.",
   174  				err))
   175  		}
   176  		return err
   177  	}
   178  
   179  	appendDistributionSourceLabel(ctx, realStore, targetRef, target)
   180  
   181  	i.LogImageEvent(reference.FamiliarString(targetRef), reference.FamiliarName(targetRef), events.ActionPush)
   182  
   183  	return nil
   184  }
   185  
   186  func appendDistributionSourceLabel(ctx context.Context, realStore content.Store, targetRef reference.Named, target ocispec.Descriptor) {
   187  	appendSource, err := docker.AppendDistributionSourceLabel(realStore, targetRef.String())
   188  	if err != nil {
   189  		// This shouldn't happen at this point because the reference would have to be invalid
   190  		// and if it was, then it would error out earlier.
   191  		log.G(ctx).WithError(err).Warn("failed to create an handler that appends distribution source label to pushed content")
   192  		return
   193  	}
   194  
   195  	handler := presentChildrenHandler(realStore, appendSource)
   196  	if err := containerdimages.Dispatch(ctx, handler, nil, target); err != nil {
   197  		// Shouldn't happen, but even if it would fail, then make it only a warning
   198  		// because it doesn't affect the pushed data.
   199  		log.G(ctx).WithError(err).Warn("failed to append distribution source labels to pushed content")
   200  	}
   201  }
   202  
   203  // findMissingMountable will walk the target descriptor recursively and return
   204  // missing contents with their distribution source which could potentially
   205  // be cross-repo mounted.
   206  func findMissingMountable(ctx context.Context, store content.Store, queue *jobs,
   207  	target ocispec.Descriptor, targetRef reference.Named, limiter *semaphore.Weighted,
   208  ) (map[digest.Digest]distributionSource, error) {
   209  	mountableBlobs := map[digest.Digest]distributionSource{}
   210  	var mutex sync.Mutex
   211  
   212  	sources, err := getDigestSources(ctx, store, target.Digest)
   213  	if err != nil {
   214  		if !errdefs.IsNotFound(err) {
   215  			return nil, err
   216  		}
   217  		log.G(ctx).WithField("target", target).Debug("distribution source label not found")
   218  		return mountableBlobs, nil
   219  	}
   220  
   221  	handler := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   222  		_, err := store.Info(ctx, desc.Digest)
   223  		if err != nil {
   224  			if !cerrdefs.IsNotFound(err) {
   225  				return nil, errdefs.System(errors.Wrapf(err, "failed to get metadata of content %s", desc.Digest.String()))
   226  			}
   227  
   228  			for _, source := range sources {
   229  				if canBeMounted(desc.MediaType, targetRef, source) {
   230  					mutex.Lock()
   231  					mountableBlobs[desc.Digest] = source
   232  					mutex.Unlock()
   233  					queue.Add(desc)
   234  					break
   235  				}
   236  			}
   237  			return nil, nil
   238  		}
   239  
   240  		return containerdimages.Children(ctx, store, desc)
   241  	}
   242  
   243  	err = containerdimages.Dispatch(ctx, containerdimages.HandlerFunc(handler), limiter, target)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	return mountableBlobs, nil
   249  }
   250  
   251  func getDigestSources(ctx context.Context, store content.Manager, digest digest.Digest) ([]distributionSource, error) {
   252  	info, err := store.Info(ctx, digest)
   253  	if err != nil {
   254  		if cerrdefs.IsNotFound(err) {
   255  			return nil, errdefs.NotFound(err)
   256  		}
   257  		return nil, errdefs.System(err)
   258  	}
   259  
   260  	sources := extractDistributionSources(info.Labels)
   261  	if sources == nil {
   262  		return nil, errdefs.NotFound(fmt.Errorf("label %q is not attached to %s", containerdlabels.LabelDistributionSource, digest.String()))
   263  	}
   264  
   265  	return sources, nil
   266  }
   267  
   268  func extractDistributionSources(labels map[string]string) []distributionSource {
   269  	var sources []distributionSource
   270  
   271  	// Check if this blob has a distributionSource label
   272  	// if yes, read it as source
   273  	for k, v := range labels {
   274  		if reg := strings.TrimPrefix(k, containerdlabels.LabelDistributionSource); reg != k {
   275  			for _, repo := range strings.Split(v, ",") {
   276  				ref, err := reference.ParseNamed(reg + "/" + repo)
   277  				if err != nil {
   278  					continue
   279  				}
   280  
   281  				sources = append(sources, distributionSource{
   282  					registryRef: ref,
   283  				})
   284  			}
   285  		}
   286  	}
   287  
   288  	return sources
   289  }
   290  
   291  type distributionSource struct {
   292  	registryRef reference.Named
   293  }
   294  
   295  // ToAnnotation returns key and value
   296  func (source distributionSource) ToAnnotation() (string, string) {
   297  	domain := reference.Domain(source.registryRef)
   298  	v := reference.Path(source.registryRef)
   299  	return containerdlabels.LabelDistributionSource + domain, v
   300  }
   301  
   302  func (source distributionSource) GetReference(dgst digest.Digest) (reference.Named, error) {
   303  	return reference.WithDigest(source.registryRef, dgst)
   304  }
   305  
   306  // canBeMounted returns if the content with given media type can be cross-repo
   307  // mounted when pushing it to a remote reference ref.
   308  func canBeMounted(mediaType string, targetRef reference.Named, source distributionSource) bool {
   309  	if containerdimages.IsManifestType(mediaType) {
   310  		return false
   311  	}
   312  	if containerdimages.IsIndexType(mediaType) {
   313  		return false
   314  	}
   315  
   316  	reg := reference.Domain(targetRef)
   317  	// Remove :port suffix from domain
   318  	// containerd distribution source label doesn't store port
   319  	if portIdx := strings.LastIndex(reg, ":"); portIdx != -1 {
   320  		reg = reg[:portIdx]
   321  	}
   322  
   323  	// If the source registry is the same as the one we are pushing to
   324  	// then the cross-repo mount will work.
   325  	return reg == reference.Domain(source.registryRef)
   326  }