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