Skip to content

Commit 1165cd5

Browse files
QiWang19umohnani8
authored andcommitted
Add reader/writer for oci-archive multi image support
Add reader/writer with helpers to allow podman save/load multi oci-archive images. Allow read oci-archive using source_index to point to the index from oci-archive manifest. Also reimplement ociArchiveImage{Source,Destination} to support this. Signed-off-by: Qi Wang <[email protected]> Signed-off-by: Urvashi Mohnani <[email protected]>
1 parent 1ca5fad commit 1165cd5

13 files changed

+552
-112
lines changed

docs/containers-transports.5.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ The _algo:digest_ refers to the image ID reported by docker-inspect(1).
6060
### **oci:**_path[:reference]_
6161

6262
An image compliant with the "Open Container Image Layout Specification" at _path_.
63-
Using a _reference_ is optional and allows for storing multiple images at the same _path_.
63+
Using a @_source-index_ is optional and allows for storing multiple images at the same _path_.
64+
For reading images, @_source-index_ is a zero-based index in manifest (to access untagged images).
65+
If neither tag nor @_source_index is specified when reading an image, the path must contain exactly one image.
6466

65-
### **oci-archive:**_path[:reference]_
67+
### **oci-archive:**_path[:{reference|@source-index}]_
6668

67-
An image compliant with the "Open Container Image Layout Specification" stored as a tar(1) archive at _path_.
69+
An image compliant with the "Open Container Image Layout Specification" stored as a tar(1) archive at _path_. For reading archives, @_source-index_ is a zero-based index in archive manifest (to access untagged images). If neither tag nor @_source_index is specified when reading an archive, the archive must contain exactly one image.
6870

6971
### **ostree:**_docker-reference[@/absolute/repo/path]_
7072

oci/archive/oci_dest.go

+52-34
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,53 @@ package archive
22

33
import (
44
"context"
5+
"fmt"
56
"io"
67
"os"
78

9+
"github.com/containers/image/v5/oci/layout"
810
"github.com/containers/image/v5/types"
911
"github.com/containers/storage/pkg/archive"
1012
digest "github.com/opencontainers/go-digest"
11-
"github.com/pkg/errors"
12-
"github.com/sirupsen/logrus"
1313
)
1414

1515
type ociArchiveImageDestination struct {
16-
ref ociArchiveReference
17-
unpackedDest types.ImageDestination
18-
tempDirRef tempDirOCIRef
16+
ref ociArchiveReference
17+
tmpDir string
18+
types.ImageDestination
1919
}
2020

2121
// newImageDestination returns an ImageDestination for writing to an existing directory.
2222
func newImageDestination(ctx context.Context, sys *types.SystemContext, ref ociArchiveReference) (types.ImageDestination, error) {
23-
tempDirRef, err := createOCIRef(sys, ref.image)
23+
var archive *Writer
24+
25+
if ref.sourceIndex != -1 {
26+
return nil, fmt.Errorf("%w: destination reference must not contain a manifest index @%d", invalidOciArchiveErr, ref.sourceIndex)
27+
}
28+
29+
if ref.archiveWriter != nil {
30+
archive = ref.archiveWriter
31+
} else {
32+
a, err := NewWriter(ctx, sys, ref.file)
33+
if err != nil {
34+
return nil, err
35+
}
36+
archive = a
37+
}
38+
newref, err := layout.NewReference(archive.tempDir, ref.image)
2439
if err != nil {
25-
return nil, errors.Wrapf(err, "creating oci reference")
40+
return nil, err
2641
}
27-
unpackedDest, err := tempDirRef.ociRefExtracted.NewImageDestination(ctx, sys)
42+
dst, err := newref.NewImageDestination(ctx, sys)
2843
if err != nil {
29-
if err := tempDirRef.deleteTempDir(); err != nil {
30-
return nil, errors.Wrapf(err, "deleting temp directory %q", tempDirRef.tempDirectory)
31-
}
3244
return nil, err
3345
}
34-
return &ociArchiveImageDestination{ref: ref,
35-
unpackedDest: unpackedDest,
36-
tempDirRef: tempDirRef}, nil
46+
47+
return &ociArchiveImageDestination{
48+
ImageDestination: dst,
49+
tmpDir: archive.tempDir,
50+
ref: ref,
51+
}, nil
3752
}
3853

