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