github.com/demonoid81/containerd@v1.3.4/content/local/store.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package local
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"math/rand"
    25  	"os"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/containerd/containerd/content"
    33  	"github.com/containerd/containerd/errdefs"
    34  	"github.com/containerd/containerd/filters"
    35  	"github.com/containerd/containerd/log"
    36  	"github.com/sirupsen/logrus"
    37  
    38  	digest "github.com/opencontainers/go-digest"
    39  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    40  	"github.com/pkg/errors"
    41  )
    42  
    43  var bufPool = sync.Pool{
    44  	New: func() interface{} {
    45  		buffer := make([]byte, 1<<20)
    46  		return &buffer
    47  	},
    48  }
    49  
    50  // LabelStore is used to store mutable labels for digests
    51  type LabelStore interface {
    52  	// Get returns all the labels for the given digest
    53  	Get(digest.Digest) (map[string]string, error)
    54  
    55  	// Set sets all the labels for a given digest
    56  	Set(digest.Digest, map[string]string) error
    57  
    58  	// Update replaces the given labels for a digest,
    59  	// a key with an empty value removes a label.
    60  	Update(digest.Digest, map[string]string) (map[string]string, error)
    61  }
    62  
    63  // Store is digest-keyed store for content. All data written into the store is
    64  // stored under a verifiable digest.
    65  //
    66  // Store can generally support multi-reader, single-writer ingest of data,
    67  // including resumable ingest.
    68  type store struct {
    69  	root string
    70  	ls   LabelStore
    71  }
    72  
    73  // NewStore returns a local content store
    74  func NewStore(root string) (content.Store, error) {
    75  	return NewLabeledStore(root, nil)
    76  }
    77  
    78  // NewLabeledStore returns a new content store using the provided label store
    79  //
    80  // Note: content stores which are used underneath a metadata store may not
    81  // require labels and should use `NewStore`. `NewLabeledStore` is primarily
    82  // useful for tests or standalone implementations.
    83  func NewLabeledStore(root string, ls LabelStore) (content.Store, error) {
    84  	if err := os.MkdirAll(filepath.Join(root, "ingest"), 0777); err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return &store{
    89  		root: root,
    90  		ls:   ls,
    91  	}, nil
    92  }
    93  
    94  func (s *store) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
    95  	p := s.blobPath(dgst)
    96  	fi, err := os.Stat(p)
    97  	if err != nil {
    98  		if os.IsNotExist(err) {
    99  			err = errors.Wrapf(errdefs.ErrNotFound, "content %v", dgst)
   100  		}
   101  
   102  		return content.Info{}, err
   103  	}
   104  	var labels map[string]string
   105  	if s.ls != nil {
   106  		labels, err = s.ls.Get(dgst)
   107  		if err != nil {
   108  			return content.Info{}, err
   109  		}
   110  	}
   111  	return s.info(dgst, fi, labels), nil
   112  }
   113  
   114  func (s *store) info(dgst digest.Digest, fi os.FileInfo, labels map[string]string) content.Info {
   115  	return content.Info{
   116  		Digest:    dgst,
   117  		Size:      fi.Size(),
   118  		CreatedAt: fi.ModTime(),
   119  		UpdatedAt: getATime(fi),
   120  		Labels:    labels,
   121  	}
   122  }
   123  
   124  // ReaderAt returns an io.ReaderAt for the blob.
   125  func (s *store) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
   126  	p := s.blobPath(desc.Digest)
   127  	fi, err := os.Stat(p)
   128  	if err != nil {
   129  		if !os.IsNotExist(err) {
   130  			return nil, err
   131  		}
   132  
   133  		return nil, errors.Wrapf(errdefs.ErrNotFound, "blob %s expected at %s", desc.Digest, p)
   134  	}
   135  
   136  	fp, err := os.Open(p)
   137  	if err != nil {
   138  		if !os.IsNotExist(err) {
   139  			return nil, err
   140  		}
   141  
   142  		return nil, errors.Wrapf(errdefs.ErrNotFound, "blob %s expected at %s", desc.Digest, p)
   143  	}
   144  
   145  	return sizeReaderAt{size: fi.Size(), fp: fp}, nil
   146  }
   147  
   148  // Delete removes a blob by its digest.
   149  //
   150  // While this is safe to do concurrently, safe exist-removal logic must hold
   151  // some global lock on the store.
   152  func (s *store) Delete(ctx context.Context, dgst digest.Digest) error {
   153  	if err := os.RemoveAll(s.blobPath(dgst)); err != nil {
   154  		if !os.IsNotExist(err) {
   155  			return err
   156  		}
   157  
   158  		return errors.Wrapf(errdefs.ErrNotFound, "content %v", dgst)
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  func (s *store) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
   165  	if s.ls == nil {
   166  		return content.Info{}, errors.Wrapf(errdefs.ErrFailedPrecondition, "update not supported on immutable content store")
   167  	}
   168  
   169  	p := s.blobPath(info.Digest)
   170  	fi, err := os.Stat(p)
   171  	if err != nil {
   172  		if os.IsNotExist(err) {
   173  			err = errors.Wrapf(errdefs.ErrNotFound, "content %v", info.Digest)
   174  		}
   175  
   176  		return content.Info{}, err
   177  	}
   178  
   179  	var (
   180  		all    bool
   181  		labels map[string]string
   182  	)
   183  	if len(fieldpaths) > 0 {
   184  		for _, path := range fieldpaths {
   185  			if strings.HasPrefix(path, "labels.") {
   186  				if labels == nil {
   187  					labels = map[string]string{}
   188  				}
   189  
   190  				key := strings.TrimPrefix(path, "labels.")
   191  				labels[key] = info.Labels[key]
   192  				continue
   193  			}
   194  
   195  			switch path {
   196  			case "labels":
   197  				all = true
   198  				labels = info.Labels
   199  			default:
   200  				return content.Info{}, errors.Wrapf(errdefs.ErrInvalidArgument, "cannot update %q field on content info %q", path, info.Digest)
   201  			}
   202  		}
   203  	} else {
   204  		all = true
   205  		labels = info.Labels
   206  	}
   207  
   208  	if all {
   209  		err = s.ls.Set(info.Digest, labels)
   210  	} else {
   211  		labels, err = s.ls.Update(info.Digest, labels)
   212  	}
   213  	if err != nil {
   214  		return content.Info{}, err
   215  	}
   216  
   217  	info = s.info(info.Digest, fi, labels)
   218  	info.UpdatedAt = time.Now()
   219  
   220  	if err := os.Chtimes(p, info.UpdatedAt, info.CreatedAt); err != nil {
   221  		log.G(ctx).WithError(err).Warnf("could not change access time for %s", info.Digest)
   222  	}
   223  
   224  	return info, nil
   225  }
   226  
   227  func (s *store) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
   228  	// TODO: Support filters
   229  	root := filepath.Join(s.root, "blobs")
   230  	var alg digest.Algorithm
   231  	return filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
   232  		if err != nil {
   233  			return err
   234  		}
   235  		if !fi.IsDir() && !alg.Available() {
   236  			return nil
   237  		}
   238  
   239  		// TODO(stevvooe): There are few more cases with subdirs that should be
   240  		// handled in case the layout gets corrupted. This isn't strict enough
   241  		// and may spew bad data.
   242  
   243  		if path == root {
   244  			return nil
   245  		}
   246  		if filepath.Dir(path) == root {
   247  			alg = digest.Algorithm(filepath.Base(path))
   248  
   249  			if !alg.Available() {
   250  				alg = ""
   251  				return filepath.SkipDir
   252  			}
   253  
   254  			// descending into a hash directory
   255  			return nil
   256  		}
   257  
   258  		dgst := digest.NewDigestFromHex(alg.String(), filepath.Base(path))
   259  		if err := dgst.Validate(); err != nil {
   260  			// log error but don't report
   261  			log.L.WithError(err).WithField("path", path).Error("invalid digest for blob path")
   262  			// if we see this, it could mean some sort of corruption of the
   263  			// store or extra paths not expected previously.
   264  		}
   265  
   266  		var labels map[string]string
   267  		if s.ls != nil {
   268  			labels, err = s.ls.Get(dgst)
   269  			if err != nil {
   270  				return err
   271  			}
   272  		}
   273  		return fn(s.info(dgst, fi, labels))
   274  	})
   275  }
   276  
   277  func (s *store) Status(ctx context.Context, ref string) (content.Status, error) {
   278  	return s.status(s.ingestRoot(ref))
   279  }
   280  
   281  func (s *store) ListStatuses(ctx context.Context, fs ...string) ([]content.Status, error) {
   282  	fp, err := os.Open(filepath.Join(s.root, "ingest"))
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	defer fp.Close()
   288  
   289  	fis, err := fp.Readdir(-1)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	filter, err := filters.ParseAll(fs...)
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  
   299  	var active []content.Status
   300  	for _, fi := range fis {
   301  		p := filepath.Join(s.root, "ingest", fi.Name())
   302  		stat, err := s.status(p)
   303  		if err != nil {
   304  			if !os.IsNotExist(err) {
   305  				return nil, err
   306  			}
   307  
   308  			// TODO(stevvooe): This is a common error if uploads are being
   309  			// completed while making this listing. Need to consider taking a
   310  			// lock on the whole store to coordinate this aspect.
   311  			//
   312  			// Another option is to cleanup downloads asynchronously and
   313  			// coordinate this method with the cleanup process.
   314  			//
   315  			// For now, we just skip them, as they really don't exist.
   316  			continue
   317  		}
   318  
   319  		if filter.Match(adaptStatus(stat)) {
   320  			active = append(active, stat)
   321  		}
   322  	}
   323  
   324  	return active, nil
   325  }
   326  
   327  // WalkStatusRefs is used to walk all status references
   328  // Failed status reads will be logged and ignored, if
   329  // this function is called while references are being altered,
   330  // these error messages may be produced.
   331  func (s *store) WalkStatusRefs(ctx context.Context, fn func(string) error) error {
   332  	fp, err := os.Open(filepath.Join(s.root, "ingest"))
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	defer fp.Close()
   338  
   339  	fis, err := fp.Readdir(-1)
   340  	if err != nil {
   341  		return err
   342  	}
   343  
   344  	for _, fi := range fis {
   345  		rf := filepath.Join(s.root, "ingest", fi.Name(), "ref")
   346  
   347  		ref, err := readFileString(rf)
   348  		if err != nil {
   349  			log.G(ctx).WithError(err).WithField("path", rf).Error("failed to read ingest ref")
   350  			continue
   351  		}
   352  
   353  		if err := fn(ref); err != nil {
   354  			return err
   355  		}
   356  	}
   357  
   358  	return nil
   359  }
   360  
   361  // status works like stat above except uses the path to the ingest.
   362  func (s *store) status(ingestPath string) (content.Status, error) {
   363  	dp := filepath.Join(ingestPath, "data")
   364  	fi, err := os.Stat(dp)
   365  	if err != nil {
   366  		if os.IsNotExist(err) {
   367  			err = errors.Wrap(errdefs.ErrNotFound, err.Error())
   368  		}
   369  		return content.Status{}, err
   370  	}
   371  
   372  	ref, err := readFileString(filepath.Join(ingestPath, "ref"))
   373  	if err != nil {
   374  		if os.IsNotExist(err) {
   375  			err = errors.Wrap(errdefs.ErrNotFound, err.Error())
   376  		}
   377  		return content.Status{}, err
   378  	}
   379  
   380  	startedAt, err := readFileTimestamp(filepath.Join(ingestPath, "startedat"))
   381  	if err != nil {
   382  		return content.Status{}, errors.Wrapf(err, "could not read startedat")
   383  	}
   384  
   385  	updatedAt, err := readFileTimestamp(filepath.Join(ingestPath, "updatedat"))
   386  	if err != nil {
   387  		return content.Status{}, errors.Wrapf(err, "could not read updatedat")
   388  	}
   389  
   390  	// because we don't write updatedat on every write, the mod time may
   391  	// actually be more up to date.
   392  	if fi.ModTime().After(updatedAt) {
   393  		updatedAt = fi.ModTime()
   394  	}
   395  
   396  	return content.Status{
   397  		Ref:       ref,
   398  		Offset:    fi.Size(),
   399  		Total:     s.total(ingestPath),
   400  		UpdatedAt: updatedAt,
   401  		StartedAt: startedAt,
   402  	}, nil
   403  }
   404  
   405  func adaptStatus(status content.Status) filters.Adaptor {
   406  	return filters.AdapterFunc(func(fieldpath []string) (string, bool) {
   407  		if len(fieldpath) == 0 {
   408  			return "", false
   409  		}
   410  		switch fieldpath[0] {
   411  		case "ref":
   412  			return status.Ref, true
   413  		}
   414  
   415  		return "", false
   416  	})
   417  }
   418  
   419  // total attempts to resolve the total expected size for the write.
   420  func (s *store) total(ingestPath string) int64 {
   421  	totalS, err := readFileString(filepath.Join(ingestPath, "total"))
   422  	if err != nil {
   423  		return 0
   424  	}
   425  
   426  	total, err := strconv.ParseInt(totalS, 10, 64)
   427  	if err != nil {
   428  		// represents a corrupted file, should probably remove.
   429  		return 0
   430  	}
   431  
   432  	return total
   433  }
   434  
   435  // Writer begins or resumes the active writer identified by ref. If the writer
   436  // is already in use, an error is returned. Only one writer may be in use per
   437  // ref at a time.
   438  //
   439  // The argument `ref` is used to uniquely identify a long-lived writer transaction.
   440  func (s *store) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   441  	var wOpts content.WriterOpts
   442  	for _, opt := range opts {
   443  		if err := opt(&wOpts); err != nil {
   444  			return nil, err
   445  		}
   446  	}
   447  	// TODO(AkihiroSuda): we could create a random string or one calculated based on the context
   448  	// https://github.com/containerd/containerd/issues/2129#issuecomment-380255019
   449  	if wOpts.Ref == "" {
   450  		return nil, errors.Wrap(errdefs.ErrInvalidArgument, "ref must not be empty")
   451  	}
   452  	var lockErr error
   453  	for count := uint64(0); count < 10; count++ {
   454  		time.Sleep(time.Millisecond * time.Duration(rand.Intn(1<<count)))
   455  		if err := tryLock(wOpts.Ref); err != nil {
   456  			if !errdefs.IsUnavailable(err) {
   457  				return nil, err
   458  			}
   459  
   460  			lockErr = err
   461  		} else {
   462  			lockErr = nil
   463  			break
   464  		}
   465  	}
   466  
   467  	if lockErr != nil {
   468  		return nil, lockErr
   469  	}
   470  
   471  	w, err := s.writer(ctx, wOpts.Ref, wOpts.Desc.Size, wOpts.Desc.Digest)
   472  	if err != nil {
   473  		unlock(wOpts.Ref)
   474  		return nil, err
   475  	}
   476  
   477  	return w, nil // lock is now held by w.
   478  }
   479  
   480  func (s *store) resumeStatus(ref string, total int64, digester digest.Digester) (content.Status, error) {
   481  	path, _, data := s.ingestPaths(ref)
   482  	status, err := s.status(path)
   483  	if err != nil {
   484  		return status, errors.Wrap(err, "failed reading status of resume write")
   485  	}
   486  	if ref != status.Ref {
   487  		// NOTE(stevvooe): This is fairly catastrophic. Either we have some
   488  		// layout corruption or a hash collision for the ref key.
   489  		return status, errors.Wrapf(err, "ref key does not match: %v != %v", ref, status.Ref)
   490  	}
   491  
   492  	if total > 0 && status.Total > 0 && total != status.Total {
   493  		return status, errors.Errorf("provided total differs from status: %v != %v", total, status.Total)
   494  	}
   495  
   496  	// TODO(stevvooe): slow slow slow!!, send to goroutine or use resumable hashes
   497  	fp, err := os.Open(data)
   498  	if err != nil {
   499  		return status, err
   500  	}
   501  
   502  	p := bufPool.Get().(*[]byte)
   503  	status.Offset, err = io.CopyBuffer(digester.Hash(), fp, *p)
   504  	bufPool.Put(p)
   505  	fp.Close()
   506  	return status, err
   507  }
   508  
   509  // writer provides the main implementation of the Writer method. The caller
   510  // must hold the lock correctly and release on error if there is a problem.
   511  func (s *store) writer(ctx context.Context, ref string, total int64, expected digest.Digest) (content.Writer, error) {
   512  	// TODO(stevvooe): Need to actually store expected here. We have
   513  	// code in the service that shouldn't be dealing with this.
   514  	if expected != "" {
   515  		p := s.blobPath(expected)
   516  		if _, err := os.Stat(p); err == nil {
   517  			return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v", expected)
   518  		}
   519  	}
   520  
   521  	path, refp, data := s.ingestPaths(ref)
   522  
   523  	var (
   524  		digester  = digest.Canonical.Digester()
   525  		offset    int64
   526  		startedAt time.Time
   527  		updatedAt time.Time
   528  	)
   529  
   530  	foundValidIngest := false
   531  	// ensure that the ingest path has been created.
   532  	if err := os.Mkdir(path, 0755); err != nil {
   533  		if !os.IsExist(err) {
   534  			return nil, err
   535  		}
   536  		status, err := s.resumeStatus(ref, total, digester)
   537  		if err == nil {
   538  			foundValidIngest = true
   539  			updatedAt = status.UpdatedAt
   540  			startedAt = status.StartedAt
   541  			total = status.Total
   542  			offset = status.Offset
   543  		} else {
   544  			logrus.Infof("failed to resume the status from path %s: %s. will recreate them", path, err.Error())
   545  		}
   546  	}
   547  
   548  	if !foundValidIngest {
   549  		startedAt = time.Now()
   550  		updatedAt = startedAt
   551  
   552  		// the ingest is new, we need to setup the target location.
   553  		// write the ref to a file for later use
   554  		if err := ioutil.WriteFile(refp, []byte(ref), 0666); err != nil {
   555  			return nil, err
   556  		}
   557  
   558  		if err := writeTimestampFile(filepath.Join(path, "startedat"), startedAt); err != nil {
   559  			return nil, err
   560  		}
   561  
   562  		if err := writeTimestampFile(filepath.Join(path, "updatedat"), startedAt); err != nil {
   563  			return nil, err
   564  		}
   565  
   566  		if total > 0 {
   567  			if err := ioutil.WriteFile(filepath.Join(path, "total"), []byte(fmt.Sprint(total)), 0666); err != nil {
   568  				return nil, err
   569  			}
   570  		}
   571  	}
   572  
   573  	fp, err := os.OpenFile(data, os.O_WRONLY|os.O_CREATE, 0666)
   574  	if err != nil {
   575  		return nil, errors.Wrap(err, "failed to open data file")
   576  	}
   577  
   578  	if _, err := fp.Seek(offset, io.SeekStart); err != nil {
   579  		return nil, errors.Wrap(err, "could not seek to current write offset")
   580  	}
   581  
   582  	return &writer{
   583  		s:         s,
   584  		fp:        fp,
   585  		ref:       ref,
   586  		path:      path,
   587  		offset:    offset,
   588  		total:     total,
   589  		digester:  digester,
   590  		startedAt: startedAt,
   591  		updatedAt: updatedAt,
   592  	}, nil
   593  }
   594  
   595  // Abort an active transaction keyed by ref. If the ingest is active, it will
   596  // be cancelled. Any resources associated with the ingest will be cleaned.
   597  func (s *store) Abort(ctx context.Context, ref string) error {
   598  	root := s.ingestRoot(ref)
   599  	if err := os.RemoveAll(root); err != nil {
   600  		if os.IsNotExist(err) {
   601  			return errors.Wrapf(errdefs.ErrNotFound, "ingest ref %q", ref)
   602  		}
   603  
   604  		return err
   605  	}
   606  
   607  	return nil
   608  }
   609  
   610  func (s *store) blobPath(dgst digest.Digest) string {
   611  	return filepath.Join(s.root, "blobs", dgst.Algorithm().String(), dgst.Hex())
   612  }
   613  
   614  func (s *store) ingestRoot(ref string) string {
   615  	dgst := digest.FromString(ref)
   616  	return filepath.Join(s.root, "ingest", dgst.Hex())
   617  }
   618  
   619  // ingestPaths are returned. The paths are the following:
   620  //
   621  // - root: entire ingest directory
   622  // - ref: name of the starting ref, must be unique
   623  // - data: file where data is written
   624  //
   625  func (s *store) ingestPaths(ref string) (string, string, string) {
   626  	var (
   627  		fp = s.ingestRoot(ref)
   628  		rp = filepath.Join(fp, "ref")
   629  		dp = filepath.Join(fp, "data")
   630  	)
   631  
   632  	return fp, rp, dp
   633  }
   634  
   635  func readFileString(path string) (string, error) {
   636  	p, err := ioutil.ReadFile(path)
   637  	return string(p), err
   638  }
   639  
   640  // readFileTimestamp reads a file with just a timestamp present.
   641  func readFileTimestamp(p string) (time.Time, error) {
   642  	b, err := ioutil.ReadFile(p)
   643  	if err != nil {
   644  		if os.IsNotExist(err) {
   645  			err = errors.Wrap(errdefs.ErrNotFound, err.Error())
   646  		}
   647  		return time.Time{}, err
   648  	}
   649  
   650  	var t time.Time
   651  	if err := t.UnmarshalText(b); err != nil {
   652  		return time.Time{}, errors.Wrapf(err, "could not parse timestamp file %v", p)
   653  	}
   654  
   655  	return t, nil
   656  }
   657  
   658  func writeTimestampFile(p string, t time.Time) error {
   659  	b, err := t.MarshalText()
   660  	if err != nil {
   661  		return err
   662  	}
   663  	return atomicWrite(p, b, 0666)
   664  }
   665  
   666  func atomicWrite(path string, data []byte, mode os.FileMode) error {
   667  	tmp := fmt.Sprintf("%s.tmp", path)
   668  	f, err := os.OpenFile(tmp, os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_SYNC, mode)
   669  	if err != nil {
   670  		return errors.Wrap(err, "create tmp file")
   671  	}
   672  	_, err = f.Write(data)
   673  	f.Close()
   674  	if err != nil {
   675  		return errors.Wrap(err, "write atomic data")
   676  	}
   677  	return os.Rename(tmp, path)
   678  }