From 623b45ef020f7e2a32f45a21c135bbf8564e2a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Fri, 10 Jun 2022 17:48:12 +0200 Subject: [PATCH 1/3] containerdstore: Initial push support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Note: With the current containerd's pull behaviour the object that get tagged as the pulled tag is not an image manifest but manifest list. This means that doing: ``` docker pull docker.io/library/ubuntu:latest docker tag docker.io/library/ubuntu:latest newrepo/ubuntu:latest docker push newrepo/ubuntu:latest ``` Will result in an error, because the pull downloads only the default platform and the newrepo/ubuntu:latest is a manifest list that references all original image's platforms - and those cannot be pushed to a remote because the pusher doesn't have them. This will will work in case of single-platform manifest lists or if you pull the manifest list along with all the platforms via ctr: ``` ctr --address /run/docker/containerd/containerd.sock -n moby \ image pull --all-platforms docker.io/library/ubuntu:latest docker push newrepo/ubuntu:latest ``` Signed-off-by: Paweł Gronowski --- daemon/containerdstore/service.go | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/daemon/containerdstore/service.go b/daemon/containerdstore/service.go index d6988a507be1b..268d604d79533 100644 --- a/daemon/containerdstore/service.go +++ b/daemon/containerdstore/service.go @@ -15,6 +15,7 @@ import ( "github.com/containerd/containerd/images/archive" "github.com/containerd/containerd/log" "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" @@ -396,7 +397,47 @@ func (cs *containerdStore) LookupImage(ctx context.Context, name string) (*types } func (cs *containerdStore) PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error { - panic("not implemented") + ref, err := reference.ParseNormalizedNamed(image) + if err != nil { + return err + } + if tag != "" { + // Push by digest is not supported, so only tags are supported. + ref, err = reference.WithTag(ref, tag) + if err != nil { + return err + } + } + + logrus.Infof("Pushing ref: %s", ref.String()) + + img, err := cs.client.ImageService().Get(ctx, ref.String()) + if err != nil { + return errors.Wrap(err, "Failed to get image") + } + + imageHandler := containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) { + logrus.Debugf("Push digest %s", desc.Digest.String()) + return nil, nil + }) + imageHandler = remotes.SkipNonDistributableBlobs(imageHandler) + + authorizer := docker.NewDockerAuthorizer(docker.WithAuthCreds(func(s string) (string, string, error) { + if authConfig.IdentityToken != "" { + return "", authConfig.IdentityToken, nil + } + return authConfig.Username, authConfig.Password, nil + })) + + resolver := docker.NewResolver(docker.ResolverOptions{ + Hosts: docker.ConfigureDefaultRegistries(docker.WithAuthorizer(authorizer)), + }) + + return cs.client.Push(ctx, ref.String(), img.Target, + containerd.WithResolver(resolver), + containerd.WithPlatformMatcher(platforms.All), + containerd.WithImageHandler(imageHandler), + ) } func (cs *containerdStore) SearchRegistryForImages(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registrytypes.SearchResults, error) { From e2008013e542ea70b146e87f5ee747d2f54305f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Thu, 23 Jun 2022 15:11:09 +0200 Subject: [PATCH 2/3] containerdstore: Push only default platform instead of whole index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Paweł Gronowski --- daemon/containerdstore/service.go | 65 ++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/daemon/containerdstore/service.go b/daemon/containerdstore/service.go index 268d604d79533..63f4e778c904a 100644 --- a/daemon/containerdstore/service.go +++ b/daemon/containerdstore/service.go @@ -409,37 +409,72 @@ func (cs *containerdStore) PushImage(ctx context.Context, image, tag string, met } } - logrus.Infof("Pushing ref: %s", ref.String()) + is := cs.client.ImageService() - img, err := cs.client.ImageService().Get(ctx, ref.String()) + img, err := is.Get(ctx, ref.String()) if err != nil { return errors.Wrap(err, "Failed to get image") } + platformMatcher := platforms.DefaultStrict() + + target := img.Target + + // If the image is an index/manifest list, then push only default platform. + // We don't want to push other platforms, because pull only fetches the default platform + // and manifest list includes other platforms, which we or the remote repository may not have. + if containerdimages.IsIndexType(img.Target.MediaType) { + children, err := containerdimages.Children(ctx, cs.client.ContentStore(), img.Target) + if err != nil { + return err + } + + for _, child := range children { + if child.Platform == nil { + continue + } + + if platformMatcher.Match(*child.Platform) { + target = child + } + } + } + imageHandler := containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) { - logrus.Debugf("Push digest %s", desc.Digest.String()) + logrus.WithField("desc", desc).Debug("Push, handle digest") return nil, nil }) imageHandler = remotes.SkipNonDistributableBlobs(imageHandler) - authorizer := docker.NewDockerAuthorizer(docker.WithAuthCreds(func(s string) (string, string, error) { - if authConfig.IdentityToken != "" { - return "", authConfig.IdentityToken, nil - } - return authConfig.Username, authConfig.Password, nil - })) - - resolver := docker.NewResolver(docker.ResolverOptions{ - Hosts: docker.ConfigureDefaultRegistries(docker.WithAuthorizer(authorizer)), - }) + resolver := newResolverFromAuthConfig(authConfig) - return cs.client.Push(ctx, ref.String(), img.Target, + log.G(ctx).WithField("desc", target).WithField("ref", ref.String()).Info("Pushing desc to remote ref") + return cs.client.Push(ctx, ref.String(), target, containerd.WithResolver(resolver), - containerd.WithPlatformMatcher(platforms.All), + containerd.WithPlatformMatcher(platformMatcher), containerd.WithImageHandler(imageHandler), ) } +func newResolverFromAuthConfig(authConfig *types.AuthConfig) remotes.Resolver { + opts := []docker.RegistryOpt{} + + if authConfig != nil { + authorizer := docker.NewDockerAuthorizer(docker.WithAuthCreds(func(s string) (string, string, error) { + if authConfig.IdentityToken != "" { + return "", authConfig.IdentityToken, nil + } + return authConfig.Username, authConfig.Password, nil + })) + + opts = append(opts, docker.WithAuthorizer(authorizer)) + } + + return docker.NewResolver(docker.ResolverOptions{ + Hosts: docker.ConfigureDefaultRegistries(opts...), + }) +} + func (cs *containerdStore) SearchRegistryForImages(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registrytypes.SearchResults, error) { panic("not implemented") } From 7c1efdacc0dd5a0c86ada3f5052132bcc07b7c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Thu, 23 Jun 2022 14:59:36 +0200 Subject: [PATCH 3/3] containerd: Initial prune MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now this deletes all images and doesn't respect any filters Signed-off-by: Paweł Gronowski --- daemon/containerdstore/prune.go | 98 +++++++++++++++++++++++++++++++ daemon/containerdstore/service.go | 4 -- 2 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 daemon/containerdstore/prune.go diff --git a/daemon/containerdstore/prune.go b/daemon/containerdstore/prune.go new file mode 100644 index 0000000000000..f00e9f8f7b267 --- /dev/null +++ b/daemon/containerdstore/prune.go @@ -0,0 +1,98 @@ +package containerdstore + +import ( + "context" + "fmt" + + "github.com/containerd/containerd/content" + containerdimages "github.com/containerd/containerd/images" + "github.com/containerd/containerd/platforms" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" +) + +// TODO: handle pruneFilters +func (cs *containerdStore) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (*types.ImagesPruneReport, error) { + is := cs.client.ImageService() + store := cs.client.ContentStore() + + images, err := is.List(ctx) + if err != nil { + return nil, errors.Wrapf(err, "Failed to list images") + } + + platform := platforms.DefaultStrict() + report := types.ImagesPruneReport{} + toDelete := map[digest.Digest]uint64{} + errs := []error{} + + for _, img := range images { + err := getContentDigestsWithSizes(ctx, img, store, platform, toDelete) + if err != nil { + errs = append(errs, err) + continue + } + + err = is.Delete(ctx, img.Name, containerdimages.SynchronousDelete()) + if err != nil { + errs = append(errs, err) + continue + } + + report.ImagesDeleted = append(report.ImagesDeleted, + types.ImageDeleteResponseItem{ + Untagged: img.Name, + }, + ) + } + + for digest, size := range toDelete { + err := store.Delete(ctx, digest) + if err != nil { + errs = append(errs, errors.Wrapf(err, "Failed to delete %s", digest.String())) + } + report.SpaceReclaimed += size + report.ImagesDeleted = append(report.ImagesDeleted, + types.ImageDeleteResponseItem{ + Deleted: digest.String(), + }, + ) + } + + if len(errs) > 1 { + return &report, MultipleErrors{all: errs} + } else if len(errs) == 1 { + return &report, errs[0] + } + + return &report, nil +} + +func getContentDigestsWithSizes(ctx context.Context, img containerdimages.Image, store content.Store, platform platforms.MatchComparer, toDelete map[digest.Digest]uint64) error { + return containerdimages.Walk(ctx, containerdimages.Handlers(containerdimages.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if desc.Size < 0 { + return nil, fmt.Errorf("invalid size %v in %v (%v)", desc.Size, desc.Digest, desc.MediaType) + } + toDelete[desc.Digest] = uint64(desc.Size) + return nil, nil + }), containerdimages.LimitManifests(containerdimages.FilterPlatforms(containerdimages.ChildrenHandler(store), platform), platform, 1)), img.Target) +} + +type MultipleErrors struct { + all []error +} + +func (m MultipleErrors) Error() string { + errString := "" + for _, err := range m.all { + if len(m.all) > 0 { + errString += "\n" + } + errString += err.Error() + } + + return errString +} diff --git a/daemon/containerdstore/service.go b/daemon/containerdstore/service.go index 63f4e778c904a..4814d110bf143 100644 --- a/daemon/containerdstore/service.go +++ b/daemon/containerdstore/service.go @@ -377,10 +377,6 @@ func (cs *containerdStore) ImageHistory(ctx context.Context, name string) ([]*im panic("not implemented") } -func (cs *containerdStore) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (*types.ImagesPruneReport, error) { - panic("not implemented") -} - func (cs *containerdStore) ImportImage(ctx context.Context, src string, repository string, platform *ocispec.Platform, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error { panic("not implemented") }