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