github.com/anuvu/nomad@v0.8.7-atom1/client/allocdir/alloc_dir.go (about)

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