docker/docker

View on GitHub
daemon/containerd/image_exporter.go

Summary

Maintainability
D
1 day
Test Coverage
package containerd

import (
    "context"
    "fmt"
    "io"
    "strings"

    "github.com/containerd/containerd"
    "github.com/containerd/containerd/content"
    cerrdefs "github.com/containerd/containerd/errdefs"
    containerdimages "github.com/containerd/containerd/images"
    "github.com/containerd/containerd/images/archive"
    "github.com/containerd/containerd/leases"
    "github.com/containerd/containerd/platforms"
    "github.com/containerd/log"
    "github.com/distribution/reference"
    "github.com/docker/docker/api/types/events"
    "github.com/docker/docker/container"
    "github.com/docker/docker/daemon/images"
    "github.com/docker/docker/errdefs"
    dockerarchive "github.com/docker/docker/pkg/archive"
    "github.com/docker/docker/pkg/streamformatter"
    ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    "github.com/pkg/errors"
)

func (i *ImageService) PerformWithBaseFS(ctx context.Context, c *container.Container, fn func(root string) error) error {
    snapshotter := i.client.SnapshotService(c.Driver)
    mounts, err := snapshotter.Mounts(ctx, c.ID)
    if err != nil {
        return err
    }
    path, err := i.refCountMounter.Mount(mounts, c.ID)
    if err != nil {
        return err
    }
    defer i.refCountMounter.Unmount(path)

    return fn(path)
}

// 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.
//
// 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 := matchAllWithPreference(platforms.Default())
    opts := []archive.ExportOpt{
        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),
        archive.WithSkipMissing(i.content),
    }

    leasesManager := i.client.LeasesService()
    lease, err := leasesManager.Create(ctx, leases.WithRandomID())
    if err != nil {
        return errdefs.System(err)
    }
    defer func() {
        if err := leasesManager.Delete(ctx, lease); err != nil {
            log.G(ctx).WithError(err).Warn("cleaning up lease")
        }
    }()

    addLease := func(ctx context.Context, target ocispec.Descriptor) error {
        return leaseContent(ctx, i.content, leasesManager, lease, target)
    }

    exportImage := func(ctx context.Context, target ocispec.Descriptor, ref reference.Named) error {
        if err := addLease(ctx, target); err != nil {
            return err
        }

        if ref != nil {
            opts = append(opts, archive.WithManifest(target, ref.String()))

            log.G(ctx).WithFields(log.Fields{
                "target": target,
                "name":   ref,
            }).Debug("export image")
        } else {
            orgTarget := target
            target.Annotations = make(map[string]string)

            for k, v := range orgTarget.Annotations {
                switch k {
                case containerdimages.AnnotationImageName, ocispec.AnnotationRefName:
                    // Strip image name/tag annotations from the descriptor.
                    // Otherwise containerd will use it as name.
                default:
                    target.Annotations[k] = v
                }
            }

            opts = append(opts, archive.WithManifest(target))

            log.G(ctx).WithFields(log.Fields{
                "target": target,
            }).Debug("export image without name")
        }

        i.LogImageEvent(target.Digest.String(), target.Digest.String(), events.ActionSave)
        return nil
    }

    exportRepository := func(ctx context.Context, ref reference.Named) error {
        imgs, err := i.getAllImagesWithRepository(ctx, ref)
        if err != nil {
            return errdefs.System(fmt.Errorf("failed to list all images from repository %s: %w", ref.Name(), err))
        }

        if len(imgs) == 0 {
            return images.ErrImageDoesNotExist{Ref: ref}
        }

        for _, img := range imgs {
            ref, err := reference.ParseNamed(img.Name)

            if err != nil {
                log.G(ctx).WithFields(log.Fields{
                    "image": img.Name,
                    "error": err,
                }).Warn("couldn't parse image name as a valid named reference")
                continue
            }

            if err := exportImage(ctx, img.Target, ref); err != nil {
                return err
            }
        }

        return nil
    }

    for _, name := range names {
        target, resolveErr := i.resolveDescriptor(ctx, name)

        // Check if the requested name is a truncated digest of the resolved descriptor.
        // If yes, that means that the user specified a specific image ID so
        // it's not referencing a repository.
        specificDigestResolved := false
        if resolveErr == nil {
            nameWithoutDigestAlgorithm := strings.TrimPrefix(name, target.Digest.Algorithm().String()+":")
            specificDigestResolved = strings.HasPrefix(target.Digest.Encoded(), nameWithoutDigestAlgorithm)
        }

        log.G(ctx).WithFields(log.Fields{
            "name":                   name,
            "resolveErr":             resolveErr,
            "specificDigestResolved": specificDigestResolved,
        }).Debug("export requested")

        ref, refErr := reference.ParseNormalizedNamed(name)

        if refErr == nil {
            if _, ok := ref.(reference.Digested); ok {
                specificDigestResolved = true
            }
        }

        if resolveErr != nil || !specificDigestResolved {
            // Name didn't resolve to anything, or name wasn't explicitly referencing a digest
            if refErr == nil && reference.IsNameOnly(ref) {
                // Reference is valid, but doesn't include a specific tag.
                // Export all images with the same repository.
                if err := exportRepository(ctx, ref); err != nil {
                    return err
                }
                continue
            }
        }

        if resolveErr != nil {
            return resolveErr
        }
        if refErr != nil {
            return refErr
        }

        // If user exports a specific digest, it shouldn't have a tag.
        if specificDigestResolved {
            ref = nil
        }
        if err := exportImage(ctx, target, ref); err != nil {
            return err
        }
    }

    return i.client.Export(ctx, outStream, opts...)
}

