github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/cas/dir/dir.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 SUSE LLC
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package dir
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"io"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	"github.com/apex/log"
    29  	"github.com/opencontainers/go-digest"
    30  	imeta "github.com/opencontainers/image-spec/specs-go"
    31  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    32  	"github.com/opencontainers/umoci/oci/cas"
    33  	"github.com/opencontainers/umoci/pkg/hardening"
    34  	"github.com/opencontainers/umoci/pkg/system"
    35  	"github.com/pkg/errors"
    36  	"golang.org/x/sys/unix"
    37  )
    38  
    39  const (
    40  	// ImageLayoutVersion is the version of the image layout we support. This
    41  	// value is *not* the same as imagespec.Version, and the meaning of this
    42  	// field is still under discussion in the spec. For now we'll just hardcode
    43  	// the value and hope for the best.
    44  	ImageLayoutVersion = "1.0.0"
    45  
    46  	// blobDirectory is the directory inside an OCI image that contains blobs.
    47  	blobDirectory = "blobs"
    48  
    49  	// indexFile is the file inside an OCI image that contains the top-level
    50  	// index.
    51  	indexFile = "index.json"
    52  
    53  	// layoutFile is the file in side an OCI image the indicates what version
    54  	// of the OCI spec the image is.
    55  	layoutFile = "oci-layout"
    56  )
    57  
    58  // blobPath returns the path to a blob given its digest, relative to the root
    59  // of the OCI image. The digest must be of the form algorithm:hex.
    60  func blobPath(digest digest.Digest) (string, error) {
    61  	if err := digest.Validate(); err != nil {
    62  		return "", errors.Wrapf(err, "invalid digest: %q", digest)
    63  	}
    64  
    65  	algo := digest.Algorithm()
    66  	hash := digest.Hex()
    67  
    68  	if algo != cas.BlobAlgorithm {
    69  		return "", errors.Errorf("unsupported algorithm: %q", algo)
    70  	}
    71  
    72  	return filepath.Join(blobDirectory, algo.String(), hash), nil
    73  }
    74  
    75  type dirEngine struct {
    76  	path     string
    77  	temp     string
    78  	tempFile *os.File
    79  }
    80  
    81  func (e *dirEngine) ensureTempDir() error {
    82  	if e.temp == "" {
    83  		tempDir, err := ioutil.TempDir(e.path, ".umoci-")
    84  		if err != nil {
    85  			return errors.Wrap(err, "create tempdir")
    86  		}
    87  
    88  		// We get an advisory lock to ensure that GC() won't delete our
    89  		// temporary directory here. Once we get the lock we know it won't do
    90  		// anything until we unlock it or exit.
    91  
    92  		e.tempFile, err = os.Open(tempDir)
    93  		if err != nil {
    94  			return errors.Wrap(err, "open tempdir for lock")
    95  		}
    96  		if err := unix.Flock(int(e.tempFile.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
    97  			return errors.Wrap(err, "lock tempdir")
    98  		}
    99  
   100  		e.temp = tempDir
   101  	}
   102  	return nil
   103  }
   104  
   105  // verify ensures that the image is valid.
   106  func (e *dirEngine) validate() error {
   107  	content, err := ioutil.ReadFile(filepath.Join(e.path, layoutFile))
   108  	if err != nil {
   109  		if os.IsNotExist(err) {
   110  			err = cas.ErrInvalid
   111  		}
   112  		return errors.Wrap(err, "read oci-layout")
   113  	}
   114  
   115  	var ociLayout ispec.ImageLayout
   116  	if err := json.Unmarshal(content, &ociLayout); err != nil {
   117  		return errors.Wrap(err, "parse oci-layout")
   118  	}
   119  
   120  	// XXX: Currently the meaning of this field is not adequately defined by
   121  	//      the spec, nor is the "official" value determined by the spec.
   122  	if ociLayout.Version != ImageLayoutVersion {
   123  		return errors.Wrap(cas.ErrInvalid, "layout version is not supported")
   124  	}
   125  
   126  	// Check that "blobs" and "index.json" exist in the image.
   127  	// FIXME: We also should check that blobs *only* contains a cas.BlobAlgorithm
   128  	//        directory (with no subdirectories) and that refs *only* contains
   129  	//        files (optionally also making sure they're all JSON descriptors).
   130  	if fi, err := os.Stat(filepath.Join(e.path, blobDirectory)); err != nil {
   131  		if os.IsNotExist(err) {
   132  			err = cas.ErrInvalid
   133  		}
   134  		return errors.Wrap(err, "check blobdir")
   135  	} else if !fi.IsDir() {
   136  		return errors.Wrap(cas.ErrInvalid, "blobdir is not a directory")
   137  	}
   138  
   139  	if fi, err := os.Stat(filepath.Join(e.path, indexFile)); err != nil {
   140  		if os.IsNotExist(err) {
   141  			err = cas.ErrInvalid
   142  		}
   143  		return errors.Wrap(err, "check index")
   144  	} else if fi.IsDir() {
   145  		return errors.Wrap(cas.ErrInvalid, "index is a directory")
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  // PutBlob adds a new blob to the image. This is idempotent; a nil error
   152  // means that "the content is stored at DIGEST" without implying "because
   153  // of this PutBlob() call".
   154  func (e *dirEngine) PutBlob(ctx context.Context, reader io.Reader) (digest.Digest, int64, error) {
   155  	if err := e.ensureTempDir(); err != nil {
   156  		return "", -1, errors.Wrap(err, "ensure tempdir")
   157  	}
   158  
   159  	digester := cas.BlobAlgorithm.Digester()
   160  
   161  	// We copy this into a temporary file because we need to get the blob hash,
   162  	// but also to avoid half-writing an invalid blob.
   163  	fh, err := ioutil.TempFile(e.temp, "blob-")
   164  	if err != nil {
   165  		return "", -1, errors.Wrap(err, "create temporary blob")
   166  	}
   167  	tempPath := fh.Name()
   168  	defer fh.Close()
   169  
   170  	writer := io.MultiWriter(fh, digester.Hash())
   171  	size, err := system.Copy(writer, reader)
   172  	if err != nil {
   173  		return "", -1, errors.Wrap(err, "copy to temporary blob")
   174  	}
   175  	if err := fh.Close(); err != nil {
   176  		return "", -1, errors.Wrap(err, "close temporary blob")
   177  	}
   178  
   179  	// Get the digest.
   180  	path, err := blobPath(digester.Digest())
   181  	if err != nil {
   182  		return "", -1, errors.Wrap(err, "compute blob name")
   183  	}
   184  
   185  	// Move the blob to its correct path.
   186  	path = filepath.Join(e.path, path)
   187  	if err := os.Rename(tempPath, path); err != nil {
   188  		return "", -1, errors.Wrap(err, "rename temporary blob")
   189  	}
   190  
   191  	return digester.Digest(), int64(size), nil
   192  }
   193  
   194  // GetBlob returns a reader for retrieving a blob from the image, which the
   195  // caller must Close(). Returns ErrNotExist if the digest is not found.
   196  //
   197  // This function will return a VerifiedReadCloser, meaning that you must call
   198  // Close() and check the error returned from Close() in order to ensure that
   199  // the hash of the blob is verified.
   200  //
   201  // Please note that calling Close() on the returned blob will read the entire
   202  // from disk and hash it (even if you didn't read any bytes before calling
   203  // Close), so if you wish to only check if a blob exists you should use
   204  // StatBlob() instead.
   205  func (e *dirEngine) GetBlob(ctx context.Context, digest digest.Digest) (io.ReadCloser, error) {
   206  	path, err := blobPath(digest)
   207  	if err != nil {
   208  		return nil, errors.Wrap(err, "compute blob path")
   209  	}
   210  	fh, err := os.Open(filepath.Join(e.path, path))
   211  	return &hardening.VerifiedReadCloser{
   212  		Reader:         fh,
   213  		ExpectedDigest: digest,
   214  		ExpectedSize:   int64(-1), // We don't know the expected size.
   215  	}, errors.Wrap(err, "open blob")
   216  }
   217  
   218  // StatBlob returns whether the specified blob exists in the image. Returns
   219  // false if the blob doesn't exist, true if it does, or an error if any error
   220  // occurred.
   221  //
   222  // NOTE: In future this API may change to return additional information.
   223  func (e *dirEngine) StatBlob(ctx context.Context, digest digest.Digest) (bool, error) {
   224  	path, err := blobPath(digest)
   225  	if err != nil {
   226  		return false, errors.Wrap(err, "compute blob path")
   227  	}
   228  	_, err = os.Stat(path)
   229  	if os.IsNotExist(err) {
   230  		return false, nil
   231  	}
   232  	if err != nil {
   233  		return false, errors.Wrap(err, "stat blob path")
   234  	}
   235  	return true, nil
   236  }
   237  
   238  // PutIndex sets the index of the OCI image to the given index, replacing the
   239  // previously existing index. This operation is atomic; any readers attempting
   240  // to access the OCI image while it is being modified will only ever see the
   241  // new or old index.
   242  func (e *dirEngine) PutIndex(ctx context.Context, index ispec.Index) error {
   243  	if err := e.ensureTempDir(); err != nil {
   244  		return errors.Wrap(err, "ensure tempdir")
   245  	}
   246  
   247  	// Make sure the index has the mediatype field set.
   248  	index.MediaType = ispec.MediaTypeImageIndex
   249  
   250  	// We copy this into a temporary index to ensure the atomicity of this
   251  	// operation.
   252  	fh, err := ioutil.TempFile(e.temp, "index-")
   253  	if err != nil {
   254  		return errors.Wrap(err, "create temporary index")
   255  	}
   256  	tempPath := fh.Name()
   257  	defer fh.Close()
   258  
   259  	// Encode the index.
   260  	if err := json.NewEncoder(fh).Encode(index); err != nil {
   261  		return errors.Wrap(err, "write temporary index")
   262  	}
   263  	if err := fh.Close(); err != nil {
   264  		return errors.Wrap(err, "close temporary index")
   265  	}
   266  
   267  	// Move the blob to its correct path.
   268  	path := filepath.Join(e.path, indexFile)
   269  	if err := os.Rename(tempPath, path); err != nil {
   270  		return errors.Wrap(err, "rename temporary index")
   271  	}
   272  	return nil
   273  }
   274  
   275  // GetIndex returns the index of the OCI image. Return ErrNotExist if the
   276  // digest is not found. If the image doesn't have an index, ErrInvalid is
   277  // returned (a valid OCI image MUST have an image index).
   278  //
   279  // It is not recommended that users of cas.Engine use this interface directly,
   280  // due to the complication of properly handling references as well as correctly
   281  // handling nested indexes. casext.Engine provides a wrapper for cas.Engine
   282  // that implements various reference resolution functions that should work for
   283  // most users.
   284  func (e *dirEngine) GetIndex(ctx context.Context) (ispec.Index, error) {
   285  	content, err := ioutil.ReadFile(filepath.Join(e.path, indexFile))
   286  	if err != nil {
   287  		if os.IsNotExist(err) {
   288  			err = cas.ErrInvalid
   289  		}
   290  		return ispec.Index{}, errors.Wrap(err, "read index")
   291  	}
   292  
   293  	var index ispec.Index
   294  	if err := json.Unmarshal(content, &index); err != nil {
   295  		return ispec.Index{}, errors.Wrap(err, "parse index")
   296  	}
   297  
   298  	return index, nil
   299  }
   300  
   301  // DeleteBlob removes a blob from the image. This is idempotent; a nil
   302  // error means "the content is not in the store" without implying "because
   303  // of this DeleteBlob() call".
   304  func (e *dirEngine) DeleteBlob(ctx context.Context, digest digest.Digest) error {
   305  	path, err := blobPath(digest)
   306  	if err != nil {
   307  		return errors.Wrap(err, "compute blob path")
   308  	}
   309  
   310  	err = os.Remove(filepath.Join(e.path, path))
   311  	if err != nil && !os.IsNotExist(err) {
   312  		return errors.Wrap(err, "remove blob")
   313  	}
   314  	return nil
   315  }
   316  
   317  // ListBlobs returns the set of blob digests stored in the image.
   318  func (e *dirEngine) ListBlobs(ctx context.Context) ([]digest.Digest, error) {
   319  	digests := []digest.Digest{}
   320  	blobDir := filepath.Join(e.path, blobDirectory, cas.BlobAlgorithm.String())
   321  
   322  	if err := filepath.Walk(blobDir, func(path string, _ os.FileInfo, _ error) error {
   323  		// Skip the actual directory.
   324  		if path == blobDir {
   325  			return nil
   326  		}
   327  
   328  		// XXX: Do we need to handle multiple-directory-deep cases?
   329  		digest := digest.NewDigestFromHex(cas.BlobAlgorithm.String(), filepath.Base(path))
   330  		digests = append(digests, digest)
   331  		return nil
   332  	}); err != nil {
   333  		return nil, errors.Wrap(err, "walk blobdir")
   334  	}
   335  
   336  	return digests, nil
   337  }
   338  
   339  // Clean executes a garbage collection of any non-blob garbage in the store
   340  // (this includes temporary files and directories not reachable from the CAS
   341  // interface). This MUST NOT remove any blobs or references in the store.
   342  func (e *dirEngine) Clean(ctx context.Context) error {
   343  	// Remove every .umoci directory that isn't flocked.
   344  	matches, err := filepath.Glob(filepath.Join(e.path, ".umoci-*"))
   345  	if err != nil {
   346  		return errors.Wrap(err, "glob .umoci-*")
   347  	}
   348  	for _, path := range matches {
   349  		err = e.cleanPath(ctx, path)
   350  		if err != nil && err != filepath.SkipDir {
   351  			return err
   352  		}
   353  	}
   354  
   355  	return nil
   356  }
   357  
   358  func (e *dirEngine) cleanPath(ctx context.Context, path string) error {
   359  	cfh, err := os.Open(path)
   360  	if err != nil && !os.IsNotExist(err) {
   361  		return errors.Wrap(err, "open for locking")
   362  	}
   363  	defer cfh.Close()
   364  
   365  	if err := unix.Flock(int(cfh.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
   366  		// If we fail to get a flock(2) then it's probably already locked,
   367  		// so we shouldn't touch it.
   368  		return filepath.SkipDir
   369  	}
   370  	defer unix.Flock(int(cfh.Fd()), unix.LOCK_UN)
   371  
   372  	if err := os.RemoveAll(path); os.IsNotExist(err) {
   373  		return nil // somebody else beat us to it
   374  	} else if err != nil {
   375  		log.Warnf("failed to remove %s: %v", path, err)
   376  		return filepath.SkipDir
   377  	}
   378  	log.Debugf("cleaned %s", path)
   379  
   380  	return nil
   381  }
   382  
   383  // Close releases all references held by the e. Subsequent operations may
   384  // fail.
   385  func (e *dirEngine) Close() error {
   386  	if e.temp != "" {
   387  		if err := unix.Flock(int(e.tempFile.Fd()), unix.LOCK_UN); err != nil {
   388  			return errors.Wrap(err, "unlock tempdir")
   389  		}
   390  		if err := e.tempFile.Close(); err != nil {
   391  			return errors.Wrap(err, "close tempdir")
   392  		}
   393  		if err := os.RemoveAll(e.temp); err != nil {
   394  			return errors.Wrap(err, "remove tempdir")
   395  		}
   396  	}
   397  	return nil
   398  }
   399  
   400  // Open opens a new reference to the directory-backed OCI image referenced by
   401  // the provided path.
   402  func Open(path string) (cas.Engine, error) {
   403  	engine := &dirEngine{
   404  		path: path,
   405  		temp: "",
   406  	}
   407  
   408  	if err := engine.validate(); err != nil {
   409  		return nil, errors.Wrap(err, "validate")
   410  	}
   411  
   412  	return engine, nil
   413  }
   414  
   415  // Create creates a new OCI image layout at the given path. If the path already
   416  // exists, os.ErrExist is returned. However, all of the parent components of
   417  // the path will be created if necessary.
   418  func Create(path string) error {
   419  	// We need to fail if path already exists, but we first create all of the
   420  	// parent paths.
   421  	dir := filepath.Dir(path)
   422  	if dir != "." {
   423  		if err := os.MkdirAll(dir, 0755); err != nil {
   424  			return errors.Wrap(err, "mkdir parent")
   425  		}
   426  	}
   427  	if err := os.Mkdir(path, 0755); err != nil {
   428  		return errors.Wrap(err, "mkdir")
   429  	}
   430  
   431  	// Create the necessary directories and "oci-layout" file.
   432  	if err := os.Mkdir(filepath.Join(path, blobDirectory), 0755); err != nil {
   433  		return errors.Wrap(err, "mkdir blobdir")
   434  	}
   435  	if err := os.Mkdir(filepath.Join(path, blobDirectory, cas.BlobAlgorithm.String()), 0755); err != nil {
   436  		return errors.Wrap(err, "mkdir algorithm")
   437  	}
   438  
   439  	indexFh, err := os.Create(filepath.Join(path, indexFile))
   440  	if err != nil {
   441  		return errors.Wrap(err, "create index.json")
   442  	}
   443  	defer indexFh.Close()
   444  
   445  	defaultIndex := ispec.Index{
   446  		Versioned: imeta.Versioned{
   447  			SchemaVersion: 2, // FIXME: This is hardcoded at the moment.
   448  		},
   449  		MediaType: ispec.MediaTypeImageIndex,
   450  	}
   451  	if err := json.NewEncoder(indexFh).Encode(defaultIndex); err != nil {
   452  		return errors.Wrap(err, "encode index.json")
   453  	}
   454  
   455  	layoutFh, err := os.Create(filepath.Join(path, layoutFile))
   456  	if err != nil {
   457  		return errors.Wrap(err, "create oci-layout")
   458  	}
   459  	defer layoutFh.Close()
   460  
   461  	ociLayout := ispec.ImageLayout{
   462  		Version: ImageLayoutVersion,
   463  	}
   464  	if err := json.NewEncoder(layoutFh).Encode(ociLayout); err != nil {
   465  		return errors.Wrap(err, "encode oci-layout")
   466  	}
   467  
   468  	// Everything is now set up.
   469  	return nil
   470  }