daemon/c8d: Implement TagImageWithReference

Implements image tagging under containerd image store.

If an image with this tag already exists, and there's no other image
with the same target, change its name. The name will have a special
format `moby-dangling@<digest>` which isn't a valid canonical reference
and doesn't resolve to any repository.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2023-02-02 21:57:33 +01:00
parent 1ca85e835c
commit eaa56afda9
No known key found for this signature in database
GPG key ID: B85EFCFE26DEF92A
2 changed files with 125 additions and 8 deletions

View file

@ -3,19 +3,75 @@ package containerd
import (
"context"
cerrdefs "github.com/containerd/containerd/errdefs"
containerdimages "github.com/containerd/containerd/images"
"github.com/docker/distribution/reference"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/image"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// TagImage creates the tag specified by newTag, pointing to the image named
// imageName (alternatively, imageName can also be an image ID).
func (i *ImageService) TagImage(ctx context.Context, imageName, repository, tag string) (string, error) {
return "", errdefs.NotImplemented(errors.New("not implemented"))
}
// TagImage creates an image named as newTag and targeting the given descriptor id.
func (i *ImageService) TagImage(ctx context.Context, imageID image.ID, newTag reference.Named) error {
target, err := i.resolveDescriptor(ctx, imageID.String())
if err != nil {
return errors.Wrapf(err, "failed to resolve image id %q to a descriptor", imageID.String())
}
// TagImageWithReference adds the given reference to the image ID provided.
func (i *ImageService) TagImageWithReference(ctx context.Context, imageID image.ID, newTag reference.Named) error {
return errdefs.NotImplemented(errors.New("not implemented"))
newImg := containerdimages.Image{
Name: newTag.String(),
Target: target,
}
is := i.client.ImageService()
_, createErr := is.Create(ctx, newImg)
if createErr != nil {
if !cerrdefs.IsAlreadyExists(err) {
return errdefs.System(errors.Wrapf(err, "failed to create image with name %s and target %s", newImg.Name, newImg.Target.Digest.String()))
}
replacedImg, err := is.Get(ctx, newImg.Name)
if err != nil {
return errdefs.Unknown(errors.Wrapf(err, "creating image %s failed because it already exists, but accessing it also failed", newImg.Name))
}
// Check if image we would replace already resolves to the same target.
// No need to do anything.
if replacedImg.Target.Digest == target.Digest {
return nil
}
// If there already exists an image with this tag, delete it
if err := i.softImageDelete(ctx, replacedImg); err != nil {
return errors.Wrapf(err, "failed to delete previous image %s", replacedImg.Name)
}
if _, err = is.Create(context.Background(), newImg); err != nil {
return errdefs.System(errors.Wrapf(err, "failed to create an image %s with target %s after deleting the existing one",
newImg.Name, imageID.String()))
}
}
logger := logrus.WithFields(logrus.Fields{
"imageID": imageID.String(),
"tag": newTag.String(),
})
logger.Info("image created")
// The tag succeeded, check if the source image is dangling
sourceDanglingImg, err := is.Get(context.Background(), danglingImageName(target.Digest))
if err != nil {
if !cerrdefs.IsNotFound(err) {
logger.WithError(err).Warn("unexpected error when checking if source image is dangling")
}
return nil
}
// Delete the source dangling image, as it's no longer dangling.
if err := is.Delete(context.Background(), sourceDanglingImg.Name); err != nil {
logger.WithError(err).Warn("unexpected error when deleting dangling image")
}
return nil
}

View file

@ -0,0 +1,61 @@
package containerd
import (
"context"
cerrdefs "github.com/containerd/containerd/errdefs"
containerdimages "github.com/containerd/containerd/images"
"github.com/docker/docker/errdefs"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// softImageDelete deletes the image, making sure that there are other images
// that reference the content of the deleted image.
// If no other image exists, a dangling one is created.
func (i *ImageService) softImageDelete(ctx context.Context, img containerdimages.Image) error {
is := i.client.ImageService()
// If the image already exists, persist it as dangling image
// but only if no other image has the same target.
digest := img.Target.Digest.String()
imgs, err := is.List(ctx, "target.digest=="+digest)
if err != nil {
return errdefs.System(errors.Wrapf(err, "failed to check if there are images targeting digest %s", digest))
}
// From this point explicitly ignore the passed context
// and don't allow to interrupt operation in the middle.
// Create dangling image if this is the last image pointing to this target.
if len(imgs) == 1 {
danglingImage := img
danglingImage.Name = danglingImageName(img.Target.Digest)
delete(danglingImage.Labels, "io.containerd.image.name")
delete(danglingImage.Labels, "org.opencontainers.image.ref.name")
_, err = is.Create(context.Background(), danglingImage)
// Error out in case we couldn't persist the old image.
// If it already exists, then just continue.
if err != nil && !cerrdefs.IsAlreadyExists(err) {
return errdefs.System(errors.Wrapf(err, "failed to create a dangling image for the replaced image %s with digest %s",
danglingImage.Name, danglingImage.Target.Digest.String()))
}
}
// Free the target name.
err = is.Delete(context.Background(), img.Name)
if err != nil {
if !cerrdefs.IsNotFound(err) {
return errdefs.System(errors.Wrapf(err, "failed to delete image %s which existed a moment before", img.Name))
}
}
return nil
}
func danglingImageName(digest digest.Digest) string {
return "moby-dangling@" + digest.String()
}