mirror of
https://github.com/containers/skopeo.git
synced 2025-04-27 19:05:32 +00:00
252 lines
7.9 KiB
Go
252 lines
7.9 KiB
Go
package tarball
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"maps"
|
||
"os"
|
||
"runtime"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/containers/image/v5/internal/imagesource/impl"
|
||
"github.com/containers/image/v5/internal/imagesource/stubs"
|
||
"github.com/containers/image/v5/pkg/compression"
|
||
compressionTypes "github.com/containers/image/v5/pkg/compression/types"
|
||
"github.com/containers/image/v5/types"
|
||
digest "github.com/opencontainers/go-digest"
|
||
imgspecs "github.com/opencontainers/image-spec/specs-go"
|
||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||
)
|
||
|
||
type tarballImageSource struct {
|
||
impl.Compat
|
||
impl.PropertyMethodsInitialize
|
||
impl.NoSignatures
|
||
impl.DoesNotAffectLayerInfosForCopy
|
||
stubs.NoGetBlobAtInitialize
|
||
|
||
reference tarballReference
|
||
blobs map[digest.Digest]tarballBlob
|
||
manifest []byte
|
||
}
|
||
|
||
// tarballBlob is a blob that tarballImagSource can return by GetBlob.
|
||
type tarballBlob struct {
|
||
contents []byte // or nil to read from filename below
|
||
filename string // valid if contents == nil
|
||
size int64
|
||
}
|
||
|
||
func (r *tarballReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) {
|
||
// Pick up the layer comment from the configuration's history list, if one is set.
|
||
comment := "imported from tarball"
|
||
if len(r.config.History) > 0 && r.config.History[0].Comment != "" {
|
||
comment = r.config.History[0].Comment
|
||
}
|
||
|
||
// Gather up the digests, sizes, and history information for all of the files.
|
||
blobs := map[digest.Digest]tarballBlob{}
|
||
diffIDs := []digest.Digest{}
|
||
created := time.Time{}
|
||
history := []imgspecv1.History{}
|
||
layerDescriptors := []imgspecv1.Descriptor{}
|
||
for _, filename := range r.filenames {
|
||
var reader io.Reader
|
||
var blobTime time.Time
|
||
var blob tarballBlob
|
||
if filename == "-" {
|
||
reader = bytes.NewReader(r.stdin)
|
||
blobTime = time.Now()
|
||
blob = tarballBlob{
|
||
contents: r.stdin,
|
||
size: int64(len(r.stdin)),
|
||
}
|
||
} else {
|
||
file, err := os.Open(filename)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer file.Close()
|
||
reader = file
|
||
fileinfo, err := file.Stat()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("error reading size of %q: %w", filename, err)
|
||
}
|
||
blobTime = fileinfo.ModTime()
|
||
blob = tarballBlob{
|
||
filename: filename,
|
||
size: fileinfo.Size(),
|
||
}
|
||
}
|
||
|
||
// Set up to digest the file as it is.
|
||
blobIDdigester := digest.Canonical.Digester()
|
||
reader = io.TeeReader(reader, blobIDdigester.Hash())
|
||
|
||
var layerType string
|
||
var diffIDdigester digest.Digester
|
||
// If necessary, digest the file after we decompress it.
|
||
if err := func() error { // A scope for defer
|
||
format, decompressor, reader, err := compression.DetectCompressionFormat(reader)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if decompressor != nil {
|
||
uncompressed, err := decompressor(reader)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer uncompressed.Close()
|
||
// It is compressed, so the diffID is the digest of the uncompressed version
|
||
diffIDdigester = digest.Canonical.Digester()
|
||
reader = io.TeeReader(uncompressed, diffIDdigester.Hash())
|
||
switch format.Name() {
|
||
case compressionTypes.GzipAlgorithmName:
|
||
layerType = imgspecv1.MediaTypeImageLayerGzip
|
||
case compressionTypes.ZstdAlgorithmName:
|
||
layerType = imgspecv1.MediaTypeImageLayerZstd
|
||
default: // This is incorrect, but we have no good options, and it is what this transport was historically doing.
|
||
layerType = imgspecv1.MediaTypeImageLayerGzip
|
||
}
|
||
} else {
|
||
// It is not compressed, so the diffID and the blobID are going to be the same
|
||
diffIDdigester = blobIDdigester
|
||
layerType = imgspecv1.MediaTypeImageLayer
|
||
}
|
||
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
|
||
if _, err := io.Copy(io.Discard, reader); err != nil {
|
||
return fmt.Errorf("error reading %q: %w", filename, err)
|
||
}
|
||
return nil
|
||
}(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Grab our uncompressed and possibly-compressed digests and sizes.
|
||
diffID := diffIDdigester.Digest()
|
||
blobID := blobIDdigester.Digest()
|
||
diffIDs = append(diffIDs, diffID)
|
||
blobs[blobID] = blob
|
||
|
||
history = append(history, imgspecv1.History{
|
||
Created: &blobTime,
|
||
CreatedBy: fmt.Sprintf("/bin/sh -c #(nop) ADD file:%s in %c", diffID.Encoded(), os.PathSeparator),
|
||
Comment: comment,
|
||
})
|
||
// Use the mtime of the most recently modified file as the image's creation time.
|
||
if created.Before(blobTime) {
|
||
created = blobTime
|
||
}
|
||
|
||
layerDescriptors = append(layerDescriptors, imgspecv1.Descriptor{
|
||
Digest: blobID,
|
||
Size: blob.size,
|
||
MediaType: layerType,
|
||
})
|
||
}
|
||
|
||
// Pick up other defaults from the config in the reference.
|
||
config := r.config
|
||
if config.Created == nil {
|
||
config.Created = &created
|
||
}
|
||
if config.Architecture == "" {
|
||
config.Architecture = runtime.GOARCH
|
||
}
|
||
if config.OS == "" {
|
||
config.OS = runtime.GOOS
|
||
}
|
||
config.RootFS = imgspecv1.RootFS{
|
||
Type: "layers",
|
||
DiffIDs: diffIDs,
|
||
}
|
||
config.History = history
|
||
|
||
// Encode and digest the image configuration blob.
|
||
configBytes, err := json.Marshal(&config)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("error generating configuration blob for %q: %w", strings.Join(r.filenames, separator), err)
|
||
}
|
||
configID := digest.Canonical.FromBytes(configBytes)
|
||
blobs[configID] = tarballBlob{
|
||
contents: configBytes,
|
||
size: int64(len(configBytes)),
|
||
}
|
||
|
||
// Populate a manifest with the configuration blob and the layers.
|
||
manifest := imgspecv1.Manifest{
|
||
Versioned: imgspecs.Versioned{
|
||
SchemaVersion: 2,
|
||
},
|
||
Config: imgspecv1.Descriptor{
|
||
Digest: configID,
|
||
Size: int64(len(configBytes)),
|
||
MediaType: imgspecv1.MediaTypeImageConfig,
|
||
},
|
||
Layers: layerDescriptors,
|
||
Annotations: maps.Clone(r.annotations),
|
||
}
|
||
|
||
// Encode the manifest.
|
||
manifestBytes, err := json.Marshal(&manifest)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("error generating manifest for %q: %w", strings.Join(r.filenames, separator), err)
|
||
}
|
||
|
||
// Return the image.
|
||
src := &tarballImageSource{
|
||
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
|
||
HasThreadSafeGetBlob: false,
|
||
}),
|
||
NoGetBlobAtInitialize: stubs.NoGetBlobAt(r),
|
||
|
||
reference: *r,
|
||
blobs: blobs,
|
||
manifest: manifestBytes,
|
||
}
|
||
src.Compat = impl.AddCompat(src)
|
||
|
||
return src, nil
|
||
}
|
||
|
||
func (is *tarballImageSource) Close() error {
|
||
return nil
|
||
}
|
||
|
||
// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
|
||
// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
|
||
// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location.
|
||
func (is *tarballImageSource) GetBlob(ctx context.Context, blobinfo types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
|
||
blob, ok := is.blobs[blobinfo.Digest]
|
||
if !ok {
|
||
return nil, -1, fmt.Errorf("no blob with digest %q found", blobinfo.Digest.String())
|
||
}
|
||
if blob.contents != nil {
|
||
return io.NopCloser(bytes.NewReader(blob.contents)), int64(len(blob.contents)), nil
|
||
}
|
||
reader, err := os.Open(blob.filename)
|
||
if err != nil {
|
||
return nil, -1, err
|
||
}
|
||
return reader, blob.size, nil
|
||
}
|
||
|
||
// 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).
|
||
// It may use a remote (= slow) service.
|
||
// If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list);
|
||
// this never happens if the primary manifest is not a manifest list (e.g. if the source never returns manifest lists).
|
||
func (is *tarballImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) {
|
||
if instanceDigest != nil {
|
||
return nil, "", fmt.Errorf("manifest lists are not supported by the %q transport", transportName)
|
||
}
|
||
return is.manifest, imgspecv1.MediaTypeImageManifest, nil
|
||
}
|
||
|
||
func (is *tarballImageSource) Reference() types.ImageReference {
|
||
return &is.reference
|
||
}
|