github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/image/image_test.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  	"crypto/sha256"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"log"
    26  	"os"
    27  	"path/filepath"
    28  	"reflect"
    29  	"sort"
    30  	"strings"
    31  	"testing"
    32  
    33  	"github.com/davecgh/go-spew/spew"
    34  )
    35  
    36  func sha256str(s string) string {
    37  	h := sha256.New()
    38  	if _, err := io.WriteString(h, s); err != nil {
    39  		log.Panicf("sha256 error: %v", err)
    40  	}
    41  	return fmt.Sprintf("%x", h.Sum(nil))
    42  }
    43  
    44  type fakeDownloader struct {
    45  	t         *testing.T
    46  	cancelled bool
    47  	started   chan struct{}
    48  }
    49  
    50  var _ Downloader = &fakeDownloader{}
    51  
    52  // newFakeDownloader returns a fake downloader that writes the
    53  // endpoint's url passed to it into the file instead of actually
    54  // downloading it.
    55  func newFakeDownloader(t *testing.T) *fakeDownloader {
    56  	return &fakeDownloader{t: t, started: make(chan struct{}, 100)}
    57  }
    58  
    59  func (d *fakeDownloader) DownloadFile(ctx context.Context, endpoint Endpoint, w io.Writer) error {
    60  	d.started <- struct{}{}
    61  	if strings.Contains(endpoint.URL, "cancelme") {
    62  		<-ctx.Done()
    63  		d.cancelled = true
    64  		return ctx.Err()
    65  	}
    66  	if f, ok := w.(*os.File); ok {
    67  		d.t.Logf("fakeDownloader: writing %q to %q", endpoint.URL, f.Name())
    68  	}
    69  	// add "###" prefix to endpoint URL to make the contents
    70  	// more easily distinguishable from the URLs themselves
    71  	// in the test code
    72  	if n, err := w.Write([]byte("###" + endpoint.URL)); err != nil {
    73  		return fmt.Errorf("WriteString(): %v", err)
    74  	} else if n < len(endpoint.URL) {
    75  		return fmt.Errorf("WriteString(): short write")
    76  	}
    77  	return nil
    78  }
    79  
    80  func fakeVirtualSize(imagePath string) (uint64, error) {
    81  	var fi os.FileInfo
    82  	var err error
    83  
    84  	if fi, err = os.Stat(imagePath); err != nil {
    85  		return 0, err
    86  	}
    87  
    88  	return uint64(fi.Size()) + 1000, nil
    89  }
    90  
    91  type ifsTester struct {
    92  	t                *testing.T
    93  	tmpDir           string
    94  	downloader       *fakeDownloader
    95  	store            *FileStore
    96  	images           []*Image
    97  	refs             []string
    98  	referencedImages []string
    99  	translatorPrefix string
   100  }
   101  
   102  func newIfsTester(t *testing.T) *ifsTester {
   103  	tmpDir, err := ioutil.TempDir("", "images")
   104  	if err != nil {
   105  		t.Fatalf("TempDir(): %v", err)
   106  	}
   107  
   108  	downloader := newFakeDownloader(t)
   109  	tst := &ifsTester{
   110  		t:          t,
   111  		tmpDir:     tmpDir,
   112  		downloader: downloader,
   113  		store:      NewFileStore(tmpDir, downloader, fakeVirtualSize),
   114  	}
   115  	tst.images, tst.refs = tst.sampleImages()
   116  	tst.store.SetRefGetter(func() (map[string]bool, error) {
   117  		r := make(map[string]bool)
   118  		for _, imgSpec := range tst.referencedImages {
   119  			r[imgSpec] = true
   120  		}
   121  		return r, nil
   122  	})
   123  	return tst
   124  }
   125  
   126  func (tst *ifsTester) teardown() {
   127  	os.RemoveAll(tst.tmpDir)
   128  }
   129  
   130  func (tst *ifsTester) translateImageName(ctx context.Context, name string) Endpoint {
   131  	if name == "foobar" {
   132  		name = "baz"
   133  	}
   134  	return Endpoint{URL: tst.translatorPrefix + name, MaxRedirects: -1}
   135  }
   136  
   137  func (tst *ifsTester) subpath(p string) string {
   138  	return filepath.Join(tst.tmpDir, p)
   139  }
   140  
   141  // sampleImages returns a list of sample images.
   142  // 1th (from zero) and 2nd images share the same data file.
   143  func (tst *ifsTester) sampleImages() ([]*Image, []string) {
   144  	var images []*Image
   145  	var refs []string
   146  	for _, imageName := range []string{"example.com:1234/foo/bar", "baz"} {
   147  		sha256 := sha256str("###" + imageName)
   148  		image := &Image{
   149  			// fakeDownloader writes URL to the image file,
   150  			// and the image digest contains sha256 of the file
   151  			Digest: "sha256:" + sha256,
   152  			Name:   imageName,
   153  			Path:   tst.subpath("data/" + sha256),
   154  			Size:   uint64(len(imageName) + 3),
   155  		}
   156  		images = append(images, image)
   157  		refs = append(refs, image.Name+"@"+image.Digest)
   158  	}
   159  	sameDataImage := *images[1]
   160  	sameDataImage.Name = "foobar" // translated to baz by the fake translator
   161  	return append(images, &sameDataImage), append(refs, sameDataImage.Name+"@"+sameDataImage.Digest)
   162  }
   163  
   164  func (tst *ifsTester) verifyFileContents(p string, expectedContents string) {
   165  	if bs, err := ioutil.ReadFile(p); err != nil {
   166  		tst.t.Errorf("can't verify the contents of %q: %v", p, err)
   167  	} else if string(bs) != expectedContents {
   168  		tst.t.Errorf("bad contents of %q: %q instead of %q", p, bs, expectedContents)
   169  	}
   170  }
   171  
   172  func (tst *ifsTester) verifySubpathContents(p string, expectedContents string) {
   173  	tst.verifyFileContents(tst.subpath(p), expectedContents)
   174  }
   175  
   176  func (tst *ifsTester) verifyListImages(filter string, expectedImages ...*Image) {
   177  	switch images, err := tst.store.ListImages(filter); {
   178  	case err != nil:
   179  		tst.t.Errorf("ListImages(): %v", err)
   180  	case len(expectedImages) == 0 && len(images) == 0:
   181  		return
   182  	case reflect.DeepEqual(images, expectedImages):
   183  		return
   184  	default:
   185  		tst.t.Errorf("ListImages(): bad result:\n%s\n-- instead of --\n%s", spew.Sdump(images), spew.Sdump(expectedImages))
   186  	}
   187  }
   188  
   189  func (tst *ifsTester) verifyImage(ref string, expectedContents string) {
   190  	if path, digest, vsize, err := tst.store.GetImagePathDigestAndVirtualSize(ref); err != nil {
   191  		tst.t.Errorf("GetImagePathAndVirtualSize(): %v", err)
   192  	} else {
   193  		expectedDigest := "sha256:" + sha256str(expectedContents)
   194  		if string(digest) != expectedDigest {
   195  			tst.t.Errorf("bad digest: %s instead of %s", digest, expectedDigest)
   196  		}
   197  		tst.verifyFileContents(path, expectedContents)
   198  		expectedVirtualSize := uint64(len(expectedContents)) + 1000
   199  		if vsize != expectedVirtualSize {
   200  			tst.t.Errorf("bad virtual size: %d instead of %d", vsize, expectedVirtualSize)
   201  		}
   202  	}
   203  }
   204  
   205  func (tst *ifsTester) verifyImageStatus(name string, expectedImage *Image) {
   206  	switch image, err := tst.store.ImageStatus(name); {
   207  	case err != nil:
   208  		tst.t.Errorf("ImageStatus(): %v", err)
   209  	case reflect.DeepEqual(image, expectedImage):
   210  		return
   211  	default:
   212  		tst.t.Errorf("ImageStatus(): bad result:\n%s\n-- instead of --\n%s", spew.Sdump(image), spew.Sdump(expectedImage))
   213  	}
   214  }
   215  
   216  func (tst *ifsTester) verifyDataDirIsEmpty() {
   217  	items, err := filepath.Glob(filepath.Join(tst.tmpDir, "data/*"))
   218  	if err != nil {
   219  		tst.t.Fatalf("Glob(): %v", err)
   220  	}
   221  	if len(items) != 0 {
   222  		tst.t.Errorf("unexpected files found: %v", items)
   223  	}
   224  }
   225  
   226  func (tst *ifsTester) pullImage(name, ref string) {
   227  	if s, err := tst.store.PullImage(context.Background(), name, tst.translateImageName); err != nil {
   228  		tst.t.Errorf("PullImage(): %v", err)
   229  	} else if s != ref {
   230  		tst.t.Errorf("bad image ref returned: %q instead of %q", s, ref)
   231  	}
   232  }
   233  
   234  func (tst *ifsTester) pullAllImages() {
   235  	for n, image := range tst.images {
   236  		tst.pullImage(image.Name, tst.refs[n])
   237  	}
   238  	tst.verifyListImages("", tst.images[1], tst.images[0], tst.images[2]) // alphabetically sorted by name
   239  }
   240  
   241  func (tst *ifsTester) removeFile(relPath string) {
   242  	p := filepath.Join(tst.tmpDir, relPath)
   243  	if err := os.Remove(p); err != nil {
   244  		tst.t.Errorf("failed to remove %q: %v", p, err)
   245  	}
   246  }
   247  
   248  func (tst *ifsTester) verifyDataFiles(expectedNames ...string) {
   249  	dataPath := filepath.Join(tst.tmpDir, "data")
   250  	infos, err := ioutil.ReadDir(dataPath)
   251  	if err != nil {
   252  		tst.t.Errorf("readdir %q: %v", dataPath, err)
   253  		return
   254  	}
   255  	var names []string
   256  	for _, fi := range infos {
   257  		names = append(names, fi.Name())
   258  	}
   259  	nameStr := strings.Join(names, "\n")
   260  	sort.Strings(names)
   261  	sort.Strings(expectedNames)
   262  	expectedNameStr := strings.Join(expectedNames, "\n")
   263  	if nameStr != expectedNameStr {
   264  		tst.t.Errorf("bad file list:\n%s\n-- instead of --\n%s", nameStr, expectedNameStr)
   265  	}
   266  }
   267  
   268  func TestPullListStatus(t *testing.T) {
   269  	tst := newIfsTester(t)
   270  	defer tst.teardown()
   271  	tst.verifyListImages("")
   272  	tst.verifyListImages("foobar")
   273  
   274  	// make sure that pulling the same image multiple times is ok
   275  	for i := 0; i < 3; i++ {
   276  		tst.pullImage(tst.images[0].Name, tst.refs[0])
   277  		tst.verifyListImages("foobar")
   278  		tst.verifyImageStatus("foobar", nil)
   279  		tst.verifyListImages("", tst.images[0])
   280  		tst.verifyListImages(tst.images[0].Name, tst.images[0])
   281  		tst.verifySubpathContents("links/example.com:1234%foo%bar", "###example.com:1234/foo/bar")
   282  		tst.verifyImage(tst.refs[0], "###example.com:1234/foo/bar")
   283  		tst.verifyImage(tst.images[0].Name, "###example.com:1234/foo/bar")
   284  		tst.verifyImage(tst.images[0].Digest, "###example.com:1234/foo/bar")
   285  		tst.verifyImageStatus(tst.images[0].Name, tst.images[0])
   286  	}
   287  
   288  	tst.pullImage(tst.images[1].Name+":latest", tst.refs[1])
   289  	tst.verifyListImages("", tst.images[1], tst.images[0]) // alphabetically sorted by name
   290  	tst.verifyListImages(tst.images[0].Name, tst.images[0])
   291  	tst.verifyListImages(tst.images[1].Name, tst.images[1])
   292  	tst.verifySubpathContents("links/example.com:1234%foo%bar", "###example.com:1234/foo/bar")
   293  	tst.verifySubpathContents("links/baz", "###baz")
   294  	tst.verifyImage(tst.refs[0], "###example.com:1234/foo/bar")
   295  	tst.verifyImage(tst.refs[1], "###baz")
   296  	tst.verifyImageStatus(tst.images[0].Name, tst.images[0])
   297  	tst.verifyImageStatus(tst.images[1].Name, tst.images[1])
   298  
   299  	tst.pullImage(tst.images[2].Name, tst.refs[2])
   300  	tst.verifyListImages("", tst.images[1], tst.images[0], tst.images[2]) // alphabetically sorted by name
   301  	tst.verifySubpathContents("links/foobar", "###baz")
   302  
   303  	tst.verifyListImages(tst.refs[1], tst.images[1])
   304  }
   305  
   306  func TestReplaceImage(t *testing.T) {
   307  	tst := newIfsTester(t)
   308  	defer tst.teardown()
   309  	tst.pullAllImages()
   310  	tst.translatorPrefix = "xx"
   311  	sha256 := sha256str("###xxbaz")
   312  	updatedImage := &Image{
   313  		Digest: "sha256:" + sha256,
   314  		Name:   tst.images[1].Name,
   315  		Path:   tst.subpath("data/" + sha256),
   316  		Size:   uint64(8),
   317  	}
   318  
   319  	updatedRef := updatedImage.Name + "@" + updatedImage.Digest
   320  	tst.pullImage(updatedImage.Name, updatedRef)
   321  	tst.verifyListImages("", updatedImage, tst.images[0], tst.images[2]) // alphabetically sorted by name
   322  	tst.verifySubpathContents("links/example.com:1234%foo%bar", "###example.com:1234/foo/bar")
   323  	tst.verifySubpathContents("links/baz", "###xxbaz")
   324  	tst.verifySubpathContents("links/foobar", "###baz")
   325  	tst.verifyImage(tst.refs[0], "###example.com:1234/foo/bar")
   326  	tst.verifyImage(updatedRef, "###xxbaz")
   327  	tst.verifyImage(tst.refs[2], "###baz")
   328  	tst.verifyImageStatus(tst.images[0].Name, tst.images[0])
   329  	tst.verifyImageStatus(updatedImage.Name, updatedImage)
   330  	tst.verifyImageStatus(tst.images[2].Name, tst.images[2])
   331  	tst.verifyDataFiles(sha256str("###example.com:1234/foo/bar"), sha256str("###baz"), sha256str("###xxbaz"))
   332  }
   333  
   334  func TestReplaceReferencedImage(t *testing.T) {
   335  	tst := newIfsTester(t)
   336  	defer tst.teardown()
   337  	tst.pullAllImages()
   338  	tst.translatorPrefix = "xx"
   339  	sha256 := sha256str("###xxexample.com:1234/foo/bar")
   340  	updatedImage := &Image{
   341  		Digest: "sha256:" + sha256,
   342  		Name:   tst.images[0].Name,
   343  		Path:   tst.subpath("data/" + sha256),
   344  		Size:   uint64(29),
   345  	}
   346  
   347  	tst.referencedImages = []string{tst.images[0].Digest}
   348  	updatedRef := updatedImage.Name + "@" + updatedImage.Digest
   349  	tst.pullImage(updatedImage.Name, updatedRef)
   350  	tst.verifyListImages("", tst.images[1], updatedImage, tst.images[2]) // alphabetically sorted by name
   351  	tst.verifySubpathContents("links/example.com:1234%foo%bar", "###xxexample.com:1234/foo/bar")
   352  	tst.verifySubpathContents("links/baz", "###baz")
   353  	tst.verifySubpathContents("links/foobar", "###baz")
   354  	tst.verifyImage(updatedRef, "###xxexample.com:1234/foo/bar")
   355  	tst.verifyImage(tst.refs[1], "###baz")
   356  	tst.verifyImage(tst.refs[2], "###baz")
   357  	tst.verifyImageStatus(updatedImage.Name, updatedImage)
   358  	tst.verifyImageStatus(tst.images[1].Name, tst.images[1])
   359  	tst.verifyImageStatus(tst.images[2].Name, tst.images[2])
   360  	// the old image must be kept
   361  	tst.verifyDataFiles(sha256str("###example.com:1234/foo/bar"), sha256str("###xxexample.com:1234/foo/bar"), sha256str("###baz"))
   362  }
   363  
   364  func TestReplaceUnreferencedImage(t *testing.T) {
   365  	tst := newIfsTester(t)
   366  	defer tst.teardown()
   367  	tst.pullAllImages()
   368  	tst.translatorPrefix = "xx"
   369  	sha256 := sha256str("###xxexample.com:1234/foo/bar")
   370  	updatedImage := &Image{
   371  		Digest: "sha256:" + sha256,
   372  		Name:   tst.images[0].Name,
   373  		Path:   tst.subpath("data/" + sha256),
   374  		Size:   uint64(29),
   375  	}
   376  
   377  	updatedRef := updatedImage.Name + "@" + updatedImage.Digest
   378  	tst.pullImage(updatedImage.Name, updatedRef)
   379  	tst.verifyListImages("", tst.images[1], updatedImage, tst.images[2]) // alphabetically sorted by name
   380  	tst.verifySubpathContents("links/example.com:1234%foo%bar", "###xxexample.com:1234/foo/bar")
   381  	tst.verifySubpathContents("links/baz", "###baz")
   382  	tst.verifySubpathContents("links/foobar", "###baz")
   383  	tst.verifyImage(updatedRef, "###xxexample.com:1234/foo/bar")
   384  	tst.verifyImage(tst.refs[1], "###baz")
   385  	tst.verifyImage(tst.refs[2], "###baz")
   386  	tst.verifyImageStatus(updatedImage.Name, updatedImage)
   387  	tst.verifyImageStatus(tst.images[1].Name, tst.images[1])
   388  	tst.verifyImageStatus(tst.images[2].Name, tst.images[2])
   389  	// the old image must be removed
   390  	tst.verifyDataFiles(sha256str("###xxexample.com:1234/foo/bar"), sha256str("###baz"))
   391  }
   392  
   393  func TestRemoveImage(t *testing.T) {
   394  	tst := newIfsTester(t)
   395  	defer tst.teardown()
   396  	tst.pullAllImages()
   397  
   398  	for i := 0; i < 3; i++ {
   399  		if err := tst.store.RemoveImage(tst.images[1].Name); err != nil {
   400  			t.Errorf("RemoveImage(): %v", err)
   401  		}
   402  		tst.verifyListImages("", tst.images[0], tst.images[2]) // alphabetically sorted by name
   403  		tst.verifySubpathContents("links/example.com:1234%foo%bar", "###example.com:1234/foo/bar")
   404  		tst.verifySubpathContents("links/foobar", "###baz")
   405  	}
   406  
   407  	if err := tst.store.RemoveImage(tst.images[2].Name); err != nil {
   408  		t.Errorf("RemoveImage(): %v", err)
   409  	}
   410  	tst.verifyListImages("", tst.images[0]) // alphabetically sorted by name
   411  	tst.verifySubpathContents("links/example.com:1234%foo%bar", "###example.com:1234/foo/bar")
   412  
   413  	tst.referencedImages = []string{tst.images[0].Digest}
   414  	if err := tst.store.RemoveImage(tst.images[0].Name); err != nil {
   415  		t.Errorf("RemoveImage(): %v", err)
   416  	}
   417  	// the image is still referenced
   418  	tst.verifyDataFiles(sha256str("###example.com:1234/foo/bar"))
   419  }
   420  
   421  func TestImageGC(t *testing.T) {
   422  	tst := newIfsTester(t)
   423  	defer tst.teardown()
   424  	tst.pullAllImages()
   425  	if err := ioutil.WriteFile(
   426  		filepath.Join(tst.tmpDir, "data/part_73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"),
   427  		[]byte("4"), 0666); err != nil {
   428  		t.Errorf("WriteFile(): %v", err)
   429  	}
   430  	tst.store.GC()
   431  	// GC on the correct fs only removes part_* files (because they're never referenced by anything)
   432  	tst.verifyListImages("", tst.images[1], tst.images[0], tst.images[2])
   433  	tst.verifyDataFiles(sha256str("###example.com:1234/foo/bar"), sha256str("###baz"))
   434  
   435  	tst.removeFile("links/baz")
   436  	tst.store.GC()
   437  	// GC on the correct fs doesn't change anything
   438  	tst.verifyListImages("", tst.images[0], tst.images[2])
   439  	tst.verifyImage(tst.refs[0], "###example.com:1234/foo/bar")
   440  	tst.verifyImage(tst.refs[2], "###baz")
   441  	tst.verifyDataFiles(sha256str("###example.com:1234/foo/bar"), sha256str("###baz"))
   442  
   443  	tst.referencedImages = []string{tst.images[1].Digest}
   444  	tst.removeFile("links/example.com:1234%foo%bar")
   445  	tst.store.GC()
   446  	tst.verifyListImages("", tst.images[2])
   447  	tst.verifyImage(tst.refs[2], "###baz")
   448  	tst.verifyDataFiles(sha256str("###baz"))
   449  
   450  	tst.removeFile("links/foobar")
   451  	tst.store.GC()
   452  	tst.verifyListImages("")
   453  	tst.verifyDataFiles(sha256str("###baz"))
   454  
   455  	// the name in ref is already gone but the digest is still there
   456  	tst.referencedImages = []string{tst.refs[1]}
   457  	tst.store.GC()
   458  	tst.verifyDataFiles(sha256str("###baz"))
   459  
   460  	tst.referencedImages = nil
   461  	tst.store.GC()
   462  	tst.verifyDataFiles()
   463  }
   464  
   465  func TestCancelPullImage(t *testing.T) {
   466  	tst := newIfsTester(t)
   467  	defer tst.teardown()
   468  
   469  	ctx, cancel := context.WithCancel(context.Background())
   470  	go func() {
   471  		<-tst.downloader.started
   472  		cancel()
   473  	}()
   474  	_, err := tst.store.PullImage(ctx, "cancelme", tst.translateImageName)
   475  	switch {
   476  	case err == nil:
   477  		tst.t.Errorf("PullImage() din't return any error after being cancelled")
   478  	case !strings.Contains(err.Error(), "context canceled"):
   479  		t.Errorf("PullImage() is expected to return Cancelled error but returned %q", err)
   480  	}
   481  	if !tst.downloader.cancelled {
   482  		t.Errorf("the downloader isn't marked as canelled")
   483  	}
   484  }
   485  
   486  func TestVerifyImageChecksum(t *testing.T) {
   487  	tst := newIfsTester(t)
   488  	defer tst.teardown()
   489  
   490  	// Use image ref instead of the name.
   491  	// The ref contains sha256 sum
   492  	tst.pullImage(tst.refs[0], tst.refs[0])
   493  	tst.verifyListImages("foobar")
   494  
   495  	refWithBadDigest := tst.images[0].Name + "@sha256:0000000000000000000000000000000000000000000000000000000000000000"
   496  	_, err := tst.store.PullImage(
   497  		context.Background(),
   498  		refWithBadDigest,
   499  		tst.translateImageName)
   500  	switch {
   501  	case err == nil:
   502  		tst.t.Errorf("PullImage() din't return any error for an image with mismatching digest")
   503  	case !strings.Contains(err.Error(), "image digest mismatch"):
   504  		t.Errorf("PullImage() is expected to return invalid checksum error but returned %q", err)
   505  	}
   506  
   507  	switch _, err := tst.store.ImageStatus(refWithBadDigest); {
   508  	case err == nil:
   509  		tst.t.Errorf("ImageStatus() din't return any error for an image with mismatching digest")
   510  	case !strings.Contains(err.Error(), "image digest mismatch"):
   511  		t.Errorf("ImageStatus() is expected to return invalid checksum error but returned %q", err)
   512  	}
   513  
   514  	// the bad digest should not match any images while listing
   515  	tst.verifyListImages(refWithBadDigest)
   516  }