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