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 }