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 }