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  }