3954
// Reference returns the reference used to set up this destination.
@@ -44,42 +59,41 @@ func (d *ociArchiveImageDestination) Reference() types.ImageReference {
4459
// Close removes resources associated with an initialized ImageDestination, if any
4560
// Close deletes the temp directory of the oci-archive image
4661
func (d *ociArchiveImageDestination) Close() error {
47-
defer func() {
48-
err := d.tempDirRef.deleteTempDir()
49-
logrus.Debugf("Error deleting temporary directory: %v", err)
50-
}()
51-
return d.unpackedDest.Close()
62+
if d.ref.archiveWriter != nil {
63+
return nil
64+
}
65+
return d.ImageDestination.Close()
5266
}
5367

5468
func (d *ociArchiveImageDestination) SupportedManifestMIMETypes() []string {
55-
return d.unpackedDest.SupportedManifestMIMETypes()
69+
return d.ImageDestination.SupportedManifestMIMETypes()
5670
}
5771

5872
// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures
5973
func (d *ociArchiveImageDestination) SupportsSignatures(ctx context.Context) error {
60-
return d.unpackedDest.SupportsSignatures(ctx)
74+
return d.ImageDestination.SupportsSignatures(ctx)
6175
}
6276

6377
func (d *ociArchiveImageDestination) DesiredLayerCompression() types.LayerCompression {
64-
return d.unpackedDest.DesiredLayerCompression()
78+
return d.ImageDestination.DesiredLayerCompression()
6579
}
6680

6781
// AcceptsForeignLayerURLs returns false iff foreign layers in manifest should be actually
6882
// uploaded to the image destination, true otherwise.
6983
func (d *ociArchiveImageDestination) AcceptsForeignLayerURLs() bool {
70-
return d.unpackedDest.AcceptsForeignLayerURLs()
84+
return d.ImageDestination.AcceptsForeignLayerURLs()
7185
}
7286

7387
// MustMatchRuntimeOS returns true iff the destination can store only images targeted for the current runtime architecture and OS. False otherwise
7488
func (d *ociArchiveImageDestination) MustMatchRuntimeOS() bool {
75-
return d.unpackedDest.MustMatchRuntimeOS()
89+
return d.ImageDestination.MustMatchRuntimeOS()
7690
}
7791

7892
// IgnoresEmbeddedDockerReference returns true iff the destination does not care about Image.EmbeddedDockerReferenceConflicts(),
7993
// and would prefer to receive an unmodified manifest instead of one modified for the destination.
8094
// Does not make a difference if Reference().DockerReference() is nil.
8195
func (d *ociArchiveImageDestination) IgnoresEmbeddedDockerReference() bool {
82-
return d.unpackedDest.IgnoresEmbeddedDockerReference()
96+
return d.ImageDestination.IgnoresEmbeddedDockerReference()
8397
}
8498

8599
// HasThreadSafePutBlob indicates whether PutBlob can be executed concurrently.
@@ -96,7 +110,7 @@ func (d *ociArchiveImageDestination) HasThreadSafePutBlob() bool {
96110
// to any other readers for download using the supplied digest.
97111
// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far.
98112
func (d *ociArchiveImageDestination) PutBlob(ctx context.Context, stream io.Reader, inputInfo types.BlobInfo, cache types.BlobInfoCache, isConfig bool) (types.BlobInfo, error) {
99-
return d.unpackedDest.PutBlob(ctx, stream, inputInfo, cache, isConfig)
113+
return d.ImageDestination.PutBlob(ctx, stream, inputInfo, cache, isConfig)
100114
}
101115

102116
// TryReusingBlob checks whether the transport already contains, or can efficiently reuse, a blob, and if so, applies it to the current destination
@@ -109,7 +123,7 @@ func (d *ociArchiveImageDestination) PutBlob(ctx context.Context, stream io.Read
109123
// If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure.
110124
// May use and/or update cache.
111125
func (d *ociArchiveImageDestination) TryReusingBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache, canSubstitute bool) (bool, types.BlobInfo, error) {
112-
return d.unpackedDest.TryReusingBlob(ctx, info, cache, canSubstitute)
126+
return d.ImageDestination.TryReusingBlob(ctx, info, cache, canSubstitute)
113127
}
114128

115129
// PutManifest writes the manifest to the destination.
@@ -118,14 +132,14 @@ func (d *ociArchiveImageDestination) TryReusingBlob(ctx context.Context, info ty
118132
// It is expected but not enforced that the instanceDigest, when specified, matches the digest of `manifest` as generated
119133
// by `manifest.Digest()`.
120134
func (d *ociArchiveImageDestination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error {
121-
return d.unpackedDest.PutManifest(ctx, m, instanceDigest)
135+
return d.ImageDestination.PutManifest(ctx, m, instanceDigest)
122136
}
123137

124138
// PutSignatures writes a set of signatures to the destination.
125139
// If instanceDigest is not nil, it contains a digest of the specific manifest instance to write or overwrite the signatures for
126140
// (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list.
127141
func (d *ociArchiveImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error {
128-
return d.unpackedDest.PutSignatures(ctx, signatures, instanceDigest)
142+
return d.ImageDestination.PutSignatures(ctx, signatures, instanceDigest)
129143
}
130144

131145
// Commit marks the process of storing the image as successful and asks for the image to be persisted
@@ -134,12 +148,16 @@ func (d *ociArchiveImageDestination) PutSignatures(ctx context.Context, signatur
134148
// original manifest list digest, if desired.
135149
// after the directory is made, it is tarred up into a file and the directory is deleted
136150
func (d *ociArchiveImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error {
137-
if err := d.unpackedDest.Commit(ctx, unparsedToplevel); err != nil {
138-
return errors.Wrapf(err, "storing image %q", d.ref.image)
151+
if err := d.ImageDestination.Commit(ctx, unparsedToplevel); err != nil {
152+
return fmt.Errorf("%w: storing image %q", err, d.ref.image)
153+
}
154+
155+
if d.ref.archiveWriter != nil {
156+
return nil
139157
}
140158

141159
// path of directory to tar up
142-
src := d.tempDirRef.tempDirectory
160+
src := d.tmpDir
143161
// path to save tarred up file
144162
dst := d.ref.resolvedFile
145163
return tarDirectory(src, dst)
@@ -150,13 +168,13 @@ func tarDirectory(src, dst string) error {
150168
// input is a stream of bytes from the archive of the directory at path
151169
input, err := archive.Tar(src, archive.Uncompressed)
152170
if err != nil {
153-
return errors.Wrapf(err, "retrieving stream of bytes from %q", src)
171+
return fmt.Errorf("%w: retrieving stream of bytes from %q", err, src)
154172
}
155173

156174
// creates the tar file
157175
outFile, err := os.Create(dst)
158176
if err != nil {
159-
return errors.Wrapf(err, "creating tar file %q", dst)
177+
return fmt.Errorf("%w: creating tar file %q", err, dst)
160178
}
161179
defer outFile.Close()
162180

oci/archive/oci_src.go

+40-28
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,51 @@ package archive
22

33
import (
44
"context"
5+
"fmt"
56
"io"
67

8+
"github.com/containers/image/v5/oci/layout"
79
ocilayout "github.com/containers/image/v5/oci/layout"
810
"github.com/containers/image/v5/types"
911
digest "github.com/opencontainers/go-digest"
1012
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
11-
"github.com/pkg/errors"
1213
"github.com/sirupsen/logrus"
1314
)
1415

1516
type ociArchiveImageSource struct {
16-
ref ociArchiveReference
17-
unpackedSrc types.ImageSource
18-
tempDirRef tempDirOCIRef
17+
ref ociArchiveReference
18+
types.ImageSource
1919
}
2020

2121
// newImageSource returns an ImageSource for reading from an existing directory.
22-
// newImageSource untars the file and saves it in a temp directory
2322
func newImageSource(ctx context.Context, sys *types.SystemContext, ref ociArchiveReference) (types.ImageSource, error) {
24-
tempDirRef, err := createUntarTempDir(sys, ref)
25-
if err != nil {
26-
return nil, errors.Wrap(err, "creating temp directory")
23+
var archive *Reader
24+
25+
if ref.archiveReader != nil {
26+
archive = ref.archiveReader
27+
ref.closeArchive = false
28+
} else {
29+
a, err := NewReader(ctx, sys, ref)
30+
if err != nil {
31+
return nil, err
32+
}
33+
archive = a
34+
ref.closeArchive = true
2735
}
2836

29-
unpackedSrc, err := tempDirRef.ociRefExtracted.NewImageSource(ctx, sys)
37+
newref, err := layout.NewReference(archive.tempDirectory, ref.image)
38+
if err != nil {
39+
return nil, err
40+
}
41+
src, err := newref.NewImageSource(ctx, sys)
3042
if err != nil {
31-
if err := tempDirRef.deleteTempDir(); err != nil {
32-
return nil, errors.Wrapf(err, "deleting temp directory %q", tempDirRef.tempDirectory)
33-
}
3443
return nil, err
3544
}
36-
return &ociArchiveImageSource{ref: ref,
37-
unpackedSrc: unpackedSrc,
38-
tempDirRef: tempDirRef}, nil
45+
46+
return &ociArchiveImageSource{
47+
ImageSource: src,
48+
ref: ref,
49+
}, nil
3950
}
4051

4152
// LoadManifestDescriptor loads the manifest
@@ -48,11 +59,11 @@ func LoadManifestDescriptor(imgRef types.ImageReference) (imgspecv1.Descriptor,
4859
func LoadManifestDescriptorWithContext(sys *types.SystemContext, imgRef types.ImageReference) (imgspecv1.Descriptor, error) {
4960
ociArchRef, ok := imgRef.(ociArchiveReference)
5061
if !ok {
51-
return imgspecv1.Descriptor{}, errors.Errorf("error typecasting, need type ociArchiveReference")
62+
return imgspecv1.Descriptor{}, fmt.Errorf("error typecasting, need type ociArchiveReference")
5263
}
5364
tempDirRef, err := createUntarTempDir(sys, ociArchRef)
5465
if err != nil {
55-
return imgspecv1.Descriptor{}, errors.Wrap(err, "creating temp directory")
66+
return imgspecv1.Descriptor{}, fmt.Errorf("%w: creating temp directory", err)
5667
}
5768
defer func() {
5869
err := tempDirRef.deleteTempDir()
@@ -61,7 +72,7 @@ func LoadManifestDescriptorWithContext(sys *types.SystemContext, imgRef types.Im
6172

6273
descriptor, err := ocilayout.LoadManifestDescriptor(tempDirRef.ociRefExtracted)
6374
if err != nil {
64-
return imgspecv1.Descriptor{}, errors.Wrap(err, "loading index")
75+
return imgspecv1.Descriptor{}, fmt.Errorf("%w: loading index", err)
6576
}
6677
return descriptor, nil
6778
}
@@ -72,21 +83,22 @@ func (s *ociArchiveImageSource) Reference() types.ImageReference {
7283
}
7384

7485
// Close removes resources associated with an initialized ImageSource, if any.
75-
// Close deletes the temporary directory at dst
7686
func (s *ociArchiveImageSource) Close() error {
77-
defer func() {
78-
err := s.tempDirRef.deleteTempDir()
79-
logrus.Debugf("error deleting tmp dir: %v", err)
80-
}()
81-
return s.unpackedSrc.Close()
87+
if s.ref.archiveReader != nil {
88+
return nil
89+
}
90+
if s.ref.closeArchive {
91+
return s.ImageSource.Close()
92+
}
93+
return nil
8294
}
8395

8496
// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
8597
// It may use a remote (= slow) service.
8698
// If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list);
8799
// this never happens if the primary manifest is not a manifest list (e.g. if the source never returns manifest lists).
88100
func (s *ociArchiveImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) {
89-
return s.unpackedSrc.GetManifest(ctx, instanceDigest)
101+
return s.ImageSource.GetManifest(ctx, instanceDigest)
90102
}
91103

92104
// HasThreadSafeGetBlob indicates whether GetBlob can be executed concurrently.
@@ -98,15 +110,15 @@ func (s *ociArchiveImageSource) HasThreadSafeGetBlob() bool {
98110
// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
99111
// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
100112
func (s *ociArchiveImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
101-
return s.unpackedSrc.GetBlob(ctx, info, cache)
113+
return s.ImageSource.GetBlob(ctx, info, cache)
102114
}
103115

104116
// GetSignatures returns the image's signatures. It may use a remote (= slow) service.
105117
// If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve signatures for
106118
// (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list
107119
// (e.g. if the source never returns manifest lists).
108120
func (s *ociArchiveImageSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) {
109-
return s.unpackedSrc.GetSignatures(ctx, instanceDigest)
121+
return s.ImageSource.GetSignatures(ctx, instanceDigest)
110122
}
111123

112124
// LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer
@@ -118,5 +130,5 @@ func (s *ociArchiveImageSource) GetSignatures(ctx context.Context, instanceDiges
118130
// The Digest field is guaranteed to be provided; Size may be -1.
119131
// WARNING: The list may contain duplicates, and they are semantically relevant.
120132
func (s *ociArchiveImageSource) LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]types.BlobInfo, error) {
121-
return s.unpackedSrc.LayerInfosForCopy(ctx, instanceDigest)
133+
return s.ImageSource.LayerInfosForCopy(ctx, instanceDigest)
122134
}

0 commit comments

Comments
 (0)