github.com/moby/docker@v26.1.3+incompatible/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  	"github.com/containerd/containerd/platforms"
    16  	"github.com/containerd/log"
    17  	"github.com/distribution/reference"
    18  	"github.com/docker/docker/api/types/events"
    19  	"github.com/docker/docker/container"
    20  	"github.com/docker/docker/daemon/images"
    21  	"github.com/docker/docker/errdefs"
    22  	dockerarchive "github.com/docker/docker/pkg/archive"
    23  	"github.com/docker/docker/pkg/streamformatter"
    24  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    25  	"github.com/pkg/errors"
    26  )
    27  
    28  func (i *ImageService) PerformWithBaseFS(ctx context.Context, c *container.Container, fn func(root string) error) error {
    29  	snapshotter := i.client.SnapshotService(c.Driver)
    30  	mounts, err := snapshotter.Mounts(ctx, c.ID)
    31  	if err != nil {
    32  		return err
    33  	}
    34  	path, err := i.refCountMounter.Mount(mounts, c.ID)
    35  	if err != nil {
    36  		return err
    37  	}
    38  	defer i.refCountMounter.Unmount(path)
    39  
    40  	return fn(path)
    41  }
    42  
    43  // ExportImage exports a list of images to the given output stream. The
    44  // exported images are archived into a tar when written to the output
    45  // stream. All images with the given tag and all versions containing
    46  // the same tag are exported. names is the set of tags to export, and
    47  // outStream is the writer which the images are written to.
    48  //
    49  // TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
    50  func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error {
    51  	platform := matchAllWithPreference(platforms.Default())
    52  	opts := []archive.ExportOpt{
    53  		archive.WithSkipNonDistributableBlobs(),
    54  
    55  		// This makes the exported archive also include `manifest.json`
    56  		// when the image is a manifest list. It is needed for backwards
    57  		// compatibility with Docker image format.
    58  		// The containerd will choose only one manifest for the `manifest.json`.
    59  		// Our preference is to have it point to the default platform.
    60  		// Example:
    61  		//  Daemon is running on linux/arm64
    62  		//  When we export linux/amd64 and linux/arm64, manifest.json will point to linux/arm64.
    63  		//  When we export linux/amd64 only, manifest.json will point to linux/amd64.
    64  		// Note: This is only applicable if importing this archive into non-containerd Docker.
    65  		// Importing the same archive into containerd, will not restrict the platforms.
    66  		archive.WithPlatform(platform),
    67  		archive.WithSkipMissing(i.content),
    68  	}
    69  
    70  	leasesManager := i.client.LeasesService()
    71  	lease, err := leasesManager.Create(ctx, leases.WithRandomID())
    72  	if err != nil {
    73  		return errdefs.System(err)
    74  	}
    75  	defer func() {
    76  		if err := leasesManager.Delete(ctx, lease); err != nil {
    77  			log.G(ctx).WithError(err).Warn("cleaning up lease")
    78  		}
    79  	}()
    80  
    81  	addLease := func(ctx context.Context, target ocispec.Descriptor) error {
    82  		return leaseContent(ctx, i.content, leasesManager, lease, target)
    83  	}
    84  
    85  	exportImage := func(ctx context.Context, target ocispec.Descriptor, ref reference.Named) error {
    86  		if err := addLease(ctx, target); err != nil {
    87  			return err
    88  		}
    89  
    90  		if ref != nil {
    91  			opts = append(opts, archive.WithManifest(target, ref.String()))
    92  
    93  			log.G(ctx).WithFields(log.Fields{
    94  				"target": target,
    95  				"name":   ref,
    96  			}).Debug("export image")
    97  		} else {
    98  			orgTarget := target
    99  			target.Annotations = make(map[string]string)
   100  
   101  			for k, v := range orgTarget.Annotations {
   102  				switch k {
   103  				case containerdimages.AnnotationImageName, ocispec.AnnotationRefName:
   104  					// Strip image name/tag annotations from the descriptor.
   105  					// Otherwise containerd will use it as name.
   106  				default:
   107  					target.Annotations[k] = v
   108  				}
   109  			}
   110  
   111  			opts = append(opts, archive.WithManifest(target))
   112  
   113  			log.G(ctx).WithFields(log.Fields{
   114  				"target": target,
   115  			}).Debug("export image without name")
   116  		}
   117  
   118  		i.LogImageEvent(target.Digest.String(), target.Digest.String(), events.ActionSave)
   119  		return nil
   120  	}
   121  
   122  	exportRepository := func(ctx context.Context, ref reference.Named) error {
   123  		imgs, err := i.getAllImagesWithRepository(ctx, ref)
   124  		if err != nil {
   125  			return errdefs.System(fmt.Errorf("failed to list all images from repository %s: %w", ref.Name(), err))
   126  		}
   127  
   128  		if len(imgs) == 0 {
   129  			return images.ErrImageDoesNotExist{Ref: ref}
   130  		}
   131  
   132  		for _, img := range imgs {
   133  			ref, err := reference.ParseNamed(img.Name)
   134  
   135  			if err != nil {
   136  				log.G(ctx).WithFields(log.Fields{
   137  					"image": img.Name,
   138  					"error": err,
   139  				}).Warn("couldn't parse image name as a valid named reference")
   140  				continue
   141  			}
   142  
   143  			if err := exportImage(ctx, img.Target, ref); err != nil {
   144  				return err
   145  			}
   146  		}
   147  
   148  		return nil
   149  	}
   150  
   151  	for _, name := range names {
   152  		target, resolveErr := i.resolveDescriptor(ctx, name)
   153  
   154  		// Check if the requested name is a truncated digest of the resolved descriptor.
   155  		// If yes, that means that the user specified a specific image ID so
   156  		// it's not referencing a repository.
   157  		specificDigestResolved := false
   158  		if resolveErr == nil {
   159  			nameWithoutDigestAlgorithm := strings.TrimPrefix(name, target.Digest.Algorithm().String()+":")
   160  			specificDigestResolved = strings.HasPrefix(target.Digest.Encoded(), nameWithoutDigestAlgorithm)
   161  		}
   162  
   163  		log.G(ctx).WithFields(log.Fields{
   164  			"name":                   name,
   165  			"resolveErr":             resolveErr,
   166  			"specificDigestResolved": specificDigestResolved,
   167  		}).Debug("export requested")
   168  
   169  		ref, refErr := reference.ParseNormalizedNamed(name)
   170  
   171  		if refErr == nil {
   172  			if _, ok := ref.(reference.Digested); ok {
   173  				specificDigestResolved = true
   174  			}
   175  		}
   176  
   177  		if resolveErr != nil || !specificDigestResolved {
   178  			// Name didn't resolve to anything, or name wasn't explicitly referencing a digest
   179  			if refErr == nil && reference.IsNameOnly(ref) {
   180  				// Reference is valid, but doesn't include a specific tag.
   181  				// Export all images with the same repository.
   182  				if err := exportRepository(ctx, ref); err != nil {
   183  					return err
   184  				}
   185  				continue
   186  			}
   187  		}
   188  
   189  		if resolveErr != nil {
   190  			return resolveErr
   191  		}
   192  		if refErr != nil {
   193  			return refErr
   194  		}
   195  
   196  		// If user exports a specific digest, it shouldn't have a tag.
   197  		if specificDigestResolved {
   198  			ref = nil
   199  		}
   200  		if err := exportImage(ctx, target, ref); err != nil {
   201  			return err
   202  		}
   203  	}
   204  
   205  	return i.client.Export(ctx, outStream, opts...)
   206  }
   207  
   208  // leaseContent will add a resource to the lease for each child of the descriptor making sure that it and
   209  // its children won't be deleted while the lease exists
   210  func leaseContent(ctx context.Context, store content.Store, leasesManager leases.Manager, lease leases.Lease, desc ocispec.Descriptor) error {
   211  	return containerdimages.Walk(ctx, containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   212  		_, err := store.Info(ctx, desc.Digest)
   213  		if err != nil {
   214  			if errors.Is(err, cerrdefs.ErrNotFound) {
   215  				return nil, nil
   216  			}
   217  			return nil, errdefs.System(err)
   218  		}
   219  
   220  		r := leases.Resource{
   221  			ID:   desc.Digest.String(),
   222  			Type: "content",
   223  		}
   224  		if err := leasesManager.AddResource(ctx, lease, r); err != nil {
   225  			return nil, errdefs.System(err)
   226  		}
   227  
   228  		return containerdimages.Children(ctx, store, desc)
   229  	}), desc)
   230  }
   231  
   232  // LoadImage uploads a set of images into the repository. This is the
   233  // complement of ExportImage.  The input stream is an uncompressed tar
   234  // ball containing images and metadata.
   235  func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
   236  	decompressed, err := dockerarchive.DecompressStream(inTar)
   237  	if err != nil {
   238  		return errors.Wrap(err, "failed to decompress input tar archive")
   239  	}
   240  	defer decompressed.Close()
   241  
   242  	opts := []containerd.ImportOpt{
   243  		// TODO(vvoland): Allow user to pass platform
   244  		containerd.WithImportPlatform(platforms.All),
   245  
   246  		containerd.WithSkipMissing(),
   247  
   248  		// Create an additional image with dangling name for imported images...
   249  		containerd.WithDigestRef(danglingImageName),
   250  		// ... but only if they don't have a name or it's invalid.
   251  		containerd.WithSkipDigestRef(func(nameFromArchive string) bool {
   252  			if nameFromArchive == "" {
   253  				return false
   254  			}
   255  			_, err := reference.ParseNormalizedNamed(nameFromArchive)
   256  			return err == nil
   257  		}),
   258  	}
   259  
   260  	imgs, err := i.client.Import(ctx, decompressed, opts...)
   261  	if err != nil {
   262  		log.G(ctx).WithError(err).Debug("failed to import image to containerd")
   263  		return errdefs.System(err)
   264  	}
   265  
   266  	progress := streamformatter.NewStdoutWriter(outStream)
   267  
   268  	for _, img := range imgs {
   269  		name := img.Name
   270  		loadedMsg := "Loaded image"
   271  
   272  		if isDanglingImage(img) {
   273  			name = img.Target.Digest.String()
   274  			loadedMsg = "Loaded image ID"
   275  		} else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
   276  			name = reference.FamiliarString(reference.TagNameOnly(named))
   277  		}
   278  
   279  		err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error {
   280  			logger := log.G(ctx).WithFields(log.Fields{
   281  				"image":    name,
   282  				"manifest": platformImg.Target().Digest,
   283  			})
   284  
   285  			if isPseudo, err := platformImg.IsPseudoImage(ctx); isPseudo || err != nil {
   286  				if err != nil {
   287  					logger.WithError(err).Warn("failed to read manifest")
   288  				} else {
   289  					logger.Debug("don't unpack non-image manifest")
   290  				}
   291  				return nil
   292  			}
   293  
   294  			unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
   295  			if err != nil {
   296  				logger.WithError(err).Warn("failed to check if image is unpacked")
   297  				return nil
   298  			}
   299  
   300  			if !unpacked {
   301  				err = platformImg.Unpack(ctx, i.snapshotter)
   302  
   303  				if err != nil {
   304  					return errdefs.System(err)
   305  				}
   306  			}
   307  			logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack")
   308  			return nil
   309  		})
   310  		if err != nil {
   311  			return errors.Wrap(err, "failed to unpack loaded image")
   312  		}
   313  
   314  		fmt.Fprintf(progress, "%s: %s\n", loadedMsg, name)
   315  		i.LogImageEvent(img.Target.Digest.String(), img.Target.Digest.String(), events.ActionLoad)
   316  	}
   317  
   318  	return nil
   319  }