| /* |
| Copyright The containerd Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package client |
| |
| import ( |
| "context" |
| "io" |
| |
| "github.com/containerd/containerd/v2/core/content" |
| "github.com/containerd/containerd/v2/core/images" |
| "github.com/containerd/containerd/v2/core/images/archive" |
| "github.com/containerd/errdefs" |
| "github.com/containerd/platforms" |
| digest "github.com/opencontainers/go-digest" |
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| ) |
| |
| type importOpts struct { |
| indexName string |
| imageRefT func(string) string |
| dgstRefT func(digest.Digest) string |
| skipDgstRef func(string) bool |
| allPlatforms bool |
| platformMatcher platforms.MatchComparer |
| compress bool |
| discardLayers bool |
| skipMissing bool |
| imageLabels map[string]string |
| referrers content.ReferrersProvider |
| } |
| |
| // ImportOpt allows the caller to specify import specific options |
| type ImportOpt func(*importOpts) error |
| |
| // WithImageRefTranslator is used to translate the index reference |
| // to an image reference for the image store. |
| func WithImageRefTranslator(f func(string) string) ImportOpt { |
| return func(c *importOpts) error { |
| c.imageRefT = f |
| return nil |
| } |
| } |
| |
| // WithImageLabels are the image labels to apply to a new image |
| func WithImageLabels(labels map[string]string) ImportOpt { |
| return func(c *importOpts) error { |
| c.imageLabels = labels |
| return nil |
| } |
| } |
| |
| // WithDigestRef is used to create digest images for each |
| // manifest in the index. |
| func WithDigestRef(f func(digest.Digest) string) ImportOpt { |
| return func(c *importOpts) error { |
| c.dgstRefT = f |
| return nil |
| } |
| } |
| |
| // WithSkipDigestRef is used to specify when to skip applying |
| // WithDigestRef. The callback receives an image reference (or an empty |
| // string if not specified in the image). When the callback returns true, |
| // the skip occurs. |
| func WithSkipDigestRef(f func(string) bool) ImportOpt { |
| return func(c *importOpts) error { |
| c.skipDgstRef = f |
| return nil |
| } |
| } |
| |
| // WithIndexName creates a tag pointing to the imported index |
| func WithIndexName(name string) ImportOpt { |
| return func(c *importOpts) error { |
| c.indexName = name |
| return nil |
| } |
| } |
| |
| // WithAllPlatforms is used to import content for all platforms. |
| func WithAllPlatforms(allPlatforms bool) ImportOpt { |
| return func(c *importOpts) error { |
| c.allPlatforms = allPlatforms |
| return nil |
| } |
| } |
| |
| // WithImportPlatform is used to import content for specific platform. |
| func WithImportPlatform(platformMacher platforms.MatchComparer) ImportOpt { |
| return func(c *importOpts) error { |
| c.platformMatcher = platformMacher |
| return nil |
| } |
| } |
| |
| // WithImportCompression compresses uncompressed layers on import. |
| // This is used for import formats which do not include the manifest. |
| func WithImportCompression() ImportOpt { |
| return func(c *importOpts) error { |
| c.compress = true |
| return nil |
| } |
| } |
| |
| // WithDiscardUnpackedLayers allows the garbage collector to clean up |
| // layers from content store after unpacking. |
| func WithDiscardUnpackedLayers() ImportOpt { |
| return func(c *importOpts) error { |
| c.discardLayers = true |
| return nil |
| } |
| } |
| |
| // WithSkipMissing allows to import an archive which doesn't contain all the |
| // referenced blobs. |
| func WithSkipMissing() ImportOpt { |
| return func(c *importOpts) error { |
| c.skipMissing = true |
| return nil |
| } |
| } |
| |
| // WithImportReferrers allows to set a custom referrers provider |
| // for setting referrer objects during import. |
| func WithImportReferrers(r content.ReferrersProvider) ImportOpt { |
| return func(c *importOpts) error { |
| c.referrers = r |
| return nil |
| } |
| } |
| |
| // Import imports an image from a Tar stream using reader. |
| // Caller needs to specify importer. Future version may use oci.v1 as the default. |
| // Note that unreferenced blobs may be imported to the content store as well. |
| func (c *Client) Import(ctx context.Context, reader io.Reader, opts ...ImportOpt) ([]images.Image, error) { |
| var iopts importOpts |
| for _, o := range opts { |
| if err := o(&iopts); err != nil { |
| return nil, err |
| } |
| } |
| |
| ctx, done, err := c.WithLease(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer done(ctx) |
| |
| var aio []archive.ImportOpt |
| if iopts.compress { |
| aio = append(aio, archive.WithImportCompression()) |
| } |
| |
| index, err := archive.ImportIndex(ctx, c.ContentStore(), reader, aio...) |
| if err != nil { |
| return nil, err |
| } |
| |
| var ( |
| imgs []images.Image |
| cs = c.ContentStore() |
| is = c.ImageService() |
| ) |
| |
| if iopts.indexName != "" { |
| imgs = append(imgs, images.Image{ |
| Name: iopts.indexName, |
| Target: index, |
| }) |
| } |
| var platformMatcher = c.platform |
| if iopts.allPlatforms { |
| platformMatcher = platforms.All |
| } else if iopts.platformMatcher != nil { |
| platformMatcher = iopts.platformMatcher |
| } |
| |
| var handler images.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { |
| // Only save images at top level |
| if desc.Digest != index.Digest { |
| // Don't set labels on missing content. |
| children, err := images.Children(ctx, cs, desc) |
| if iopts.skipMissing && errdefs.IsNotFound(err) { |
| return nil, images.ErrSkipDesc |
| } |
| return children, err |
| } |
| |
| idx, err := decodeIndex(ctx, cs, desc) |
| if err != nil { |
| return nil, err |
| } |
| |
| for _, m := range idx.Manifests { |
| name := imageName(m.Annotations, iopts.imageRefT) |
| if name != "" { |
| imgs = append(imgs, images.Image{ |
| Name: name, |
| Target: m, |
| }) |
| } |
| // don't create images for referrer descriptors |
| if _, ok := m.Annotations[images.AnnotationManifestSubject]; ok { |
| continue |
| } |
| if iopts.skipDgstRef != nil { |
| if iopts.skipDgstRef(name) { |
| continue |
| } |
| } |
| if iopts.dgstRefT != nil { |
| ref := iopts.dgstRefT(m.Digest) |
| if ref != "" { |
| imgs = append(imgs, images.Image{ |
| Name: ref, |
| Target: m, |
| }) |
| } |
| } |
| } |
| |
| return idx.Manifests, nil |
| } |
| |
| handler = images.FilterPlatforms(handler, platformMatcher) |
| if iopts.referrers != nil { |
| handler = images.SetReferrers(iopts.referrers, handler) |
| } |
| if iopts.discardLayers { |
| handler = images.SetChildrenMappedLabels(cs, handler, images.ChildGCLabelsFilterLayers) |
| } else { |
| handler = images.SetChildrenLabels(cs, handler) |
| } |
| if err := images.WalkNotEmpty(ctx, handler, index); err != nil { |
| return nil, err |
| } |
| |
| for i := range imgs { |
| fieldsPath := []string{"target"} |
| if iopts.imageLabels != nil { |
| fieldsPath = append(fieldsPath, "labels") |
| imgs[i].Labels = iopts.imageLabels |
| } |
| img, err := is.Update(ctx, imgs[i], fieldsPath...) |
| if err != nil { |
| if !errdefs.IsNotFound(err) { |
| return nil, err |
| } |
| |
| img, err = is.Create(ctx, imgs[i]) |
| if err != nil { |
| return nil, err |
| } |
| } |
| imgs[i] = img |
| } |
| |
| return imgs, nil |
| } |
| |
| func imageName(annotations map[string]string, ociCleanup func(string) string) string { |
| name := annotations[images.AnnotationImageName] |
| if name != "" { |
| return name |
| } |
| name = annotations[ocispec.AnnotationRefName] |
| if name != "" { |
| if ociCleanup != nil { |
| name = ociCleanup(name) |
| } |
| } |
| return name |
| } |