From 0e5137528591ff779a384a8cc7aa5f535222f19e Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 18 Nov 2015 13:46:43 -0800 Subject: [PATCH] Revert "Make drivers take arguments as a list and not as a string" --- client/alloc_runner_test.go | 6 +-- client/client_test.go | 2 +- client/driver/args/args.go | 29 ++++++++++---- client/driver/args/args_test.go | 41 +++++++++++++++++--- client/driver/docker.go | 13 ++++--- client/driver/docker_test.go | 10 ++--- client/driver/exec.go | 16 +++++--- client/driver/exec_test.go | 16 +++----- client/driver/executor/exec_basic.go | 8 +++- client/driver/executor/exec_linux.go | 7 +++- client/driver/executor/test_harness_test.go | 4 +- client/driver/java.go | 16 ++++---- client/driver/java_test.go | 3 +- client/driver/raw_exec.go | 8 +++- client/driver/raw_exec_test.go | 16 +++----- client/driver/rkt.go | 11 ++++-- client/driver/rkt_test.go | 6 +-- client/task_runner_test.go | 6 +-- nomad/fsm.go | 11 +++++- nomad/mock/mock.go | 4 +- nomad/rpc.go | 13 ++++++- nomad/structs/structs.go | 7 ++-- nomad/timetable_test.go | 5 +-- website/source/docs/drivers/docker.html.md | 6 +-- website/source/docs/drivers/exec.html.md | 22 +++++------ website/source/docs/drivers/java.html.md | 20 +++++----- website/source/docs/drivers/qemu.html.md | 9 ++--- website/source/docs/drivers/raw_exec.html.md | 22 +++++------ website/source/docs/drivers/rkt.html.md | 17 ++++---- 29 files changed, 205 insertions(+), 149 deletions(-) diff --git a/client/alloc_runner_test.go b/client/alloc_runner_test.go index bc4a7aa4f..15077c76c 100644 --- a/client/alloc_runner_test.go +++ b/client/alloc_runner_test.go @@ -65,7 +65,7 @@ func TestAllocRunner_Destroy(t *testing.T) { // Ensure task takes some time task := ar.alloc.Job.TaskGroups[0].Tasks[0] task.Config["command"] = "/bin/sleep" - task.Config["args"] = []string{"10"} + task.Config["args"] = "10" go ar.Run() start := time.Now() @@ -97,7 +97,7 @@ func TestAllocRunner_Update(t *testing.T) { // Ensure task takes some time task := ar.alloc.Job.TaskGroups[0].Tasks[0] task.Config["command"] = "/bin/sleep" - task.Config["args"] = []string{"10"} + task.Config["args"] = "10" go ar.Run() defer ar.Destroy() start := time.Now() @@ -130,7 +130,7 @@ func TestAllocRunner_SaveRestoreState(t *testing.T) { // Ensure task takes some time task := ar.alloc.Job.TaskGroups[0].Tasks[0] task.Config["command"] = "/bin/sleep" - task.Config["args"] = []string{"10"} + task.Config["args"] = "10" go ar.Run() defer ar.Destroy() diff --git a/client/client_test.go b/client/client_test.go index 935c63dea..90fecad6b 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -336,7 +336,7 @@ func TestClient_SaveRestoreState(t *testing.T) { alloc1.NodeID = c1.Node().ID task := alloc1.Job.TaskGroups[0].Tasks[0] task.Config["command"] = "/bin/sleep" - task.Config["args"] = []string{"10"} + task.Config["args"] = "10" state := s1.State() err := state.UpsertAllocs(100, diff --git a/client/driver/args/args.go b/client/driver/args/args.go index 9e8a9980c..51793bd8b 100644 --- a/client/driver/args/args.go +++ b/client/driver/args/args.go @@ -1,6 +1,11 @@ package args -import "regexp" +import ( + "fmt" + "regexp" + + "github.com/mattn/go-shellwords" +) var ( envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`) @@ -8,17 +13,27 @@ var ( // ParseAndReplace takes the user supplied args and a map of environment // variables. It replaces any instance of an environment variable in the args -// with the actual value. -func ParseAndReplace(args []string, env map[string]string) []string { - replaced := make([]string, len(args)) - for i, arg := range args { +// with the actual value and does correct splitting of the arg list. +func ParseAndReplace(args string, env map[string]string) ([]string, error) { + // Set up parser. + p := shellwords.NewParser() + p.ParseEnv = false + p.ParseBacktick = false + + parsed, err := p.Parse(args) + if err != nil { + return nil, fmt.Errorf("Couldn't parse args %v: %v", args, err) + } + + replaced := make([]string, len(parsed)) + for i, arg := range parsed { replaced[i] = ReplaceEnv(arg, env) } - return replaced + return replaced, nil } -// ReplaceEnv takes an arg and replaces all occurences of environment variables. +// replaceEnv takes an arg and replaces all occurences of environment variables. // If the variable is found in the passed map it is replaced, otherwise the // original string is returned. func ReplaceEnv(arg string, env map[string]string) string { diff --git a/client/driver/args/args_test.go b/client/driver/args/args_test.go index 5e7cbca4a..4e1d88b59 100644 --- a/client/driver/args/args_test.go +++ b/client/driver/args/args_test.go @@ -21,9 +21,12 @@ var ( ) func TestDriverArgs_ParseAndReplaceInvalidEnv(t *testing.T) { - input := []string{"invalid", "$FOO"} + input := "invalid $FOO" exp := []string{"invalid", "$FOO"} - act := ParseAndReplace(input, envVars) + act, err := ParseAndReplace(input, envVars) + if err != nil { + t.Fatalf("Failed to parse valid input args %v: %v", input, err) + } if !reflect.DeepEqual(act, exp) { t.Fatalf("ParseAndReplace(%v, %v) returned %#v; want %#v", input, envVars, act, exp) @@ -31,9 +34,12 @@ func TestDriverArgs_ParseAndReplaceInvalidEnv(t *testing.T) { } func TestDriverArgs_ParseAndReplaceValidEnv(t *testing.T) { - input := []string{"nomad_ip", fmt.Sprintf(`"$%v"!`, ipKey)} + input := fmt.Sprintf("nomad_ip \\\"$%v\\\"!", ipKey) exp := []string{"nomad_ip", fmt.Sprintf("\"%s\"!", ipVal)} - act := ParseAndReplace(input, envVars) + act, err := ParseAndReplace(input, envVars) + if err != nil { + t.Fatalf("Failed to parse valid input args %v: %v", input, err) + } if !reflect.DeepEqual(act, exp) { t.Fatalf("ParseAndReplace(%v, %v) returned %#v; want %#v", input, envVars, act, exp) @@ -41,9 +47,32 @@ func TestDriverArgs_ParseAndReplaceValidEnv(t *testing.T) { } func TestDriverArgs_ParseAndReplaceChainedEnv(t *testing.T) { - input := []string{"-foo", fmt.Sprintf("$%s$%s", ipKey, portKey)} + input := fmt.Sprintf("-foo $%s$%s", ipKey, portKey) exp := []string{"-foo", fmt.Sprintf("%s%s", ipVal, portVal)} - act := ParseAndReplace(input, envVars) + act, err := ParseAndReplace(input, envVars) + if err != nil { + t.Fatalf("Failed to parse valid input args %v: %v", input, err) + } + + if !reflect.DeepEqual(act, exp) { + t.Fatalf("ParseAndReplace(%v, %v) returned %#v; want %#v", input, envVars, act, exp) + } +} + +func TestDriverArgs_ParseAndReplaceInvalidArgEscape(t *testing.T) { + input := "-c \"echo \"foo\\\" > bar.txt\"" + if _, err := ParseAndReplace(input, envVars); err == nil { + t.Fatalf("ParseAndReplace(%v, %v) should have failed", input, envVars) + } +} + +func TestDriverArgs_ParseAndReplaceValidArgEscape(t *testing.T) { + input := "-c \"echo \\\"foo\\\" > bar.txt\"" + exp := []string{"-c", "echo \"foo\" > bar.txt"} + act, err := ParseAndReplace(input, envVars) + if err != nil { + t.Fatalf("Failed to parse valid input args %v: %v", input, err) + } if !reflect.DeepEqual(act, exp) { t.Fatalf("ParseAndReplace(%v, %v) returned %#v; want %#v", input, envVars, act, exp) diff --git a/client/driver/docker.go b/client/driver/docker.go index 4aa97e13b..6fa1af600 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -35,7 +35,7 @@ type DockerDriverAuth struct { type DockerDriverConfig struct { ImageName string `mapstructure:"image"` // Container's Image Name Command string `mapstructure:"command"` // The Command/Entrypoint to run when the container starts up - Args []string `mapstructure:"args"` // The arguments to the Command/Entrypoint + Args string `mapstructure:"args"` // The arguments to the Command/Entrypoint NetworkMode string `mapstructure:"network_mode"` // The network mode of the container - host, net and none PortMap []map[string]int `mapstructure:"port_map"` // A map of host port labels and the ports exposed on the container Privileged bool `mapstructure:"privileged"` // Flag to run the container in priviledged mode @@ -293,18 +293,21 @@ func (d *DockerDriver) createContainer(ctx *ExecContext, task *structs.Task, dri config.ExposedPorts = exposedPorts } - parsedArgs := args.ParseAndReplace(driverConfig.Args, env.Map()) + parsedArgs, err := args.ParseAndReplace(driverConfig.Args, env.Map()) + if err != nil { + return c, err + } // If the user specified a custom command to run as their entrypoint, we'll // inject it here. if driverConfig.Command != "" { cmd := []string{driverConfig.Command} - if len(driverConfig.Args) != 0 { + if driverConfig.Args != "" { cmd = append(cmd, parsedArgs...) } d.logger.Printf("[DEBUG] driver.docker: setting container startup command to: %s", strings.Join(cmd, " ")) config.Cmd = cmd - } else if len(driverConfig.Args) != 0 { + } else if driverConfig.Args != "" { d.logger.Println("[DEBUG] driver.docker: ignoring command arguments because command is not specified") } @@ -428,7 +431,7 @@ func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle if len(containers) != 1 { log.Printf("[ERR] driver.docker: failed to get id for container %s", config.Name) - return nil, fmt.Errorf("Failed to get id for container %s", config.Name) + return nil, fmt.Errorf("Failed to get id for container %s: %s", config.Name, err) } log.Printf("[INFO] driver.docker: a container with the name %s already exists; will attempt to purge and re-create", config.Name) diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index a61de381c..4c9300579 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -137,7 +137,7 @@ func TestDockerDriver_Start_Wait(t *testing.T) { Config: map[string]interface{}{ "image": "redis", "command": "redis-server", - "args": []string{"-v"}, + "args": "-v", }, Resources: &structs.Resources{ MemoryMB: 256, @@ -190,11 +190,7 @@ func TestDockerDriver_Start_Wait_AllocDir(t *testing.T) { Config: map[string]interface{}{ "image": "redis", "command": "/bin/bash", - "args": []string{ - "-c", - fmt.Sprintf(`sleep 1; echo -n %s > $%s/%s`, - string(exp), environment.AllocDir, file), - }, + "args": fmt.Sprintf(`-c "sleep 1; echo -n %s > $%s/%s"`, string(exp), environment.AllocDir, file), }, Resources: &structs.Resources{ MemoryMB: 256, @@ -247,7 +243,7 @@ func TestDockerDriver_Start_Kill_Wait(t *testing.T) { Config: map[string]interface{}{ "image": "redis", "command": "/bin/sleep", - "args": []string{"10"}, + "args": "10", }, Resources: basicResources, } diff --git a/client/driver/exec.go b/client/driver/exec.go index a0a136523..1f9e5b7d9 100644 --- a/client/driver/exec.go +++ b/client/driver/exec.go @@ -24,10 +24,10 @@ type ExecDriver struct { fingerprint.StaticFingerprinter } type ExecDriverConfig struct { - ArtifactSource string `mapstructure:"artifact_source"` - Checksum string `mapstructure:"checksum"` - Command string `mapstructure:"command"` - Args []string `mapstructure:"args"` + ArtifactSource string `mapstructure:"artifact_source"` + Checksum string `mapstructure:"checksum"` + Command string `mapstructure:"command"` + Args string `mapstructure:"args"` } // execHandle is returned from Start/Open as a handle to the PID @@ -91,8 +91,14 @@ func (d *ExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, // Get the environment variables. envVars := TaskEnvironmentVariables(ctx, task) + // Look for arguments + var args []string + if driverConfig.Args != "" { + args = append(args, driverConfig.Args) + } + // Setup the command - cmd := executor.Command(command, driverConfig.Args...) + cmd := executor.Command(command, args...) if err := cmd.Limit(task.Resources); err != nil { return nil, fmt.Errorf("failed to constrain resources: %s", err) } diff --git a/client/driver/exec_test.go b/client/driver/exec_test.go index f5470074a..7e6554e1d 100644 --- a/client/driver/exec_test.go +++ b/client/driver/exec_test.go @@ -39,7 +39,7 @@ func TestExecDriver_StartOpen_Wait(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/sleep", - "args": []string{"5"}, + "args": "5", }, Resources: basicResources, } @@ -73,7 +73,7 @@ func TestExecDriver_Start_Wait(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/sleep", - "args": []string{"2"}, + "args": "2", }, Resources: basicResources, } @@ -161,10 +161,7 @@ func TestExecDriver_Start_Artifact_expanded(t *testing.T) { Config: map[string]interface{}{ "artifact_source": fmt.Sprintf("https://dl.dropboxusercontent.com/u/47675/jar_thing/%s", file), "command": "/bin/bash", - "args": []string{ - "-c", - fmt.Sprintf(`/bin/sleep 1 && %s`, filepath.Join("$NOMAD_TASK_DIR", file)), - }, + "args": fmt.Sprintf("-c '/bin/sleep 1 && %s'", filepath.Join("$NOMAD_TASK_DIR", file)), }, Resources: basicResources, } @@ -207,10 +204,7 @@ func TestExecDriver_Start_Wait_AllocDir(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/bash", - "args": []string{ - "-c", - fmt.Sprintf(`sleep 1; echo -n %s > $%s/%s`, string(exp), environment.AllocDir, file), - }, + "args": fmt.Sprintf("-c \"sleep 1; echo -n %s > $%s/%s\"", string(exp), environment.AllocDir, file), }, Resources: basicResources, } @@ -256,7 +250,7 @@ func TestExecDriver_Start_Kill_Wait(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/sleep", - "args": []string{"1"}, + "args": "1", }, Resources: basicResources, } diff --git a/client/driver/executor/exec_basic.go b/client/driver/executor/exec_basic.go index 2ef4fbedc..438ae6b92 100644 --- a/client/driver/executor/exec_basic.go +++ b/client/driver/executor/exec_basic.go @@ -29,6 +29,7 @@ type BasicExecutor struct { allocDir string } +// TODO: Have raw_exec use this as well. func NewBasicExecutor() Executor { return &BasicExecutor{} } @@ -62,7 +63,12 @@ func (e *BasicExecutor) Start() error { } e.cmd.Path = args.ReplaceEnv(e.cmd.Path, envVars.Map()) - e.cmd.Args = args.ParseAndReplace(e.cmd.Args, envVars.Map()) + combined := strings.Join(e.cmd.Args, " ") + parsed, err := args.ParseAndReplace(combined, envVars.Map()) + if err != nil { + return err + } + e.cmd.Args = parsed spawnState := filepath.Join(e.allocDir, fmt.Sprintf("%s_%s", e.taskName, "exit_status")) e.spawn = spawn.NewSpawner(spawnState) diff --git a/client/driver/executor/exec_linux.go b/client/driver/executor/exec_linux.go index 43c9270e0..14f4f809c 100644 --- a/client/driver/executor/exec_linux.go +++ b/client/driver/executor/exec_linux.go @@ -167,7 +167,12 @@ func (e *LinuxExecutor) Start() error { } e.cmd.Path = args.ReplaceEnv(e.cmd.Path, envVars.Map()) - e.cmd.Args = args.ParseAndReplace(e.cmd.Args, envVars.Map()) + combined := strings.Join(e.cmd.Args, " ") + parsed, err := args.ParseAndReplace(combined, envVars.Map()) + if err != nil { + return err + } + e.cmd.Args = parsed spawnState := filepath.Join(e.allocDir, fmt.Sprintf("%s_%s", e.taskName, "exit_status")) e.spawn = spawn.NewSpawner(spawnState) diff --git a/client/driver/executor/test_harness_test.go b/client/driver/executor/test_harness_test.go index b5413700c..10cbac371 100644 --- a/client/driver/executor/test_harness_test.go +++ b/client/driver/executor/test_harness_test.go @@ -112,7 +112,7 @@ func Executor_Start_Wait(t *testing.T, command buildExecCommand) { expected := "hello world" file := filepath.Join(allocdir.TaskLocal, "output.txt") absFilePath := filepath.Join(taskDir, file) - cmd := fmt.Sprintf(`/bin/sleep 1 ; echo -n %v > %v`, expected, file) + cmd := fmt.Sprintf(`"%v \"%v\" > %v"`, "/bin/sleep 1 ; echo -n", expected, file) e := command("/bin/bash", "-c", cmd) if err := e.Limit(constraint); err != nil { @@ -190,7 +190,7 @@ func Executor_Open(t *testing.T, command buildExecCommand, newExecutor func() Ex expected := "hello world" file := filepath.Join(allocdir.TaskLocal, "output.txt") absFilePath := filepath.Join(taskDir, file) - cmd := fmt.Sprintf(`/bin/sleep 1 ; echo -n %v > %v`, expected, file) + cmd := fmt.Sprintf(`"%v \"%v\" > %v"`, "/bin/sleep 1 ; echo -n", expected, file) e := command("/bin/bash", "-c", cmd) if err := e.Limit(constraint); err != nil { diff --git a/client/driver/java.go b/client/driver/java.go index eb475db32..eb2930a28 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -28,10 +28,10 @@ type JavaDriver struct { } type JavaDriverConfig struct { - JvmOpts []string `mapstructure:"jvm_options"` - ArtifactSource string `mapstructure:"artifact_source"` - Checksum string `mapstructure:"checksum"` - Args []string `mapstructure:"args"` + JvmOpts string `mapstructure:"jvm_options"` + ArtifactSource string `mapstructure:"artifact_source"` + Checksum string `mapstructure:"checksum"` + Args string `mapstructure:"args"` } // javaHandle is returned from Start/Open as a handle to the PID @@ -126,15 +126,15 @@ func (d *JavaDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, args := []string{} // Look for jvm options - if len(driverConfig.JvmOpts) != 0 { + if driverConfig.JvmOpts != "" { d.logger.Printf("[DEBUG] driver.java: found JVM options: %s", driverConfig.JvmOpts) - args = append(args, driverConfig.JvmOpts...) + args = append(args, driverConfig.JvmOpts) } // Build the argument list. args = append(args, "-jar", filepath.Join(allocdir.TaskLocal, jarName)) - if len(driverConfig.Args) != 0 { - args = append(args, driverConfig.Args...) + if driverConfig.Args != "" { + args = append(args, driverConfig.Args) } // Setup the command diff --git a/client/driver/java_test.go b/client/driver/java_test.go index af6e44d6d..a0c6d3b80 100644 --- a/client/driver/java_test.go +++ b/client/driver/java_test.go @@ -51,7 +51,7 @@ func TestJavaDriver_StartOpen_Wait(t *testing.T) { Name: "demo-app", Config: map[string]interface{}{ "artifact_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/demoapp.jar", - "jvm_options": []string{"-Xmx64m", "-Xms32m"}, + "jvm_options": "-Xmx2048m -Xms256m", "checksum": "sha256:58d6e8130308d32e197c5108edd4f56ddf1417408f743097c2e662df0f0b17c8", }, Resources: basicResources, @@ -97,6 +97,7 @@ func TestJavaDriver_Start_Wait(t *testing.T) { Name: "demo-app", Config: map[string]interface{}{ "artifact_source": "https://dl.dropboxusercontent.com/u/47675/jar_thing/demoapp.jar", + "jvm_options": "-Xmx2048m -Xms256m", "checksum": "sha256:58d6e8130308d32e197c5108edd4f56ddf1417408f743097c2e662df0f0b17c8", }, Resources: basicResources, diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go index 5b1d98269..d5202fc39 100644 --- a/client/driver/raw_exec.go +++ b/client/driver/raw_exec.go @@ -93,9 +93,15 @@ func (d *RawExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandl // Get the environment variables. envVars := TaskEnvironmentVariables(ctx, task) + // Look for arguments + var args []string + if driverConfig.Args != "" { + args = append(args, driverConfig.Args) + } + // Setup the command cmd := executor.NewBasicExecutor() - executor.SetCommand(cmd, command, driverConfig.Args) + executor.SetCommand(cmd, command, args) if err := cmd.Limit(task.Resources); err != nil { return nil, fmt.Errorf("failed to constrain resources: %s", err) } diff --git a/client/driver/raw_exec_test.go b/client/driver/raw_exec_test.go index 8b6e0c649..1b7a0c8db 100644 --- a/client/driver/raw_exec_test.go +++ b/client/driver/raw_exec_test.go @@ -53,7 +53,7 @@ func TestRawExecDriver_StartOpen_Wait(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/sleep", - "args": []string{"1"}, + "args": "1", }, Resources: basicResources, } @@ -151,10 +151,7 @@ func TestRawExecDriver_Start_Artifact_expanded(t *testing.T) { Config: map[string]interface{}{ "artifact_source": fmt.Sprintf("https://dl.dropboxusercontent.com/u/47675/jar_thing/%s", file), "command": "/bin/bash", - "args": []string{ - "-c", - fmt.Sprintf(`'/bin/sleep 1 && %s'`, filepath.Join("$NOMAD_TASK_DIR", file)), - }, + "args": fmt.Sprintf("-c '/bin/sleep 1 && %s'", filepath.Join("$NOMAD_TASK_DIR", file)), }, Resources: basicResources, } @@ -193,7 +190,7 @@ func TestRawExecDriver_Start_Wait(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/sleep", - "args": []string{"1"}, + "args": "1", }, Resources: basicResources, } @@ -235,10 +232,7 @@ func TestRawExecDriver_Start_Wait_AllocDir(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/bash", - "args": []string{ - "-c", - fmt.Sprintf(`sleep 1; echo -n %s > $%s/%s`, string(exp), environment.AllocDir, file), - }, + "args": fmt.Sprintf(`-c "sleep 1; echo -n %s > $%s/%s"`, string(exp), environment.AllocDir, file), }, Resources: basicResources, } @@ -283,7 +277,7 @@ func TestRawExecDriver_Start_Kill_Wait(t *testing.T) { Name: "sleep", Config: map[string]interface{}{ "command": "/bin/sleep", - "args": []string{"1"}, + "args": "1", }, Resources: basicResources, } diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 1fb6e9e06..d09eac1db 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -37,8 +37,8 @@ type RktDriver struct { } type RktDriverConfig struct { - ImageName string `mapstructure:"image"` - Args []string `mapstructure:"args"` + ImageName string `mapstructure:"image"` + Args string `mapstructure:"args"` } // rktHandle is returned from Start/Open as a handle to the PID @@ -150,8 +150,11 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e } // Add user passed arguments. - if len(driverConfig.Args) != 0 { - parsed := args.ParseAndReplace(driverConfig.Args, envVars.Map()) + if driverConfig.Args != "" { + parsed, err := args.ParseAndReplace(driverConfig.Args, envVars.Map()) + if err != nil { + return nil, err + } // Need to start arguments with "--" if len(parsed) > 0 { diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index 15db27b27..a6b3dfb78 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -119,7 +119,7 @@ func TestRktDriver_Start_Wait(t *testing.T) { "trust_prefix": "coreos.com/etcd", "image": "coreos.com/etcd:v2.0.4", "command": "/etcd", - "args": []string{"--version"}, + "args": "--version", }, } @@ -160,7 +160,7 @@ func TestRktDriver_Start_Wait_Skip_Trust(t *testing.T) { Config: map[string]interface{}{ "image": "coreos.com/etcd:v2.0.4", "command": "/etcd", - "args": []string{"--version"}, + "args": "--version", }, } @@ -202,7 +202,7 @@ func TestRktDriver_Start_Wait_Logs(t *testing.T) { "trust_prefix": "coreos.com/etcd", "image": "coreos.com/etcd:v2.0.4", "command": "/etcd", - "args": []string{"--version"}, + "args": "--version", }, } diff --git a/client/task_runner_test.go b/client/task_runner_test.go index 1ada5060b..f8bc9e466 100644 --- a/client/task_runner_test.go +++ b/client/task_runner_test.go @@ -89,7 +89,7 @@ func TestTaskRunner_Destroy(t *testing.T) { // Change command to ensure we run for a bit tr.task.Config["command"] = "/bin/sleep" - tr.task.Config["args"] = []string{"10"} + tr.task.Config["args"] = "10" go tr.Run() // Begin the tear down @@ -128,7 +128,7 @@ func TestTaskRunner_Update(t *testing.T) { // Change command to ensure we run for a bit tr.task.Config["command"] = "/bin/sleep" - tr.task.Config["args"] = []string{"10"} + tr.task.Config["args"] = "10" go tr.Run() defer tr.Destroy() defer tr.ctx.AllocDir.Destroy() @@ -153,7 +153,7 @@ func TestTaskRunner_SaveRestoreState(t *testing.T) { // Change command to ensure we run for a bit tr.task.Config["command"] = "/bin/sleep" - tr.task.Config["args"] = []string{"10"} + tr.task.Config["args"] = "10" go tr.Run() defer tr.Destroy() diff --git a/nomad/fsm.go b/nomad/fsm.go index 71c40d68f..21b3538e4 100644 --- a/nomad/fsm.go +++ b/nomad/fsm.go @@ -13,6 +13,13 @@ import ( "github.com/hashicorp/raft" ) +var ( + msgpackHandle = &codec.MsgpackHandle{ + RawToString: true, + WriteExt: true, + } +) + const ( // timeTableGranularity is the granularity of index to time tracking timeTableGranularity = 5 * time.Minute @@ -321,7 +328,7 @@ func (n *nomadFSM) Restore(old io.ReadCloser) error { defer restore.Abort() // Create a decoder - dec := codec.NewDecoder(old, structs.MsgpackHandle) + dec := codec.NewDecoder(old, msgpackHandle) // Read in the header var header snapshotHeader @@ -405,7 +412,7 @@ func (n *nomadFSM) Restore(old io.ReadCloser) error { func (s *nomadSnapshot) Persist(sink raft.SnapshotSink) error { defer metrics.MeasureSince([]string{"nomad", "fsm", "persist"}, time.Now()) // Register the nodes - encoder := codec.NewEncoder(sink, structs.MsgpackHandle) + encoder := codec.NewEncoder(sink, msgpackHandle) // Write the header header := snapshotHeader{} diff --git a/nomad/mock/mock.go b/nomad/mock/mock.go index 25ca60ef2..12a8484c0 100644 --- a/nomad/mock/mock.go +++ b/nomad/mock/mock.go @@ -86,7 +86,7 @@ func Job() *structs.Job { Driver: "exec", Config: map[string]interface{}{ "command": "/bin/date", - "args": []string{"+%s"}, + "args": "+%s", }, Env: map[string]string{ "FOO": "bar", @@ -151,7 +151,7 @@ func SystemJob() *structs.Job { Driver: "exec", Config: map[string]interface{}{ "command": "/bin/date", - "args": []string{"+%s"}, + "args": "+%s", }, Resources: &structs.Resources{ CPU: 500, diff --git a/nomad/rpc.go b/nomad/rpc.go index e52e258f0..123c35028 100644 --- a/nomad/rpc.go +++ b/nomad/rpc.go @@ -11,6 +11,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/net-rpc-msgpackrpc" "github.com/hashicorp/nomad/nomad/state" "github.com/hashicorp/nomad/nomad/structs" @@ -52,16 +53,24 @@ const ( enqueueLimit = 30 * time.Second ) +var ( + // rpcHandle is the MsgpackHandle to be used by both Client and Server codecs. + rpcHandle = &codec.MsgpackHandle{ + // Enables proper encoding of strings within nil interfaces. + RawToString: true, + } +) + // NewClientCodec returns a new rpc.ClientCodec to be used to make RPC calls to // the Nomad Server. func NewClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec { - return msgpackrpc.NewCodecFromHandle(true, true, conn, structs.MsgpackHandle) + return msgpackrpc.NewCodecFromHandle(true, true, conn, rpcHandle) } // NewServerCodec returns a new rpc.ServerCodec to be used by the Nomad Server // to handle rpcs. func NewServerCodec(conn io.ReadWriteCloser) rpc.ServerCodec { - return msgpackrpc.NewCodecFromHandle(true, true, conn, structs.MsgpackHandle) + return msgpackrpc.NewCodecFromHandle(true, true, conn, rpcHandle) } // listen is used to listen for incoming RPC connections diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 5ff8d8c3b..7fde437cc 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -1704,26 +1704,25 @@ func (p *PlanResult) FullCommit(plan *Plan) (bool, int, int) { } // msgpackHandle is a shared handle for encoding/decoding of structs -var MsgpackHandle = func() *codec.MsgpackHandle { +var msgpackHandle = func() *codec.MsgpackHandle { h := &codec.MsgpackHandle{RawToString: true} // Sets the default type for decoding a map into a nil interface{}. // This is necessary in particular because we store the driver configs as a // nil interface{}. h.MapType = reflect.TypeOf(map[string]interface{}(nil)) - h.SliceType = reflect.TypeOf([]string{}) return h }() // Decode is used to decode a MsgPack encoded object func Decode(buf []byte, out interface{}) error { - return codec.NewDecoder(bytes.NewReader(buf), MsgpackHandle).Decode(out) + return codec.NewDecoder(bytes.NewReader(buf), msgpackHandle).Decode(out) } // Encode is used to encode a MsgPack object with type prefix func Encode(t MessageType, msg interface{}) ([]byte, error) { var buf bytes.Buffer buf.WriteByte(uint8(t)) - err := codec.NewEncoder(&buf, MsgpackHandle).Encode(msg) + err := codec.NewEncoder(&buf, msgpackHandle).Encode(msg) return buf.Bytes(), err } diff --git a/nomad/timetable_test.go b/nomad/timetable_test.go index a76446a13..3a771d6fa 100644 --- a/nomad/timetable_test.go +++ b/nomad/timetable_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/hashicorp/go-msgpack/codec" - "github.com/hashicorp/nomad/nomad/structs" ) func TestTimeTable(t *testing.T) { @@ -105,14 +104,14 @@ func TestTimeTable_SerializeDeserialize(t *testing.T) { tt.Witness(50, plusHour) var buf bytes.Buffer - enc := codec.NewEncoder(&buf, structs.MsgpackHandle) + enc := codec.NewEncoder(&buf, msgpackHandle) err := tt.Serialize(enc) if err != nil { t.Fatalf("err: %v", err) } - dec := codec.NewDecoder(&buf, structs.MsgpackHandle) + dec := codec.NewDecoder(&buf, msgpackHandle) tt2 := NewTimeTable(time.Second, time.Minute) err = tt2.Deserialize(dec) diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index a50bc1cec..d1c6bafbe 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -19,13 +19,13 @@ and cleaning up after containers. The `docker` driver supports the following configuration in the job specification: -* `image` - The Docker image to run. The image may include a tag or +* `image` - (Required) The Docker image to run. The image may include a tag or custom URL. By default it will be fetched from Docker Hub. * `command` - (Optional) The command to run when starting the container. -* `args` - (Optional) A list of arguments to the optional `command`. If no - `command` is present, `args` are ignored. +* `args` - (Optional) Arguments to the optional `command`. If no `command` is + present, `args` are ignored. * `network_mode` - (Optional) The network mode to be used for the container. In order to support userspace networking plugins in Docker 1.9 this accepts any diff --git a/website/source/docs/drivers/exec.html.md b/website/source/docs/drivers/exec.html.md index 3d921aa6d..f897b1ea4 100644 --- a/website/source/docs/drivers/exec.html.md +++ b/website/source/docs/drivers/exec.html.md @@ -20,19 +20,15 @@ scripts or other wrappers which provide higher level features. The `exec` driver supports the following configuration in the job spec: -* `command` - The command to execute. Must be provided. - -* `artifact_source` – (Optional) Source location of an executable artifact. Must - be accessible from the Nomad client. If you specify an `artifact_source` to be - executed, you must reference it in the `command` as show in the examples below - -* `checksum` - (Optional) The checksum type and value for the `artifact_source` - image. The format is `type:value`, where type is any of `md5`, `sha1`, - `sha256`, or `sha512`, and the value is the computed checksum. If a checksum - is supplied and does not match the downloaded artifact, the driver will fail - to start - -* `args` - (Optional) A list of arguments to the `command`. +* `command` - (Required) The command to execute. Must be provided. +* `artifact_source` – (Optional) Source location of an executable artifact. Must be accessible +from the Nomad client. If you specify an `artifact_source` to be executed, you +must reference it in the `command` as show in the examples below +* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image. +The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`, +and the value is the computed checksum. If a checksum is supplied and does not +match the downloaded artifact, the driver will fail to start +* `args` - The argument list to the command, space seperated. Optional. ## Client Requirements diff --git a/website/source/docs/drivers/java.html.md b/website/source/docs/drivers/java.html.md index 45baacb72..f2bbd2b76 100644 --- a/website/source/docs/drivers/java.html.md +++ b/website/source/docs/drivers/java.html.md @@ -18,19 +18,17 @@ HTTP from the Nomad client. The `java` driver supports the following configuration in the job spec: -* `artifact_source` - The hosted location of the source Jar file. Must be - accessible from the Nomad client +* `artifact_source` - **(Required)** The hosted location of the source Jar file. Must be accessible +from the Nomad client +* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image. +The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`, +and the value is the computed checksum. If a checksum is supplied and does not +match the downloaded artifact, the driver will fail to start -* `checksum` - (Optional) The checksum type and value for the `artifact_source` - image. The format is `type:value`, where type is any of `md5`, `sha1`, - `sha256`, or `sha512`, and the value is the computed checksum. If a checksum - is supplied and does not match the downloaded artifact, the driver will fail - to start +* `args` - **(Optional)** The argument list for the `java` command, space separated. -* `args` - (Optional) A list of arguments to the `java` command. - -* `jvm_options` - (Optional) A list of JVM options to be passed while invoking - java. These options are passed not validated in any way in Nomad. +* `jvm_options` - **(Optional)** JVM options to be passed while invoking java. These options + are passed not validated in any way in Nomad. ## Client Requirements diff --git a/website/source/docs/drivers/qemu.html.md b/website/source/docs/drivers/qemu.html.md index d329c7a8e..84909e331 100644 --- a/website/source/docs/drivers/qemu.html.md +++ b/website/source/docs/drivers/qemu.html.md @@ -23,19 +23,16 @@ The `Qemu` driver can execute any regular `qemu` image (e.g. `qcow`, `img`, The `Qemu` driver supports the following configuration in the job spec: -* `artifact_source` - The hosted location of the source Qemu image. Must be accessible +* `artifact_source` - **(Required)** The hosted location of the source Qemu image. Must be accessible from the Nomad client, via HTTP. - -* `checksum` - (Optional) The checksum type and value for the `artifact_source` image. +* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image. The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`, and the value is the computed checksum. If a checksum is supplied and does not match the downloaded artifact, the driver will fail to start - * `accelerator` - (Optional) The type of accelerator to use in the invocation. If the host machine has `Qemu` installed with KVM support, users can specify `kvm` for the `accelerator`. Default is `tcg` - -* `port_map` - (Optional) A `map[string]int` that maps port labels to ports +* `port_map` - **(Optional)** A `map[string]int` that maps port labels to ports on the guest. This forwards the host port to the guest vm. For example, `port_map { db = 6539 }` would forward the host port with label `db` to the guest vm's port 6539. diff --git a/website/source/docs/drivers/raw_exec.html.md b/website/source/docs/drivers/raw_exec.html.md index c76825f25..2dc741887 100644 --- a/website/source/docs/drivers/raw_exec.html.md +++ b/website/source/docs/drivers/raw_exec.html.md @@ -18,19 +18,15 @@ As such, it should be used with extreme care and is disabled by default. The `raw_exec` driver supports the following configuration in the job spec: -* `command` - The command to execute. Must be provided. - -* `artifact_source` – (Optional) Source location of an executable artifact. Must - be accessible from the Nomad client. If you specify an `artifact_source` to be - executed, you must reference it in the `command` as show in the examples below - -* `checksum` - (Optional) The checksum type and value for the `artifact_source` - image. The format is `type:value`, where type is any of `md5`, `sha1`, - `sha256`, or `sha512`, and the value is the computed checksum. If a checksum - is supplied and does not match the downloaded artifact, the driver will fail - to start - -* `args` - (Optional) A list of arguments to the `command`. +* `command` - (Required) The command to execute. Must be provided. +* `artifact_source` – (Optional) Source location of an executable artifact. Must be accessible +from the Nomad client. If you specify an `artifact_source` to be executed, you +must reference it in the `command` as show in the examples below +* `checksum` - **(Optional)** The checksum type and value for the `artifact_source` image. +The format is `type:value`, where type is any of `md5`, `sha1`, `sha256`, or `sha512`, +and the value is the computed checksum. If a checksum is supplied and does not +match the downloaded artifact, the driver will fail to start +* `args` - The argument list to the command, space seperated. Optional. ## Client Requirements diff --git a/website/source/docs/drivers/rkt.html.md b/website/source/docs/drivers/rkt.html.md index 7ce3e1a49..ddbb76601 100644 --- a/website/source/docs/drivers/rkt.html.md +++ b/website/source/docs/drivers/rkt.html.md @@ -20,16 +20,13 @@ being marked as experimental and should be used with care. The `rkt` driver supports the following configuration in the job spec: -* `image` - The image to run which may be specified by name, hash, ACI address - or docker registry. - -* `command` - (Optional) A command to execute on the ACI. - -* `args` - (Optional) A list of arguments to the image. - -* `trust_prefix` - (Optional) The trust prefix to be passed to rkt. Must be - reachable from the box running the nomad agent. If not specified, the image is - run without verifying the image signature. +* `trust_prefix` - **(Optional)** The trust prefix to be passed to rkt. Must be reachable from +the box running the nomad agent. If not specified, the image is run without +verifying the image signature. +* `image` - **(Required)** The image to run which may be specified by name, +hash, ACI address or docker registry. +* `command` - **(Optional**) A command to execute on the ACI. +* `args` - **(Optional**) A string of args to pass into the image. ## Task Directories