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