github.com/coreos/rocket@v1.30.1-0.20200224141603-171c416fac02/store/treestore/tree.go (about)

     1  // Copyright 2015 The rkt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package treestore
    16  
    17  import (
    18  	"archive/tar"
    19  	"crypto/sha512"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"hash"
    24  	"io"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"sort"
    29  	"strings"
    30  	"syscall"
    31  
    32  	specaci "github.com/appc/spec/aci"
    33  	"github.com/appc/spec/pkg/acirenderer"
    34  	"github.com/appc/spec/pkg/tarheader"
    35  	"github.com/appc/spec/schema/types"
    36  	"github.com/hashicorp/errwrap"
    37  	"github.com/rkt/rkt/pkg/aci"
    38  	"github.com/rkt/rkt/pkg/fileutil"
    39  	"github.com/rkt/rkt/pkg/lock"
    40  	"github.com/rkt/rkt/pkg/sys"
    41  	"github.com/rkt/rkt/pkg/user"
    42  	"github.com/rkt/rkt/store/imagestore"
    43  )
    44  
    45  const (
    46  	hashfilename     = "hash"
    47  	renderedfilename = "rendered"
    48  	imagefilename    = "image"
    49  
    50  	// To ameliorate excessively long paths, keys for the (blob)store use
    51  	// only the first half of a sha512 rather than the entire sum
    52  	hashPrefix = "sha512-"
    53  	lenHash    = sha512.Size       // raw byte size
    54  	lenHashKey = (lenHash / 2) * 2 // half length, in hex characters
    55  	lenKey     = len(hashPrefix) + lenHashKey
    56  	minlenKey  = len(hashPrefix) + 2 // at least sha512-aa
    57  )
    58  
    59  var (
    60  	// ErrReadHashfile when the hashfile cannot be read
    61  	ErrReadHashfile = errors.New("cannot read hash file")
    62  )
    63  
    64  // Store represents a store of rendered ACIs
    65  type Store struct {
    66  	dir string
    67  	// TODO(sgotti) make this an interface (acirenderer.ACIRegistry) when the ACIStore functions to update treestore size will be removed
    68  	store   *imagestore.Store
    69  	lockDir string
    70  }
    71  
    72  // NewStore creates a Store for managing filesystem trees, including the dependency graph resolution of the underlying image layers.
    73  func NewStore(dir string, store *imagestore.Store) (*Store, error) {
    74  	// TODO(sgotti) backward compatibility with the current tree store paths. Needs a migration path to better paths.
    75  	ts := &Store{dir: filepath.Join(dir, "tree"), store: store}
    76  
    77  	ts.lockDir = filepath.Join(dir, "treestorelocks")
    78  	if err := os.MkdirAll(ts.lockDir, 0755); err != nil {
    79  		return nil, err
    80  	}
    81  	return ts, nil
    82  }
    83  
    84  // GetID calculates the treestore ID for the given image key.
    85  // The treeStoreID is computed as an hash of the flattened dependency tree
    86  // image keys. In this way the ID may change for the same key if the image's
    87  // dependencies change.
    88  func (ts *Store) GetID(key string) (string, error) {
    89  	hash, err := types.NewHash(key)
    90  	if err != nil {
    91  		return "", err
    92  	}
    93  	images, err := acirenderer.CreateDepListFromImageID(*hash, ts.store)
    94  	if err != nil {
    95  		return "", err
    96  	}
    97  
    98  	var keys []string
    99  	for _, image := range images {
   100  		keys = append(keys, image.Key)
   101  	}
   102  	imagesString := strings.Join(keys, ",")
   103  	h := sha512.New()
   104  	h.Write([]byte(imagesString))
   105  	return "deps-" + hashToKey(h), nil
   106  }
   107  
   108  // Render renders a treestore for the given image key if it's not
   109  // already fully rendered.
   110  // Users of treestore should call s.Render before using it to ensure
   111  // that the treestore is completely rendered.
   112  // Returns the id and hash of the rendered treestore if it is newly rendered,
   113  // and only the id if it is already rendered.
   114  func (ts *Store) Render(key string, rebuild bool) (id string, hash string, err error) {
   115  	id, err = ts.GetID(key)
   116  	if err != nil {
   117  		return "", "", errwrap.Wrap(errors.New("cannot calculate treestore id"), err)
   118  	}
   119  
   120  	// this lock references the treestore dir for the specified id.
   121  	treeStoreKeyLock, err := lock.ExclusiveKeyLock(ts.lockDir, id)
   122  	if err != nil {
   123  		return "", "", errwrap.Wrap(errors.New("error locking tree store"), err)
   124  	}
   125  	defer treeStoreKeyLock.Close()
   126  
   127  	if !rebuild {
   128  		rendered, err := ts.IsRendered(id)
   129  		if err != nil {
   130  			return "", "", errwrap.Wrap(errors.New("cannot determine if tree is already rendered"), err)
   131  		}
   132  		if rendered {
   133  			return id, "", nil
   134  		}
   135  	}
   136  	// Firstly remove a possible partial treestore if existing.
   137  	// This is needed as a previous ACI removal operation could have failed
   138  	// cleaning the tree store leaving some stale files.
   139  	if err := ts.remove(id); err != nil {
   140  		return "", "", err
   141  	}
   142  	if hash, err = ts.render(id, key); err != nil {
   143  		return "", "", err
   144  	}
   145  
   146  	return id, hash, nil
   147  }
   148  
   149  // Check verifies the treestore consistency for the specified id.
   150  func (ts *Store) Check(id string) (string, error) {
   151  	treeStoreKeyLock, err := lock.SharedKeyLock(ts.lockDir, id)
   152  	if err != nil {
   153  		return "", errwrap.Wrap(errors.New("error locking tree store"), err)
   154  	}
   155  	defer treeStoreKeyLock.Close()
   156  
   157  	return ts.check(id)
   158  }
   159  
   160  // Remove removes the rendered image in tree store with the given id.
   161  func (ts *Store) Remove(id string) error {
   162  	treeStoreKeyLock, err := lock.ExclusiveKeyLock(ts.lockDir, id)
   163  	if err != nil {
   164  		return errwrap.Wrap(errors.New("error locking tree store"), err)
   165  	}
   166  	defer treeStoreKeyLock.Close()
   167  
   168  	if err := ts.remove(id); err != nil {
   169  		return errwrap.Wrap(errors.New("error removing the tree store"), err)
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  // GetIDs returns a slice containing all the treeStore's IDs available
   176  // (both fully or partially rendered).
   177  func (ts *Store) GetIDs() ([]string, error) {
   178  	var treeStoreIDs []string
   179  	ls, err := ioutil.ReadDir(ts.dir)
   180  	if err != nil {
   181  		if !os.IsNotExist(err) {
   182  			return nil, errwrap.Wrap(errors.New("cannot read treestore directory"), err)
   183  		}
   184  	}
   185  
   186  	for _, p := range ls {
   187  		if p.IsDir() {
   188  			id := filepath.Base(p.Name())
   189  			treeStoreIDs = append(treeStoreIDs, id)
   190  		}
   191  	}
   192  	return treeStoreIDs, nil
   193  }
   194  
   195  // render renders the ACI with the provided key in the treestore. id references
   196  // that specific tree store rendered image.
   197  // render, to avoid having a rendered ACI with old stale files, requires that
   198  // the destination directory doesn't exist (usually remove should be called
   199  // before render)
   200  func (ts *Store) render(id string, key string) (string, error) {
   201  	treepath := ts.GetPath(id)
   202  	fi, _ := os.Stat(treepath)
   203  	if fi != nil {
   204  		return "", fmt.Errorf("path %s already exists", treepath)
   205  	}
   206  	imageID, err := types.NewHash(key)
   207  	if err != nil {
   208  		return "", errwrap.Wrap(errors.New("cannot convert key to imageID"), err)
   209  	}
   210  	if err := os.MkdirAll(treepath, 0755); err != nil {
   211  		return "", errwrap.Wrap(fmt.Errorf("cannot create treestore directory %s", treepath), err)
   212  	}
   213  	err = aci.RenderACIWithImageID(*imageID, treepath, ts.store, user.NewBlankUidRange())
   214  	if err != nil {
   215  		return "", errwrap.Wrap(errors.New("cannot render aci"), err)
   216  	}
   217  	hash, err := ts.Hash(id)
   218  	if err != nil {
   219  		return "", errwrap.Wrap(errors.New("cannot calculate tree hash"), err)
   220  	}
   221  	err = ioutil.WriteFile(filepath.Join(treepath, hashfilename), []byte(hash), 0644)
   222  	if err != nil {
   223  		return "", errwrap.Wrap(errors.New("cannot write hash file"), err)
   224  	}
   225  	// before creating the "rendered" flag file we need to ensure that all data is fsynced
   226  	dfd, err := syscall.Open(treepath, syscall.O_RDONLY, 0)
   227  	if err != nil {
   228  		return "", err
   229  	}
   230  	defer syscall.Close(dfd)
   231  	if err := sys.Syncfs(dfd); err != nil {
   232  		return "", errwrap.Wrap(errors.New("failed to sync data"), err)
   233  	}
   234  	// Create rendered file
   235  	f, err := os.Create(filepath.Join(treepath, renderedfilename))
   236  	if err != nil {
   237  		return "", errwrap.Wrap(errors.New("failed to write rendered file"), err)
   238  	}
   239  	f.Close()
   240  
   241  	// Write the hash of the image that will use this tree store
   242  	err = ioutil.WriteFile(filepath.Join(treepath, imagefilename), []byte(key), 0644)
   243  	if err != nil {
   244  		return "", errwrap.Wrap(errors.New("cannot write image file"), err)
   245  	}
   246  
   247  	if err := syscall.Fsync(dfd); err != nil {
   248  		return "", errwrap.Wrap(errors.New("failed to sync tree store directory"), err)
   249  	}
   250  
   251  	// TODO(sgotti) this is wrong for various reasons:
   252  	// * Doesn't consider that can there can be multiple treestore per ACI
   253  	// (and fixing this adding/subtracting sizes is bad since cannot be
   254  	// atomic and could bring to duplicated/missing subtractions causing
   255  	// wrong sizes)
   256  	// * ImageStore and TreeStore are decoupled (TreeStore should just use acirenderer.ACIRegistry interface)
   257  	treeSize, err := ts.Size(id)
   258  	if err != nil {
   259  		return "", err
   260  	}
   261  
   262  	if err := ts.store.UpdateTreeStoreSize(key, treeSize); err != nil {
   263  		return "", err
   264  	}
   265  
   266  	return string(hash), nil
   267  }
   268  
   269  // remove cleans the directory for the provided id
   270  func (ts *Store) remove(id string) error {
   271  	treepath := ts.GetPath(id)
   272  	// If tree path doesn't exist we're done
   273  	_, err := os.Stat(treepath)
   274  	if err != nil && os.IsNotExist(err) {
   275  		return nil
   276  	}
   277  	if err != nil {
   278  		return errwrap.Wrap(errors.New("failed to open tree store directory"), err)
   279  	}
   280  
   281  	renderedFilePath := filepath.Join(treepath, renderedfilename)
   282  	// The "rendered" flag file should be the firstly removed file. So if
   283  	// the removal ends with some error leaving some stale files IsRendered()
   284  	// will return false.
   285  	_, err = os.Stat(renderedFilePath)
   286  	if err != nil && !os.IsNotExist(err) {
   287  		return err
   288  	}
   289  	if !os.IsNotExist(err) {
   290  		err := os.Remove(renderedFilePath)
   291  		// Ensure that the treepath directory is fsynced after removing the
   292  		// "rendered" flag file
   293  		f, err := os.Open(treepath)
   294  		if err != nil {
   295  			return errwrap.Wrap(errors.New("failed to open tree store directory"), err)
   296  		}
   297  		defer f.Close()
   298  		err = f.Sync()
   299  		if err != nil {
   300  			return errwrap.Wrap(errors.New("failed to sync tree store directory"), err)
   301  		}
   302  	}
   303  
   304  	// Ignore error retrieving image hash
   305  	key, _ := ts.GetImageHash(id)
   306  
   307  	if err := os.RemoveAll(treepath); err != nil {
   308  		return err
   309  	}
   310  
   311  	if key != "" {
   312  		return ts.store.UpdateTreeStoreSize(key, 0)
   313  	}
   314  
   315  	return nil
   316  }
   317  
   318  // IsRendered checks if the tree store with the provided id is fully rendered
   319  func (ts *Store) IsRendered(id string) (bool, error) {
   320  	// if the "rendered" flag file exists, assume that the store is already
   321  	// fully rendered.
   322  	treepath := ts.GetPath(id)
   323  	_, err := os.Stat(filepath.Join(treepath, renderedfilename))
   324  	if os.IsNotExist(err) {
   325  		return false, nil
   326  	}
   327  	if err != nil {
   328  		return false, err
   329  	}
   330  	return true, nil
   331  }
   332  
   333  // GetPath returns the absolute path of the treestore for the provided id.
   334  // It doesn't ensure that the path exists and is fully rendered. This should
   335  // be done calling IsRendered()
   336  func (ts *Store) GetPath(id string) string {
   337  	return filepath.Join(ts.dir, id)
   338  }
   339  
   340  // GetRootFS returns the absolute path of the rootfs for the provided id.
   341  // It doesn't ensure that the rootfs exists and is fully rendered. This should
   342  // be done calling IsRendered()
   343  func (ts *Store) GetRootFS(id string) string {
   344  	return filepath.Join(ts.GetPath(id), "rootfs")
   345  }
   346  
   347  // Hash calculates an hash of the rendered ACI. It uses the same functions
   348  // used to create a tar but instead of writing the full archive is just
   349  // computes the sha512 sum of the file infos and contents.
   350  func (ts *Store) Hash(id string) (string, error) {
   351  	treepath := ts.GetPath(id)
   352  
   353  	hash := sha512.New()
   354  	iw := newHashWriter(hash)
   355  	err := filepath.Walk(treepath, buildWalker(treepath, iw))
   356  	if err != nil {
   357  		return "", errwrap.Wrap(errors.New("error walking rootfs"), err)
   358  	}
   359  
   360  	hashstring := hashToKey(hash)
   361  
   362  	return hashstring, nil
   363  }
   364  
   365  // check calculates the actual rendered ACI's hash and verifies that it matches
   366  // the saved value. Returns the calculated hash.
   367  func (ts *Store) check(id string) (string, error) {
   368  	treepath := ts.GetPath(id)
   369  	hash, err := ioutil.ReadFile(filepath.Join(treepath, hashfilename))
   370  	if err != nil {
   371  		return "", errwrap.Wrap(ErrReadHashfile, err)
   372  	}
   373  	curhash, err := ts.Hash(id)
   374  	if err != nil {
   375  		return "", errwrap.Wrap(errors.New("cannot calculate tree hash"), err)
   376  	}
   377  	if curhash != string(hash) {
   378  		return "", fmt.Errorf("wrong tree hash: %s, expected: %s", curhash, hash)
   379  	}
   380  	return curhash, nil
   381  }
   382  
   383  // Size returns the size of the rootfs for the provided id. It is a relatively
   384  // expensive operation, it goes through all the files and adds up their size.
   385  func (ts *Store) Size(id string) (int64, error) {
   386  	sz, err := fileutil.DirSize(ts.GetPath(id))
   387  	if err != nil {
   388  		return -1, errwrap.Wrap(errors.New("error calculating size"), err)
   389  	}
   390  	return sz, nil
   391  }
   392  
   393  // GetImageHash returns the hash of the image that uses the tree store
   394  // identified by id.
   395  func (ts *Store) GetImageHash(id string) (string, error) {
   396  	treepath := ts.GetPath(id)
   397  
   398  	imgHash, err := ioutil.ReadFile(filepath.Join(treepath, imagefilename))
   399  	if err != nil {
   400  		return "", errwrap.Wrap(errors.New("cannot read image file"), err)
   401  	}
   402  
   403  	return string(imgHash), nil
   404  }
   405  
   406  type xattr struct {
   407  	Name  string
   408  	Value string
   409  }
   410  
   411  // Like tar Header but, to keep json output reproducible:
   412  // * Xattrs as a slice
   413  // * Skip Uname and Gname
   414  // TODO. Should ModTime/AccessTime/ChangeTime be saved? For validation its
   415  // probably enough to hash the file contents and the other infos and avoid
   416  // problems due to them changing.
   417  // TODO(sgotti) Is it possible that json output will change between go
   418  // versions? Use another or our own Marshaller?
   419  type fileInfo struct {
   420  	Name     string // name of header file entry
   421  	Mode     int64  // permission and mode bits
   422  	UID      int    // user id of owner
   423  	GID      int    // group id of owner
   424  	Size     int64  // length in bytes
   425  	Typeflag byte   // type of header entry
   426  	Linkname string // target name of link
   427  	Devmajor int64  // major number of character or block device
   428  	Devminor int64  // minor number of character or block device
   429  	Xattrs   []xattr
   430  }
   431  
   432  func fileInfoFromHeader(hdr *tar.Header) *fileInfo {
   433  	fi := &fileInfo{
   434  		Name:     hdr.Name,
   435  		Mode:     hdr.Mode,
   436  		UID:      hdr.Uid,
   437  		GID:      hdr.Gid,
   438  		Size:     hdr.Size,
   439  		Typeflag: hdr.Typeflag,
   440  		Linkname: hdr.Linkname,
   441  		Devmajor: hdr.Devmajor,
   442  		Devminor: hdr.Devminor,
   443  	}
   444  	keys := make([]string, 0, len(hdr.Xattrs))
   445  	for k := range hdr.Xattrs {
   446  		keys = append(keys, k)
   447  	}
   448  	sort.Strings(keys)
   449  
   450  	xattrs := make([]xattr, 0, len(keys))
   451  	for _, k := range keys {
   452  		xattrs = append(xattrs, xattr{Name: k, Value: hdr.Xattrs[k]})
   453  	}
   454  	fi.Xattrs = xattrs
   455  	return fi
   456  }
   457  
   458  // TODO(sgotti) this func is copied from appcs/spec/aci/build.go but also
   459  // removes the hash, rendered and image files. Find a way to reuse it.
   460  func buildWalker(root string, aw specaci.ArchiveWriter) filepath.WalkFunc {
   461  	// cache of inode -> filepath, used to leverage hard links in the archive
   462  	inos := map[uint64]string{}
   463  	return func(path string, info os.FileInfo, err error) error {
   464  		if err != nil {
   465  			return err
   466  		}
   467  		relpath, err := filepath.Rel(root, path)
   468  		if err != nil {
   469  			return err
   470  		}
   471  		if relpath == "." {
   472  			return nil
   473  		}
   474  		if relpath == specaci.ManifestFile ||
   475  			relpath == hashfilename ||
   476  			relpath == renderedfilename ||
   477  			relpath == imagefilename {
   478  			// ignore; this will be written by the archive writer
   479  			// TODO(jonboulle): does this make sense? maybe just remove from archivewriter?
   480  			return nil
   481  		}
   482  
   483  		link := ""
   484  		var r io.Reader
   485  		switch info.Mode() & os.ModeType {
   486  		case os.ModeSocket:
   487  			return nil
   488  		case os.ModeNamedPipe:
   489  		case os.ModeCharDevice:
   490  		case os.ModeDevice:
   491  		case os.ModeDir:
   492  		case os.ModeSymlink:
   493  			target, err := os.Readlink(path)
   494  			if err != nil {
   495  				return err
   496  			}
   497  			link = target
   498  		default:
   499  			file, err := os.Open(path)
   500  			if err != nil {
   501  				return err
   502  			}
   503  			defer file.Close()
   504  			r = file
   505  		}
   506  
   507  		hdr, err := tar.FileInfoHeader(info, link)
   508  		if err != nil {
   509  			panic(err)
   510  		}
   511  		// Because os.FileInfo's Name method returns only the base
   512  		// name of the file it describes, it may be necessary to
   513  		// modify the Name field of the returned header to provide the
   514  		// full path name of the file.
   515  		hdr.Name = relpath
   516  		tarheader.Populate(hdr, info, inos)
   517  		// If the file is a hard link to a file we've already seen, we
   518  		// don't need the contents
   519  		if hdr.Typeflag == tar.TypeLink {
   520  			hdr.Size = 0
   521  			r = nil
   522  		}
   523  
   524  		return aw.AddFile(hdr, r)
   525  	}
   526  }
   527  
   528  type imageHashWriter struct {
   529  	io.Writer
   530  }
   531  
   532  func newHashWriter(w io.Writer) specaci.ArchiveWriter {
   533  	return &imageHashWriter{w}
   534  }
   535  
   536  func (aw *imageHashWriter) AddFile(hdr *tar.Header, r io.Reader) error {
   537  	// Write the json encoding of the FileInfo struct
   538  	hdrj, err := json.Marshal(fileInfoFromHeader(hdr))
   539  	if err != nil {
   540  		return err
   541  	}
   542  	_, err = aw.Writer.Write(hdrj)
   543  	if err != nil {
   544  		return err
   545  	}
   546  
   547  	if r != nil {
   548  		// Write the file data
   549  		_, err := io.Copy(aw.Writer, r)
   550  		if err != nil {
   551  			return err
   552  		}
   553  	}
   554  
   555  	return nil
   556  }
   557  
   558  func (aw *imageHashWriter) Close() error {
   559  	return nil
   560  }
   561  
   562  func hashToKey(h hash.Hash) string {
   563  	s := h.Sum(nil)
   564  	return fmt.Sprintf("%s%x", hashPrefix, s)[0:lenKey]
   565  }