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