1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package containerd
18
19import (
20	"context"
21	"encoding/json"
22	"io"
23
24	"github.com/containerd/containerd/content"
25	"github.com/containerd/containerd/errdefs"
26	"github.com/containerd/containerd/images"
27	"github.com/containerd/containerd/images/archive"
28	"github.com/containerd/containerd/platforms"
29	digest "github.com/opencontainers/go-digest"
30	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
31)
32
33type importOpts struct {
34	indexName    string
35	imageRefT    func(string) string
36	dgstRefT     func(digest.Digest) string
37	allPlatforms bool
38	compress     bool
39}
40
41// ImportOpt allows the caller to specify import specific options
42type ImportOpt func(*importOpts) error
43
44// WithImageRefTranslator is used to translate the index reference
45// to an image reference for the image store.
46func WithImageRefTranslator(f func(string) string) ImportOpt {
47	return func(c *importOpts) error {
48		c.imageRefT = f
49		return nil
50	}
51}
52
53// WithDigestRef is used to create digest images for each
54// manifest in the index.
55func WithDigestRef(f func(digest.Digest) string) ImportOpt {
56	return func(c *importOpts) error {
57		c.dgstRefT = f
58		return nil
59	}
60}
61
62// WithIndexName creates a tag pointing to the imported index
63func WithIndexName(name string) ImportOpt {
64	return func(c *importOpts) error {
65		c.indexName = name
66		return nil
67	}
68}
69
70// WithAllPlatforms is used to import content for all platforms.
71func WithAllPlatforms(allPlatforms bool) ImportOpt {
72	return func(c *importOpts) error {
73		c.allPlatforms = allPlatforms
74		return nil
75	}
76}
77
78// WithImportCompression compresses uncompressed layers on import.
79// This is used for import formats which do not include the manifest.
80func WithImportCompression() ImportOpt {
81	return func(c *importOpts) error {
82		c.compress = true
83		return nil
84	}
85}
86
87// Import imports an image from a Tar stream using reader.
88// Caller needs to specify importer. Future version may use oci.v1 as the default.
89// Note that unreferenced blobs may be imported to the content store as well.
90func (c *Client) Import(ctx context.Context, reader io.Reader, opts ...ImportOpt) ([]images.Image, error) {
91	var iopts importOpts
92	for _, o := range opts {
93		if err := o(&iopts); err != nil {
94			return nil, err
95		}
96	}
97
98	ctx, done, err := c.WithLease(ctx)
99	if err != nil {
100		return nil, err
101	}
102	defer done(ctx)
103
104	var aio []archive.ImportOpt
105	if iopts.compress {
106		aio = append(aio, archive.WithImportCompression())
107	}
108
109	index, err := archive.ImportIndex(ctx, c.ContentStore(), reader, aio...)
110	if err != nil {
111		return nil, err
112	}
113
114	var (
115		imgs []images.Image
116		cs   = c.ContentStore()
117		is   = c.ImageService()
118	)
119
120	if iopts.indexName != "" {
121		imgs = append(imgs, images.Image{
122			Name:   iopts.indexName,
123			Target: index,
124		})
125	}
126	var platformMatcher = platforms.All
127	if !iopts.allPlatforms {
128		platformMatcher = c.platform
129	}
130
131	var handler images.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
132		// Only save images at top level
133		if desc.Digest != index.Digest {
134			return images.Children(ctx, cs, desc)
135		}
136
137		p, err := content.ReadBlob(ctx, cs, desc)
138		if err != nil {
139			return nil, err
140		}
141
142		var idx ocispec.Index
143		if err := json.Unmarshal(p, &idx); err != nil {
144			return nil, err
145		}
146
147		for _, m := range idx.Manifests {
148			name := imageName(m.Annotations, iopts.imageRefT)
149			if name != "" {
150				imgs = append(imgs, images.Image{
151					Name:   name,
152					Target: m,
153				})
154			}
155			if iopts.dgstRefT != nil {
156				ref := iopts.dgstRefT(m.Digest)
157				if ref != "" {
158					imgs = append(imgs, images.Image{
159						Name:   ref,
160						Target: m,
161					})
162				}
163			}
164		}
165
166		return idx.Manifests, nil
167	}
168
169	handler = images.FilterPlatforms(handler, platformMatcher)
170	handler = images.SetChildrenLabels(cs, handler)
171	if err := images.Walk(ctx, handler, index); err != nil {
172		return nil, err
173	}
174
175	for i := range imgs {
176		img, err := is.Update(ctx, imgs[i], "target")
177		if err != nil {
178			if !errdefs.IsNotFound(err) {
179				return nil, err
180			}
181
182			img, err = is.Create(ctx, imgs[i])
183			if err != nil {
184				return nil, err
185			}
186		}
187		imgs[i] = img
188	}
189
190	return imgs, nil
191}
192
193func imageName(annotations map[string]string, ociCleanup func(string) string) string {
194	name := annotations[images.AnnotationImageName]
195	if name != "" {
196		return name
197	}
198	name = annotations[ocispec.AnnotationRefName]
199	if name != "" {
200		if ociCleanup != nil {
201			name = ociCleanup(name)
202		}
203	}
204	return name
205}
206