@@ -28,8 +28,13 @@ import (
28
28
"github.com/google/go-containerregistry/pkg/name"
29
29
v1 "github.com/google/go-containerregistry/pkg/v1"
30
30
"github.com/google/go-containerregistry/pkg/v1/partial"
31
+ "github.com/google/go-containerregistry/pkg/v1/types"
31
32
)
32
33
34
+ const layoutFile = `{
35
+ "imageLayoutVersion": "1.0.0"
36
+ }`
37
+
33
38
// WriteToFile writes in the compressed format to a tarball, on disk.
34
39
// This is just syntactic sugar wrapping tarball.Write with a new file.
35
40
func WriteToFile (p string , ref name.Reference , img v1.Image , opts ... WriteOption ) error {
@@ -99,12 +104,12 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer, opts ...
99
104
}
100
105
101
106
imageToTags := dedupRefToImage (refToImage )
102
- size , mBytes , err := getSizeAndManifest (imageToTags )
107
+ size , mBytes , iBytes , err := getSizeAndManifests (imageToTags )
103
108
if err != nil {
104
109
return sendUpdateReturn (o , err )
105
110
}
106
111
107
- return writeImagesToTar (imageToTags , mBytes , size , w , o )
112
+ return writeImagesToTar (imageToTags , mBytes , iBytes , size , w , o )
108
113
}
109
114
110
115
// sendUpdateReturn return the passed in error message, also sending on update channel, if it exists
@@ -126,7 +131,7 @@ func sendProgressWriterReturn(pw *progressWriter, err error) error {
126
131
}
127
132
128
133
// writeImagesToTar writes the images to the tarball
129
- func writeImagesToTar (imageToTags map [v1.Image ][]string , m []byte , size int64 , w io.Writer , o * writeOptions ) (err error ) {
134
+ func writeImagesToTar (imageToTags map [v1.Image ][]string , m , idx []byte , size int64 , w io.Writer , o * writeOptions ) (err error ) {
130
135
if w == nil {
131
136
return sendUpdateReturn (o , errors .New ("must pass valid writer" ))
132
137
}
@@ -148,9 +153,40 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
148
153
tf := tar .NewWriter (tw )
149
154
defer tf .Close ()
150
155
156
+ if err := tf .WriteHeader (& tar.Header {
157
+ Name : "blobs" ,
158
+ Mode : 0644 ,
159
+ Typeflag : tar .TypeDir ,
160
+ }); err != nil {
161
+ return err
162
+ }
163
+
164
+ if err := tf .WriteHeader (& tar.Header {
165
+ Name : "blobs/sha256" ,
166
+ Mode : 0644 ,
167
+ Typeflag : tar .TypeDir ,
168
+ }); err != nil {
169
+ return err
170
+ }
171
+
151
172
seenLayerDigests := make (map [string ]struct {})
152
173
153
174
for img := range imageToTags {
175
+ // Write the manifest.
176
+ dig , err := img .Digest ()
177
+ if err != nil {
178
+ return sendProgressWriterReturn (pw , err )
179
+ }
180
+
181
+ mFile := fmt .Sprintf ("blobs/%s/%s" , dig .Algorithm , dig .Hex )
182
+ m , err := img .RawManifest ()
183
+ if err != nil {
184
+ return sendProgressWriterReturn (pw , err )
185
+ }
186
+ if err := writeTarEntry (tf , mFile , bytes .NewReader (m ), int64 (len (m ))); err != nil {
187
+ return sendProgressWriterReturn (pw , err )
188
+ }
189
+
154
190
// Write the config.
155
191
cfgName , err := img .ConfigName ()
156
192
if err != nil {
@@ -160,7 +196,8 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
160
196
if err != nil {
161
197
return sendProgressWriterReturn (pw , err )
162
198
}
163
- if err := writeTarEntry (tf , cfgName .String (), bytes .NewReader (cfgBlob ), int64 (len (cfgBlob ))); err != nil {
199
+ configFile := fmt .Sprintf ("blobs/%s/%s" , cfgName .Algorithm , cfgName .Hex )
200
+ if err := writeTarEntry (tf , configFile , bytes .NewReader (cfgBlob ), int64 (len (cfgBlob ))); err != nil {
164
201
return sendProgressWriterReturn (pw , err )
165
202
}
166
203
@@ -175,21 +212,13 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
175
212
if err != nil {
176
213
return sendProgressWriterReturn (pw , err )
177
214
}
178
- // Munge the file name to appease ancient technology.
179
- //
180
- // tar assumes anything with a colon is a remote tape drive:
181
- // https://www.gnu.org/software/tar/manual/html_section/tar_45.html
182
- // Drop the algorithm prefix, e.g. "sha256:"
183
- hex := d .Hex
184
215
185
- // gunzip expects certain file extensions:
186
- // https://www.gnu.org/software/gzip/manual/html_node/Overview.html
187
- layerFiles [i ] = fmt .Sprintf ("%s.tar.gz" , hex )
216
+ layerFiles [i ] = fmt .Sprintf ("blobs/%s/%s" , d .Algorithm , d .Hex )
188
217
189
- if _ , ok := seenLayerDigests [hex ]; ok {
218
+ if _ , ok := seenLayerDigests [d . Hex ]; ok {
190
219
continue
191
220
}
192
- seenLayerDigests [hex ] = struct {}{}
221
+ seenLayerDigests [d . Hex ] = struct {}{}
193
222
194
223
r , err := l .Compressed ()
195
224
if err != nil {
@@ -205,9 +234,15 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
205
234
}
206
235
}
207
236
}
237
+ if err := writeTarEntry (tf , "index.json" , bytes .NewReader (idx ), int64 (len (idx ))); err != nil {
238
+ return sendProgressWriterReturn (pw , err )
239
+ }
208
240
if err := writeTarEntry (tf , "manifest.json" , bytes .NewReader (m ), int64 (len (m ))); err != nil {
209
241
return sendProgressWriterReturn (pw , err )
210
242
}
243
+ if err := writeTarEntry (tf , "oci-layout" , strings .NewReader (layoutFile ), int64 (len (layoutFile ))); err != nil {
244
+ return sendProgressWriterReturn (pw , err )
245
+ }
211
246
212
247
// be sure to close the tar writer so everything is flushed out before we send our EOF
213
248
if err := tf .Close (); err != nil {
@@ -230,6 +265,8 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
230
265
return nil , err
231
266
}
232
267
268
+ configFile := fmt .Sprintf ("blobs/%s/%s" , cfgName .Algorithm , cfgName .Hex )
269
+
233
270
// Store foreign layer info.
234
271
layerSources := make (map [v1.Hash ]v1.Descriptor )
235
272
@@ -244,16 +281,8 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
244
281
if err != nil {
245
282
return nil , err
246
283
}
247
- // Munge the file name to appease ancient technology.
248
- //
249
- // tar assumes anything with a colon is a remote tape drive:
250
- // https://www.gnu.org/software/tar/manual/html_section/tar_45.html
251
- // Drop the algorithm prefix, e.g. "sha256:"
252
- hex := d .Hex
253
284
254
- // gunzip expects certain file extensions:
255
- // https://www.gnu.org/software/gzip/manual/html_node/Overview.html
256
- layerFiles [i ] = fmt .Sprintf ("%s.tar.gz" , hex )
285
+ layerFiles [i ] = fmt .Sprintf ("blobs/%s/%s" , d .Algorithm , d .Hex )
257
286
258
287
// Add to LayerSources if it's a foreign layer.
259
288
desc , err := partial .BlobDescriptor (img , d )
@@ -271,7 +300,7 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
271
300
272
301
// Generate the tar descriptor and write it.
273
302
m = append (m , Descriptor {
274
- Config : cfgName . String () ,
303
+ Config : configFile ,
275
304
RepoTags : tags ,
276
305
Layers : layerFiles ,
277
306
LayerSources : layerSources ,
@@ -286,34 +315,80 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
286
315
return m , nil
287
316
}
288
317
318
+ // calculateIndex calculates the oci-layout style index
319
+ func calculateIndex (imageToTags map [v1.Image ][]string ) (* v1.IndexManifest , error ) {
320
+ if len (imageToTags ) == 0 {
321
+ return nil , errors .New ("set of images is empty" )
322
+ }
323
+
324
+ idx := v1.IndexManifest {
325
+ SchemaVersion : 2 ,
326
+ MediaType : types .OCIImageIndex ,
327
+ Manifests : make ([]v1.Descriptor , 0 , len (imageToTags )),
328
+ }
329
+
330
+ // TODO: Tags in here too.
331
+ for img := range imageToTags {
332
+ desc , err := partial .Descriptor (img )
333
+ if err != nil {
334
+ return nil , err
335
+ }
336
+
337
+ // Generate the tar descriptor and write it.
338
+ idx .Manifests = append (idx .Manifests , * desc )
339
+ }
340
+
341
+ // Sort by size because why not.
342
+ sort .Slice (idx .Manifests , func (i , j int ) bool {
343
+ return idx .Manifests [i ].Size < idx .Manifests [j ].Size
344
+ })
345
+
346
+ return & idx , nil
347
+ }
348
+
289
349
// CalculateSize calculates the expected complete size of the output tar file
290
350
func CalculateSize (refToImage map [name.Reference ]v1.Image ) (size int64 , err error ) {
291
351
imageToTags := dedupRefToImage (refToImage )
292
- size , _ , err = getSizeAndManifest (imageToTags )
352
+ size , _ , _ , err = getSizeAndManifests (imageToTags )
293
353
return size , err
294
354
}
295
355
296
- func getSizeAndManifest (imageToTags map [v1.Image ][]string ) (int64 , []byte , error ) {
356
+ func getSizeAndManifests (imageToTags map [v1.Image ][]string ) (int64 , [] byte , []byte , error ) {
297
357
m , err := calculateManifest (imageToTags )
298
358
if err != nil {
299
- return 0 , nil , fmt .Errorf ("unable to calculate manifest: %w" , err )
359
+ return 0 , nil , nil , fmt .Errorf ("unable to calculate manifest: %w" , err )
300
360
}
301
361
mBytes , err := json .Marshal (m )
302
362
if err != nil {
303
- return 0 , nil , fmt .Errorf ("could not marshall manifest to bytes: %w" , err )
363
+ return 0 , nil , nil , fmt .Errorf ("could not marshall manifest to bytes: %w" , err )
364
+ }
365
+
366
+ i , err := calculateIndex (imageToTags )
367
+ if err != nil {
368
+ return 0 , nil , nil , fmt .Errorf ("calculating index: %w" , err )
369
+ }
370
+ iBytes , err := json .Marshal (i )
371
+ if err != nil {
372
+ return 0 , nil , nil , fmt .Errorf ("marshaling index: %w" , err )
304
373
}
305
374
306
- size , err := calculateTarballSize (imageToTags , mBytes )
375
+ size , err := calculateTarballSize (imageToTags , mBytes , iBytes )
307
376
if err != nil {
308
- return 0 , nil , fmt .Errorf ("error calculating tarball size: %w" , err )
377
+ return 0 , nil , nil , fmt .Errorf ("error calculating tarball size: %w" , err )
309
378
}
310
- return size , mBytes , nil
379
+ return size , mBytes , iBytes , nil
311
380
}
312
381
313
382
// calculateTarballSize calculates the size of the tar file
314
- func calculateTarballSize (imageToTags map [v1.Image ][]string , mBytes []byte ) (size int64 , err error ) {
383
+ func calculateTarballSize (imageToTags map [v1.Image ][]string , mBytes , iBytes []byte ) (size int64 , err error ) {
315
384
seenLayerDigests := make (map [string ]struct {})
316
385
for img , name := range imageToTags {
386
+ mSize , err := img .Size ()
387
+ if err != nil {
388
+ return size , fmt .Errorf ("unable to get manifest size for img %s: %w" , name , err )
389
+ }
390
+ size += calculateSingleFileInTarSize (mSize )
391
+
317
392
manifest , err := img .Manifest ()
318
393
if err != nil {
319
394
return size , fmt .Errorf ("unable to get manifest for img %s: %w" , name , err )
@@ -328,9 +403,15 @@ func calculateTarballSize(imageToTags map[v1.Image][]string, mBytes []byte) (siz
328
403
size += calculateSingleFileInTarSize (l .Size )
329
404
}
330
405
}
406
+
331
407
// add the manifest
332
408
size += calculateSingleFileInTarSize (int64 (len (mBytes )))
333
409
410
+ // add OCI stuff
411
+ size += 1024 // for blobs/sha256 (if sha512 happens oh well this doesn't matter)
412
+ size += calculateSingleFileInTarSize (int64 (len (layoutFile )))
413
+ size += calculateSingleFileInTarSize (int64 (len (iBytes )))
414
+
334
415
// add the two padding blocks that indicate end of a tar file
335
416
size += 1024
336
417
return size , nil
0 commit comments