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