10c3bad652
Fixes #2522 Skip embedding client.alloc_dir when building chroot. If a user configures a Nomad client agent so that the chroot_env will embed the client.alloc_dir, Nomad will happily infinitely recurse while building the chroot until something horrible happens. The best case scenario is the filesystem's path length limit is hit. The worst case scenario is disk space is exhausted. A bad agent configuration will look something like this: ```hcl data_dir = "/tmp/nomad-badagent" client { enabled = true chroot_env { # Note that the source matches the data_dir "/tmp/nomad-badagent" = "/ohno" # ... } } ``` Note that `/ohno/client` (the state_dir) will still be created but not `/ohno/alloc` (the alloc_dir). While I cannot think of a good reason why someone would want to embed Nomad's client (and possibly server) directories in chroots, there should be no cause for harm. chroots are only built when Nomad runs as root, and Nomad disables running exec jobs as root by default. Therefore even if client state is copied into chroots, it will be inaccessible to tasks. Skipping the `data_dir` and `{client,server}.state_dir` is possible, but this PR attempts to implement the minimum viable solution to reduce risk of unintended side effects or bugs. When running tests as root in a vm without the fix, the following error occurs: ``` === RUN TestAllocDir_SkipAllocDir alloc_dir_test.go:520: Error Trace: alloc_dir_test.go:520 Error: Received unexpected error: Couldn't create destination file /tmp/TestAllocDir_SkipAllocDir1457747331/001/nomad/test/testtask/nomad/test/testtask/.../nomad/test/testtask/secrets/.nomad-mount: open /tmp/TestAllocDir_SkipAllocDir1457747331/001/nomad/test/.../testtask/secrets/.nomad-mount: file name too long Test: TestAllocDir_SkipAllocDir --- FAIL: TestAllocDir_SkipAllocDir (22.76s) ``` Also removed unused Copy methods on AllocDir and TaskDir structs. Thanks to @eveld for not letting me forget about this!
240 lines
6.5 KiB
Go
240 lines
6.5 KiB
Go
package allocdir
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
hclog "github.com/hashicorp/go-hclog"
|
|
)
|
|
|
|
// TaskDir contains all of the paths relevant to a task. All paths are on the
|
|
// host system so drivers should mount/link into task containers as necessary.
|
|
type TaskDir struct {
|
|
// AllocDir is the path to the alloc directory on the host
|
|
AllocDir string
|
|
|
|
// Dir is the path to Task directory on the host
|
|
Dir string
|
|
|
|
// SharedAllocDir is the path to shared alloc directory on the host
|
|
// <alloc_dir>/alloc/
|
|
SharedAllocDir string
|
|
|
|
// SharedTaskDir is the path to the shared alloc directory linked into
|
|
// the task directory on the host.
|
|
// <task_dir>/alloc/
|
|
SharedTaskDir string
|
|
|
|
// LocalDir is the path to the task's local directory on the host
|
|
// <task_dir>/local/
|
|
LocalDir string
|
|
|
|
// LogDir is the path to the task's log directory on the host
|
|
// <alloc_dir>/alloc/logs/
|
|
LogDir string
|
|
|
|
// SecretsDir is the path to secrets/ directory on the host
|
|
// <task_dir>/secrets/
|
|
SecretsDir string
|
|
|
|
// skip embedding these paths in chroots. Used for avoiding embedding
|
|
// client.alloc_dir recursively.
|
|
skip map[string]struct{}
|
|
|
|
logger hclog.Logger
|
|
}
|
|
|
|
// newTaskDir creates a TaskDir struct with paths set. Call Build() to
|
|
// create paths on disk.
|
|
//
|
|
// Call AllocDir.NewTaskDir to create new TaskDirs
|
|
func newTaskDir(logger hclog.Logger, clientAllocDir, allocDir, taskName string) *TaskDir {
|
|
taskDir := filepath.Join(allocDir, taskName)
|
|
|
|
logger = logger.Named("task_dir").With("task_name", taskName)
|
|
|
|
// skip embedding client.alloc_dir in chroots
|
|
skip := map[string]struct{}{clientAllocDir: {}}
|
|
|
|
return &TaskDir{
|
|
AllocDir: allocDir,
|
|
Dir: taskDir,
|
|
SharedAllocDir: filepath.Join(allocDir, SharedAllocName),
|
|
LogDir: filepath.Join(allocDir, SharedAllocName, LogDirName),
|
|
SharedTaskDir: filepath.Join(taskDir, SharedAllocName),
|
|
LocalDir: filepath.Join(taskDir, TaskLocal),
|
|
SecretsDir: filepath.Join(taskDir, TaskSecrets),
|
|
skip: skip,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Build default directories and permissions in a task directory. chrootCreated
|
|
// allows skipping chroot creation if the caller knows it has already been
|
|
// done. client.alloc_dir will be skipped.
|
|
func (t *TaskDir) Build(createChroot bool, chroot map[string]string) error {
|
|
if err := os.MkdirAll(t.Dir, 0777); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Make the task directory have non-root permissions.
|
|
if err := dropDirPermissions(t.Dir, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a local directory that each task can use.
|
|
if err := os.MkdirAll(t.LocalDir, 0777); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dropDirPermissions(t.LocalDir, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the directories that should be in every task.
|
|
for dir, perms := range TaskDirs {
|
|
absdir := filepath.Join(t.Dir, dir)
|
|
if err := os.MkdirAll(absdir, perms); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dropDirPermissions(absdir, perms); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Only link alloc dir into task dir for chroot fs isolation.
|
|
// Image based isolation will bind the shared alloc dir in the driver.
|
|
// If there's no isolation the task will use the host path to the
|
|
// shared alloc dir.
|
|
if createChroot {
|
|
// If the path doesn't exist OR it exists and is empty, link it
|
|
empty, _ := pathEmpty(t.SharedTaskDir)
|
|
if !pathExists(t.SharedTaskDir) || empty {
|
|
if err := linkDir(t.SharedAllocDir, t.SharedTaskDir); err != nil {
|
|
return fmt.Errorf("Failed to mount shared directory for task: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the secret directory
|
|
if err := createSecretDir(t.SecretsDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dropDirPermissions(t.SecretsDir, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Build chroot if chroot filesystem isolation is going to be used
|
|
if createChroot {
|
|
if err := t.buildChroot(chroot); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// buildChroot takes a mapping of absolute directory or file paths on the host
|
|
// to their intended, relative location within the task directory. This
|
|
// attempts hardlink and then defaults to copying. If the path exists on the
|
|
// host and can't be embedded an error is returned.
|
|
func (t *TaskDir) buildChroot(entries map[string]string) error {
|
|
return t.embedDirs(entries)
|
|
}
|
|
|
|
func (t *TaskDir) embedDirs(entries map[string]string) error {
|
|
subdirs := make(map[string]string)
|
|
for source, dest := range entries {
|
|
if _, ok := t.skip[source]; ok {
|
|
// source in skip list
|
|
continue
|
|
}
|
|
|
|
// Check to see if directory exists on host.
|
|
s, err := os.Stat(source)
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
// Embedding a single file
|
|
if !s.IsDir() {
|
|
if err := createDir(t.Dir, filepath.Dir(dest)); err != nil {
|
|
return fmt.Errorf("Couldn't create destination directory %v: %v", dest, err)
|
|
}
|
|
|
|
// Copy the file.
|
|
taskEntry := filepath.Join(t.Dir, dest)
|
|
uid, gid := getOwner(s)
|
|
if err := linkOrCopy(source, taskEntry, uid, gid, s.Mode().Perm()); err != nil {
|
|
return err
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// Create destination directory.
|
|
destDir := filepath.Join(t.Dir, dest)
|
|
|
|
if err := createDir(t.Dir, dest); err != nil {
|
|
return fmt.Errorf("Couldn't create destination directory %v: %v", destDir, err)
|
|
}
|
|
|
|
// Enumerate the files in source.
|
|
dirEntries, err := ioutil.ReadDir(source)
|
|
if err != nil {
|
|
return fmt.Errorf("Couldn't read directory %v: %v", source, err)
|
|
}
|
|
|
|
for _, entry := range dirEntries {
|
|
hostEntry := filepath.Join(source, entry.Name())
|
|
taskEntry := filepath.Join(destDir, filepath.Base(hostEntry))
|
|
if entry.IsDir() {
|
|
subdirs[hostEntry] = filepath.Join(dest, filepath.Base(hostEntry))
|
|
continue
|
|
}
|
|
|
|
// Check if entry exists. This can happen if restarting a failed
|
|
// task.
|
|
if _, err := os.Lstat(taskEntry); err == nil {
|
|
continue
|
|
}
|
|
|
|
if !entry.Mode().IsRegular() {
|
|
// If it is a symlink we can create it, otherwise we skip it.
|
|
if entry.Mode()&os.ModeSymlink == 0 {
|
|
continue
|
|
}
|
|
|
|
link, err := os.Readlink(hostEntry)
|
|
if err != nil {
|
|
return fmt.Errorf("Couldn't resolve symlink for %v: %v", source, err)
|
|
}
|
|
|
|
if err := os.Symlink(link, taskEntry); err != nil {
|
|
// Symlinking twice
|
|
if err.(*os.LinkError).Err.Error() != "file exists" {
|
|
return fmt.Errorf("Couldn't create symlink: %v", err)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
uid, gid := getOwner(entry)
|
|
if err := linkOrCopy(hostEntry, taskEntry, uid, gid, entry.Mode().Perm()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recurse on self to copy subdirectories.
|
|
if len(subdirs) != 0 {
|
|
return t.embedDirs(subdirs)
|
|
}
|
|
|
|
return nil
|
|
}
|