diff --git a/daemon/containerd/image_exporter.go b/daemon/containerd/image_exporter.go index 3f99718f1017f..cf27af02b3a1c 100644 --- a/daemon/containerd/image_exporter.go +++ b/daemon/containerd/image_exporter.go @@ -5,34 +5,17 @@ import ( "io" "github.com/containerd/containerd" + cerrdefs "github.com/containerd/containerd/errdefs" + containerdimages "github.com/containerd/containerd/images" "github.com/containerd/containerd/images/archive" + "github.com/containerd/containerd/images/converter" "github.com/containerd/containerd/platforms" "github.com/docker/distribution/reference" + "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -// ExportImage exports a list of images to the given output stream. The -// exported images are archived into a tar when written to the output -// stream. All images with the given tag and all versions containing -// the same tag are exported. names is the set of tags to export, and -// outStream is the writer which the images are written to. -func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error { - opts := []archive.ExportOpt{ - archive.WithPlatform(platforms.Ordered(platforms.DefaultSpec())), - archive.WithSkipNonDistributableBlobs(), - } - is := i.client.ImageService() - for _, imageRef := range names { - named, err := reference.ParseDockerRef(imageRef) - if err != nil { - return err - } - opts = append(opts, archive.WithImage(is, named.String())) - } - return i.client.Export(ctx, outStream, opts...) -} - // LoadImage uploads a set of images into the repository. This is the // complement of ExportImage. The input stream is an uncompressed tar // ball containing images and metadata. @@ -64,3 +47,82 @@ func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outSt } return nil } + +// ExportImage exports a list of images to the given output stream. The +// exported images are archived into a tar when written to the output +// stream. All images with the given tag and all versions containing +// the same tag are exported. names is the set of tags to export, and +// outStream is the writer which the images are written to. +func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error { + opts := []archive.ExportOpt{ + archive.WithSkipNonDistributableBlobs(), + } + + for _, imageRef := range names { + var err error + opts, err = i.appendImageForExport(ctx, opts, imageRef) + if err != nil { + return err + } + } + + return i.client.Export(ctx, outStream, opts...) +} + +func (i *ImageService) appendImageForExport(ctx context.Context, opts []archive.ExportOpt, name string) ([]archive.ExportOpt, error) { + ref, err := reference.ParseDockerRef(name) + if err != nil { + return opts, err + } + + is := i.client.ImageService() + + img, err := is.Get(ctx, ref.String()) + if err != nil { + return opts, err + } + + store := i.client.ContentStore() + + if containerdimages.IsIndexType(img.Target.MediaType) { + children, err := containerdimages.Children(ctx, store, img.Target) + if err != nil { + return opts, err + } + + // Check which platform manifests we have blobs for. + missingPlatforms := []v1.Platform{} + presentPlatforms := []v1.Platform{} + for _, child := range children { + if containerdimages.IsManifestType(child.MediaType) { + _, err := store.ReaderAt(ctx, child) + if cerrdefs.IsNotFound(err) { + missingPlatforms = append(missingPlatforms, *child.Platform) + logrus.WithField("digest", child.Digest.String()).Debug("missing blob, not exporting") + continue + } else if err != nil { + return opts, err + } + presentPlatforms = append(presentPlatforms, *child.Platform) + } + } + + // If we have all the manifests, just export the original index. + if len(missingPlatforms) == 0 { + return append(opts, archive.WithImage(is, img.Name)), nil + } + + // Create a new manifest which contains only the manifests we have in store. + srcRef := ref.String() + targetRef := srcRef + "-tmp-export" + newImg, err := converter.Convert(ctx, i.client, targetRef, srcRef, + converter.WithPlatform(platforms.Any(presentPlatforms...))) + if err != nil { + return opts, err + } + defer i.client.ImageService().Delete(ctx, newImg.Name, containerdimages.SynchronousDelete()) + return append(opts, archive.WithManifest(newImg.Target, srcRef)), nil + } + + return append(opts, archive.WithImage(is, img.Name)), nil +}