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 }