diff --git a/client/config/config.go b/client/config/config.go
index 3c968480a..1d50c5669 100644
--- a/client/config/config.go
+++ b/client/config/config.go
@@ -99,6 +99,10 @@ type Config struct {
// devices and IPs.
GloballyReservedPorts []int
+ // A mapping of directories on the host OS to attempt to embed inside each
+ // task's chroot.
+ ChrootEnv map[string]string
+
// Options provides arbitrary key-value configuration for nomad internals,
// like fingerprinters and drivers. The format is:
//
diff --git a/client/driver/exec.go b/client/driver/exec.go
index ab534b820..728ed3f5f 100644
--- a/client/driver/exec.go
+++ b/client/driver/exec.go
@@ -121,11 +121,12 @@ func (d *ExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle,
return nil, err
}
executorCtx := &executor.ExecutorContext{
- TaskEnv: d.taskEnv,
- Driver: "exec",
- AllocDir: ctx.AllocDir,
- AllocID: ctx.AllocID,
- Task: task,
+ TaskEnv: d.taskEnv,
+ Driver: "exec",
+ AllocDir: ctx.AllocDir,
+ AllocID: ctx.AllocID,
+ ChrootEnv: d.config.ChrootEnv,
+ Task: task,
}
ps, err := exec.LaunchCmd(&executor.ExecCommand{
diff --git a/client/driver/executor/executor.go b/client/driver/executor/executor.go
index 7685bfdf2..7fc973e63 100644
--- a/client/driver/executor/executor.go
+++ b/client/driver/executor/executor.go
@@ -102,6 +102,10 @@ type ExecutorContext struct {
// AllocID is the allocation id to which the task belongs
AllocID string
+ // A mapping of directories on the host OS to attempt to embed inside each
+ // task's chroot.
+ ChrootEnv map[string]string
+
// Driver is the name of the driver that invoked the executor
Driver string
diff --git a/client/driver/executor/executor_linux.go b/client/driver/executor/executor_linux.go
index 1f6fef13c..c673555cc 100644
--- a/client/driver/executor/executor_linux.go
+++ b/client/driver/executor/executor_linux.go
@@ -227,7 +227,12 @@ func (e *UniversalExecutor) configureChroot() error {
return err
}
- if err := allocDir.Embed(e.ctx.Task.Name, chrootEnv); err != nil {
+ chroot := chrootEnv
+ if len(e.ctx.ChrootEnv) > 0 {
+ chroot = e.ctx.ChrootEnv
+ }
+
+ if err := allocDir.Embed(e.ctx.Task.Name, chroot); err != nil {
return err
}
diff --git a/client/driver/executor/executor_linux_test.go b/client/driver/executor/executor_linux_test.go
index 9af75dec5..c3006071a 100644
--- a/client/driver/executor/executor_linux_test.go
+++ b/client/driver/executor/executor_linux_test.go
@@ -9,15 +9,38 @@ import (
"strings"
"testing"
+ "github.com/hashicorp/nomad/client/driver/env"
cstructs "github.com/hashicorp/nomad/client/driver/structs"
"github.com/hashicorp/nomad/client/testutil"
+ "github.com/hashicorp/nomad/nomad/mock"
)
+func testExecutorContextWithChroot(t *testing.T) *ExecutorContext {
+ taskEnv := env.NewTaskEnvironment(mock.Node())
+ task, allocDir := mockAllocDir(t)
+ ctx := &ExecutorContext{
+ TaskEnv: taskEnv,
+ Task: task,
+ AllocDir: allocDir,
+ ChrootEnv: map[string]string{
+ "/etc/ld.so.cache": "/etc/ld.so.cache",
+ "/etc/ld.so.conf": "/etc/ld.so.conf",
+ "/etc/ld.so.conf.d": "/etc/ld.so.conf.d",
+ "/lib": "/lib",
+ "/lib64": "/lib64",
+ "/usr/lib": "/usr/lib",
+ "/bin/ls": "/bin/ls",
+ "/foobar": "/does/not/exist",
+ },
+ }
+ return ctx
+}
+
func TestExecutor_IsolationAndConstraints(t *testing.T) {
testutil.ExecCompatible(t)
- execCmd := ExecCommand{Cmd: "/bin/echo", Args: []string{"hello world"}}
- ctx := testExecutorContext(t)
+ execCmd := ExecCommand{Cmd: "/bin/ls", Args: []string{"-F", "/", "/etc/"}}
+ ctx := testExecutorContextWithChroot(t)
defer ctx.AllocDir.Destroy()
execCmd.FSIsolation = true
@@ -58,7 +81,7 @@ func TestExecutor_IsolationAndConstraints(t *testing.T) {
t.Fatalf("file %v hasn't been removed", memLimits)
}
- expected := "hello world"
+ expected := "/:\nalloc/\nbin/\ndev/\netc/\nlib/\nlib64/\nlocal/\nproc/\ntmp/\nusr/\n\n/etc/:\nld.so.cache\nld.so.conf\nld.so.conf.d/"
file := filepath.Join(ctx.AllocDir.LogDir(), "web.stdout.0")
output, err := ioutil.ReadFile(file)
if err != nil {
diff --git a/client/driver/java.go b/client/driver/java.go
index 532199433..24e6ce6a8 100644
--- a/client/driver/java.go
+++ b/client/driver/java.go
@@ -203,11 +203,12 @@ func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle,
return nil, err
}
executorCtx := &executor.ExecutorContext{
- TaskEnv: d.taskEnv,
- Driver: "java",
- AllocDir: ctx.AllocDir,
- AllocID: ctx.AllocID,
- Task: task,
+ TaskEnv: d.taskEnv,
+ Driver: "java",
+ AllocDir: ctx.AllocDir,
+ AllocID: ctx.AllocID,
+ ChrootEnv: d.config.ChrootEnv,
+ Task: task,
}
absPath, err := GetAbsolutePath("java")
diff --git a/command/agent/agent.go b/command/agent/agent.go
index 5b5bfaa1e..b50d89f60 100644
--- a/command/agent/agent.go
+++ b/command/agent/agent.go
@@ -274,6 +274,7 @@ func (a *Agent) clientConfig() (*clientconfig.Config, error) {
if a.config.Client.NetworkInterface != "" {
conf.NetworkInterface = a.config.Client.NetworkInterface
}
+ conf.ChrootEnv = a.config.Client.ChrootEnv
conf.Options = a.config.Client.Options
// Logging deprecation messages about consul related configuration in client
// options
diff --git a/command/agent/config-test-fixtures/basic.hcl b/command/agent/config-test-fixtures/basic.hcl
index 8715ef988..2e132ddd3 100644
--- a/command/agent/config-test-fixtures/basic.hcl
+++ b/command/agent/config-test-fixtures/basic.hcl
@@ -33,6 +33,10 @@ client {
foo = "bar"
baz = "zip"
}
+ chroot_env {
+ "/opt/myapp/etc" = "/etc"
+ "/opt/myapp/bin" = "/bin"
+ }
network_interface = "eth0"
network_speed = 100
reserved {
diff --git a/command/agent/config.go b/command/agent/config.go
index 32bfd4d42..8a0a6e482 100644
--- a/command/agent/config.go
+++ b/command/agent/config.go
@@ -156,6 +156,10 @@ type ClientConfig struct {
// Metadata associated with the node
Meta map[string]string `mapstructure:"meta"`
+ // A mapping of directories on the host OS to attempt to embed inside each
+ // task's chroot.
+ ChrootEnv map[string]string `mapstructure:"chroot_env"`
+
// Interface to use for network fingerprinting
NetworkInterface string `mapstructure:"network_interface"`
@@ -720,6 +724,14 @@ func (a *ClientConfig) Merge(b *ClientConfig) *ClientConfig {
result.Meta[k] = v
}
+ // Add the chroot_env map values
+ if result.ChrootEnv == nil {
+ result.ChrootEnv = make(map[string]string)
+ }
+ for k, v := range b.ChrootEnv {
+ result.ChrootEnv[k] = v
+ }
+
return &result
}
diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go
index b3d3dd969..8ea5eb206 100644
--- a/command/agent/config_parse.go
+++ b/command/agent/config_parse.go
@@ -315,6 +315,7 @@ func parseClient(result **ClientConfig, list *ast.ObjectList) error {
"node_class",
"options",
"meta",
+ "chroot_env",
"network_interface",
"network_speed",
"max_kill_timeout",
@@ -334,6 +335,7 @@ func parseClient(result **ClientConfig, list *ast.ObjectList) error {
delete(m, "options")
delete(m, "meta")
+ delete(m, "chroot_env")
delete(m, "reserved")
delete(m, "stats")
@@ -370,6 +372,20 @@ func parseClient(result **ClientConfig, list *ast.ObjectList) error {
}
}
+ // Parse out chroot_env fields. These are in HCL as a list so we need to
+ // iterate over them and merge them.
+ if chrootEnvO := listVal.Filter("chroot_env"); len(chrootEnvO.Items) > 0 {
+ for _, o := range chrootEnvO.Elem().Items {
+ var m map[string]interface{}
+ if err := hcl.DecodeObject(&m, o.Val); err != nil {
+ return err
+ }
+ if err := mapstructure.WeakDecode(m, &config.ChrootEnv); err != nil {
+ return err
+ }
+ }
+ }
+
// Parse reserved config
if o := listVal.Filter("reserved"); len(o.Items) > 0 {
if err := parseReserved(&config.Reserved, o); err != nil {
diff --git a/command/agent/config_parse_test.go b/command/agent/config_parse_test.go
index 61cf7a3b5..0e126eac4 100644
--- a/command/agent/config_parse_test.go
+++ b/command/agent/config_parse_test.go
@@ -53,6 +53,10 @@ func TestConfig_Parse(t *testing.T) {
"foo": "bar",
"baz": "zip",
},
+ ChrootEnv: map[string]string{
+ "/opt/myapp/etc": "/etc",
+ "/opt/myapp/bin": "/bin",
+ },
NetworkInterface: "eth0",
NetworkSpeed: 100,
MaxKillTimeout: "10s",
diff --git a/command/agent/config_test.go b/command/agent/config_test.go
index 830601228..b83454bb1 100644
--- a/command/agent/config_test.go
+++ b/command/agent/config_test.go
@@ -139,6 +139,7 @@ func TestConfig_Merge(t *testing.T) {
"foo": "bar",
"baz": "zip",
},
+ ChrootEnv: map[string]string{},
ClientMaxPort: 20000,
ClientMinPort: 22000,
NetworkSpeed: 105,
diff --git a/website/source/docs/agent/config.html.md b/website/source/docs/agent/config.html.md
index 371c1137d..c28fe1b89 100644
--- a/website/source/docs/agent/config.html.md
+++ b/website/source/docs/agent/config.html.md
@@ -421,6 +421,9 @@ configured on server nodes.
* `options`: This is a key/value mapping of internal
configuration for clients, such as for driver configuration. Please see
[here](#options_map) for a description of available options.
+ * `chroot_env`: This is a key/value mapping that
+ defines the chroot environment for jobs using the Exec and Java drivers.
+ Please see [here](#chroot_env_map) for an example and further information.
* `network_interface`: This is a string to force
network fingerprinting to use a specific network interface
* `network_speed`: This is an int that sets the
@@ -496,6 +499,31 @@ documentation [here](/docs/drivers/index.html)
If specified, fingerprinters not in the whitelist will be disabled. If the
whitelist is empty, all fingerprinters are used.
+### Client ChrootEnv Map
+
+Drivers based on [Isolated Fork/Exec](/docs/drivers/exec.html) implement file
+system isolation using chroot on Linux. The `chroot_env` map allows the chroot
+environment to be configured using source paths on the host operating system.
+The mapping format is: `source_path -> dest_path`.
+
+The following example specifies a chroot which contains just enough to run the
+`ls` utility, and not much else:
+
+```
+chroot_env {
+ "/bin/ls" = "/bin/ls"
+ "/etc/ld.so.cache" = "/etc/ld.so.cache"
+ "/etc/ld.so.conf" = "/etc/ld.so.conf"
+ "/etc/ld.so.conf.d" = "/etc/ld.so.conf.d"
+ "/lib" = "/lib"
+ "/lib64" = "/lib64"
+}
+```
+
+When `chroot_env` is unspecified, the `exec` driver will use a default chroot
+environment with the most commonly used parts of the operating system. See
+`exec` documentation for the full list [here](/docs/drivers/exec.html#chroot).
+
## Command-line Options
A subset of the available Nomad agent configuration can optionally be passed in
diff --git a/website/source/docs/drivers/exec.html.md b/website/source/docs/drivers/exec.html.md
index 4477c860e..20dfea5f7 100644
--- a/website/source/docs/drivers/exec.html.md
+++ b/website/source/docs/drivers/exec.html.md
@@ -96,9 +96,12 @@ the client and the configuration.
On Linux, Nomad will use cgroups, and a chroot to isolate the
resources of a process and as such the Nomad agent must be run as root.
-### Chroot
+### Chroot
The chroot is populated with data in the following folders from the host
machine:
`["/bin", "/etc", "/lib", "/lib32", "/lib64", "/run/resolvconf", "/sbin",
"/usr"]`
+
+This list is configurable through the agent client
+[configuration file](/docs/agent/config.html#chroot_env).