Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support run/plan jobfile via URL location #1511

Merged
merged 9 commits into from
Aug 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions command/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
67 changes: 67 additions & 0 deletions command/helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package command

import (
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -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)
}
}
37 changes: 6 additions & 31 deletions command/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -44,7 +39,8 @@ Usage: nomad plan [options] <file>
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
Expand Down Expand Up @@ -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
}

Expand Down
30 changes: 23 additions & 7 deletions command/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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()

Expand Down Expand Up @@ -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() {
Expand All @@ -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)
}
}
41 changes: 11 additions & 30 deletions command/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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 {
Expand All @@ -38,7 +33,8 @@ Usage: nomad run [options] <path>
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
Expand Down Expand Up @@ -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
}

Expand Down
Loading