mirror of https://github.com/containers/image.git
298 lines
11 KiB
Go
298 lines
11 KiB
Go
package directory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/containers/image/v5/internal/imagedestination/impl"
|
|
"github.com/containers/image/v5/internal/imagedestination/stubs"
|
|
"github.com/containers/image/v5/internal/private"
|
|
"github.com/containers/image/v5/internal/putblobdigest"
|
|
"github.com/containers/image/v5/internal/signature"
|
|
"github.com/containers/image/v5/types"
|
|
"github.com/containers/storage/pkg/fileutils"
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const version = "Directory Transport Version: 1.1\n"
|
|
|
|
// ErrNotContainerImageDir indicates that the directory doesn't match the expected contents of a directory created
|
|
// using the 'dir' transport
|
|
var ErrNotContainerImageDir = errors.New("not a containers image directory, don't want to overwrite important data")
|
|
|
|
type dirImageDestination struct {
|
|
impl.Compat
|
|
impl.PropertyMethodsInitialize
|
|
stubs.IgnoresOriginalOCIConfig
|
|
stubs.NoPutBlobPartialInitialize
|
|
stubs.AlwaysSupportsSignatures
|
|
|
|
ref dirReference
|
|
}
|
|
|
|
// newImageDestination returns an ImageDestination for writing to a directory.
|
|
func newImageDestination(sys *types.SystemContext, ref dirReference) (private.ImageDestination, error) {
|
|
desiredLayerCompression := types.PreserveOriginal
|
|
if sys != nil {
|
|
if sys.DirForceCompress {
|
|
desiredLayerCompression = types.Compress
|
|
|
|
if sys.DirForceDecompress {
|
|
return nil, fmt.Errorf("Cannot compress and decompress at the same time")
|
|
}
|
|
}
|
|
if sys.DirForceDecompress {
|
|
desiredLayerCompression = types.Decompress
|
|
}
|
|
}
|
|
|
|
// If directory exists check if it is empty
|
|
// if not empty, check whether the contents match that of a container image directory and overwrite the contents
|
|
// if the contents don't match throw an error
|
|
dirExists, err := pathExists(ref.resolvedPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("checking for path %q: %w", ref.resolvedPath, err)
|
|
}
|
|
if dirExists {
|
|
isEmpty, err := isDirEmpty(ref.resolvedPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !isEmpty {
|
|
versionExists, err := pathExists(ref.versionPath())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("checking if path exists %q: %w", ref.versionPath(), err)
|
|
}
|
|
if versionExists {
|
|
contents, err := os.ReadFile(ref.versionPath())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// check if contents of version file is what we expect it to be
|
|
if string(contents) != version {
|
|
return nil, ErrNotContainerImageDir
|
|
}
|
|
} else {
|
|
return nil, ErrNotContainerImageDir
|
|
}
|
|
// delete directory contents so that only one image is in the directory at a time
|
|
if err = removeDirContents(ref.resolvedPath); err != nil {
|
|
return nil, fmt.Errorf("erasing contents in %q: %w", ref.resolvedPath, err)
|
|
}
|
|
logrus.Debugf("overwriting existing container image directory %q", ref.resolvedPath)
|
|
}
|
|
} else {
|
|
// create directory if it doesn't exist
|
|
if err := os.MkdirAll(ref.resolvedPath, 0755); err != nil {
|
|
return nil, fmt.Errorf("unable to create directory %q: %w", ref.resolvedPath, err)
|
|
}
|
|
}
|
|
// create version file
|
|
err = os.WriteFile(ref.versionPath(), []byte(version), 0644)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating version file %q: %w", ref.versionPath(), err)
|
|
}
|
|
|
|
d := &dirImageDestination{
|
|
PropertyMethodsInitialize: impl.PropertyMethods(impl.Properties{
|
|
SupportedManifestMIMETypes: nil,
|
|
DesiredLayerCompression: desiredLayerCompression,
|
|
AcceptsForeignLayerURLs: false,
|
|
MustMatchRuntimeOS: false,
|
|
IgnoresEmbeddedDockerReference: false, // N/A, DockerReference() returns nil.
|
|
HasThreadSafePutBlob: true,
|
|
}),
|
|
NoPutBlobPartialInitialize: stubs.NoPutBlobPartial(ref),
|
|
|
|
ref: ref,
|
|
}
|
|
d.Compat = impl.AddCompat(d)
|
|
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 *dirImageDestination) Reference() types.ImageReference {
|
|
return d.ref
|
|
}
|
|
|
|
// Close removes resources associated with an initialized ImageDestination, if any.
|
|
func (d *dirImageDestination) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// PutBlobWithOptions writes contents of stream and returns data representing the result.
|
|
// inputInfo.Digest can be optionally provided if known; if provided, and stream is read to the end without error, the digest MUST match the stream contents.
|
|
// inputInfo.Size is the expected length of stream, if known.
|
|
// inputInfo.MediaType describes the blob format, 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, PutBlobWithOptions MUST 1) fail, and 2) delete any data stored so far.
|
|
func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io.Reader, inputInfo types.BlobInfo, options private.PutBlobOptions) (private.UploadedBlob, error) {
|
|
blobFile, err := os.CreateTemp(d.ref.path, "dir-put-blob")
|
|
if err != nil {
|
|
return private.UploadedBlob{}, err
|
|
}
|
|
succeeded := false
|
|
explicitClosed := false
|
|
defer func() {
|
|
if !explicitClosed {
|
|
blobFile.Close()
|
|
}
|
|
if !succeeded {
|
|
os.Remove(blobFile.Name())
|
|
}
|
|
}()
|
|
|
|
digester, stream := putblobdigest.DigestIfCanonicalUnknown(stream, inputInfo)
|
|
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
|
|
size, err := io.Copy(blobFile, stream)
|
|
if err != nil {
|
|
return private.UploadedBlob{}, err
|
|
}
|
|
blobDigest := digester.Digest()
|
|
if inputInfo.Size != -1 && size != inputInfo.Size {
|
|
return private.UploadedBlob{}, fmt.Errorf("Size mismatch when copying %s, expected %d, got %d", blobDigest, inputInfo.Size, size)
|
|
}
|
|
if err := blobFile.Sync(); err != nil {
|
|
return private.UploadedBlob{}, 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 private.UploadedBlob{}, err
|
|
}
|
|
}
|
|
|
|
blobPath, err := d.ref.layerPath(blobDigest)
|
|
if err != nil {
|
|
return private.UploadedBlob{}, 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 private.UploadedBlob{}, err
|
|
}
|
|
succeeded = true
|
|
return private.UploadedBlob{Digest: blobDigest, Size: size}, nil
|
|
}
|
|
|
|
// TryReusingBlobWithOptions checks whether the transport already contains, or can efficiently reuse, a blob, and if so, applies it to the current destination
|
|
// (e.g. if the blob is a filesystem layer, this signifies that the changes it describes need to be applied again when composing a filesystem tree).
|
|
// info.Digest must not be empty.
|
|
// If the blob has been successfully reused, returns (true, info, nil).
|
|
// If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure.
|
|
func (d *dirImageDestination) TryReusingBlobWithOptions(ctx context.Context, info types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) {
|
|
if !impl.OriginalCandidateMatchesTryReusingBlobOptions(options) {
|
|
return false, private.ReusedBlob{}, nil
|
|
}
|
|
if info.Digest == "" {
|
|
return false, private.ReusedBlob{}, fmt.Errorf("Can not check for a blob with unknown digest")
|
|
}
|
|
blobPath, err := d.ref.layerPath(info.Digest)
|
|
if err != nil {
|
|
return false, private.ReusedBlob{}, err
|
|
}
|
|
finfo, err := os.Stat(blobPath)
|
|
if err != nil && os.IsNotExist(err) {
|
|
return false, private.ReusedBlob{}, nil
|
|
}
|
|
if err != nil {
|
|
return false, private.ReusedBlob{}, err
|
|
}
|
|
return true, private.ReusedBlob{Digest: info.Digest, Size: finfo.Size()}, nil
|
|
}
|
|
|
|
// PutManifest writes manifest to the destination.
|
|
// If instanceDigest is not nil, it contains a digest of the specific manifest instance to write the manifest for (when
|
|
// the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list.
|
|
// It is expected but not enforced that the instanceDigest, when specified, matches the digest of `manifest` as generated
|
|
// by `manifest.Digest()`.
|
|
// 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 *dirImageDestination) PutManifest(ctx context.Context, manifest []byte, instanceDigest *digest.Digest) error {
|
|
path, err := d.ref.manifestPath(instanceDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, manifest, 0644)
|
|
}
|
|
|
|
// PutSignaturesWithFormat writes a set of signatures to the destination.
|
|
// If instanceDigest is not nil, it contains a digest of the specific manifest instance to write or overwrite the signatures for
|
|
// (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list.
|
|
// MUST be called after PutManifest (signatures may reference manifest contents).
|
|
func (d *dirImageDestination) PutSignaturesWithFormat(ctx context.Context, signatures []signature.Signature, instanceDigest *digest.Digest) error {
|
|
for i, sig := range signatures {
|
|
blob, err := signature.Blob(sig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path, err := d.ref.signaturePath(i, instanceDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(path, blob, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CommitWithOptions 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 CommitWithOptions() is called
|
|
// - Uploaded data MAY be removed or MAY remain around if Close() is called without CommitWithOptions() (i.e. rollback is allowed but not guaranteed)
|
|
func (d *dirImageDestination) CommitWithOptions(ctx context.Context, options private.CommitOptions) error {
|
|
return nil
|
|
}
|
|
|
|
// returns true if path exists
|
|
func pathExists(path string) (bool, error) {
|
|
err := fileutils.Exists(path)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// returns true if directory is empty
|
|
func isDirEmpty(path string) (bool, error) {
|
|
files, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return len(files) == 0, nil
|
|
}
|
|
|
|
// deletes the contents of a directory
|
|
func removeDirContents(path string) error {
|
|
files, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, file := range files {
|
|
if err := os.RemoveAll(filepath.Join(path, file.Name())); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|