Skip to content

Commit d75bc05

Browse files
authored
Merge pull request containerd#9554 from vvoland/archive-sparse
import/export: Support references to missing content
2 parents f8b0736 + 0d3c3ef commit d75bc05

File tree

5 files changed

+527
-128
lines changed

5 files changed

+527
-128
lines changed

client/import.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type importOpts struct {
3737
platformMatcher platforms.MatchComparer
3838
compress bool
3939
discardLayers bool
40+
skipMissing bool
4041
}
4142

4243
// ImportOpt allows the caller to specify import specific options
@@ -113,6 +114,15 @@ func WithDiscardUnpackedLayers() ImportOpt {
113114
}
114115
}
115116

117+
// WithSkipMissing allows to import an archive which doesn't contain all the
118+
// referenced blobs.
119+
func WithSkipMissing() ImportOpt {
120+
return func(c *importOpts) error {
121+
c.skipMissing = true
122+
return nil
123+
}
124+
}
125+
116126
// Import imports an image from a Tar stream using reader.
117127
// Caller needs to specify importer. Future version may use oci.v1 as the default.
118128
// Note that unreferenced blobs may be imported to the content store as well.
@@ -162,7 +172,12 @@ func (c *Client) Import(ctx context.Context, reader io.Reader, opts ...ImportOpt
162172
var handler images.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
163173
// Only save images at top level
164174
if desc.Digest != index.Digest {
165-
return images.Children(ctx, cs, desc)
175+
// Don't set labels on missing content.
176+
children, err := images.Children(ctx, cs, desc)
177+
if iopts.skipMissing && errdefs.IsNotFound(err) {
178+
return nil, images.ErrSkipDesc
179+
}
180+
return children, err
166181
}
167182

168183
idx, err := decodeIndex(ctx, cs, desc)

content/helpers.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,14 @@ func copyWithBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
320320
}
321321
return
322322
}
323+
324+
// Exists returns whether an attempt to access the content would not error out
325+
// with an ErrNotFound error. It will return an encountered error if it was
326+
// different than ErrNotFound.
327+
func Exists(ctx context.Context, provider InfoProvider, desc ocispec.Descriptor) (bool, error) {
328+
_, err := provider.Info(ctx, desc.Digest)
329+
if errdefs.IsNotFound(err) {
330+
return false, nil
331+
}
332+
return err == nil, err
333+
}

