github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/image/image.go (about)

     1  /*
     2  Copyright 2018 Mirantis
     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 image
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  	"sync"
    27  
    28  	"github.com/aykevl/osfs"
    29  	"github.com/docker/distribution/reference"
    30  	"github.com/golang/glog"
    31  	digest "github.com/opencontainers/go-digest"
    32  
    33  	"github.com/Mirantis/virtlet/pkg/fs"
    34  	"github.com/Mirantis/virtlet/pkg/metadata/types"
    35  )
    36  
    37  // Image describes an image.
    38  type Image struct {
    39  	Digest string
    40  	Name   string
    41  	Path   string
    42  	Size   uint64
    43  }
    44  
    45  func (img *Image) hexDigest() (string, error) {
    46  	var d digest.Digest
    47  	var err error
    48  	if d, err = digest.Parse(img.Digest); err != nil {
    49  		return "", err
    50  	}
    51  	return d.Hex(), nil
    52  }
    53  
    54  // Translator translates image name to a Endpoint.
    55  type Translator func(context.Context, string) Endpoint
    56  
    57  // RefGetter is a function that returns the list of images
    58  // that are currently in use.
    59  type RefGetter func() (map[string]bool, error)
    60  
    61  // Store is an interface for the image store.
    62  type Store interface {
    63  	// ListImage returns the list of images in the store.
    64  	// If filter is specified, the list will only contain the
    65  	// image with the same name as the value of 'filter',
    66  	// or no images at all if there are no such images.
    67  	ListImages(filter string) ([]*Image, error)
    68  
    69  	// ImageStatus returns the description of the specified image.
    70  	// If the image doesn't exist, no error is returned, just
    71  	// nil instead of an image.
    72  	ImageStatus(name string) (*Image, error)
    73  
    74  	// PullImage pulls the image using specified image name translation
    75  	// function.
    76  	PullImage(ctx context.Context, name string, translator Translator) (string, error)
    77  
    78  	// RemoveImage removes the specified image.
    79  	RemoveImage(name string) error
    80  
    81  	// GC removes all unused or partially downloaded images.
    82  	GC() error
    83  
    84  	// GetImagePathDigestAndVirtualSize returns the path to image
    85  	// data, the digest and the virtual size for the specified
    86  	// image. It accepts an image reference or a digest.
    87  	GetImagePathDigestAndVirtualSize(ref string) (string, digest.Digest, uint64, error)
    88  
    89  	// SetRefGetter sets a function that will be used to determine
    90  	// the set of images that are currently in use.
    91  	SetRefGetter(imageRefGetter RefGetter)
    92  
    93  	// FilesystemStats returns disk space and inode usage info for this store.
    94  	FilesystemStats() (*types.FilesystemStats, error)
    95  
    96  	// BytesUsedBy returns disk usage of the file in this store.
    97  	BytesUsedBy(path string) (uint64, error)
    98  }
    99  
   100  // VirtualSizeFunc specifies a function that returns the virtual
   101  // size of the specified QCOW2 image file.
   102  type VirtualSizeFunc func(string) (uint64, error)
   103  
   104  // FileStore implements Store. For more info on its
   105  // workings, see docs/images.md
   106  type FileStore struct {
   107  	sync.Mutex
   108  	dir        string
   109  	downloader Downloader
   110  	vsizeFunc  VirtualSizeFunc
   111  	refGetter  RefGetter
   112  }
   113  
   114  var _ Store = &FileStore{}
   115  
   116  // NewFileStore creates a new FileStore that will be using
   117  // the specified dir to store the images, image downloader and
   118  // a function for getting virtual size of the image. If vsizeFunc
   119  // is nil, the default GetImageVirtualSize function will be used.
   120  func NewFileStore(dir string, downloader Downloader, vsizeFunc VirtualSizeFunc) *FileStore {
   121  	if vsizeFunc == nil {
   122  		vsizeFunc = GetImageVirtualSize
   123  	}
   124  	return &FileStore{
   125  		dir:        dir,
   126  		downloader: downloader,
   127  		vsizeFunc:  vsizeFunc,
   128  	}
   129  }
   130  
   131  func (s *FileStore) linkDir() string {
   132  	return filepath.Join(s.dir, "links")
   133  }
   134  
   135  func (s *FileStore) linkDirExists() (bool, error) {
   136  	switch _, err := os.Stat(s.linkDir()); {
   137  	case err == nil:
   138  		return true, nil
   139  	case os.IsNotExist(err):
   140  		return false, nil
   141  	default:
   142  		return false, fmt.Errorf("error checking for link dir %q: %v", s.linkDir(), err)
   143  	}
   144  }
   145  
   146  func (s *FileStore) dataDir() string {
   147  	return filepath.Join(s.dir, "data")
   148  }
   149  
   150  func (s *FileStore) dataFileName(hexDigest string) string {
   151  	return filepath.Join(s.dataDir(), hexDigest)
   152  }
   153  
   154  func (s *FileStore) linkFileName(imageName string) string {
   155  	imageName, _ = SplitImageName(imageName)
   156  	return filepath.Join(s.linkDir(), strings.Replace(imageName, "/", "%", -1))
   157  }
   158  
   159  func (s *FileStore) renameIfNewOrDelete(oldPath string, newPath string) (bool, error) {
   160  	switch _, err := os.Stat(newPath); {
   161  	case err == nil:
   162  		if err := os.Remove(oldPath); err != nil {
   163  			return false, fmt.Errorf("error removing %q: %v", oldPath, err)
   164  		}
   165  		return false, nil
   166  	case os.IsNotExist(err):
   167  		return true, os.Rename(oldPath, newPath)
   168  	default:
   169  		return false, err
   170  	}
   171  }
   172  
   173  func (s *FileStore) getImageHexDigestsInUse() (map[string]bool, error) {
   174  	imagesInUse := make(map[string]bool)
   175  	var imgList []string
   176  	if s.refGetter != nil {
   177  		refSet, err := s.refGetter()
   178  		if err != nil {
   179  			return nil, fmt.Errorf("error listing images in use: %v", err)
   180  		}
   181  		for spec, present := range refSet {
   182  			if present {
   183  				imgList = append(imgList, spec)
   184  			}
   185  		}
   186  	}
   187  	for _, imgSpec := range imgList {
   188  		if d := GetHexDigest(imgSpec); d != "" {
   189  			imagesInUse[d] = true
   190  		}
   191  	}
   192  	images, err := s.listImagesUnlocked("")
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	for _, img := range images {
   197  		if hexDigest, err := img.hexDigest(); err != nil {
   198  			glog.Warningf("GC: error calculating digest for image %q: %v", img.Name, err)
   199  		} else {
   200  			imagesInUse[hexDigest] = true
   201  		}
   202  	}
   203  	return imagesInUse, nil
   204  }
   205  
   206  func (s *FileStore) removeIfUnreferenced(hexDigest string) error {
   207  	imagesInUse, err := s.getImageHexDigestsInUse()
   208  	switch {
   209  	case err != nil:
   210  		return err
   211  	case imagesInUse[hexDigest]:
   212  		return nil
   213  	default:
   214  		dataFileName := s.dataFileName(hexDigest)
   215  		return os.Remove(dataFileName)
   216  	}
   217  }
   218  
   219  // removeImageUnlocked removes the specified image unless its dataFile name
   220  // is equal to one passed us keepData. Returns true if the file did not
   221  // exist or was removed.
   222  func (s *FileStore) removeImageIfItsNotNeeded(name, keepData string) (bool, error) {
   223  	linkFileName := s.linkFileName(name)
   224  	switch _, err := os.Lstat(linkFileName); {
   225  	case err == nil:
   226  		dest, err := os.Readlink(linkFileName)
   227  		if err != nil {
   228  			return false, fmt.Errorf("error reading link %q: %v", linkFileName, err)
   229  		}
   230  		destName := filepath.Base(dest)
   231  		if destName == keepData {
   232  			return false, nil
   233  		}
   234  		if err := os.Remove(linkFileName); err != nil {
   235  			return false, fmt.Errorf("can't remove %q: %v", linkFileName, err)
   236  		}
   237  		return true, s.removeIfUnreferenced(destName)
   238  	case os.IsNotExist(err):
   239  		return true, nil
   240  	default:
   241  		return false, fmt.Errorf("can't stat %q: %v", linkFileName, err)
   242  	}
   243  }
   244  
   245  func (s *FileStore) placeImage(tempPath string, dataName string, imageName string) error {
   246  	s.Lock()
   247  	defer s.Unlock()
   248  
   249  	dataPath := s.dataFileName(dataName)
   250  	isNew, err := s.renameIfNewOrDelete(tempPath, dataPath)
   251  	if err != nil {
   252  		return fmt.Errorf("error placing the image %q to %q: %v", imageName, dataName, err)
   253  	}
   254  
   255  	if err := os.MkdirAll(s.linkDir(), 0777); err != nil {
   256  		return fmt.Errorf("mkdir %q: %v", s.linkDir(), err)
   257  	}
   258  
   259  	linkFileName := s.linkFileName(imageName)
   260  	switch _, err := os.Stat(linkFileName); {
   261  	case err == nil:
   262  		if removed, err := s.removeImageIfItsNotNeeded(imageName, dataName); err != nil {
   263  			return fmt.Errorf("error removing old symlink %q: %v", linkFileName, err)
   264  		} else if !removed {
   265  			// same image with the same name
   266  			return nil
   267  		}
   268  	case os.IsNotExist(err):
   269  		// let's create the link
   270  	default:
   271  		return fmt.Errorf("error checking for symlink %q: %v", linkFileName, err)
   272  	}
   273  
   274  	if err := os.Symlink(filepath.Join("../data/", dataName), linkFileName); err != nil {
   275  		if isNew {
   276  			if err := os.Remove(dataPath); err != nil {
   277  				glog.Warningf("error removing %q: %v", dataPath, err)
   278  			}
   279  		}
   280  		return fmt.Errorf("error creating symbolic link %q for image %q: %v", linkFileName, imageName, err)
   281  	}
   282  	return nil
   283  }
   284  
   285  func (s *FileStore) imageInfo(fi os.FileInfo) (*Image, error) {
   286  	fullPath := filepath.Join(s.linkDir(), fi.Name())
   287  	if fi.Mode()&os.ModeSymlink == 0 {
   288  		return nil, fmt.Errorf("%q is not a symbolic link", fullPath)
   289  	}
   290  	dest, err := os.Readlink(fullPath)
   291  	if err != nil {
   292  		return nil, fmt.Errorf("error reading link %q: %v", fullPath, err)
   293  	}
   294  	fullDataPath := filepath.Join(s.linkDir(), dest)
   295  	destFi, err := os.Stat(fullDataPath)
   296  	if err != nil {
   297  		return nil, fmt.Errorf("stat %q: %v", fullDataPath, err)
   298  	}
   299  	absPath, err := filepath.Abs(fullDataPath)
   300  	if err != nil {
   301  		return nil, fmt.Errorf("can't get abs path for %q: %v", fullDataPath, err)
   302  	}
   303  	if relPath, err := filepath.Rel(s.dataDir(), absPath); err != nil {
   304  		return nil, fmt.Errorf("checking data path %q: %v", fullDataPath, err)
   305  	} else if strings.HasPrefix(relPath, "..") {
   306  		return nil, fmt.Errorf("not a proper data path %q", fullDataPath)
   307  	}
   308  	d := digest.NewDigestFromHex(string(digest.SHA256), destFi.Name())
   309  	return &Image{
   310  		Digest: d.String(),
   311  		Name:   strings.Replace(fi.Name(), "%", "/", -1),
   312  		Path:   absPath,
   313  		Size:   uint64(destFi.Size()),
   314  	}, nil
   315  }
   316  
   317  func (s *FileStore) listImagesUnlocked(filter string) ([]*Image, error) {
   318  	var digestSpec digest.Digest
   319  	if filter != "" {
   320  		filter, digestSpec = SplitImageName(filter)
   321  	}
   322  
   323  	if linkDirExists, err := s.linkDirExists(); err != nil {
   324  		return nil, err
   325  	} else if !linkDirExists {
   326  		return nil, nil
   327  	}
   328  
   329  	infos, err := ioutil.ReadDir(s.linkDir())
   330  	if err != nil {
   331  		return nil, fmt.Errorf("readdir %q: %v", s.linkDir(), err)
   332  	}
   333  
   334  	var r []*Image
   335  	for _, fi := range infos {
   336  		if fi.Mode().IsDir() {
   337  			continue
   338  		}
   339  		image, err := s.imageInfo(fi)
   340  		switch {
   341  		case err != nil:
   342  			glog.Warningf("listing images: skipping image link %q: %v", fi.Name(), err)
   343  			continue
   344  		case filter != "" && image.Name != filter:
   345  			continue
   346  		case digestSpec != "" && digest.Digest(image.Digest) != digestSpec:
   347  			continue
   348  		}
   349  		r = append(r, image)
   350  	}
   351  
   352  	return r, nil
   353  }
   354  
   355  // ListImages implements ListImages method of ImageStore interface.
   356  func (s *FileStore) ListImages(filter string) ([]*Image, error) {
   357  	s.Lock()
   358  	defer s.Unlock()
   359  	return s.listImagesUnlocked(filter)
   360  }
   361  
   362  func (s *FileStore) imageStatusUnlocked(name string) (*Image, error) {
   363  	linkFileName := s.linkFileName(name)
   364  	// get info about the link itself, not its target
   365  	switch fi, err := os.Lstat(linkFileName); {
   366  	case err == nil:
   367  		info, err := s.imageInfo(fi)
   368  		if err != nil {
   369  			return nil, err
   370  		}
   371  		_, digestSpec := SplitImageName(name)
   372  		if digestSpec != "" && digest.Digest(info.Digest) != digestSpec {
   373  			return nil, fmt.Errorf("image digest mismatch: %s instead of %s", info.Digest, digestSpec)
   374  		}
   375  		return info, nil
   376  	case os.IsNotExist(err):
   377  		return nil, nil
   378  	default:
   379  		return nil, fmt.Errorf("can't stat %q: %v", linkFileName, err)
   380  	}
   381  }
   382  
   383  // ImageStatus implements ImageStatus method of Store interface.
   384  func (s *FileStore) ImageStatus(name string) (*Image, error) {
   385  	s.Lock()
   386  	defer s.Unlock()
   387  	return s.imageStatusUnlocked(name)
   388  }
   389  
   390  // PullImage implements PullImage method of Store interface.
   391  func (s *FileStore) PullImage(ctx context.Context, name string, translator Translator) (string, error) {
   392  	name, specDigest := SplitImageName(name)
   393  	ep := translator(ctx, name)
   394  	glog.V(1).Infof("Image translation: %q -> %q", name, ep.URL)
   395  	if err := os.MkdirAll(s.dataDir(), 0777); err != nil {
   396  		return "", fmt.Errorf("mkdir %q: %v", s.dataDir(), err)
   397  	}
   398  	tempFile, err := ioutil.TempFile(s.dataDir(), "part_")
   399  	if err != nil {
   400  		return "", fmt.Errorf("failed to create a temporary file: %v", err)
   401  	}
   402  	defer func() {
   403  		if tempFile != nil {
   404  			tempFile.Close()
   405  		}
   406  	}()
   407  	if err := s.downloader.DownloadFile(ctx, ep, tempFile); err != nil {
   408  		tempFile.Close()
   409  		if err := os.Remove(tempFile.Name()); err != nil {
   410  			glog.Warningf("Error removing %q: %v", tempFile.Name(), err)
   411  		}
   412  		return "", fmt.Errorf("error downloading %q: %v", ep.URL, err)
   413  	}
   414  
   415  	if _, err := tempFile.Seek(0, os.SEEK_SET); err != nil {
   416  		return "", fmt.Errorf("can't get the digest for %q: Seek(): %v", tempFile.Name(), err)
   417  	}
   418  
   419  	d, err := digest.FromReader(tempFile)
   420  	if err != nil {
   421  		return "", err
   422  	}
   423  	if err := tempFile.Close(); err != nil {
   424  		return "", fmt.Errorf("closing %q: %v", tempFile.Name(), err)
   425  	}
   426  	fileName := tempFile.Name()
   427  	tempFile = nil
   428  	if specDigest != "" && d != specDigest {
   429  		return "", fmt.Errorf("image digest mismatch: %s instead of %s", d, specDigest)
   430  	}
   431  	if err := s.placeImage(fileName, d.Hex(), name); err != nil {
   432  		return "", err
   433  	}
   434  	named, err := reference.WithName(name)
   435  	if err != nil {
   436  		return "", err
   437  	}
   438  	withDigest, err := reference.WithDigest(named, d)
   439  	if err != nil {
   440  		return "", err
   441  	}
   442  	return withDigest.String(), nil
   443  }
   444  
   445  // RemoveImage implements RemoveImage method of Store interface.
   446  func (s *FileStore) RemoveImage(name string) error {
   447  	s.Lock()
   448  	defer s.Unlock()
   449  	_, err := s.removeImageIfItsNotNeeded(name, "")
   450  	return err
   451  }
   452  
   453  // GC implements GC method of Store interface.
   454  func (s *FileStore) GC() error {
   455  	s.Lock()
   456  	defer s.Unlock()
   457  	imagesInUse, err := s.getImageHexDigestsInUse()
   458  	if err != nil {
   459  		return err
   460  	}
   461  	globExpr := filepath.Join(s.dataDir(), "*")
   462  	matches, err := filepath.Glob(globExpr)
   463  	if err != nil {
   464  		return fmt.Errorf("Glob(): %q: %v", globExpr, err)
   465  	}
   466  	for _, m := range matches {
   467  		if imagesInUse[filepath.Base(m)] {
   468  			continue
   469  		}
   470  		glog.V(1).Infof("GC: removing unreferenced image file %q", m)
   471  		if err := os.Remove(m); err != nil {
   472  			glog.Warningf("GC: removing %q: %v", m, err)
   473  		}
   474  	}
   475  	return nil
   476  }
   477  
   478  // GetImagePathDigestAndVirtualSize implements GetImagePathDigestAndVirtualSize method of Store interface.
   479  func (s *FileStore) GetImagePathDigestAndVirtualSize(ref string) (string, digest.Digest, uint64, error) {
   480  	s.Lock()
   481  	defer s.Unlock()
   482  	glog.V(3).Infof("GetImagePathDigestAndVirtualSize(): %q", ref)
   483  
   484  	var pathViaDigest, pathViaName string
   485  	// parsing digest as ref gives bad results
   486  	d, err := digest.Parse(ref)
   487  	if err == nil {
   488  		if d.Algorithm() != digest.SHA256 {
   489  			return "", "", 0, fmt.Errorf("bad image digest (need sha256): %q", d)
   490  		}
   491  		pathViaDigest = s.dataFileName(d.Hex())
   492  	} else {
   493  		parsed, err := reference.Parse(ref)
   494  		if err != nil {
   495  			return "", "", 0, fmt.Errorf("bad image reference %q: %v", ref, err)
   496  		}
   497  
   498  		d = ""
   499  		if digested, ok := parsed.(reference.Digested); ok {
   500  			if digested.Digest().Algorithm() != digest.SHA256 {
   501  				return "", "", 0, fmt.Errorf("bad image digest (need sha256): %q", digested.Digest())
   502  			}
   503  			d = digested.Digest()
   504  			pathViaDigest = s.dataFileName(d.Hex())
   505  		}
   506  
   507  		if named, ok := parsed.(reference.Named); ok && named.Name() != "" {
   508  			linkFileName := s.linkFileName(named.Name())
   509  			if pathViaName, err = os.Readlink(linkFileName); err != nil {
   510  				glog.Warningf("error reading link %q: %v", pathViaName, err)
   511  			} else {
   512  				pathViaName = filepath.Join(s.linkDir(), pathViaName)
   513  				d = digest.NewDigestFromHex(string(digest.SHA256), filepath.Base(pathViaName))
   514  			}
   515  		}
   516  	}
   517  
   518  	path := pathViaDigest
   519  	switch {
   520  	case pathViaDigest == "" && pathViaName == "":
   521  		return "", "", 0, fmt.Errorf("bad image reference %q", ref)
   522  	case pathViaDigest == "":
   523  		path = pathViaName
   524  	case pathViaName != "":
   525  		fi1, err := os.Stat(pathViaName)
   526  		if err != nil {
   527  			return "", "", 0, err
   528  		}
   529  		fi2, err := os.Stat(pathViaDigest)
   530  		if err != nil {
   531  			return "", "", 0, err
   532  		}
   533  		if !os.SameFile(fi1, fi2) {
   534  			return "", "", 0, fmt.Errorf("digest / name path mismatch: %q vs %q", pathViaDigest, pathViaName)
   535  		}
   536  	}
   537  
   538  	vsize, err := s.vsizeFunc(path)
   539  	if err != nil {
   540  		return "", "", 0, fmt.Errorf("error getting image size for %q: %v", path, err)
   541  	}
   542  	return path, d, vsize, nil
   543  }
   544  
   545  // SetRefGetter implements SetRefGetter method of Store interface.
   546  func (s *FileStore) SetRefGetter(imageRefGetter RefGetter) {
   547  	s.refGetter = imageRefGetter
   548  }
   549  
   550  // SplitImageName parses image nmae and returns the name sans tag and
   551  // the digest, if any.
   552  func SplitImageName(imageName string) (string, digest.Digest) {
   553  	ref, err := reference.Parse(imageName)
   554  	if err != nil {
   555  		glog.Warningf("StripTags: failed to parse image name as ref: %q: %v", imageName, err)
   556  		return imageName, ""
   557  	}
   558  
   559  	named, ok := ref.(reference.Named)
   560  	if !ok {
   561  		return imageName, ""
   562  	}
   563  
   564  	if digested, ok := ref.(reference.Digested); ok {
   565  		return named.Name(), digested.Digest()
   566  	}
   567  
   568  	return named.Name(), ""
   569  }
   570  
   571  // GetHexDigest returns the hex digest contained in imageSpec, if any,
   572  // or an empty string if imageSpec doesn't have the spec.
   573  func GetHexDigest(imageSpec string) string {
   574  	if d, err := digest.Parse(imageSpec); err == nil {
   575  		if d.Algorithm() != digest.SHA256 {
   576  			return ""
   577  		}
   578  		return d.Hex()
   579  	}
   580  
   581  	parsed, err := reference.Parse(imageSpec)
   582  	if err != nil {
   583  		return ""
   584  	}
   585  
   586  	if digested, ok := parsed.(reference.Digested); ok && digested.Digest().Algorithm() == digest.SHA256 {
   587  		return digested.Digest().Hex()
   588  	}
   589  
   590  	return ""
   591  }
   592  
   593  // FilesystemStats returns disk space and inode usage info for this store.
   594  // TODO: instead of returning data from filesystem we should retrieve from
   595  // metadata store sizes of images and sum them, or even retrieve precalculated
   596  // sum. That's because same filesystem could be used by other things than images.
   597  func (s *FileStore) FilesystemStats() (*types.FilesystemStats, error) {
   598  	occupiedBytes, occupiedInodes, err := fs.GetFsStatsForPath(s.dir)
   599  	if err != nil {
   600  		return nil, err
   601  	}
   602  	info, err := osfs.Read()
   603  	if err != nil {
   604  		return nil, err
   605  	}
   606  	mount, err := info.GetPath(s.dir)
   607  	if err != nil {
   608  		return nil, err
   609  	}
   610  	return &types.FilesystemStats{
   611  		Mountpoint: mount.FSRoot,
   612  		UsedBytes:  occupiedBytes,
   613  		UsedInodes: occupiedInodes,
   614  	}, nil
   615  }
   616  
   617  // BytesUsedBy return disk usage of provided file as seen in store
   618  func (s *FileStore) BytesUsedBy(path string) (uint64, error) {
   619  	fstat, err := os.Stat(path)
   620  	if err != nil {
   621  		return 0, err
   622  	}
   623  	return uint64(fstat.Size()), nil
   624  }