Prechádzať zdrojové kódy

daemon/c8d: Implement save and load

This makes the `docker save` and `docker load` work with the containerd
image store. The archive is both OCI and Docker compatible.

Saved archive will only contain content which is available locally.  In
case the saved image is a multi-platform manifest list, the behavior
depends on the local availability of the content. This is to be
reconsidered when we have the `--platform` option in the CLI.

- If all manifests and their contents, referenced by the manifest list
  are present, then the manifest-list is exported directly and the ID
will be the same.
- If only one platform manifest is present, only that manifest is
  exported (the image id will change and will be the id of
platform-specific manifest, instead of the full manifest list).
- If multiple, but not all, platform manifests are available, a new
  manifest list will be created which will be a subset of the original
manifest list.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Gronowski 2 rokov pred
rodič
commit
af32603ae3
1 zmenil súbory, kde vykonal 174 pridanie a 22 odobranie
  1. 174 22
      daemon/containerd/image_exporter.go

+ 174 - 22
daemon/containerd/image_exporter.go

@@ -2,13 +2,20 @@ package containerd
 
 import (
 	"context"
+	"fmt"
 	"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/platforms"
+	cplatforms "github.com/containerd/containerd/platforms"
 	"github.com/docker/distribution/reference"
-	"github.com/pkg/errors"
+	"github.com/docker/docker/errdefs"
+	"github.com/docker/docker/pkg/platforms"
+	"github.com/docker/docker/pkg/streamformatter"
+	"github.com/opencontainers/image-spec/specs-go"
+	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/sirupsen/logrus"
 )
 
