github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/daemon/containerd/image_push.go (about)

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