Skip to content

Commit 1a9e4ba

Browse files
committed
Make default tarball also oci-flavored
Signed-off-by: Jon Johnson <[email protected]>
1 parent 06dcd85 commit 1a9e4ba

File tree

3 files changed

+219
-45
lines changed

3 files changed

+219
-45
lines changed

pkg/v1/tarball/progress_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func ExampleWithProgress() {
4545
case update.Error != nil && errors.Is(update.Error, io.EOF):
4646
fmt.Fprintf(os.Stderr, "receive error message: %v\n", err)
4747
fmt.Printf("%d/%d", update.Complete, update.Total)
48-
// Output: 4096/4096
48+
// Output: 8192/8192
4949
return
5050
case update.Error != nil:
5151
fmt.Printf("error writing tarball: %v\n", update.Error)

pkg/v1/tarball/write.go

+114-33
Original file line numberDiff line numberDiff line change
@@ -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.
3540
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 ...
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
290350
func 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

Comments
 (0)