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