github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/client/allocdir/alloc_dir.go (about)

     1  package allocdir
     2  
     3  import (
     4  	"archive/tar"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"time"
    11  
    12  	"gopkg.in/tomb.v1"
    13  
    14  	"github.com/hashicorp/go-multierror"
    15  	"github.com/hashicorp/nomad/nomad/structs"
    16  	"github.com/hpcloud/tail/watch"
    17  )
    18  
    19  var (
    20  	// The name of the directory that is shared across tasks in a task group.
    21  	SharedAllocName = "alloc"
    22  
    23  	// Name of the directory where logs of Tasks are written
    24  	LogDirName = "logs"
    25  
    26  	// The set of directories that exist inside eache shared alloc directory.
    27  	SharedAllocDirs = []string{LogDirName, "tmp", "data"}
    28  
    29  	// The name of the directory that exists inside each task directory
    30  	// regardless of driver.
    31  	TaskLocal = "local"
    32  
    33  	// TaskSecrets is the name of the secret directory inside each task
    34  	// directory
    35  	TaskSecrets = "secrets"
    36  
    37  	// TaskDirs is the set of directories created in each tasks directory.
    38  	TaskDirs = []string{"tmp"}
    39  )
    40  
    41  type AllocDir struct {
    42  	// AllocDir is the directory used for storing any state
    43  	// of this allocation. It will be purged on alloc destroy.
    44  	AllocDir string
    45  
    46  	// The shared directory is available to all tasks within the same task
    47  	// group.
    48  	SharedDir string
    49  
    50  	// TaskDirs is a mapping of task names to their non-shared directory.
    51  	TaskDirs map[string]string
    52  }
    53  
    54  // AllocFileInfo holds information about a file inside the AllocDir
    55  type AllocFileInfo struct {
    56  	Name     string
    57  	IsDir    bool
    58  	Size     int64
    59  	FileMode string
    60  	ModTime  time.Time
    61  }
    62  
    63  // AllocDirFS exposes file operations on the alloc dir
    64  type AllocDirFS interface {
    65  	List(path string) ([]*AllocFileInfo, error)
    66  	Stat(path string) (*AllocFileInfo, error)
    67  	ReadAt(path string, offset int64) (io.ReadCloser, error)
    68  	Snapshot(w io.Writer) error
    69  	BlockUntilExists(path string, t *tomb.Tomb) (chan error, error)
    70  	ChangeEvents(path string, curOffset int64, t *tomb.Tomb) (*watch.FileChanges, error)
    71  }
    72  
    73  // NewAllocDir initializes the AllocDir struct with allocDir as base path for
    74  // the allocation directory.
    75  func NewAllocDir(allocDir string) *AllocDir {
    76  	d := &AllocDir{
    77  		AllocDir: allocDir,
    78  		TaskDirs: make(map[string]string),
    79  	}
    80  	d.SharedDir = filepath.Join(d.AllocDir, SharedAllocName)
    81  	return d
    82  }
    83  
    84  // Snapshot creates an archive of the files and directories in the data dir of
    85  // the allocation and the task local directories
    86  func (d *AllocDir) Snapshot(w io.Writer) error {
    87  	allocDataDir := filepath.Join(d.SharedDir, "data")
    88  	rootPaths := []string{allocDataDir}
    89  	for _, path := range d.TaskDirs {
    90  		taskLocaPath := filepath.Join(path, "local")
    91  		rootPaths = append(rootPaths, taskLocaPath)
    92  	}
    93  
    94  	tw := tar.NewWriter(w)
    95  	defer tw.Close()
    96  
    97  	walkFn := func(path string, fileInfo os.FileInfo, err error) error {
    98  		// Ignore if the file is a symlink
    99  		if fileInfo.Mode() == os.ModeSymlink {
   100  			return nil
   101  		}
   102  
   103  		// Include the path of the file name relative to the alloc dir
   104  		// so that we can put the files in the right directories
   105  		relPath, err := filepath.Rel(d.AllocDir, path)
   106  		if err != nil {
   107  			return err
   108  		}
   109  		hdr, err := tar.FileInfoHeader(fileInfo, "")
   110  		if err != nil {
   111  			return fmt.Errorf("error creating file header: %v", err)
   112  		}
   113  		hdr.Name = relPath
   114  		tw.WriteHeader(hdr)
   115  
   116  		// If it's a directory we just write the header into the tar
   117  		if fileInfo.IsDir() {
   118  			return nil
   119  		}
   120  
   121  		// Write the file into the archive
   122  		file, err := os.Open(path)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		defer file.Close()
   127  
   128  		if _, err := io.Copy(tw, file); err != nil {
   129  			return err
   130  		}
   131  		return nil
   132  	}
   133  
   134  	// Walk through all the top level directories and add the files and
   135  	// directories in the archive
   136  	for _, path := range rootPaths {
   137  		if err := filepath.Walk(path, walkFn); err != nil {
   138  			return err
   139  		}
   140  	}
   141  
   142  	return nil
   143  }
   144  
   145  // Move moves the shared data and task local dirs
   146  func (d *AllocDir) Move(other *AllocDir, tasks []*structs.Task) error {
   147  	// Move the data directory
   148  	otherDataDir := filepath.Join(other.SharedDir, "data")
   149  	dataDir := filepath.Join(d.SharedDir, "data")
   150  	if fileInfo, err := os.Stat(otherDataDir); fileInfo != nil && err == nil {
   151  		if err := os.Rename(otherDataDir, dataDir); err != nil {
   152  			return fmt.Errorf("error moving data dir: %v", err)
   153  		}
   154  	}
   155  
   156  	// Move the task directories
   157  	for _, task := range tasks {
   158  		taskDir := filepath.Join(other.AllocDir, task.Name)
   159  		otherTaskLocal := filepath.Join(taskDir, TaskLocal)
   160  
   161  		if fileInfo, err := os.Stat(otherTaskLocal); fileInfo != nil && err == nil {
   162  			if taskDir, ok := d.TaskDirs[task.Name]; ok {
   163  				if err := os.Rename(otherTaskLocal, filepath.Join(taskDir, TaskLocal)); err != nil {
   164  					return fmt.Errorf("error moving task local dir: %v", err)
   165  				}
   166  			}
   167  		}
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // Tears down previously build directory structure.
   174  func (d *AllocDir) Destroy() error {
   175  
   176  	// Unmount all mounted shared alloc dirs.
   177  	var mErr multierror.Error
   178  	if err := d.UnmountAll(); err != nil {
   179  		mErr.Errors = append(mErr.Errors, err)
   180  	}
   181  
   182  	if err := os.RemoveAll(d.AllocDir); err != nil {
   183  		mErr.Errors = append(mErr.Errors, err)
   184  	}
   185  
   186  	return mErr.ErrorOrNil()
   187  }
   188  
   189  func (d *AllocDir) UnmountAll() error {
   190  	var mErr multierror.Error
   191  	for _, dir := range d.TaskDirs {
   192  		// Check if the directory has the shared alloc mounted.
   193  		taskAlloc := filepath.Join(dir, SharedAllocName)
   194  		if d.pathExists(taskAlloc) {
   195  			if err := d.unmountSharedDir(taskAlloc); err != nil {
   196  				mErr.Errors = append(mErr.Errors,
   197  					fmt.Errorf("failed to unmount shared alloc dir %q: %v", taskAlloc, err))
   198  			} else if err := os.RemoveAll(taskAlloc); err != nil {
   199  				mErr.Errors = append(mErr.Errors,
   200  					fmt.Errorf("failed to delete shared alloc dir %q: %v", taskAlloc, err))
   201  			}
   202  		}
   203  
   204  		taskSecret := filepath.Join(dir, TaskSecrets)
   205  		if d.pathExists(taskSecret) {
   206  			if err := d.removeSecretDir(taskSecret); err != nil {
   207  				mErr.Errors = append(mErr.Errors,
   208  					fmt.Errorf("failed to remove the secret dir %q: %v", taskSecret, err))
   209  			}
   210  		}
   211  
   212  		// Unmount dev/ and proc/ have been mounted.
   213  		d.unmountSpecialDirs(dir)
   214  	}
   215  
   216  	return mErr.ErrorOrNil()
   217  }
   218  
   219  // Given a list of a task build the correct alloc structure.
   220  func (d *AllocDir) Build(tasks []*structs.Task) error {
   221  	// Make the alloc directory, owned by the nomad process.
   222  	if err := os.MkdirAll(d.AllocDir, 0755); err != nil {
   223  		return fmt.Errorf("Failed to make the alloc directory %v: %v", d.AllocDir, err)
   224  	}
   225  
   226  	// Make the shared directory and make it available to all user/groups.
   227  	if err := os.MkdirAll(d.SharedDir, 0777); err != nil {
   228  		return err
   229  	}
   230  
   231  	// Make the shared directory have non-root permissions.
   232  	if err := d.dropDirPermissions(d.SharedDir); err != nil {
   233  		return err
   234  	}
   235  
   236  	for _, dir := range SharedAllocDirs {
   237  		p := filepath.Join(d.SharedDir, dir)
   238  		if err := os.MkdirAll(p, 0777); err != nil {
   239  			return err
   240  		}
   241  		if err := d.dropDirPermissions(p); err != nil {
   242  			return err
   243  		}
   244  	}
   245  
   246  	// Make the task directories.
   247  	for _, t := range tasks {
   248  		taskDir := filepath.Join(d.AllocDir, t.Name)
   249  		if err := os.MkdirAll(taskDir, 0777); err != nil {
   250  			return err
   251  		}
   252  
   253  		// Make the task directory have non-root permissions.
   254  		if err := d.dropDirPermissions(taskDir); err != nil {
   255  			return err
   256  		}
   257  
   258  		// Create a local directory that each task can use.
   259  		local := filepath.Join(taskDir, TaskLocal)
   260  		if err := os.MkdirAll(local, 0777); err != nil {
   261  			return err
   262  		}
   263  
   264  		if err := d.dropDirPermissions(local); err != nil {
   265  			return err
   266  		}
   267  
   268  		d.TaskDirs[t.Name] = taskDir
   269  
   270  		// Create the directories that should be in every task.
   271  		for _, dir := range TaskDirs {
   272  			local := filepath.Join(taskDir, dir)
   273  			if err := os.MkdirAll(local, 0777); err != nil {
   274  				return err
   275  			}
   276  
   277  			if err := d.dropDirPermissions(local); err != nil {
   278  				return err
   279  			}
   280  		}
   281  
   282  		// Create the secret directory
   283  		secret := filepath.Join(taskDir, TaskSecrets)
   284  		if err := d.createSecretDir(secret); err != nil {
   285  			return err
   286  		}
   287  
   288  		if err := d.dropDirPermissions(secret); err != nil {
   289  			return err
   290  		}
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  // Embed takes a mapping of absolute directory or file paths on the host to
   297  // their intended, relative location within the task directory. Embed attempts
   298  // hardlink and then defaults to copying. If the path exists on the host and
   299  // can't be embedded an error is returned.
   300  func (d *AllocDir) Embed(task string, entries map[string]string) error {
   301  	taskdir, ok := d.TaskDirs[task]
   302  	if !ok {
   303  		return fmt.Errorf("Task directory doesn't exist for task %v", task)
   304  	}
   305  
   306  	subdirs := make(map[string]string)
   307  	for source, dest := range entries {
   308  		// Check to see if directory exists on host.
   309  		s, err := os.Stat(source)
   310  		if os.IsNotExist(err) {
   311  			continue
   312  		}
   313  
   314  		// Embedding a single file
   315  		if !s.IsDir() {
   316  			if err := d.createDir(taskdir, filepath.Dir(dest)); err != nil {
   317  				return fmt.Errorf("Couldn't create destination directory %v: %v", dest, err)
   318  			}
   319  
   320  			// Copy the file.
   321  			taskEntry := filepath.Join(taskdir, dest)
   322  			if err := d.linkOrCopy(source, taskEntry, s.Mode().Perm()); err != nil {
   323  				return err
   324  			}
   325  
   326  			continue
   327  		}
   328  
   329  		// Create destination directory.
   330  		destDir := filepath.Join(taskdir, dest)
   331  
   332  		if err := d.createDir(taskdir, dest); err != nil {
   333  			return fmt.Errorf("Couldn't create destination directory %v: %v", destDir, err)
   334  		}
   335  
   336  		// Enumerate the files in source.
   337  		dirEntries, err := ioutil.ReadDir(source)
   338  		if err != nil {
   339  			return fmt.Errorf("Couldn't read directory %v: %v", source, err)
   340  		}
   341  
   342  		for _, entry := range dirEntries {
   343  			hostEntry := filepath.Join(source, entry.Name())
   344  			taskEntry := filepath.Join(destDir, filepath.Base(hostEntry))
   345  			if entry.IsDir() {
   346  				subdirs[hostEntry] = filepath.Join(dest, filepath.Base(hostEntry))
   347  				continue
   348  			}
   349  
   350  			// Check if entry exists. This can happen if restarting a failed
   351  			// task.
   352  			if _, err := os.Lstat(taskEntry); err == nil {
   353  				continue
   354  			}
   355  
   356  			if !entry.Mode().IsRegular() {
   357  				// If it is a symlink we can create it, otherwise we skip it.
   358  				if entry.Mode()&os.ModeSymlink == 0 {
   359  					continue
   360  				}
   361  
   362  				link, err := os.Readlink(hostEntry)
   363  				if err != nil {
   364  					return fmt.Errorf("Couldn't resolve symlink for %v: %v", source, err)
   365  				}
   366  
   367  				if err := os.Symlink(link, taskEntry); err != nil {
   368  					// Symlinking twice
   369  					if err.(*os.LinkError).Err.Error() != "file exists" {
   370  						return fmt.Errorf("Couldn't create symlink: %v", err)
   371  					}
   372  				}
   373  				continue
   374  			}
   375  
   376  			if err := d.linkOrCopy(hostEntry, taskEntry, entry.Mode().Perm()); err != nil {
   377  				return err
   378  			}
   379  		}
   380  	}
   381  
   382  	// Recurse on self to copy subdirectories.
   383  	if len(subdirs) != 0 {
   384  		return d.Embed(task, subdirs)
   385  	}
   386  
   387  	return nil
   388  }
   389  
   390  // MountSharedDir mounts the shared directory into the specified task's
   391  // directory. Mount is documented at an OS level in their respective
   392  // implementation files.
   393  func (d *AllocDir) MountSharedDir(task string) error {
   394  	taskDir, ok := d.TaskDirs[task]
   395  	if !ok {
   396  		return fmt.Errorf("No task directory exists for %v", task)
   397  	}
   398  
   399  	taskLoc := filepath.Join(taskDir, SharedAllocName)
   400  	if err := d.mountSharedDir(taskLoc); err != nil {
   401  		return fmt.Errorf("Failed to mount shared directory for task %v: %v", task, err)
   402  	}
   403  
   404  	return nil
   405  }
   406  
   407  // LogDir returns the log dir in the current allocation directory
   408  func (d *AllocDir) LogDir() string {
   409  	return filepath.Join(d.AllocDir, SharedAllocName, LogDirName)
   410  }
   411  
   412  // List returns the list of files at a path relative to the alloc dir
   413  func (d *AllocDir) List(path string) ([]*AllocFileInfo, error) {
   414  	if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
   415  		return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
   416  	} else if escapes {
   417  		return nil, fmt.Errorf("Path escapes the alloc directory")
   418  	}
   419  
   420  	p := filepath.Join(d.AllocDir, path)
   421  	finfos, err := ioutil.ReadDir(p)
   422  	if err != nil {
   423  		return []*AllocFileInfo{}, err
   424  	}
   425  	files := make([]*AllocFileInfo, len(finfos))
   426  	for idx, info := range finfos {
   427  		files[idx] = &AllocFileInfo{
   428  			Name:     info.Name(),
   429  			IsDir:    info.IsDir(),
   430  			Size:     info.Size(),
   431  			FileMode: info.Mode().String(),
   432  			ModTime:  info.ModTime(),
   433  		}
   434  	}
   435  	return files, err
   436  }
   437  
   438  // Stat returns information about the file at a path relative to the alloc dir
   439  func (d *AllocDir) Stat(path string) (*AllocFileInfo, error) {
   440  	if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
   441  		return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
   442  	} else if escapes {
   443  		return nil, fmt.Errorf("Path escapes the alloc directory")
   444  	}
   445  
   446  	p := filepath.Join(d.AllocDir, path)
   447  	info, err := os.Stat(p)
   448  	if err != nil {
   449  		return nil, err
   450  	}
   451  
   452  	return &AllocFileInfo{
   453  		Size:     info.Size(),
   454  		Name:     info.Name(),
   455  		IsDir:    info.IsDir(),
   456  		FileMode: info.Mode().String(),
   457  		ModTime:  info.ModTime(),
   458  	}, nil
   459  }
   460  
   461  // ReadAt returns a reader for a file at the path relative to the alloc dir
   462  func (d *AllocDir) ReadAt(path string, offset int64) (io.ReadCloser, error) {
   463  	if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
   464  		return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
   465  	} else if escapes {
   466  		return nil, fmt.Errorf("Path escapes the alloc directory")
   467  	}
   468  
   469  	p := filepath.Join(d.AllocDir, path)
   470  
   471  	// Check if it is trying to read into a secret directory
   472  	for _, dir := range d.TaskDirs {
   473  		sdir := filepath.Join(dir, TaskSecrets)
   474  		if filepath.HasPrefix(p, sdir) {
   475  			return nil, fmt.Errorf("Reading secret file prohibited: %s", path)
   476  		}
   477  	}
   478  
   479  	f, err := os.Open(p)
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  	if _, err := f.Seek(offset, 0); err != nil {
   484  		return nil, fmt.Errorf("can't seek to offset %q: %v", offset, err)
   485  	}
   486  	return f, nil
   487  }
   488  
   489  // BlockUntilExists blocks until the passed file relative the allocation
   490  // directory exists. The block can be cancelled with the passed tomb.
   491  func (d *AllocDir) BlockUntilExists(path string, t *tomb.Tomb) (chan error, error) {
   492  	if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
   493  		return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
   494  	} else if escapes {
   495  		return nil, fmt.Errorf("Path escapes the alloc directory")
   496  	}
   497  
   498  	// Get the path relative to the alloc directory
   499  	p := filepath.Join(d.AllocDir, path)
   500  	watcher := getFileWatcher(p)
   501  	returnCh := make(chan error, 1)
   502  	go func() {
   503  		returnCh <- watcher.BlockUntilExists(t)
   504  		close(returnCh)
   505  	}()
   506  	return returnCh, nil
   507  }
   508  
   509  // ChangeEvents watches for changes to the passed path relative to the
   510  // allocation directory. The offset should be the last read offset. The tomb is
   511  // used to clean up the watch.
   512  func (d *AllocDir) ChangeEvents(path string, curOffset int64, t *tomb.Tomb) (*watch.FileChanges, error) {
   513  	if escapes, err := structs.PathEscapesAllocDir(path); err != nil {
   514  		return nil, fmt.Errorf("Failed to check if path escapes alloc directory: %v", err)
   515  	} else if escapes {
   516  		return nil, fmt.Errorf("Path escapes the alloc directory")
   517  	}
   518  
   519  	// Get the path relative to the alloc directory
   520  	p := filepath.Join(d.AllocDir, path)
   521  	watcher := getFileWatcher(p)
   522  	return watcher.ChangeEvents(t, curOffset)
   523  }
   524  
   525  // getFileWatcher returns a FileWatcher for the given path.
   526  func getFileWatcher(path string) watch.FileWatcher {
   527  	return watch.NewPollingFileWatcher(path)
   528  }
   529  
   530  func fileCopy(src, dst string, perm os.FileMode) error {
   531  	// Do a simple copy.
   532  	srcFile, err := os.Open(src)
   533  	if err != nil {
   534  		return fmt.Errorf("Couldn't open src file %v: %v", src, err)
   535  	}
   536  	defer srcFile.Close()
   537  
   538  	dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, perm)
   539  	if err != nil {
   540  		return fmt.Errorf("Couldn't create destination file %v: %v", dst, err)
   541  	}
   542  	defer dstFile.Close()
   543  
   544  	if _, err := io.Copy(dstFile, srcFile); err != nil {
   545  		return fmt.Errorf("Couldn't copy %v to %v: %v", src, dst, err)
   546  	}
   547  
   548  	return nil
   549  }
   550  
   551  // pathExists is a helper function to check if the path exists.
   552  func (d *AllocDir) pathExists(path string) bool {
   553  	if _, err := os.Stat(path); err != nil {
   554  		if os.IsNotExist(err) {
   555  			return false
   556  		}
   557  	}
   558  	return true
   559  }
   560  
   561  func (d *AllocDir) GetSecretDir(task string) (string, error) {
   562  	if t, ok := d.TaskDirs[task]; !ok {
   563  		return "", fmt.Errorf("Allocation directory doesn't contain task %q", task)
   564  	} else {
   565  		return filepath.Join(t, TaskSecrets), nil
   566  	}
   567  }
   568  
   569  // createDir creates a directory structure inside the basepath. This functions
   570  // preserves the permissions of each of the subdirectories in the relative path
   571  // by looking up the permissions in the host.
   572  func (d *AllocDir) createDir(basePath, relPath string) error {
   573  	filePerms, err := d.splitPath(relPath)
   574  	if err != nil {
   575  		return err
   576  	}
   577  
   578  	// We are going backwards since we create the root of the directory first
   579  	// and then create the entire nested structure.
   580  	for i := len(filePerms) - 1; i >= 0; i-- {
   581  		fi := filePerms[i]
   582  		destDir := filepath.Join(basePath, fi.Name)
   583  		if err := os.MkdirAll(destDir, fi.Perm); err != nil {
   584  			return err
   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  
   596  // splitPath stats each subdirectory of a path. The first element of the array
   597  // is the file passed to this method, and the last element is the root of the
   598  // path.
   599  func (d *AllocDir) splitPath(path string) ([]fileInfo, error) {
   600  	var mode os.FileMode
   601  	i, err := os.Stat(path)
   602  
   603  	// If the path is not present in the host then we respond with the most
   604  	// flexible permission.
   605  	if err != nil {
   606  		mode = os.ModePerm
   607  	} else {
   608  		mode = i.Mode()
   609  	}
   610  	var dirs []fileInfo
   611  	dirs = append(dirs, fileInfo{Name: path, Perm: mode})
   612  	currentDir := path
   613  	for {
   614  		dir := filepath.Dir(filepath.Clean(currentDir))
   615  		if dir == currentDir {
   616  			break
   617  		}
   618  
   619  		// We try to find the permission of the file in the host. If the path is not
   620  		// present in the host then we respond with the most flexible permission.
   621  		i, err = os.Stat(dir)
   622  		if err != nil {
   623  			mode = os.ModePerm
   624  		} else {
   625  			mode = i.Mode()
   626  		}
   627  		dirs = append(dirs, fileInfo{Name: dir, Perm: mode})
   628  		currentDir = dir
   629  	}
   630  	return dirs, nil
   631  }