github.com/dctrud/umoci@v0.4.3-0.20191016193643-05a1d37de015/oci/cas/dir/dir.go (about)

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