Merge pull request #46533 from vvoland/c8d-save-multiple-repo
c8d/save-load: Reimplement non-c8d idiosyncrasies
This commit is contained in:
commit
5a34c7c245
5 changed files with 132 additions and 23 deletions
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
addLease := func(ctx context.Context, target ocispec.Descriptor) error {
|
||||||
target, err := i.resolveDescriptor(ctx, name)
|
return leaseContent(ctx, contentStore, leasesManager, lease, target)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,15 +100,27 @@ func (i *ImageService) ExportImage(ctx context.Context, names []string, outStrea
|
||||||
target = desc
|
target = desc
|
||||||
}
|
}
|
||||||
|
|
||||||
if ref, err := reference.ParseNormalizedNamed(name); err == nil {
|
if ref != nil {
|
||||||
ref = reference.TagNameOnly(ref)
|
|
||||||
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 {
|
||||||
|
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))
|
opts = append(opts, archive.WithManifest(target))
|
||||||
|
|
||||||
log.G(ctx).WithFields(log.Fields{
|
log.G(ctx).WithFields(log.Fields{
|
||||||
|
@ -116,6 +129,84 @@ 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 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...)
|
return i.client.Export(ctx, outStream, opts...)
|
||||||
|
@ -187,7 +278,7 @@ func (i *ImageService) LoadImage(ctx context.Context, inTar io.ReadCloser, outSt
|
||||||
name = img.Target.Digest.String()
|
name = img.Target.Digest.String()
|
||||||
loadedMsg = "Loaded image ID"
|
loadedMsg = "Loaded image ID"
|
||||||
} else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
|
} else if named, err := reference.ParseNormalizedNamed(img.Name); err == nil {
|
||||||
name = reference.FamiliarName(reference.TagNameOnly(named))
|
name = reference.FamiliarString(reference.TagNameOnly(named))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error {
|
err = i.walkImageManifests(ctx, img, func(platformImg *ImageManifest) error {
|
||||||
|
|
|
@ -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.getAllImagesWithRepository(ctx, sourceRef)
|
||||||
imgs, err := i.client.ImageService().List(ctx, "name~="+strconv.Quote(nameFilter))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/docker/docker/testutil"
|
"github.com/docker/docker/testutil"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
"gotest.tools/v3/icmd"
|
"gotest.tools/v3/icmd"
|
||||||
|
"gotest.tools/v3/skip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// save a repo and try to load it using stdout
|
// save a repo and try to load it using stdout
|
||||||
|
@ -71,6 +72,9 @@ func (s *DockerCLISaveLoadSuite) TestSaveAndLoadRepoStdout(c *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DockerCLISaveLoadSuite) TestSaveAndLoadWithProgressBar(c *testing.T) {
|
func (s *DockerCLISaveLoadSuite) TestSaveAndLoadWithProgressBar(c *testing.T) {
|
||||||
|
// TODO(vvoland): https://github.com/moby/moby/issues/43910
|
||||||
|
skip.If(c, testEnv.UsingSnapshotter(), "TODO: Not implemented yet")
|
||||||
|
|
||||||
name := "test-load"
|
name := "test-load"
|
||||||
buildImageSuccessfully(c, name, build.WithDockerfile(`FROM busybox
|
buildImageSuccessfully(c, name, build.WithDockerfile(`FROM busybox
|
||||||
RUN touch aa
|
RUN touch aa
|
||||||
|
|
|
@ -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
|
expected := []string{idBusybox, idFoo, idBar}
|
||||||
img, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest")
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
expected := []string{img.ID, 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(actual)
|
||||||
sort.Strings(expected)
|
sort.Strings(expected)
|
||||||
assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, 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) {
|
||||||
|
|
Loading…
Reference in a new issue