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

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  
     9  	"github.com/containerd/containerd"
    10  	"github.com/containerd/containerd/content"
    11  	cerrdefs "github.com/containerd/containerd/errdefs"
    12  	containerdimages "github.com/containerd/containerd/images"
    13  	"github.com/containerd/containerd/images/archive"
    14  	"github.com/containerd/containerd/leases"
    15  	cplatforms "github.com/containerd/containerd/platforms"
    16  	"github.com/containerd/log"
    17  	"github.com/distribution/reference"
    18  	"github.com/Prakhar-Agarwal-byte/moby/api/types/events"
    19  	"github.com/Prakhar-Agarwal-byte/moby/container"
    20  	"github.com/Prakhar-Agarwal-byte/moby/daemon/images"
    21  	"github.com/Prakhar-Agarwal-byte/moby/errdefs"
    22  	dockerarchive "github.com/Prakhar-Agarwal-byte/moby/pkg/archive"
    23  	"github.com/Prakhar-Agarwal-byte/moby/pkg/platforms"
    24  	"github.com/Prakhar-Agarwal-byte/moby/pkg/streamformatter"
    25  	"github.com/opencontainers/image-spec/specs-go"
    26  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  func (i *ImageService) PerformWithBaseFS(ctx context.Context, c *container.Container, fn func(root string) error) error {
    31  	snapshotter := i.client.SnapshotService(c.Driver)
    32  	mounts, err := snapshotter.Mounts(ctx, c.ID)
    33  	if err != nil {
    34  		return err
    35  	}
    36  	path, err := i.refCountMounter.Mount(mounts, c.ID)
    37  	if err != nil {
    38  		return err
    39  	}
    40  	defer i.refCountMounter.Unmount(path)
    41  
    42  	return fn(path)
    43  }
    44  
    45  // ExportImage exports a list of images to the given output stream. The
    46  // exported images are archived into a tar when written to the output
    47  // stream. All images with the given tag and all versions containing
    48  // the same tag are exported. names is the set of tags to export, and
    49  // outStream is the writer which the images are written to.
    50  //
    51  // TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
    52  func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error {
    53  	platform := platforms.AllPlatformsWithPreference(cplatforms.Default())
    54  	opts := []archive.ExportOpt{
    55  		archive.WithSkipNonDistributableBlobs(),
    56  
    57  		// This makes the exported archive also include `manifest.json`
    58  		// when the image is a manifest list. It is needed for backwards
    59  		// compatibility with Docker image format.
    60  		// The containerd will choose only one manifest for the `manifest.json`.
    61  		// Our preference is to have it point to the default platform.
    62  		// Example:
    63  		//  Daemon is running on linux/arm64
    64  		//  When we export linux/amd64 and linux/arm64, manifest.json will point to linux/arm64.
    65  		//  When we export linux/amd64 only, manifest.json will point to linux/amd64.
    66  		// Note: This is only applicable if importing this archive into non-containerd Docker.
    67  		// Importing the same archive into containerd, will not restrict the platforms.
    68  		archive.WithPlatform(platform),
    69  	}
    70  
    71  	contentStore := i.client.ContentStore()
    72  	leasesManager := i.client.LeasesService()
    73  	lease, err := leasesManager.Create(ctx, leases.WithRandomID())
    74  	if err != nil {
    75  		return errdefs.System(err)
    76  	}
    77  	defer func() {
    78  		if err := leasesManager.Delete(ctx, lease); err != nil {
    79  			log.G(ctx).WithError(err).Warn("cleaning up lease")
    80  		}
    81  	}()
    82  
    83  	addLease := func(ctx context.Context, target ocispec.Descriptor) error {
    84  		return leaseContent(ctx, contentStore, leasesManager, lease, target)
    85  	}
    86  
    87  	exportImage := func(ctx context.Context, target ocispec.Descriptor, ref reference.Named) error {
    88  		if err := addLease(ctx, target); err != nil {
    89  			return err
    90  		}
    91  
    92  		// We may not have locally all the platforms that are specified in the index.
    93  		// Export only those manifests that we have.
    94  		// TODO(vvoland): Reconsider this when `--platform` is added.
    95  		if containerdimages.IsIndexType(target.MediaType) {
    96  			desc, err := i.getBestDescriptorForExport(ctx, target)
    97  			if err != nil {
    98  				return err
    99  			}
   100  			target = desc
   101  		}
   102  
   103  		if ref != nil {
   104  			opts = append(opts, archive.WithManifest(target, ref.String()))
   105  
   106  			log.G(ctx).WithFields(log.Fields{
   107  				"target": target,
   108  				"name":   ref,
   109  			}).Debug("export image")
   110  		} else {
   111  			orgTarget := target
   112  			target.Annotations = make(map[string]string)
   113  
   114  			for k, v := range orgTarget.Annotations {
   115  				switch k {
   116  				case containerdimages.AnnotationImageName, ocispec.AnnotationRefName:
   117  					// Strip image name/tag annotations from the descriptor.
   118  					// Otherwise containerd will use it as name.
   119  				default:
   120  					target.Annotations[k] = v
   121  				}
   122  			}
   123  
   124  			opts = append(opts, archive.WithManifest(target))
   125  
   126  			log.G(ctx).WithFields(log.Fields{
   127  				"target": target,
   128  			}).Debug("export image without name")
   129  		}
   130  
   131  		i.LogImageEvent(target.Digest.String(), target.Digest.String(), events.ActionSave)
   132  		return nil
   133  	}
   134  
   135  	exportRepository := func(ctx context.Context, ref reference.Named) error {
   136  		imgs, err := i.getAllImagesWithRepository(ctx, ref)
   137  		if err != nil {
   138  			return errdefs.System(fmt.Errorf("failed to list all images from repository %s: %w", ref.Name(), err))
   139  		}
   140  
   141  		if len(imgs) == 0 {
   142  			return images.ErrImageDoesNotExist{Ref: ref}
   143  		}
   144  
   145  		for _, img := range imgs {
   146  			ref, err := reference.ParseNamed(img.Name)
   147  
   148  			if err != nil {
   149  				log.G(ctx).WithFields(log.Fields{
   150  					"image": img.Name,
   151  					"error": err,
   152  				}).Warn("couldn't parse image name as a valid named reference")
   153  				continue
   154  			}
   155  
   156  			if err := exportImage(ctx, img.Target, ref); err != nil {
   157  				return err
   158  			}
   159  		}
   160  
   161  		return nil
   162  	}
   163  
   164  	for _, name := range names {
   165  		target, resolveErr := i.resolveDescriptor(ctx, name)
   166  
   167  		// Check if the requested name is a truncated digest of the resolved descriptor.
   168  		// If yes, that means that the user specified a specific image ID so
   169  		// it's not referencing a repository.
   170  		specificDigestResolved := false
   171  		if resolveErr == nil {
   172  			nameWithoutDigestAlgorithm := strings.TrimPrefix(name, target.Digest.Algorithm().String()+":")
   173  			specificDigestResolved = strings.HasPrefix(target.Digest.Encoded(), nameWithoutDigestAlgorithm)
   174  		}
   175  
   176  		log.G(ctx).WithFields(log.Fields{
   177  			"name":                   name,
   178  			"resolveErr":             resolveErr,
   179  			"specificDigestResolved": specificDigestResolved,
   180  		}).Debug("export requested")
   181  
   182  		ref, refErr := reference.ParseNormalizedNamed(name)
   183  
   184  		if resolveErr != nil || !specificDigestResolved {
   185  			// Name didn't resolve to anything, or name wasn't explicitly referencing a digest
   186  			if refErr == nil && reference.IsNameOnly(ref) {
   187  				// Reference is valid, but doesn't include a specific tag.
   188  				// Export all images with the same repository.
   189  				if err := exportRepository(ctx, ref); err != nil {
   190  					return err
   191  				}
   192  				continue
   193  			}
   194  		}
   195  
   196  		if resolveErr != nil {
   197  			return resolveErr
   198  		}
   199  		if refErr != nil {
   200  			return refErr
   201  		}
   202  
   203  		// If user exports a specific digest, it shouldn't have a tag.
   204  		if specificDigestResolved {
   205  			ref = nil
   206  		}
   207  		if err := exportImage(ctx, target, ref); err != nil {
   208  			return err
   209  		}
   210  	}
   211  
   212  	return i.client.Export(ctx, outStream, opts...)
   213  }
   214  
   215  // leaseContent will add a resource to the lease for each child of the descriptor making sure that it and
   216  // its children won't be deleted while the lease exists
   217  func leaseContent(ctx context.Context, store content.Store, leasesManager leases.Manager, lease leases.Lease, desc ocispec.Descriptor) error {
   218  	return containerdimages.Walk(ctx, containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   219  		_, err := store.Info(ctx, desc.Digest)
   220  		if err != nil {
   221  			if errors.Is(err, cerrdefs.ErrNotFound) {
   222  				return nil, nil
   223  			}
   224  			return nil, errdefs.System(err)
   225  		}
   226  
   227  		r := leases.Resource{
   228  			ID:   desc.Digest.String(),
   229  			Type: "content",
   230  		}
   231  		if err := leasesManager.AddResource(ctx, lease, r); err != nil {
   232  			return nil, errdefs.System(err)
   233  		}
   234  
   235  		return containerdimages.Children(ctx, store, desc)
   236  	}), desc)
   237  }
   238  
   239  // LoadImage uploads a set of images into the repository. This is the
   240  // complement of ExportImage.  The input stream is an uncompressed tar
   241  // ball containing images and metadata.
   242  func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
   243  	decompressed, err := dockerarchive.DecompressStream(inTar)
   244  	if err != nil {
   245  		return errors.Wrap(err, "failed to decompress input tar archive")
   246  	}
   247  	defer decompressed.Close()
   248  
   249  	opts := []containerd.ImportOpt{
   250  		// TODO(vvoland): Allow user to pass platform
   251  		containerd.WithImportPlatform(cplatforms.All),
   252  
   253  		// Create an additional image with dangling name for imported images...
   254  		containerd.WithDigestRef(danglingImageName),
   255  		// ... but only if they don't have a name or it's invalid.
   256  		containerd.WithSkipDigestRef(func(nameFromArchive string) bool {
   257  			if nameFromArchive == "" {
   258  				return false
   259  			}
   260  			_, err := reference.ParseNormalizedNamed(nameFromArchive)
   261  			return err == nil
   262  		}),
   263  	}
   264  
   265  	imgs, err := i.client.Import(ctx, decompressed, opts...)
   266  	if err != nil {
   267  		log.G(ctx).WithError(err).Debug("failed to import image to containerd")
   268  		return errdefs.System(err)
   269  	}
   270  
   271  	progress := streamformatter.NewStdoutWriter(outStream)
   272  
   273  	for _, img := range imgs {
   274  		name := img.Name
   275  		loadedMsg := "Loaded image"
   276  
   277  		if isDanglingImage(img) {
   278  			name = img.Target.Digest.String()
   279  			loadedMsg = "Loaded image ID"
   280  		} else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
   281  			name = reference.FamiliarString(reference.TagNameOnly(named))
   282  		}
   283  
   284  		err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error {
   285  			logger := log.G(ctx).WithFields(log.Fields{
   286  				"image":    name,
   287  				"manifest": platformImg.Target().Digest,
   288  			})
   289  
   290  			if isPseudo, err := platformImg.IsPseudoImage(ctx); isPseudo || err != nil {
   291  				if err != nil {
   292  					logger.WithError(err).Warn("failed to read manifest")
   293  				} else {
   294  					logger.Debug("don't unpack non-image manifest")
   295  				}
   296  				return nil
   297  			}
   298  
   299  			unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
   300  			if err != nil {
   301  				logger.WithError(err).Warn("failed to check if image is unpacked")
   302  				return nil
   303  			}
   304  
   305  			if !unpacked {
   306  				err = platformImg.Unpack(ctx, i.snapshotter)
   307  
   308  				if err != nil {
   309  					return errdefs.System(err)
   310  				}
   311  			}
   312  			logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack")
   313  			return nil
   314  		})
   315  		if err != nil {
   316  			return errors.Wrap(err, "failed to unpack loaded image")
   317  		}
   318  
   319  		fmt.Fprintf(progress, "%s: %s\n", loadedMsg, name)
   320  		i.LogImageEvent(img.Target.Digest.String(), img.Target.Digest.String(), events.ActionLoad)
   321  	}
   322  
   323  	return nil
   324  }
   325  
   326  // getBestDescriptorForExport returns a descriptor which only references content available locally.
   327  // The returned descriptor can be:
   328  // - The same index descriptor - if all content is available
   329  // - Platform specific manifest - if only one manifest from the whole index is available
   330  // - Reduced index descriptor - if not all, but more than one manifest is available
   331  //
   332  // The reduced index descriptor is stored in the content store and may be garbage collected.
   333  // It's advised to pass a context with a lease that's long enough to cover usage of the blob.
   334  func (i *ImageService) getBestDescriptorForExport(ctx context.Context, indexDesc ocispec.Descriptor) (ocispec.Descriptor, error) {
   335  	none := ocispec.Descriptor{}
   336  
   337  	if !containerdimages.IsIndexType(indexDesc.MediaType) {
   338  		err := fmt.Errorf("index/manifest-list descriptor expected, got: %s", indexDesc.MediaType)
   339  		return none, errdefs.InvalidParameter(err)
   340  	}
   341  	store := i.client.ContentStore()
   342  	children, err := containerdimages.Children(ctx, store, indexDesc)
   343  	if err != nil {
   344  		if cerrdefs.IsNotFound(err) {
   345  			return none, errdefs.NotFound(err)
   346  		}
   347  		return none, errdefs.System(err)
   348  	}
   349  
   350  	// Check which platform manifests have all their blobs available.
   351  	hasMissingManifests := false
   352  	var presentManifests []ocispec.Descriptor
   353  	for _, mfst := range children {
   354  		if containerdimages.IsManifestType(mfst.MediaType) {
   355  			available, _, _, missing, err := containerdimages.Check(ctx, store, mfst, nil)
   356  			if err != nil {
   357  				hasMissingManifests = true
   358  				log.G(ctx).WithField("manifest", mfst.Digest).Warn("failed to check manifest's blob availability, won't export")
   359  				continue
   360  			}
   361  
   362  			if available && len(missing) == 0 {
   363  				presentManifests = append(presentManifests, mfst)
   364  				log.G(ctx).WithField("manifest", mfst.Digest).Debug("manifest content present, will export")
   365  			} else {
   366  				hasMissingManifests = true
   367  				log.G(ctx).WithFields(log.Fields{
   368  					"manifest": mfst.Digest,
   369  					"missing":  missing,
   370  				}).Debug("manifest is missing, won't export")
   371  			}
   372  		}
   373  	}
   374  
   375  	if !hasMissingManifests || len(children) == 0 {
   376  		// If we have the full image, or it has no manifests, just export the original index.
   377  		return indexDesc, nil
   378  	} else if len(presentManifests) == 1 {
   379  		// If only one platform is present, export that one manifest.
   380  		return presentManifests[0], nil
   381  	} else if len(presentManifests) == 0 {
   382  		// Return error when none of the image's manifest is present.
   383  		return none, errdefs.NotFound(fmt.Errorf("none of the manifests is fully present in the content store"))
   384  	}
   385  
   386  	// Create a new index which contains only the manifests we have in store.
   387  	index := ocispec.Index{
   388  		Versioned: specs.Versioned{
   389  			SchemaVersion: 2,
   390  		},
   391  		MediaType:   ocispec.MediaTypeImageIndex,
   392  		Manifests:   presentManifests,
   393  		Annotations: indexDesc.Annotations,
   394  	}
   395  
   396  	reducedIndexDesc, err := storeJson(ctx, store, index.MediaType, index, nil)
   397  	if err != nil {
   398  		return none, err
   399  	}
   400  
   401  	return reducedIndexDesc, nil
   402  }