@@ -20,18 +27,64 @@ import (
 //
 // TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
 func (i *ImageService) ExportImage(ctx context.Context, names []string, outStream io.Writer) error {
+	platform := platforms.AllPlatformsWithPreference(cplatforms.Default())
 	opts := []archive.ExportOpt{
-		archive.WithPlatform(platforms.Ordered(platforms.DefaultSpec())),
 		archive.WithSkipNonDistributableBlobs(),
+
+		// This makes the exported archive also include `manifest.json`
+		// when the image is a manifest list. It is needed for backwards
+		// compatibility with Docker image format.
+		// The containerd will choose only one manifest for the `manifest.json`.
+		// Our preference is to have it point to the default platform.
+		// Example:
+		//  Daemon is running on linux/arm64
+		//  When we export linux/amd64 and linux/arm64, manifest.json will point to linux/arm64.
+		//  When we export linux/amd64 only, manifest.json will point to linux/amd64.
+		// Note: This is only applicable if importing this archive into non-containerd Docker.
+		// Importing the same archive into containerd, will not restrict the platforms.
+		archive.WithPlatform(platform),
+	}
+
+	ctx, release, err := i.client.WithLease(ctx)
+	if err != nil {
+		return errdefs.System(err)
 	}
-	is := i.client.ImageService()
-	for _, imageRef := range names {
-		named, err := reference.ParseDockerRef(imageRef)
+	defer release(ctx)
+
+	for _, name := range names {
+		target, err := i.resolveDescriptor(ctx, name)
 		if err != nil {
 			return err
 		}
-		opts = append(opts, archive.WithImage(is, named.String()))
+
+		// We may not have locally all the platforms that are specified in the index.
+		// Export only those manifests that we have.
+		// TODO(vvoland): Reconsider this when `--platform` is added.
+		if containerdimages.IsIndexType(target.MediaType) {
+			desc, err := i.getBestDescriptorForExport(ctx, target)
+			if err != nil {
+				return err
+			}
+			target = desc
+		}
+
+		if ref, err := reference.ParseNormalizedNamed(name); err == nil {
+			ref = reference.TagNameOnly(ref)
+			opts = append(opts, archive.WithManifest(target, ref.String()))
+
+			logrus.WithFields(logrus.Fields{
+				"target": target,
+				"name":   ref.String(),
+			}).Debug("export image")
+		} else {
+			opts = append(opts, archive.WithManifest(target))
+
+			logrus.WithFields(logrus.Fields{
+				"target": target,
+			}).Debug("export image without name")
+		}
 	}
+
 	return i.client.Export(ctx, outStream, opts...)
 }
 
@@ -41,33 +94,132 @@ func (i *ImageService) ExportImage(ctx context.Context, names []string, outStrea
 //
 // TODO(thaJeztah): produce JSON stream progress response and image events; see https://github.com/moby/moby/issues/43910
 func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
-	platform := platforms.All
+	// TODO(vvoland): Allow user to pass platform
+	platform := cplatforms.All
 	imgs, err := i.client.Import(ctx, inTar, containerd.WithImportPlatform(platform))
 
 	if err != nil {
-		// TODO(thaJeztah): remove this log or change to debug once we can; see https://github.com/moby/moby/pull/43822#discussion_r937502405
-		logrus.WithError(err).Warn("failed to import image to containerd")
-		return errors.Wrap(err, "failed to import image")
+		logrus.WithError(err).Debug("failed to import image to containerd")
+		return errdefs.System(err)
 	}
 
-	for _, img := range imgs {
-		platformImg := containerd.NewImageWithPlatform(i.client, img, platform)
+	store := i.client.ContentStore()
+	progress := streamformatter.NewStdoutWriter(outStream)
 
-		unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
+	for _, img := range imgs {
+		allPlatforms, err := containerdimages.Platforms(ctx, store, img.Target)
 		if err != nil {
-			// TODO(thaJeztah): remove this log or change to debug once we can; see https://github.com/moby/moby/pull/43822#discussion_r937502405
-			logrus.WithError(err).WithField("image", img.Name).Debug("failed to check if image is unpacked")
-			continue
+			logrus.WithError(err).WithField("image", img.Name).Debug("failed to get image platforms")
+			return errdefs.Unknown(err)
+		}
+
+		name := img.Name
+		if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
+			name = reference.FamiliarName(named)
 		}
 
-		if !unpacked {
-			err := platformImg.Unpack(ctx, i.snapshotter)
+		for _, platform := range allPlatforms {
+			logger := logrus.WithFields(logrus.Fields{
+				"platform": platform,
+				"image":    name,
+			})
+			platformImg := containerd.NewImageWithPlatform(i.client, img, cplatforms.OnlyStrict(platform))
+
+			unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
 			if err != nil {
-				// TODO(thaJeztah): remove this log or change to debug once we can; see https://github.com/moby/moby/pull/43822#discussion_r937502405
-				logrus.WithError(err).WithField("image", img.Name).Warn("failed to unpack image")
-				return errors.Wrap(err, "failed to unpack image")
+				logger.WithError(err).Debug("failed to check if image is unpacked")
+				continue
 			}
+
+			if !unpacked {
+				err = platformImg.Unpack(ctx, i.snapshotter)
+
+				if err != nil {
+					return errdefs.System(err)
+				}
+			}
+			logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack")
 		}
+
+		fmt.Fprintf(progress, "Loaded image: %s\n", name)
 	}
 	return nil
 }
+
+// getBestDescriptorForExport returns a descriptor which only references content available locally.
+// The returned descriptor can be:
+// - The same index descriptor - if all content is available
+// - Platform specific manifest - if only one manifest from the whole index is available
+// - Reduced index descriptor - if not all, but more than one manifest is available
+//
+// The reduced index descriptor is stored in the content store and may be garbage collected.
+// It's advised to pass a context with a lease that's long enough to cover usage of the blob.
+func (i *ImageService) getBestDescriptorForExport(ctx context.Context, indexDesc ocispec.Descriptor) (ocispec.Descriptor, error) {
+	none := ocispec.Descriptor{}
+
+	if !containerdimages.IsIndexType(indexDesc.MediaType) {
+		err := fmt.Errorf("index/manifest-list descriptor expected, got: %s", indexDesc.MediaType)
+		return none, errdefs.InvalidParameter(err)
+	}
+	store := i.client.ContentStore()
+	children, err := containerdimages.Children(ctx, store, indexDesc)
+	if err != nil {
+		if cerrdefs.IsNotFound(err) {
+			return none, errdefs.NotFound(err)
+		}
+		return none, errdefs.System(err)
+	}
+
+	// Check which platform manifests have all their blobs available.
+	hasMissingManifests := false
+	var presentManifests []ocispec.Descriptor
+	for _, mfst := range children {
+		if containerdimages.IsManifestType(mfst.MediaType) {
+			available, _, _, missing, err := containerdimages.Check(ctx, store, mfst, nil)
+			if err != nil {
+				hasMissingManifests = true
+				logrus.WithField("manifest", mfst.Digest).Warn("failed to check manifest's blob availability, won't export")
+				continue
+			}
+
+			if available && len(missing) == 0 {
+				presentManifests = append(presentManifests, mfst)
+				logrus.WithField("manifest", mfst.Digest).Debug("manifest content present, will export")
+			} else {
+				hasMissingManifests = true
+				logrus.WithFields(logrus.Fields{
+					"manifest": mfst.Digest,
+					"missing":  missing,
+				}).Debug("manifest is missing, won't export")
+			}
+		}
+	}
+
+	if !hasMissingManifests || len(children) == 0 {
+		// If we have the full image, or it has no manifests, just export the original index.
+		return indexDesc, nil
+	} else if len(presentManifests) == 1 {
+		// If only one platform is present, export that one manifest.
+		return presentManifests[0], nil
+	} else if len(presentManifests) == 0 {
+		// Return error when none of the image's manifest is present.
+		return none, errdefs.NotFound(fmt.Errorf("none of the manifests is fully present in the content store"))
+	}
+
+	// Create a new index which contains only the manifests we have in store.
+	index := ocispec.Index{
+		Versioned: specs.Versioned{
+			SchemaVersion: 2,
+		},
+		MediaType:   ocispec.MediaTypeImageIndex,
+		Manifests:   presentManifests,
+		Annotations: indexDesc.Annotations,
+	}
+
+	reducedIndexDesc, err := storeJson(ctx, store, index.MediaType, index, nil)
+	if err != nil {
+		return none, err
+	}
+
+	return reducedIndexDesc, nil
+}