// leaseContent will add a resource to the lease for each child of the descriptor making sure that it and
// its children won't be deleted while the lease exists
func leaseContent(ctx context.Context, store content.Store, leasesManager leases.Manager, lease leases.Lease, desc ocispec.Descriptor) error {
    return containerdimages.Walk(ctx, containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
        _, err := store.Info(ctx, desc.Digest)
        if err != nil {
            if errors.Is(err, cerrdefs.ErrNotFound) {
                return nil, nil
            }
            return nil, errdefs.System(err)
        }

        r := leases.Resource{
            ID:   desc.Digest.String(),
            Type: "content",
        }
        if err := leasesManager.AddResource(ctx, lease, r); err != nil {
            return nil, errdefs.System(err)
        }

        return containerdimages.Children(ctx, store, desc)
    }), desc)
}

// 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.
func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
    decompressed, err := dockerarchive.DecompressStream(inTar)
    if err != nil {
        return errors.Wrap(err, "failed to decompress input tar archive")
    }
    defer decompressed.Close()

    opts := []containerd.ImportOpt{
        // TODO(vvoland): Allow user to pass platform
        containerd.WithImportPlatform(platforms.All),

        containerd.WithSkipMissing(),

        // Create an additional image with dangling name for imported images...
        containerd.WithDigestRef(danglingImageName),
        // ... but only if they don't have a name or it's invalid.
        containerd.WithSkipDigestRef(func(nameFromArchive string) bool {
            if nameFromArchive == "" {
                return false
            }
            _, err := reference.ParseNormalizedNamed(nameFromArchive)
            return err == nil
        }),
    }

    imgs, err := i.client.Import(ctx, decompressed, opts...)
    if err != nil {
        log.G(ctx).WithError(err).Debug("failed to import image to containerd")
        return errdefs.System(err)
    }

    progress := streamformatter.NewStdoutWriter(outStream)

    for _, img := range imgs {
        name := img.Name
        loadedMsg := "Loaded image"

        if isDanglingImage(img) {
            name = img.Target.Digest.String()
            loadedMsg = "Loaded image ID"
        } else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
            name = reference.FamiliarString(reference.TagNameOnly(named))
        }

        err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error {
            logger := log.G(ctx).WithFields(log.Fields{
                "image":    name,
                "manifest": platformImg.Target().Digest,
            })

            if isPseudo, err := platformImg.IsPseudoImage(ctx); isPseudo || err != nil {
                if err != nil {
                    logger.WithError(err).Warn("failed to read manifest")
                } else {
                    logger.Debug("don't unpack non-image manifest")
                }
                return nil
            }

            unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
            if err != nil {
                logger.WithError(err).Warn("failed to check if image is unpacked")
                return nil
            }

            if !unpacked {
                err = platformImg.Unpack(ctx, i.snapshotter)

                if err != nil {
                    return errdefs.System(err)
                }
            }
            logger.WithField("alreadyUnpacked", unpacked).WithError(err).Debug("unpack")
            return nil
        })
        if err != nil {
            return errors.Wrap(err, "failed to unpack loaded image")
        }

        fmt.Fprintf(progress, "%s: %s\n", loadedMsg, name)
        i.LogImageEvent(img.Target.Digest.String(), img.Target.Digest.String(), events.ActionLoad)
    }

    return nil
}