0651d3a8de
Bump containers/image to 3d0304a02154dddc8f97cc833aa0861cea5e9ade, and containers/storage to 0d32dfce498e06c132c60dac945081bf44c22464. Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
294 lines
9.8 KiB
Go
294 lines
9.8 KiB
Go
package layout
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/containers/image/manifest"
|
|
"github.com/containers/image/types"
|
|
"github.com/opencontainers/go-digest"
|
|
imgspec "github.com/opencontainers/image-spec/specs-go"
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
type ociImageDestination struct {
|
|
ref ociReference
|
|
index imgspecv1.Index
|
|
sharedBlobDir string
|
|
}
|
|
|
|
// newImageDestination returns an ImageDestination for writing to an existing directory.
|
|
func newImageDestination(ctx *types.SystemContext, ref ociReference) (types.ImageDestination, error) {
|
|
if ref.image == "" {
|
|
return nil, errors.Errorf("cannot save image with empty image.ref.name")
|
|
}
|
|
|
|
var index *imgspecv1.Index
|
|
if indexExists(ref) {
|
|
var err error
|
|
index, err = ref.getIndex()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
index = &imgspecv1.Index{
|
|
Versioned: imgspec.Versioned{
|
|
SchemaVersion: 2,
|
|
},
|
|
}
|
|
}
|
|
|
|
d := &ociImageDestination{ref: ref, index: *index}
|
|
if ctx != nil {
|
|
d.sharedBlobDir = ctx.OCISharedBlobDirPath
|
|
}
|
|
|
|
if err := ensureDirectoryExists(d.ref.dir); err != nil {
|
|
return nil, err
|
|
}
|
|
// Per the OCI image specification, layouts MUST have a "blobs" subdirectory,
|
|
// but it MAY be empty (e.g. if we never end up calling PutBlob)
|
|
// https://github.com/opencontainers/image-spec/blame/7c889fafd04a893f5c5f50b7ab9963d5d64e5242/image-layout.md#L19
|
|
if err := ensureDirectoryExists(filepath.Join(d.ref.dir, "blobs")); err != nil {
|
|
return nil, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent,
|
|
// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects.
|
|
func (d *ociImageDestination) Reference() types.ImageReference {
|
|
return d.ref
|
|
}
|
|
|
|
// Close removes resources associated with an initialized ImageDestination, if any.
|
|
func (d *ociImageDestination) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (d *ociImageDestination) SupportedManifestMIMETypes() []string {
|
|
return []string{
|
|
imgspecv1.MediaTypeImageManifest,
|
|
}
|
|
}
|
|
|
|
// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures.
|
|
// Note: It is still possible for PutSignatures to fail if SupportsSignatures returns nil.
|
|
func (d *ociImageDestination) SupportsSignatures() error {
|
|
return errors.Errorf("Pushing signatures for OCI images is not supported")
|
|
}
|
|
|
|
// ShouldCompressLayers returns true iff it is desirable to compress layer blobs written to this destination.
|
|
func (d *ociImageDestination) ShouldCompressLayers() bool {
|
|
return true
|
|
}
|
|
|
|
// AcceptsForeignLayerURLs returns false iff foreign layers in manifest should be actually
|
|
// uploaded to the image destination, true otherwise.
|
|
func (d *ociImageDestination) AcceptsForeignLayerURLs() bool {
|
|
return true
|
|
}
|
|
|
|
// MustMatchRuntimeOS returns true iff the destination can store only images targeted for the current runtime OS. False otherwise.
|
|
func (d *ociImageDestination) MustMatchRuntimeOS() bool {
|
|
return false
|
|
}
|
|
|
|
// PutBlob writes contents of stream and returns data representing the result (with all data filled in).
|
|
// inputInfo.Digest can be optionally provided if known; it is not mandatory for the implementation to verify it.
|
|
// inputInfo.Size is the expected length of stream, if known.
|
|
// WARNING: The contents of stream are being verified on the fly. Until stream.Read() returns io.EOF, the contents of the data SHOULD NOT be available
|
|
// to any other readers for download using the supplied digest.
|
|
// 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.
|
|
func (d *ociImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) {
|
|
blobFile, err := ioutil.TempFile(d.ref.dir, "oci-put-blob")
|
|
if err != nil {
|
|
return types.BlobInfo{}, err
|
|
}
|
|
succeeded := false
|
|
explicitClosed := false
|
|
defer func() {
|
|
if !explicitClosed {
|
|
blobFile.Close()
|
|
}
|
|
if !succeeded {
|
|
os.Remove(blobFile.Name())
|
|
}
|
|
}()
|
|
|
|
digester := digest.Canonical.Digester()
|
|
tee := io.TeeReader(stream, digester.Hash())
|
|
|
|
size, err := io.Copy(blobFile, tee)
|
|
if err != nil {
|
|
return types.BlobInfo{}, err
|
|
}
|
|
computedDigest := digester.Digest()
|
|
if inputInfo.Size != -1 && size != inputInfo.Size {
|
|
return types.BlobInfo{}, errors.Errorf("Size mismatch when copying %s, expected %d, got %d", computedDigest, inputInfo.Size, size)
|
|
}
|
|
if err := blobFile.Sync(); err != nil {
|
|
return types.BlobInfo{}, err
|
|
}
|
|
|
|
// On POSIX systems, blobFile was created with mode 0600, so we need to make it readable.
|
|
// On Windows, the “permissions of newly created files” argument to syscall.Open is
|
|
// ignored and the file is already readable; besides, blobFile.Chmod, i.e. syscall.Fchmod,
|
|
// always fails on Windows.
|
|
if runtime.GOOS != "windows" {
|
|
if err := blobFile.Chmod(0644); err != nil {
|
|
return types.BlobInfo{}, err
|
|
}
|
|
}
|
|
|
|
blobPath, err := d.ref.blobPath(computedDigest, d.sharedBlobDir)
|
|
if err != nil {
|
|
return types.BlobInfo{}, err
|
|
}
|
|
if err := ensureParentDirectoryExists(blobPath); err != nil {
|
|
return types.BlobInfo{}, err
|
|
}
|
|
|
|
// need to explicitly close the file, since a rename won't otherwise not work on Windows
|
|
blobFile.Close()
|
|
explicitClosed = true
|
|
if err := os.Rename(blobFile.Name(), blobPath); err != nil {
|
|
return types.BlobInfo{}, err
|
|
}
|
|
succeeded = true
|
|
return types.BlobInfo{Digest: computedDigest, Size: size}, nil
|
|
}
|
|
|
|
// HasBlob returns true iff the image destination already contains a blob with the matching digest which can be reapplied using ReapplyBlob.
|
|
// Unlike PutBlob, the digest can not be empty. If HasBlob returns true, the size of the blob must also be returned.
|
|
// If the destination does not contain the blob, or it is unknown, HasBlob ordinarily returns (false, -1, nil);
|
|
// it returns a non-nil error only on an unexpected failure.
|
|
func (d *ociImageDestination) HasBlob(info types.BlobInfo) (bool, int64, error) {
|
|
if info.Digest == "" {
|
|
return false, -1, errors.Errorf(`"Can not check for a blob with unknown digest`)
|
|
}
|
|
blobPath, err := d.ref.blobPath(info.Digest, d.sharedBlobDir)
|
|
if err != nil {
|
|
return false, -1, err
|
|
}
|
|
finfo, err := os.Stat(blobPath)
|
|
if err != nil && os.IsNotExist(err) {
|
|
return false, -1, nil
|
|
}
|
|
if err != nil {
|
|
return false, -1, err
|
|
}
|
|
return true, finfo.Size(), nil
|
|
}
|
|
|
|
func (d *ociImageDestination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo, error) {
|
|
return info, nil
|
|
}
|
|
|
|
// PutManifest writes manifest to the destination.
|
|
// FIXME? This should also receive a MIME type if known, to differentiate between schema versions.
|
|
// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema),
|
|
// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError.
|
|
func (d *ociImageDestination) PutManifest(m []byte) error {
|
|
digest, err := manifest.Digest(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
desc := imgspecv1.Descriptor{}
|
|
desc.Digest = digest
|
|
// TODO(runcom): beaware and add support for OCI manifest list
|
|
desc.MediaType = imgspecv1.MediaTypeImageManifest
|
|
desc.Size = int64(len(m))
|
|
|
|
blobPath, err := d.ref.blobPath(digest, d.sharedBlobDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ensureParentDirectoryExists(blobPath); err != nil {
|
|
return err
|
|
}
|
|
if err := ioutil.WriteFile(blobPath, m, 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.ref.image == "" {
|
|
return errors.Errorf("cannot save image with empyt image.ref.name")
|
|
}
|
|
|
|
annotations := make(map[string]string)
|
|
annotations["org.opencontainers.image.ref.name"] = d.ref.image
|
|
desc.Annotations = annotations
|
|
desc.Platform = &imgspecv1.Platform{
|
|
Architecture: runtime.GOARCH,
|
|
OS: runtime.GOOS,
|
|
}
|
|
d.addManifest(&desc)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *ociImageDestination) addManifest(desc *imgspecv1.Descriptor) {
|
|
for i, manifest := range d.index.Manifests {
|
|
if manifest.Annotations["org.opencontainers.image.ref.name"] == desc.Annotations["org.opencontainers.image.ref.name"] {
|
|
// TODO Should there first be a cleanup based on the descriptor we are going to replace?
|
|
d.index.Manifests[i] = *desc
|
|
return
|
|
}
|
|
}
|
|
d.index.Manifests = append(d.index.Manifests, *desc)
|
|
}
|
|
|
|
func (d *ociImageDestination) PutSignatures(signatures [][]byte) error {
|
|
if len(signatures) != 0 {
|
|
return errors.Errorf("Pushing signatures for OCI images is not supported")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Commit marks the process of storing the image as successful and asks for the image to be persisted.
|
|
// WARNING: This does not have any transactional semantics:
|
|
// - Uploaded data MAY be visible to others before Commit() is called
|
|
// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed)
|
|
func (d *ociImageDestination) Commit() error {
|
|
if err := ioutil.WriteFile(d.ref.ociLayoutPath(), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0644); err != nil {
|
|
return err
|
|
}
|
|
indexJSON, err := json.Marshal(d.index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(d.ref.indexPath(), indexJSON, 0644)
|
|
}
|
|
|
|
func ensureDirectoryExists(path string) error {
|
|
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
|
|
if err := os.MkdirAll(path, 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ensureParentDirectoryExists ensures the parent of the supplied path exists.
|
|
func ensureParentDirectoryExists(path string) error {
|
|
return ensureDirectoryExists(filepath.Dir(path))
|
|
}
|
|
|
|
// indexExists checks whether the index location specified in the OCI reference exists.
|
|
// The implementation is opinionated, since in case of unexpected errors false is returned
|
|
func indexExists(ref ociReference) bool {
|
|
_, err := os.Stat(ref.indexPath())
|
|
if err == nil {
|
|
return true
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|