@@ -24,11 +24,14 @@ import (
24
24
"io"
25
25
"path"
26
26
"sort"
27
+ "strings"
27
28
28
29
"github.com/containerd/containerd/v2/content"
29
30
"github.com/containerd/containerd/v2/errdefs"
30
31
"github.com/containerd/containerd/v2/images"
32
+ "github.com/containerd/containerd/v2/labels"
31
33
"github.com/containerd/containerd/v2/platforms"
34
+ "github.com/containerd/log"
32
35
digest "github.com/opencontainers/go-digest"
33
36
ocispecs "github.com/opencontainers/image-spec/specs-go"
34
37
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -140,6 +143,45 @@ func WithSkipNonDistributableBlobs() ExportOpt {
140
143
return WithBlobFilter (f )
141
144
}
142
145
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
+
143
185
func addNameAnnotation (name string , base map [string ]string ) map [string ]string {
144
186
annotations := map [string ]string {}
145
187
for k , v := range base {
@@ -152,8 +194,31 @@ func addNameAnnotation(name string, base map[string]string) map[string]string {
152
194
return annotations
153
195
}
154
196
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
+
155
220
// 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 {
157
222
var eo exportOptions
158
223
for _ , opt := range opts {
159
224
if err := opt (ctx , & eo ); err != nil {
@@ -163,13 +228,22 @@ func Export(ctx context.Context, store content.Provider, writer io.Writer, opts
163
228
164
229
records := []tarRecord {
165
230
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 )
167
241
}
168
242
169
243
algorithms := map [string ]struct {}{}
170
244
dManifests := map [digest.Digest ]* exportManifest {}
171
245
resolvedIndex := map [digest.Digest ]digest.Digest {}
172
- for _ , desc := range eo . manifests {
246
+ for _ , desc := range manifests {
173
247
if images .IsManifestType (desc .MediaType ) {
174
248
mt , ok := dManifests [desc .Digest ]
175
249
if ! ok {
@@ -259,6 +333,8 @@ func Export(ctx context.Context, store content.Provider, writer io.Writer, opts
259
333
}
260
334
}
261
335
336
+ records = append (records , ociIndexRecord (manifests ))
337
+
262
338
if ! eo .skipDockerManifest && len (dManifests ) > 0 {
263
339
tr , err := manifestsRecord (ctx , store , dManifests )
264
340
if err != nil {
@@ -291,7 +367,10 @@ func getRecords(ctx context.Context, store content.Provider, desc ocispec.Descri
291
367
return nil , nil
292
368
}
293
369
294
- childrenHandler := images .ChildrenHandler (store )
370
+ childrenHandler := brOpts .childrenHandler
371
+ if childrenHandler == nil {
372
+ childrenHandler = images .ChildrenHandler (store )
373
+ }
295
374
296
375
handlers := images .Handlers (
297
376
childrenHandler ,
@@ -313,7 +392,8 @@ type tarRecord struct {
313
392
}
314
393
315
394
type blobRecordOptions struct {
316
- blobFilter BlobFilter
395
+ blobFilter BlobFilter
396
+ childrenHandler images.HandlerFunc
317
397
}
318
398
319
399
func blobRecord (cs content.Provider , desc ocispec.Descriptor , opts * blobRecordOptions ) tarRecord {
0 commit comments