mirror of
https://github.com/containers/image.git
synced 2025-04-18 19:44:05 +03:00
207 lines
6.5 KiB
Go
207 lines
6.5 KiB
Go
package sif
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
|
||
"github.com/containers/image/v5/internal/imagesource/impl"
|
||
"github.com/containers/image/v5/internal/imagesource/stubs"
|
||
"github.com/containers/image/v5/internal/private"
|
||
"github.com/containers/image/v5/internal/tmpdir"
|
||
"github.com/containers/image/v5/types"
|
||
"github.com/opencontainers/go-digest"
|
||
imgspecs "github.com/opencontainers/image-spec/specs-go"
|
||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||
"github.com/sirupsen/logrus"
|
||
"github.com/sylabs/sif/v2/pkg/sif"
|
||
)
|
||
|
||
type sifImageSource struct {
|
||
impl.Compat
|
||
impl.PropertyMethodsInitialize
|
||
impl.NoSignatures
|
||
impl.DoesNotAffectLayerInfosForCopy
|
||
stubs.NoGetBlobAtInitialize
|
||
|
||
ref sifReference
|
||
workDir string
|
||
layerDigest digest.Digest
|
||
layerSize int64
|
||
layerFile string
|
||
config []byte
|
||
configDigest digest.Digest
|
||
manifest []byte
|
||
}
|
||
|
||
// getBlobInfo returns the digest, and size of the provided file.
|
||
func getBlobInfo(path string) (digest.Digest, int64, error) {
|
||
f, err := os.Open(path)
|
||
if err != nil {
|
||
return "", -1, fmt.Errorf("opening %q for reading: %w", path, err)
|
||
}
|
||
defer f.Close()
|
||
|
||
// TODO: Instead of writing the tar file to disk, and reading
|
||
// it here again, stream the tar file to a pipe and
|
||
// compute the digest while writing it to disk.
|
||
logrus.Debugf("Computing a digest of the SIF conversion output...")
|
||
digester := digest.Canonical.Digester()
|
||
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
|
||
size, err := io.Copy(digester.Hash(), f)
|
||
if err != nil {
|
||
return "", -1, fmt.Errorf("reading %q: %w", path, err)
|
||
}
|
||
digest := digester.Digest()
|
||
logrus.Debugf("... finished computing the digest of the SIF conversion output")
|
||
|
||
return digest, size, nil
|
||
}
|
||
|
||
// newImageSource returns an ImageSource for reading from an existing directory.
|
||
// newImageSource extracts SIF objects and saves them in a temp directory.
|
||
func newImageSource(ctx context.Context, sys *types.SystemContext, ref sifReference) (private.ImageSource, error) {
|
||
sifImg, err := sif.LoadContainerFromPath(ref.file, sif.OptLoadWithFlag(os.O_RDONLY))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("loading SIF file: %w", err)
|
||
}
|
||
defer func() {
|
||
_ = sifImg.UnloadContainer()
|
||
}()
|
||
|
||
workDir, err := tmpdir.MkDirBigFileTemp(sys, "sif")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("creating temp directory: %w", err)
|
||
}
|
||
succeeded := false
|
||
defer func() {
|
||
if !succeeded {
|
||
os.RemoveAll(workDir)
|
||
}
|
||
}()
|
||
|
||
layerPath, commandLine, err := convertSIFToElements(ctx, sifImg, workDir)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("converting rootfs from SquashFS to Tarball: %w", err)
|
||
}
|
||
|
||
layerDigest, layerSize, err := getBlobInfo(layerPath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("gathering blob information: %w", err)
|
||
}
|
||
|
||
created := sifImg.ModifiedAt()
|
||
config := imgspecv1.Image{
|
||
Created: &created,
|
||
Platform: imgspecv1.Platform{
|
||
Architecture: sifImg.PrimaryArch(),
|
||
OS: "linux",
|
||
},
|
||
Config: imgspecv1.ImageConfig{
|
||
Cmd: commandLine,
|
||
},
|
||
RootFS: imgspecv1.RootFS{
|
||
Type: "layers",
|
||
DiffIDs: []digest.Digest{layerDigest},
|
||
},
|
||
History: []imgspecv1.History{
|
||
{
|
||
Created: &created,
|
||
CreatedBy: fmt.Sprintf("/bin/sh -c #(nop) ADD file:%s in %c", layerDigest.Encoded(), os.PathSeparator),
|
||
Comment: "imported from SIF, uuid: " + sifImg.ID(),
|
||
},
|
||
{
|
||
Created: &created,
|
||
CreatedBy: "/bin/sh -c #(nop) CMD [\"bash\"]",
|
||
EmptyLayer: true,
|
||
},
|
||
},
|
||
}
|
||
configBytes, err := json.Marshal(&config)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("generating configuration blob for %q: %w", ref.resolvedFile, err)
|
||
}
|
||
configDigest := digest.Canonical.FromBytes(configBytes)
|
||
|
||
manifest := imgspecv1.Manifest{
|
||
Versioned: imgspecs.Versioned{SchemaVersion: 2},
|
||
MediaType: imgspecv1.MediaTypeImageManifest,
|
||
Config: imgspecv1.Descriptor{
|
||
Digest: configDigest,
|
||
Size: int64(len(configBytes)),
|
||
MediaType: imgspecv1.MediaTypeImageConfig,
|
||
},
|
||
Layers: []imgspecv1.Descriptor{{
|
||
Digest: layerDigest,
|
||
Size: layerSize,
|
||
MediaType: imgspecv1.MediaTypeImageLayer,
|
||
}},
|
||
}
|
||
manifestBytes, err := json.Marshal(&manifest)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("generating manifest for %q: %w", ref.resolvedFile, err)
|
||
}
|
||
|
||
succeeded = true
|
||
s := &sifImageSource{
|
||
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
|
||
HasThreadSafeGetBlob: true,
|
||
}),
|
||
NoGetBlobAtInitialize: stubs.NoGetBlobAt(ref),
|
||
|
||
ref: ref,
|
||
workDir: workDir,
|
||
layerDigest: layerDigest,
|
||
layerSize: layerSize,
|
||
layerFile: layerPath,
|
||
config: configBytes,
|
||
configDigest: configDigest,
|
||
manifest: manifestBytes,
|
||
}
|
||
s.Compat = impl.AddCompat(s)
|
||
return s, nil
|
||
}
|
||
|
||
// Reference returns the reference used to set up this source.
|
||
func (s *sifImageSource) Reference() types.ImageReference {
|
||
return s.ref
|
||
}
|
||
|
||
// Close removes resources associated with an initialized ImageSource, if any.
|
||
func (s *sifImageSource) Close() error {
|
||
return os.RemoveAll(s.workDir)
|
||
}
|
||
|
||
// 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 (s *sifImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) {
|
||
switch info.Digest {
|
||
case s.configDigest:
|
||
return io.NopCloser(bytes.NewReader(s.config)), int64(len(s.config)), nil
|
||
case s.layerDigest:
|
||
reader, err := os.Open(s.layerFile)
|
||
if err != nil {
|
||
return nil, -1, fmt.Errorf("opening %q: %w", s.layerFile, err)
|
||
}
|
||
return reader, s.layerSize, nil
|
||
default:
|
||
return nil, -1, fmt.Errorf("no blob with digest %q found", info.Digest.String())
|
||
}
|
||
}
|
||
|
||
// 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 (s *sifImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) {
|
||
if instanceDigest != nil {
|
||
return nil, "", errors.New("manifest lists are not supported by the sif transport")
|
||
}
|
||
return s.manifest, imgspecv1.MediaTypeImageManifest, nil
|
||
}
|