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