github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/client/allocdir/alloc_dir.go (about) 1 package allocdir 2 3 import ( 4 "archive/tar" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "sync" 12 "time" 13 14 "net/http" 15 "strings" 16 17 hclog "github.com/hashicorp/go-hclog" 18 multierror "github.com/hashicorp/go-multierror" 19 cstructs "github.com/hashicorp/nomad/client/structs" 20 "github.com/hashicorp/nomad/nomad/structs" 21 "github.com/hpcloud/tail/watch" 22 tomb "gopkg.in/tomb.v1" 23 ) 24 25 const ( 26 // idUnsupported is what the uid/gid will be set to on platforms (eg 27 // Windows) that don't support integer ownership identifiers. 28 idUnsupported = -1 29 ) 30 31 var ( 32 // SnapshotErrorTime is the sentinel time that will be used on the 33 // error file written by Snapshot when it encounters as error. 34 SnapshotErrorTime = time.Date(2000, 0, 0, 0, 0, 0, 0, time.UTC) 35 36 // The name of the directory that is shared across tasks in a task group. 37 SharedAllocName = "alloc" 38 39 // Name of the directory where logs of Tasks are written 40 LogDirName = "logs" 41 42 // SharedDataDir is one of the shared allocation directories. It is 43 // included in snapshots. 44 SharedDataDir = "data" 45 46 // TmpDirName is the name of the temporary directory in each alloc and 47 // task. 48 TmpDirName = "tmp" 49 50 // The set of directories that exist inside each shared alloc directory. 51 SharedAllocDirs = []string{LogDirName, TmpDirName, SharedDataDir} 52 53 // The name of the directory that exists inside each task directory 54 // regardless of driver. 55 TaskLocal = "local" 56 57 // TaskSecrets is the name of the secret directory inside each task 58 // directory 59 TaskSecrets = "secrets" 60 61 // TaskDirs is the set of directories created in each tasks directory. 62 TaskDirs = map[string]os.FileMode{TmpDirName: os.ModeSticky | 0777} 63 64 // AllocGRPCSocket is the path relative to the task dir root for the 65 // unix socket connected to Consul's gRPC endpoint. 66 AllocGRPCSocket = filepath.Join(SharedAllocName, TmpDirName, "consul_grpc.sock") 67 68 // AllocHTTPSocket is the path relative to the task dir root for the unix 69 // socket connected to Consul's HTTP endpoint. 70 AllocHTTPSocket = filepath.Join(SharedAllocName, TmpDirName, "consul_http.sock") 71 ) 72 73 // AllocDir allows creating, destroying, and accessing an allocation's 74 // directory. All methods are safe for concurrent use. 75 type AllocDir struct { 76 // AllocDir is the directory used for storing any state 77 // of this allocation. It will be purged on alloc destroy. 78 AllocDir string 79 80 // The shared directory is available to all tasks within the same task 81 // group. 82 SharedDir string 83 84 // TaskDirs is a mapping of task names to their non-shared directory. 85 TaskDirs map[string]*TaskDir 86 87 // built is true if Build has successfully run 88 built bool 89 90 mu sync.RWMutex 91 92 logger hclog.Logger 93 } 94 95 // AllocDirFS exposes file operations on the alloc dir 96 type AllocDirFS interface { 97 List(path string) ([]*cstructs.AllocFileInfo, error) 98 Stat(path string) (*cstructs.AllocFileInfo, error) 99 ReadAt(path string, offset int64) (io.ReadCloser, error) 100 Snapshot(w io.Writer) error 101 BlockUntilExists(ctx context.Context, path string) (chan error, error) 102 ChangeEvents(ctx context.Context, path string, curOffset int64) (*watch.FileChanges, error) 103 } 104 105 // NewAllocDir initializes the AllocDir struct with allocDir as base path for 106 // the allocation directory. 107 func NewAllocDir(logger hclog.Logger, allocDir string) *AllocDir { 108 logger = logger.Named("alloc_dir") 109 return &AllocDir{ 110 AllocDir: allocDir, 111 SharedDir: filepath.Join(allocDir, SharedAllocName), 112 TaskDirs: make(map[string]*TaskDir), 113 logger: logger, 114 } 115 } 116 117 // Copy an AllocDir and all of its TaskDirs. Returns nil if AllocDir is 118 // nil. 119 func (d *AllocDir) Copy() *AllocDir { 120 if d == nil { 121 return nil 122 } 123 124 d.mu.RLock() 125 defer d.mu.RUnlock() 126 127 dcopy := &AllocDir{ 128 AllocDir: d.AllocDir, 129 SharedDir: d.SharedDir, 130 TaskDirs: make(map[string]*TaskDir, len(d.TaskDirs)), 131 logger: d.logger, 132 } 133 for k, v := range d.TaskDirs { 134 dcopy.TaskDirs[k] = v.Copy() 135 } 136 return dcopy 137 } 138 139 // NewTaskDir creates a new TaskDir and adds it to the AllocDirs TaskDirs map. 140 func (d *AllocDir) NewTaskDir(name string) *TaskDir { 141 d.mu.Lock() 142 defer d.mu.Unlock() 143 144 td := newTaskDir(d.logger, d.AllocDir, name) 145 d.TaskDirs[name] = td 146 return td 147 } 148 149 // Snapshot creates an archive of the files and directories in the data dir of 150 // the allocation and the task local directories 151 // 152 // Since a valid tar may have been written even when an error occurs, a special 153 // file "NOMAD-${ALLOC_ID}-ERROR.log" will be appended to the tar with the 154 // error message as the contents. 155 func (d *AllocDir) Snapshot(w io.Writer) error { 156 d.mu.RLock() 157 defer d.mu.RUnlock() 158 159 allocDataDir := filepath.Join(d.SharedDir, SharedDataDir) 160 rootPaths := []string{allocDataDir} 161 for _, taskdir := range d.TaskDirs { 162 rootPaths = append(rootPaths, taskdir.LocalDir) 163 } 164 165 tw := tar.NewWriter(w) 166 defer tw.Close() 167 168 walkFn := func(path string, fileInfo os.FileInfo, err error) error { 169 if err != nil { 170 return err 171 } 172 173 // Include the path of the file name relative to the alloc dir 174 // so that we can put the files in the right directories 175 relPath, err := filepath.Rel(d.AllocDir, path) 176 if err != nil { 177 return err 178 } 179 link := "" 180 if fileInfo.Mode()&os.ModeSymlink != 0 { 181 target, err := os.Readlink(path) 182 if err != nil { 183 return fmt.Errorf("error reading symlink: %v", err) 184 } 185 link = target 186 } 187 hdr, err := tar.FileInfoHeader(fileInfo, link) 188 if err != nil { 189 return fmt.Errorf("error creating file header: %v", err) 190 } 191 hdr.Name = relPath 192 if err := tw.WriteHeader(hdr); err != nil { 193 return err 194 } 195 196 // If it's a directory or symlink we just write the header into the tar 197 if fileInfo.IsDir() || (fileInfo.Mode()&os.ModeSymlink != 0) { 198 return nil 199 } 200 201 // Write the file into the archive 202 file, err := os.Open(path) 203 if err != nil { 204 return err 205 } 206 defer file.Close() 207 208 if _, err := io.Copy(tw, file); err != nil { 209 return err 210 } 211 return nil 212 } 213 214 // Walk through all the top level directories and add the files and 215 // directories in the archive 216 for _, path := range rootPaths { 217 if err := filepath.Walk(path, walkFn); err != nil { 218 allocID := filepath.Base(d.AllocDir) 219 if writeErr := writeError(tw, allocID, err); writeErr != nil { 220 // This could be bad; other side won't know 221 // snapshotting failed. It could also just mean 222 // the snapshotting side closed the connect 223 // prematurely and won't try to use the tar 224 // anyway. 225 d.logger.Warn("snapshotting failed and unable to write error marker", "error", writeErr) 226 } 227 return fmt.Errorf("failed to snapshot %s: %v", path, err) 228 } 229 } 230 231 return nil 232 } 233 234 // Move other alloc directory's shared path and local dir to this alloc dir. 235 func (d *AllocDir) Move(other *AllocDir, tasks []*structs.Task) error { 236 d.mu.RLock() 237 if !d.built { 238 // Enforce the invariant that Build is called before Move 239 d.mu.RUnlock() 240 return fmt.Errorf("unable to move to %q - alloc dir is not built", d.AllocDir) 241 } 242 243 // Moving is slow and only reads immutable fields, so unlock during heavy IO 244 d.mu.RUnlock() 245 246 // Move the data directory 247 otherDataDir := filepath.Join(other.SharedDir, SharedDataDir) 248 dataDir := filepath.Join(d.SharedDir, SharedDataDir) 249 if fileInfo, err := os.Stat(otherDataDir); fileInfo != nil && err == nil { 250 os.Remove(dataDir) // remove an empty data dir if it exists 251 if err := os.Rename(otherDataDir, dataDir); err != nil { 252 return fmt.Errorf("error moving data dir: %v", err) 253 } 254 } 255 256 // Move the task directories 257 for _, task := range tasks { 258 otherTaskDir := filepath.Join(other.AllocDir, task.Name) 259 otherTaskLocal := filepath.Join(otherTaskDir, TaskLocal) 260 261 fileInfo, err := os.Stat(otherTaskLocal) 262 if fileInfo != nil && err == nil { 263 // TaskDirs haven't been built yet, so create it 264 newTaskDir := filepath.Join(d.AllocDir, task.Name) 265 if err := os.MkdirAll(newTaskDir, 0777); err != nil { 266 return fmt.Errorf("error creating task %q dir: %v", task.Name, err) 267 } 268 localDir := filepath.Join(newTaskDir, TaskLocal) 269 os.Remove(localDir) // remove an empty local dir if it exists 270 if err := os.Rename(otherTaskLocal, localDir); err != nil { 271 return fmt.Errorf("error moving task %q local dir: %v", task.Name, err) 272 } 273 } 274 } 275 276 return nil 277 } 278 279 // Tears down previously build directory structure. 280 func (d *AllocDir) Destroy() error { 281 // Unmount all mounted shared alloc dirs. 282 var mErr multierror.Error 283 if err := d.UnmountAll(); err != nil { 284 mErr.Errors = append(mErr.Errors, err) 285 } 286 287 if err := os.RemoveAll(d.AllocDir); err != nil { 288 mErr.Errors = append(mErr.Errors, fmt.Errorf("failed to remove alloc dir %q: %v", d.AllocDir, err)) 289 } 290 291 // Unset built since the alloc dir has been destroyed. 292 d.mu.Lock() 293 d.built = false 294 d.mu.Unlock() 295 return mErr.ErrorOrNil() 296 } 297 298 // UnmountAll linked/mounted directories in task dirs. 299 func (d *AllocDir) UnmountAll() error { 300 d.mu.RLock() 301 defer d.mu.RUnlock() 302 303 var mErr multierror.Error 304 for _, dir := range d.TaskDirs { 305 // Check if the directory has the shared alloc mounted. 306 if pathExists(dir.SharedTaskDir) { 307 if err := unlinkDir(dir.SharedTaskDir); err != nil { 308 mErr.Errors = append(mErr.Errors, 309 fmt.Errorf("failed to unmount shared alloc dir %q: %v", dir.SharedTaskDir, err)) 310 } else if err := os.RemoveAll(dir.SharedTaskDir); err != nil { 311 mErr.Errors = append(mErr.Errors, 312 fmt.Errorf("failed to delete shared alloc dir %q: %v", dir.SharedTaskDir, err)) 313 } 314 } 315 316 if pathExists(dir.SecretsDir) { 317 if err := removeSecretDir(dir.SecretsDir); err != nil { 318 mErr.Errors = append(mErr.Errors, 319 fmt.Errorf("failed to remove the secret dir %q: %v", dir.SecretsDir, err)) 320 } 321 } 322 323 // Unmount dev/ and proc/ have been mounted. 324 if err := dir.unmountSpecialDirs(); err != nil { 325 mErr.Errors = append(mErr.Errors, err) 326 } 327 } 328 329 return mErr.ErrorOrNil() 330 } 331 332 // Build the directory tree for an allocation. 333 func (d *AllocDir) Build() error { 334 // Make the alloc directory, owned by the nomad process. 335 if err := os.MkdirAll(d.AllocDir, 0755); err != nil { 336 return fmt.Errorf("Failed to make the alloc directory %v: %v", d.AllocDir, err) 337 } 338 339 // Make the shared directory and make it available to all user/groups. 340 if err := os.MkdirAll(d.SharedDir, 0777); err != nil { 341 return err 342 } 343 344 // Make the shared directory have non-root permissions. 345 if err := dropDirPermissions(d.SharedDir, os.ModePerm); err != nil { 346 return err 347 } 348 349 // Create shared subdirs 350 for _, dir := range SharedAllocDirs { 351 p := filepath.Join(d.SharedDir, dir) 352 if err := os.MkdirAll(p, 0777); err != nil { 353 return err 354 } 355 if err := dropDirPermissions(p, os.ModePerm); err != nil { 356 return err 357 } 358 } 359 360 // Mark as built 361 d.mu.Lock() 362 d.built = true 363 d.mu.Unlock() 364 return nil 365 } 366 367 // List returns the list of files at a path relative to the alloc dir 368 func (d *AllocDir) List(path string) ([]*cstructs.AllocFileInfo, error) { 369 if escapes, err := structs.PathEscapesAllocDir("", path); err != nil { 370 return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err) 371 } else if escapes { 372 return nil, fmt.Errorf("Path escapes the alloc directory") 373 } 374 375 p := filepath.Join(d.AllocDir, path) 376 finfos, err := ioutil.ReadDir(p) 377 if err != nil { 378 return []*cstructs.AllocFileInfo{}, err 379 } 380 files := make([]*cstructs.AllocFileInfo, len(finfos)) 381 for idx, info := range finfos { 382 files[idx] = &cstructs.AllocFileInfo{ 383 Name: info.Name(), 384 IsDir: info.IsDir(), 385 Size: info.Size(), 386 FileMode: info.Mode().String(), 387 ModTime: info.ModTime(), 388 } 389 } 390 return files, err 391 } 392 393 // Stat returns information about the file at a path relative to the alloc dir 394 func (d *AllocDir) Stat(path string) (*cstructs.AllocFileInfo, error) { 395 if escapes, err := structs.PathEscapesAllocDir("", path); err != nil { 396 return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err) 397 } else if escapes { 398 return nil, fmt.Errorf("Path escapes the alloc directory") 399 } 400 401 p := filepath.Join(d.AllocDir, path) 402 info, err := os.Stat(p) 403 if err != nil { 404 return nil, err 405 } 406 407 contentType := detectContentType(info, p) 408 409 return &cstructs.AllocFileInfo{ 410 Size: info.Size(), 411 Name: info.Name(), 412 IsDir: info.IsDir(), 413 FileMode: info.Mode().String(), 414 ModTime: info.ModTime(), 415 ContentType: contentType, 416 }, nil 417 } 418 419 // detectContentType tries to infer the file type by reading the first 420 // 512 bytes of the file. Json file extensions are special cased. 421 func detectContentType(fileInfo os.FileInfo, path string) string { 422 contentType := "application/octet-stream" 423 if !fileInfo.IsDir() { 424 f, err := os.Open(path) 425 // Best effort content type detection 426 // We ignore errors because this is optional information 427 if err == nil { 428 fileBytes := make([]byte, 512) 429 _, err := f.Read(fileBytes) 430 if err == nil { 431 contentType = http.DetectContentType(fileBytes) 432 } 433 f.Close() 434 } 435 } 436 // Special case json files 437 if strings.HasSuffix(path, ".json") { 438 contentType = "application/json" 439 } 440 return contentType 441 } 442 443 // ReadAt returns a reader for a file at the path relative to the alloc dir 444 func (d *AllocDir) ReadAt(path string, offset int64) (io.ReadCloser, error) { 445 if escapes, err := structs.PathEscapesAllocDir("", path); err != nil { 446 return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err) 447 } else if escapes { 448 return nil, fmt.Errorf("Path escapes the alloc directory") 449 } 450 451 p := filepath.Join(d.AllocDir, path) 452 453 // Check if it is trying to read into a secret directory 454 d.mu.RLock() 455 for _, dir := range d.TaskDirs { 456 if filepath.HasPrefix(p, dir.SecretsDir) { 457 d.mu.RUnlock() 458 return nil, fmt.Errorf("Reading secret file prohibited: %s", path) 459 } 460 } 461 d.mu.RUnlock() 462 463 f, err := os.Open(p) 464 if err != nil { 465 return nil, err 466 } 467 if _, err := f.Seek(offset, 0); err != nil { 468 return nil, fmt.Errorf("can't seek to offset %q: %v", offset, err) 469 } 470 return f, nil 471 } 472 473 // BlockUntilExists blocks until the passed file relative the allocation 474 // directory exists. The block can be cancelled with the passed context. 475 func (d *AllocDir) BlockUntilExists(ctx context.Context, path string) (chan error, error) { 476 if escapes, err := structs.PathEscapesAllocDir("", path); err != nil { 477 return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err) 478 } else if escapes { 479 return nil, fmt.Errorf("Path escapes the alloc directory") 480 } 481 482 // Get the path relative to the alloc directory 483 p := filepath.Join(d.AllocDir, path) 484 watcher := getFileWatcher(p) 485 returnCh := make(chan error, 1) 486 t := &tomb.Tomb{} 487 go func() { 488 <-ctx.Done() 489 t.Kill(nil) 490 }() 491 go func() { 492 returnCh <- watcher.BlockUntilExists(t) 493 close(returnCh) 494 }() 495 return returnCh, nil 496 } 497 498 // ChangeEvents watches for changes to the passed path relative to the 499 // allocation directory. The offset should be the last read offset. The context is 500 // used to clean up the watch. 501 func (d *AllocDir) ChangeEvents(ctx context.Context, path string, curOffset int64) (*watch.FileChanges, error) { 502 if escapes, err := structs.PathEscapesAllocDir("", path); err != nil { 503 return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err) 504 } else if escapes { 505 return nil, fmt.Errorf("Path escapes the alloc directory") 506 } 507 508 t := &tomb.Tomb{} 509 go func() { 510 <-ctx.Done() 511 t.Kill(nil) 512 }() 513 514 // Get the path relative to the alloc directory 515 p := filepath.Join(d.AllocDir, path) 516 watcher := getFileWatcher(p) 517 return watcher.ChangeEvents(t, curOffset) 518 } 519 520 // getFileWatcher returns a FileWatcher for the given path. 521 func getFileWatcher(path string) watch.FileWatcher { 522 return watch.NewPollingFileWatcher(path) 523 } 524 525 // fileCopy from src to dst setting the permissions and owner (if uid & gid are 526 // both greater than 0) 527 func fileCopy(src, dst string, uid, gid int, perm os.FileMode) error { 528 // Do a simple copy. 529 srcFile, err := os.Open(src) 530 if err != nil { 531 return fmt.Errorf("Couldn't open src file %v: %v", src, err) 532 } 533 defer srcFile.Close() 534 535 dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, perm) 536 if err != nil { 537 return fmt.Errorf("Couldn't create destination file %v: %v", dst, err) 538 } 539 defer dstFile.Close() 540 541 if _, err := io.Copy(dstFile, srcFile); err != nil { 542 return fmt.Errorf("Couldn't copy %q to %q: %v", src, dst, err) 543 } 544 545 if uid != idUnsupported && gid != idUnsupported { 546 if err := dstFile.Chown(uid, gid); err != nil { 547 return fmt.Errorf("Couldn't copy %q to %q: %v", src, dst, err) 548 } 549 } 550 551 return nil 552 } 553 554 // pathExists is a helper function to check if the path exists. 555 func pathExists(path string) bool { 556 if _, err := os.Stat(path); err != nil { 557 if os.IsNotExist(err) { 558 return false 559 } 560 } 561 return true 562 } 563 564 // pathEmpty returns true if a path exists, is listable, and is empty. If the 565 // path does not exist or is not listable an error is returned. 566 func pathEmpty(path string) (bool, error) { 567 f, err := os.Open(path) 568 if err != nil { 569 return false, err 570 } 571 defer f.Close() 572 entries, err := f.Readdir(1) 573 if err != nil && err != io.EOF { 574 return false, err 575 } 576 return len(entries) == 0, nil 577 } 578 579 // createDir creates a directory structure inside the basepath. This functions 580 // preserves the permissions of each of the subdirectories in the relative path 581 // by looking up the permissions in the host. 582 func createDir(basePath, relPath string) error { 583 filePerms, err := splitPath(relPath) 584 if err != nil { 585 return err 586 } 587 588 // We are going backwards since we create the root of the directory first 589 // and then create the entire nested structure. 590 for i := len(filePerms) - 1; i >= 0; i-- { 591 fi := filePerms[i] 592 destDir := filepath.Join(basePath, fi.Name) 593 if err := os.MkdirAll(destDir, fi.Perm); err != nil { 594 return err 595 } 596 597 if fi.Uid != idUnsupported && fi.Gid != idUnsupported { 598 if err := os.Chown(destDir, fi.Uid, fi.Gid); err != nil { 599 return err 600 } 601 } 602 } 603 return nil 604 } 605 606 // fileInfo holds the path and the permissions of a file 607 type fileInfo struct { 608 Name string 609 Perm os.FileMode 610 611 // Uid and Gid are unsupported on Windows 612 Uid int 613 Gid int 614 } 615 616 // splitPath stats each subdirectory of a path. The first element of the array 617 // is the file passed to this function, and the last element is the root of the 618 // path. 619 func splitPath(path string) ([]fileInfo, error) { 620 var mode os.FileMode 621 fi, err := os.Stat(path) 622 623 // If the path is not present in the host then we respond with the most 624 // flexible permission. 625 uid, gid := idUnsupported, idUnsupported 626 if err != nil { 627 mode = os.ModePerm 628 } else { 629 uid, gid = getOwner(fi) 630 mode = fi.Mode() 631 } 632 var dirs []fileInfo 633 dirs = append(dirs, fileInfo{Name: path, Perm: mode, Uid: uid, Gid: gid}) 634 currentDir := path 635 for { 636 dir := filepath.Dir(filepath.Clean(currentDir)) 637 if dir == currentDir { 638 break 639 } 640 641 // We try to find the permission of the file in the host. If the path is not 642 // present in the host then we respond with the most flexible permission. 643 uid, gid := idUnsupported, idUnsupported 644 fi, err := os.Stat(dir) 645 if err != nil { 646 mode = os.ModePerm 647 } else { 648 uid, gid = getOwner(fi) 649 mode = fi.Mode() 650 } 651 dirs = append(dirs, fileInfo{Name: dir, Perm: mode, Uid: uid, Gid: gid}) 652 currentDir = dir 653 } 654 return dirs, nil 655 } 656 657 // SnapshotErrorFilename returns the filename which will exist if there was an 658 // error snapshotting a tar. 659 func SnapshotErrorFilename(allocID string) string { 660 return fmt.Sprintf("NOMAD-%s-ERROR.log", allocID) 661 } 662 663 // writeError writes a special file to a tar archive with the error encountered 664 // during snapshotting. See Snapshot(). 665 func writeError(tw *tar.Writer, allocID string, err error) error { 666 contents := []byte(fmt.Sprintf("Error snapshotting: %v", err)) 667 hdr := tar.Header{ 668 Name: SnapshotErrorFilename(allocID), 669 Mode: 0666, 670 Size: int64(len(contents)), 671 AccessTime: SnapshotErrorTime, 672 ChangeTime: SnapshotErrorTime, 673 ModTime: SnapshotErrorTime, 674 Typeflag: tar.TypeReg, 675 } 676 677 if err := tw.WriteHeader(&hdr); err != nil { 678 return err 679 } 680 681 _, err = tw.Write(contents) 682 return err 683 }