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 }