images/archive/exporter.go

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@ import (
2424
"io"
2525
"path"
2626
"sort"
27+
"strings"
2728

2829
"github.com/containerd/containerd/v2/content"
2930
"github.com/containerd/containerd/v2/errdefs"
3031
"github.com/containerd/containerd/v2/images"
32+
"github.com/containerd/containerd/v2/labels"
3133
"github.com/containerd/containerd/v2/platforms"
34+
"github.com/containerd/log"
3235
digest "github.com/opencontainers/go-digest"
3336
ocispecs "github.com/opencontainers/image-spec/specs-go"
3437
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -140,6 +143,45 @@ func WithSkipNonDistributableBlobs() ExportOpt {
140143
return WithBlobFilter(f)
141144
}
142145

146+
// WithSkipMissing excludes blobs referenced by manifests if not all blobs
147+
// would be included in the archive.
148+
// The manifest itself is excluded only if it's not present locally.
149+
// This allows to export multi-platform images if not all platforms are present
150+
// while still persisting the multi-platform index.
151+
func WithSkipMissing(store ContentProvider) ExportOpt {
152+
return func(ctx context.Context, o *exportOptions) error {
153+
o.blobRecordOptions.childrenHandler = images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
154+
children, err := images.Children(ctx, store, desc)
155+
if !images.IsManifestType(desc.MediaType) {
156+
return children, err
157+
}
158+
159+
if err != nil {
160+
// If manifest itself is missing, skip it from export.
161+
if errdefs.IsNotFound(err) {
162+
return nil, images.ErrSkipDesc
163+
}
164+
return nil, err
165+
}
166+
167+
// Don't export manifest descendants if any of them doesn't exist.
168+
for _, child := range children {
169+
exists, err := content.Exists(ctx, store, child)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
// If any child is missing, only export the manifest, but don't export its descendants.
175+
if !exists {
176+
return nil, nil
177+
}
178+
}
179+
return children, nil
180+
})
181+
return nil
182+
}
183+
}
184+
143185
func addNameAnnotation(name string, base map[string]string) map[string]string {
144186
annotations := map[string]string{}
145187
for k, v := range base {
@@ -152,8 +194,31 @@ func addNameAnnotation(name string, base map[string]string) map[string]string {
152194
return annotations
153195
}
154196

197+
func copySourceLabels(ctx context.Context, infoProvider content.InfoProvider, desc ocispec.Descriptor) (ocispec.Descriptor, error) {
198+
info, err := infoProvider.Info(ctx, desc.Digest)
199+
if err != nil {
200+
return desc, err
201+
}
202+
for k, v := range info.Labels {
203+
if strings.HasPrefix(k, labels.LabelDistributionSource) {
204+
if desc.Annotations == nil {
205+
desc.Annotations = map[string]string{k: v}
206+
} else {
207+
desc.Annotations[k] = v
208+
}
209+
}
210+
}
211+
return desc, nil
212+
}
213+
214+
// ContentProvider provides both content and info about content
215+
type ContentProvider interface {
216+
content.Provider
217+
content.InfoProvider
218+
}
219+
155220
// Export implements Exporter.
156-
func Export(ctx context.Context, store content.Provider, writer io.Writer, opts ...ExportOpt) error {
221+
func Export(ctx context.Context, store ContentProvider, writer io.Writer, opts ...ExportOpt) error {
157222
var eo exportOptions
158223
for _, opt := range opts {
159224
if err := opt(ctx, &eo); err != nil {
@@ -163,13 +228,22 @@ func Export(ctx context.Context, store content.Provider, writer io.Writer, opts
163228

164229
records := []tarRecord{
165230
ociLayoutFile(""),
166-
ociIndexRecord(eo.manifests),
231+
}
232+
233+
manifests := make([]ocispec.Descriptor, 0, len(eo.manifests))
234+
for _, desc := range eo.manifests {
235+
d, err := copySourceLabels(ctx, store, desc)
236+
if err != nil {
237+
log.G(ctx).WithError(err).WithField("desc", desc).Warn("failed to copy distribution.source labels")
238+
continue
239+
}
240+
manifests = append(manifests, d)
167241
}
168242

169243
algorithms := map[string]struct{}{}
170244
dManifests := map[digest.Digest]*exportManifest{}
171245
resolvedIndex := map[digest.Digest]digest.Digest{}
172-
for _, desc := range eo.manifests {
246+
for _, desc := range manifests {
173247
if images.IsManifestType(desc.MediaType) {
174248
mt, ok := dManifests[desc.Digest]
175249
if !ok {
@@ -259,6 +333,8 @@ func Export(ctx context.Context, store content.Provider, writer io.Writer, opts
259333
}
260334
}
261335

336+
records = append(records, ociIndexRecord(manifests))
337+
262338
if !eo.skipDockerManifest && len(dManifests) > 0 {
263339
tr, err := manifestsRecord(ctx, store, dManifests)
264340
if err != nil {
@@ -291,7 +367,10 @@ func getRecords(ctx context.Context, store content.Provider, desc ocispec.Descri
291367
return nil, nil
292368
}
293369

294-
childrenHandler := images.ChildrenHandler(store)
370+
childrenHandler := brOpts.childrenHandler
371+
if childrenHandler == nil {
372+
childrenHandler = images.ChildrenHandler(store)
373+
}
295374

296375
handlers := images.Handlers(
297376
childrenHandler,
@@ -313,7 +392,8 @@ type tarRecord struct {
313392
}
314393

315394
type blobRecordOptions struct {
316-
blobFilter BlobFilter
395+
blobFilter BlobFilter
396+
childrenHandler images.HandlerFunc
317397
}
318398

319399
func blobRecord(cs content.Provider, desc ocispec.Descriptor, opts *blobRecordOptions) tarRecord {

0 commit comments

Comments
 (0)