@@ -28,8 +28,13 @@ import (
2828 "github.com/google/go-containerregistry/pkg/name"
2929 v1 "github.com/google/go-containerregistry/pkg/v1"
3030 "github.com/google/go-containerregistry/pkg/v1/partial"
31+ "github.com/google/go-containerregistry/pkg/v1/types"
3132)
3233
34+ const layoutFile = `{
35+ "imageLayoutVersion": "1.0.0"
36+ }`
37+
3338// WriteToFile writes in the compressed format to a tarball, on disk.
3439// This is just syntactic sugar wrapping tarball.Write with a new file.
3540func 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 ...
99104 }
100105
101106 imageToTags := dedupRefToImage (refToImage )
102- size , mBytes , err := getSizeAndManifest (imageToTags )
107+ size , mBytes , iBytes , err := getSizeAndManifests (imageToTags )
103108 if err != nil {
104109 return sendUpdateReturn (o , err )
105110 }
106111
107- return writeImagesToTar (imageToTags , mBytes , size , w , o )
112+ return writeImagesToTar (imageToTags , mBytes , iBytes , size , w , o )
108113}
109114
110115// 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 {
126131}
127132
128133// 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 ) {
130135 if w == nil {
131136 return sendUpdateReturn (o , errors .New ("must pass valid writer" ))
132137 }
@@ -148,9 +153,40 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
148153 tf := tar .NewWriter (tw )
149154 defer tf .Close ()
150155
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+
151172 seenLayerDigests := make (map [string ]struct {})
152173
153174 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+
154190 // Write the config.
155191 cfgName , err := img .ConfigName ()
156192 if err != nil {
@@ -160,7 +196,8 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
160196 if err != nil {
161197 return sendProgressWriterReturn (pw , err )
162198 }
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 {
164201 return sendProgressWriterReturn (pw , err )
165202 }
166203
@@ -175,21 +212,13 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
175212 if err != nil {
176213 return sendProgressWriterReturn (pw , err )
177214 }
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
184215
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 )
188217
189- if _ , ok := seenLayerDigests [hex ]; ok {
218+ if _ , ok := seenLayerDigests [d . Hex ]; ok {
190219 continue
191220 }
192- seenLayerDigests [hex ] = struct {}{}
221+ seenLayerDigests [d . Hex ] = struct {}{}
193222
194223 r , err := l .Compressed ()
195224 if err != nil {
@@ -205,9 +234,15 @@ func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w
205234 }
206235 }
207236 }
237+ if err := writeTarEntry (tf , "index.json" , bytes .NewReader (idx ), int64 (len (idx ))); err != nil {
238+ return sendProgressWriterReturn (pw , err )
239+ }
208240 if err := writeTarEntry (tf , "manifest.json" , bytes .NewReader (m ), int64 (len (m ))); err != nil {
209241 return sendProgressWriterReturn (pw , err )
210242 }
243+ if err := writeTarEntry (tf , "oci-layout" , strings .NewReader (layoutFile ), int64 (len (layoutFile ))); err != nil {
244+ return sendProgressWriterReturn (pw , err )
245+ }
211246
212247 // be sure to close the tar writer so everything is flushed out before we send our EOF
213248 if err := tf .Close (); err != nil {
@@ -230,6 +265,8 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
230265 return nil , err
231266 }
232267
268+ configFile := fmt .Sprintf ("blobs/%s/%s" , cfgName .Algorithm , cfgName .Hex )
269+
233270 // Store foreign layer info.
234271 layerSources := make (map [v1.Hash ]v1.Descriptor )
235272
@@ -244,16 +281,8 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
244281 if err != nil {
245282 return nil , err
246283 }
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
253284
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 )
257286
258287 // Add to LayerSources if it's a foreign layer.
259288 desc , err := partial .BlobDescriptor (img , d )
@@ -271,7 +300,7 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
271300
272301 // Generate the tar descriptor and write it.
273302 m = append (m , Descriptor {
274- Config : cfgName . String () ,
303+ Config : configFile ,
275304 RepoTags : tags ,
276305 Layers : layerFiles ,
277306 LayerSources : layerSources ,
@@ -286,34 +315,80 @@ func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error
286315 return m , nil
287316}
288317
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+
289349// CalculateSize calculates the expected complete size of the output tar file
290350func CalculateSize (refToImage map [name.Reference ]v1.Image ) (size int64 , err error ) {
291351 imageToTags := dedupRefToImage (refToImage )
292- size , _ , err = getSizeAndManifest (imageToTags )
352+ size , _ , _ , err = getSizeAndManifests (imageToTags )
293353 return size , err
294354}
295355
296- func getSizeAndManifest (imageToTags map [v1.Image ][]string ) (int64 , []byte , error ) {
356+ func getSizeAndManifests (imageToTags map [v1.Image ][]string ) (int64 , [] byte , []byte , error ) {
297357 m , err := calculateManifest (imageToTags )
298358 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 )
300360 }
301361 mBytes , err := json .Marshal (m )
302362 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 )
304373 }
305374
306- size , err := calculateTarballSize (imageToTags , mBytes )
375+ size , err := calculateTarballSize (imageToTags , mBytes , iBytes )
307376 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 )
309378 }
310- return size , mBytes , nil
379+ return size , mBytes , iBytes , nil
311380}
312381
313382// 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 ) {
315384 seenLayerDigests := make (map [string ]struct {})
316385 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+
317392 manifest , err := img .Manifest ()
318393 if err != nil {
319394 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
328403 size += calculateSingleFileInTarSize (l .Size )
329404 }
330405 }
406+
331407 // add the manifest
332408 size += calculateSingleFileInTarSize (int64 (len (mBytes )))
333409
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+
334415 // add the two padding blocks that indicate end of a tar file
335416 size += 1024
336417 return size , nil
0 commit comments