diff --git a/command/helpers.go b/command/helpers.go index 123f4b70e0ac..9e64b974cf8b 100644 --- a/command/helpers.go +++ b/command/helpers.go @@ -4,10 +4,16 @@ import ( "bytes" "fmt" "io" + "io/ioutil" + "os" "strconv" "time" + gg "github.com/hashicorp/go-getter" "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/jobspec" + "github.com/hashicorp/nomad/nomad/structs" + "github.com/ryanuber/columnize" ) @@ -222,3 +228,63 @@ READ: // Just stream from the underlying reader now return l.ReadCloser.Read(p) } + +type JobGetter struct { + // The fields below can be overwritten for tests + testStdin io.Reader +} + +// StructJob returns the Job struct from jobfile. +func (j *JobGetter) StructJob(jpath string) (*structs.Job, error) { + var jobfile io.Reader + switch jpath { + case "-": + if j.testStdin != nil { + jobfile = j.testStdin + } else { + jobfile = os.Stdin + } + default: + if len(jpath) == 0 { + return nil, fmt.Errorf("Error jobfile path has to be specified.") + } + + job, err := ioutil.TempFile("", "jobfile") + if err != nil { + return nil, err + } + defer os.Remove(job.Name()) + + // Get the pwd + pwd, err := os.Getwd() + if err != nil { + return nil, err + } + + client := &gg.Client{ + Src: jpath, + Pwd: pwd, + Dst: job.Name(), + } + + if err := client.Get(); err != nil { + return nil, fmt.Errorf("Error getting jobfile from %q: %v", jpath, err) + } else { + file, err := os.Open(job.Name()) + defer file.Close() + if err != nil { + return nil, fmt.Errorf("Error opening file %q: %v", jpath, err) + } + jobfile = file + } + } + + // Parse the JobFile + jobStruct, err := jobspec.Parse(jobfile) + if err != nil { + fmt.Errorf("Error parsing job file from %s: %v", jpath, err) + return nil, err + } + + return jobStruct, nil +} diff --git a/command/helpers_test.go b/command/helpers_test.go index f43e701e7749..e989fa2579ee 100644 --- a/command/helpers_test.go +++ b/command/helpers_test.go @@ -1,8 +1,11 @@ package command import ( + "fmt" "io" "io/ioutil" + "net/http" + "os" "reflect" "strings" "testing" @@ -185,3 +188,67 @@ func TestHelpers_LineLimitReader_TimeLimit(t *testing.T) { t.Fatalf("did not exit by time limit") } } + +const ( + job = `job "job1" { + type = "service" + datacenters = [ "dc1" ] + group "group1" { + count = 1 + task "task1" { + driver = "exec" + resources = {} + } + restart{ + attempts = 10 + mode = "delay" + } + } +}` +) + +// Test StructJob with local jobfile +func TestStructJobWithLocal(t *testing.T) { + fh, err := ioutil.TempFile("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh.Name()) + _, err = fh.WriteString(job) + if err != nil { + t.Fatalf("err: %s", err) + } + + j := &JobGetter{} + sj, err := j.StructJob(fh.Name()) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = sj.Validate() + if err != nil { + t.Fatalf("err: %s", err) + } +} + +// Test StructJob with jobfile from HTTP Server +func TestStructJobWithHTTPServer(t *testing.T) { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, job) + }) + go http.ListenAndServe("127.0.0.1:12345", nil) + + // Wait until HTTP Server starts certainly + time.Sleep(100 * time.Millisecond) + + j := &JobGetter{} + sj, err := j.StructJob("http://127.0.0.1:12345/") + if err != nil { + t.Fatalf("err: %s", err) + } + + err = sj.Validate() + if err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/command/plan.go b/command/plan.go index fc1bff4592a7..412a3e3e67a6 100644 --- a/command/plan.go +++ b/command/plan.go @@ -2,14 +2,11 @@ package command import ( "fmt" - "io" - "os" "sort" "strings" "time" "github.com/hashicorp/nomad/api" - "github.com/hashicorp/nomad/jobspec" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/scheduler" "github.com/mitchellh/colorstring" @@ -28,10 +25,8 @@ potentially invalid.` type PlanCommand struct { Meta + JobGetter color *colorstring.Colorize - - // The fields below can be overwritten for tests - testStdin io.Reader } func (c *PlanCommand) Help() string { @@ -44,7 +39,8 @@ Usage: nomad plan [options] successfully and how it would affect existing allocations. If the supplied path is "-", the jobfile is read from stdin. Otherwise - it is read from the file at the supplied path. + it is read from the file at the supplied path or downloaded and + read from URL specified. A job modify index is returned with the plan. This value can be used when submitting the job using "nomad run -check-index", which will check that the job @@ -101,32 +97,11 @@ func (c *PlanCommand) Run(args []string) int { return 255 } - // Read the Jobfile path := args[0] - - var f io.Reader - switch path { - case "-": - if c.testStdin != nil { - f = c.testStdin - } else { - f = os.Stdin - } - path = "stdin" - default: - file, err := os.Open(path) - defer file.Close() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error opening file %q: %v", path, err)) - return 255 - } - f = file - } - - // Parse the JobFile - job, err := jobspec.Parse(f) + // Get Job struct from Jobfile + job, err := c.JobGetter.StructJob(args[0]) if err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing job file %s: %v", path, err)) + c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err)) return 255 } diff --git a/command/plan_test.go b/command/plan_test.go index 3ec790dba0f6..79793301e797 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -30,8 +30,8 @@ func TestPlanCommand_Fails(t *testing.T) { if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 255 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error opening") { - t.Fatalf("expect parsing error, got: %s", out) + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { + t.Fatalf("expect getting job struct error, got: %s", out) } ui.ErrorWriter.Reset() @@ -47,8 +47,8 @@ func TestPlanCommand_Fails(t *testing.T) { if code := cmd.Run([]string{fh1.Name()}); code != 255 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error parsing") { - t.Fatalf("expect parsing error, got: %s", err) + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { + t.Fatalf("expect parsing error, got: %s", out) } ui.ErrorWriter.Reset() @@ -111,7 +111,7 @@ func TestPlanCommand_From_STDIN(t *testing.T) { ui := new(cli.MockUi) cmd := &PlanCommand{ Meta: Meta{Ui: ui}, - testStdin: stdinR, + JobGetter: JobGetter{testStdin: stdinR}, } go func() { @@ -136,11 +136,27 @@ job "job1" { args := []string{"-"} if code := cmd.Run(args); code != 255 { - t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String()) + t.Fatalf("expected exit code 255, got %d: %q", code, ui.ErrorWriter.String()) } if out := ui.ErrorWriter.String(); !strings.Contains(out, "connection refused") { - t.Fatalf("expected runtime error, got: %s", out) + t.Fatalf("expected connection refused error, got: %s", out) } ui.ErrorWriter.Reset() } + +func TestPlanCommand_From_URL(t *testing.T) { + ui := new(cli.MockUi) + cmd := &RunCommand{ + Meta: Meta{Ui: ui}, + } + + args := []string{"https://example.com/foo/bar"} + if code := cmd.Run(args); code != 1 { + t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String()) + } + + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") { + t.Fatalf("expected error getting jobfile, got: %s", out) + } +} diff --git a/command/run.go b/command/run.go index e0ed219a5a40..3395eb3d09c3 100644 --- a/command/run.go +++ b/command/run.go @@ -5,15 +5,12 @@ import ( "encoding/gob" "encoding/json" "fmt" - "io" - "os" "regexp" "strconv" "strings" "time" "github.com/hashicorp/nomad/api" - "github.com/hashicorp/nomad/jobspec" "github.com/hashicorp/nomad/nomad/structs" ) @@ -24,9 +21,7 @@ var ( type RunCommand struct { Meta - - // The fields below can be overwritten for tests - testStdin io.Reader + JobGetter } func (c *RunCommand) Help() string { @@ -38,7 +33,8 @@ Usage: nomad run [options] used to interact with Nomad. If the supplied path is "-", the jobfile is read from stdin. Otherwise - it is read from the file at the supplied path. + it is read from the file at the supplied path or downloaded and + read from URL specified. Upon successful job submission, this command will immediately enter an interactive monitor. This is useful to watch Nomad's @@ -116,32 +112,17 @@ func (c *RunCommand) Run(args []string) int { return 1 } - // Read the Jobfile - path := args[0] - - var f io.Reader - switch path { - case "-": - if c.testStdin != nil { - f = c.testStdin - } else { - f = os.Stdin - } - path = "stdin" - default: - file, err := os.Open(path) - defer file.Close() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error opening file %q: %v", path, err)) - return 1 - } - f = file + // Check that we got exactly one node + args = flags.Args() + if len(args) != 1 { + c.Ui.Error(c.Help()) + return 1 } - // Parse the JobFile - job, err := jobspec.Parse(f) + // Get Job struct from Jobfile + job, err := c.JobGetter.StructJob(args[0]) if err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing job file from %s: %v", path, err)) + c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err)) return 1 } diff --git a/command/run_test.go b/command/run_test.go index a606b7676823..840925e456ed 100644 --- a/command/run_test.go +++ b/command/run_test.go @@ -66,8 +66,8 @@ func TestRunCommand_Fails(t *testing.T) { if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 1 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error opening") { - t.Fatalf("expect parsing error, got: %s", out) + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { + t.Fatalf("expect getting job struct error, got: %s", out) } ui.ErrorWriter.Reset() @@ -83,8 +83,8 @@ func TestRunCommand_Fails(t *testing.T) { if code := cmd.Run([]string{fh1.Name()}); code != 1 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error parsing") { - t.Fatalf("expect parsing error, got: %s", err) + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { + t.Fatalf("expect parsing error, got: %s", out) } ui.ErrorWriter.Reset() @@ -157,7 +157,7 @@ func TestRunCommand_From_STDIN(t *testing.T) { ui := new(cli.MockUi) cmd := &RunCommand{ Meta: Meta{Ui: ui}, - testStdin: stdinR, + JobGetter: JobGetter{testStdin: stdinR}, } go func() { @@ -186,7 +186,23 @@ job "job1" { } if out := ui.ErrorWriter.String(); !strings.Contains(out, "connection refused") { - t.Fatalf("expected runtime error, got: %s", out) + t.Fatalf("expected connection refused error, got: %s", out) } ui.ErrorWriter.Reset() } + +func TestRunCommand_From_URL(t *testing.T) { + ui := new(cli.MockUi) + cmd := &RunCommand{ + Meta: Meta{Ui: ui}, + } + + args := []string{"https://example.com/foo/bar"} + if code := cmd.Run(args); code != 1 { + t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String()) + } + + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") { + t.Fatalf("expected error getting jobfile, got: %s", out) + } +} diff --git a/command/validate.go b/command/validate.go index 98abaf53496e..6557c3ee8f97 100644 --- a/command/validate.go +++ b/command/validate.go @@ -2,18 +2,12 @@ package command import ( "fmt" - "io" - "os" "strings" - - "github.com/hashicorp/nomad/jobspec" ) type ValidateCommand struct { Meta - - // The fields below can be overwritten for tests - testStdin io.Reader + JobGetter } func (c *ValidateCommand) Help() string { @@ -24,8 +18,8 @@ Usage: nomad validate [options] check for any syntax errors or validation problems with a job. If the supplied path is "-", the jobfile is read from stdin. Otherwise - it is read from the file at the supplied path. - + it is read from the file at the supplied path or downloaded and + read from URL specified. ` return strings.TrimSpace(helpText) } @@ -48,32 +42,10 @@ func (c *ValidateCommand) Run(args []string) int { return 1 } - // Read the Jobfile - path := args[0] - - var f io.Reader - switch path { - case "-": - if c.testStdin != nil { - f = c.testStdin - } else { - f = os.Stdin - } - path = "stdin" - default: - file, err := os.Open(path) - defer file.Close() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error opening file %q: %v", path, err)) - return 1 - } - f = file - } - - // Parse the JobFile - job, err := jobspec.Parse(f) + // Get Job struct from Jobfile + job, err := c.JobGetter.StructJob(args[0]) if err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing job file from %s: %v", path, err)) + c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err)) return 1 } diff --git a/command/validate_test.go b/command/validate_test.go index d566cc36a584..ff9f42b5e65d 100644 --- a/command/validate_test.go +++ b/command/validate_test.go @@ -63,8 +63,8 @@ func TestValidateCommand_Fails(t *testing.T) { if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 1 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error opening") { - t.Fatalf("expect parsing error, got: %s", out) + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { + t.Fatalf("expect getting job struct error, got: %s", out) } ui.ErrorWriter.Reset() @@ -80,8 +80,8 @@ func TestValidateCommand_Fails(t *testing.T) { if code := cmd.Run([]string{fh1.Name()}); code != 1 { t.Fatalf("expect exit 1, got: %d", code) } - if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error parsing") { - t.Fatalf("expect parsing error, got: %s", err) + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting job struct") { + t.Fatalf("expect parsing error, got: %s", out) } ui.ErrorWriter.Reset() @@ -112,7 +112,7 @@ func TestValidateCommand_From_STDIN(t *testing.T) { ui := new(cli.MockUi) cmd := &ValidateCommand{ Meta: Meta{Ui: ui}, - testStdin: stdinR, + JobGetter: JobGetter{testStdin: stdinR}, } go func() { @@ -141,3 +141,19 @@ job "job1" { } ui.ErrorWriter.Reset() } + +func TestValidateCommand_From_URL(t *testing.T) { + ui := new(cli.MockUi) + cmd := &RunCommand{ + Meta: Meta{Ui: ui}, + } + + args := []string{"https://example.com/foo/bar"} + if code := cmd.Run(args); code != 1 { + t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String()) + } + + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error getting jobfile") { + t.Fatalf("expected error getting jobfile, got: %s", out) + } +} diff --git a/website/source/docs/commands/plan.html.md.erb b/website/source/docs/commands/plan.html.md.erb index cafd3895296b..c000113a9a19 100644 --- a/website/source/docs/commands/plan.html.md.erb +++ b/website/source/docs/commands/plan.html.md.erb @@ -19,6 +19,14 @@ format. nomad plan [options] ``` +The plan command requires a single argument, specifying the path to a file +containing a [HCL job specification](/docs/jobspec/index.html). This file +will be read and the job checked for any problems. If the +supplied path is "-", the jobfile is read from STDIN. Otherwise it is read +from the file at the supplied path or downloaded and read from URL specified. +Nomad downloads jobfile using [`go-getter`](https://github.com/hashicorp/go-getter) +and support `go-getter` syntax. + Plan invokes a dry-run of the scheduler to determine the effects of submitting either a new or updated version of a job. The plan will not result in any changes to the cluster but gives insight into whether the job could be run diff --git a/website/source/docs/commands/run.html.md.erb b/website/source/docs/commands/run.html.md.erb index 7004161706b3..e3590b3965f4 100644 --- a/website/source/docs/commands/run.html.md.erb +++ b/website/source/docs/commands/run.html.md.erb @@ -22,7 +22,9 @@ The run command requires a single argument, specifying the path to a file containing a valid [job specification](/docs/jobspec/index.html). This file will be read and the job will be submitted to Nomad for scheduling. If the supplied path is "-", the jobfile is read from STDIN. Otherwise it is read -from the file at the supplied path. +from the file at the supplied path or downloaded and read from URL specified. +Nomad downloads jobfile using [`go-getter`](https://github.com/hashicorp/go-getter) +and support `go-getter` syntax. By default, on successful job submission the run command will enter an interactive monitor and display log information detailing the scheduling diff --git a/website/source/docs/commands/validate.html.md.erb b/website/source/docs/commands/validate.html.md.erb index fce0d703099a..5a05f4568a36 100644 --- a/website/source/docs/commands/validate.html.md.erb +++ b/website/source/docs/commands/validate.html.md.erb @@ -19,7 +19,11 @@ nomad validate The validate command requires a single argument, specifying the path to a file containing a [HCL job specification](/docs/jobspec/index.html). This file -will be read and the job checked for any problems. +will be read and the job checked for any problems. If the +supplied path is "-", the jobfile is read from STDIN. Otherwise it is read +from the file at the supplied path or downloaded and read from URL specified. +Nomad downloads jobfile using [`go-getter`](https://github.com/hashicorp/go-getter) +and support `go-getter` syntax. On successful validation, exit code 0 will be returned, otherwise an exit code of 1 indicates an error.