Przeglądaj źródła

c8d/save: Implement exporting all tags

Implement a behavior from the graphdriver's export where `docker save
something` (untagged reference) would export all images matching the
specified repository.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
Paweł Gronowski 1 rok temu
rodzic
commit
42af8795a3

+ 7 - 0
daemon/containerd/image.go

@@ -334,3 +334,10 @@ func (i *ImageService) resolveImage(ctx context.Context, refOrID string) (contai
 
 
 	return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed}
 	return containerdimages.Image{}, images.ErrImageDoesNotExist{Ref: parsed}
 }
 }
+
+// getAllImagesWithRepository returns a slice of images which name is a reference
+// pointing to the same repository as the given reference.
+func (i *ImageService) getAllImagesWithRepository(ctx context.Context, ref reference.Named) ([]containerdimages.Image, error) {
+	nameFilter := "^" + regexp.QuoteMeta(ref.Name()) + ":" + reference.TagRegexp.String() + "$"
+	return i.client.ImageService().List(ctx, "name~="+strconv.Quote(nameFilter))
+}

+ 83 - 9
daemon/containerd/image_exporter.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
+	"strings"
 
 
 	"github.com/containerd/containerd"
 	"github.com/containerd/containerd"
 	"github.com/containerd/containerd/content"
 	"github.com/containerd/containerd/content"
@@ -16,6 +17,7 @@ import (
 	"github.com/distribution/reference"
 	"github.com/distribution/reference"
 	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/api/types/events"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/container"
+	"github.com/docker/docker/daemon/images"
 	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/errdefs"
 	dockerarchive "github.com/docker/docker/pkg/archive"
 	dockerarchive "github.com/docker/docker/pkg/archive"
 	"github.com/docker/docker/pkg/platforms"
 	"github.com/docker/docker/pkg/platforms"
@@ -78,13 +80,12 @@ func (i *ImageService) ExportImage(ctx context.Context, names []string, outStrea
 		}
 		}
 	}()
 	}()
 
 
-	for _, name := range names {
-		target, err := i.resolveDescriptor(ctx, name)
-		if err != nil {
-			return err
-		}
+	addLease := func(ctx context.Context, target ocispec.Descriptor) error {
+		return leaseContent(ctx, contentStore, leasesManager, lease, target)
+	}
 
 
-		if err = leaseContent(ctx, contentStore, leasesManager, lease, target); err != nil {
+	exportImage := func(ctx context.Context, target ocispec.Descriptor, ref reference.Named) error {
+		if err := addLease(ctx, target); err != nil {
 			return err
 			return err
 		}
 		}
 
 
@@ -99,13 +100,12 @@ func (i *ImageService) ExportImage(ctx context.Context, names []string, outStrea
 			target = desc
 			target = desc
 		}
 		}
 
 
-		if ref, err := reference.ParseNormalizedNamed(name); err == nil {
-			ref = reference.TagNameOnly(ref)
+		if ref != nil {
 			opts = append(opts, archive.WithManifest(target, ref.String()))
 			opts = append(opts, archive.WithManifest(target, ref.String()))
 
 
 			log.G(ctx).WithFields(log.Fields{
 			log.G(ctx).WithFields(log.Fields{
 				"target": target,
 				"target": target,
-				"name":   ref.String(),
+				"name":   ref,
 			}).Debug("export image")
 			}).Debug("export image")
 		} else {
 		} else {
 			opts = append(opts, archive.WithManifest(target))
 			opts = append(opts, archive.WithManifest(target))
@@ -116,6 +116,80 @@ func (i *ImageService) ExportImage(ctx context.Context, names []string, outStrea
 		}
 		}
 
 
 		i.LogImageEvent(target.Digest.String(), target.Digest.String(), events.ActionSave)
 		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 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 err := exportImage(ctx, target, ref); err != nil {
+			return err
+		}
 	}
 	}
 
 
 	return i.client.Export(ctx, outStream, opts...)
 	return i.client.Export(ctx, outStream, opts...)

+ 1 - 4
daemon/containerd/image_push.go

@@ -4,8 +4,6 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
-	"regexp"
-	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 
 
@@ -50,8 +48,7 @@ func (i *ImageService) PushImage(ctx context.Context, sourceRef reference.Named,
 			// Image is not tagged nor digested, that means all tags push was requested.
 			// Image is not tagged nor digested, that means all tags push was requested.
 
 
 			// Find all images with the same repository.
 			// Find all images with the same repository.
-			nameFilter := "^" + regexp.QuoteMeta(sourceRef.Name()) + ":" + reference.TagRegexp.String() + "$"
-			imgs, err := i.client.ImageService().List(ctx, "name~="+strconv.Quote(nameFilter))
+			imgs, err := i.getAllImagesWithRepository(ctx, sourceRef)
 			if err != nil {
 			if err != nil {
 				return err
 				return err
 			}
 			}

+ 19 - 9
integration/image/save_test.go

@@ -113,12 +113,16 @@ func TestSaveRepoWithMultipleImages(t *testing.T) {
 		return res.ID
 		return res.ID
 	}
 	}
 
 
+	busyboxImg, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest")
+	assert.NilError(t, err)
+
 	repoName := "foobar-save-multi-images-test"
 	repoName := "foobar-save-multi-images-test"
 	tagFoo := repoName + ":foo"
 	tagFoo := repoName + ":foo"
 	tagBar := repoName + ":bar"
 	tagBar := repoName + ":bar"
 
 
 	idFoo := makeImage("busybox:latest", tagFoo)
 	idFoo := makeImage("busybox:latest", tagFoo)
 	idBar := makeImage("busybox:latest", tagBar)
 	idBar := makeImage("busybox:latest", tagBar)
+	idBusybox := busyboxImg.ID
 
 
 	client.ImageRemove(ctx, repoName, types.ImageRemoveOptions{Force: true})
 	client.ImageRemove(ctx, repoName, types.ImageRemoveOptions{Force: true})
 
 
@@ -142,20 +146,26 @@ func TestSaveRepoWithMultipleImages(t *testing.T) {
 		assert.Check(t, cmp.Nil(err))
 		assert.Check(t, cmp.Nil(err))
 	}
 	}
 
 
-	// make the list of expected layers
-	img, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest")
-	assert.NilError(t, err)
-
-	expected := []string{img.ID, idFoo, idBar}
-
+	expected := []string{idBusybox, idFoo, idBar}
 	// prefixes are not in tar
 	// prefixes are not in tar
 	for i := range expected {
 	for i := range expected {
 		expected[i] = digest.Digest(expected[i]).Encoded()
 		expected[i] = digest.Digest(expected[i]).Encoded()
 	}
 	}
 
 
-	sort.Strings(actual)
-	sort.Strings(expected)
-	assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected)
+	// With snapshotters, ID of the image is the ID of the manifest/index
+	// With graphdrivers, ID of the image is the ID of the image config
+	if testEnv.UsingSnapshotter() {
+		// ID of image won't match the Config ID from manifest.json
+		// Just check if manifests exist in blobs
+		for _, blob := range expected {
+			_, err := fs.Stat(tarfs, "blobs/sha256/"+blob)
+			assert.Check(t, cmp.Nil(err))
+		}
+	} else {
+		sort.Strings(actual)
+		sort.Strings(expected)
+		assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected)
+	}
 }
 }
 
 
 func TestSaveDirectoryPermissions(t *testing.T) {
 func TestSaveDirectoryPermissions(t *testing.T) {