From 52347d42f9e2cbc0cfc4e13ae7b0c568e6a55ab5 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 21 Mar 2016 12:46:35 -0700 Subject: [PATCH 1/2] Add nomad inspect command --- api/api.go | 30 ++++++++++++++ api/jobs.go | 14 +++++++ api/jobs_test.go | 33 ++++++++++++++++ command/inspect.go | 88 +++++++++++++++++++++++++++++++++++++++++ command/inspect_test.go | 46 +++++++++++++++++++++ commands.go | 6 ++- 6 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 command/inspect.go create mode 100644 command/inspect_test.go diff --git a/api/api.go b/api/api.go index 32d9b87b0f2..f21455bbaa6 100644 --- a/api/api.go +++ b/api/api.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "net/url" "os" @@ -34,6 +35,9 @@ type QueryOptions struct { // If set, used as prefix for resource list searches Prefix string + + // If set, pretty print the response json. + Pretty bool } // WriteOptions are used to parameterize a write @@ -156,6 +160,9 @@ func (r *request) setQueryOptions(q *QueryOptions) { if q.Prefix != "" { r.params.Set("prefix", q.Prefix) } + if q.Pretty { + r.params.Set("pretty", "true") + } } // durToMsec converts a duration to a millisecond specified string @@ -265,6 +272,29 @@ func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*Quer return qm, nil } +// rawQuery is used to do a GET request against an endpoint and return the raw +// string result. +func (c *Client) rawQuery(endpoint string, q *QueryOptions) (string, *QueryMeta, error) { + r := c.newRequest("GET", endpoint) + r.setQueryOptions(q) + rtt, resp, err := requireOK(c.doRequest(r)) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + raw, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", nil, err + } + + return string(raw), qm, nil +} + // write is used to do a PUT request against an endpoint // and serialize/deserialized using the standard Nomad conventions. func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) { diff --git a/api/jobs.go b/api/jobs.go index a00c3a169d1..1af973dd9df 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -63,6 +63,20 @@ func (j *Jobs) Info(jobID string, q *QueryOptions) (*Job, *QueryMeta, error) { return &resp, qm, nil } +// RawJob is used to retrieve information about a particular +// job given its unique ID and return the raw json. +func (j *Jobs) RawJob(jobID string, q *QueryOptions) (string, *QueryMeta, error) { + if q == nil { + q = &QueryOptions{} + } + q.Pretty = true + raw, qm, err := j.client.rawQuery("/v1/job/"+jobID, q) + if err != nil { + return "", nil, err + } + return raw, qm, nil +} + // Allocations is used to return the allocs for a given job ID. func (j *Jobs) Allocations(jobID string, q *QueryOptions) ([]*AllocationListStub, *QueryMeta, error) { var resp []*AllocationListStub diff --git a/api/jobs_test.go b/api/jobs_test.go index cb998ac8942..d696f2c6e67 100644 --- a/api/jobs_test.go +++ b/api/jobs_test.go @@ -83,6 +83,39 @@ func TestJobs_Info(t *testing.T) { } } +func TestJobs_RawJob(t *testing.T) { + c, s := makeClient(t, nil, nil) + defer s.Stop() + jobs := c.Jobs() + + // Trying to retrieve a job by ID before it exists + // returns an error + _, _, err := jobs.RawJob("job1", nil) + if err == nil || !strings.Contains(err.Error(), "not found") { + t.Fatalf("expected not found error, got: %#v", err) + } + + // Register the job + job := testJob() + _, wm, err := jobs.Register(job, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertWriteMeta(t, wm) + + // Query the job again and ensure it exists + result, qm, err := jobs.RawJob("job1", nil) + if err != nil { + t.Fatalf("err: %s", err) + } + assertQueryMeta(t, qm) + + // Check that the result is what we expect + if result == "" || !strings.Contains(result, job.ID) { + t.Fatalf("expect: %#v, got: %#v", job, result) + } +} + func TestJobs_PrefixList(t *testing.T) { c, s := makeClient(t, nil, nil) defer s.Stop() diff --git a/command/inspect.go b/command/inspect.go new file mode 100644 index 00000000000..03f27f6e5d4 --- /dev/null +++ b/command/inspect.go @@ -0,0 +1,88 @@ +package command + +import ( + "fmt" + "strings" +) + +type InspectCommand struct { + Meta +} + +func (c *InspectCommand) Help() string { + helpText := ` +Usage: nomad inspect [options] + + Inspect is used to see the specification of a submitted job. + +General Options: + + ` + generalOptionsUsage() + + return strings.TrimSpace(helpText) +} + +func (c *InspectCommand) Synopsis() string { + return "Inspect a submitted job" +} + +func (c *InspectCommand) Run(args []string) int { + flags := c.Meta.FlagSet("inspect", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got exactly one job + args = flags.Args() + if len(args) != 1 { + c.Ui.Error(c.Help()) + return 1 + } + jobID := args[0] + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Check if the job exists + job, _, err := client.Jobs().RawJob(jobID, nil) + if err != nil { + jobs, _, err := client.Jobs().PrefixList(jobID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err)) + return 1 + } + if len(jobs) == 0 { + c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) + return 1 + } + if len(jobs) > 1 { + out := make([]string, len(jobs)+1) + out[0] = "ID|Type|Priority|Status" + for i, job := range jobs { + out[i+1] = fmt.Sprintf("%s|%s|%d|%s", + job.ID, + job.Type, + job.Priority, + job.Status) + } + c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single job + job, _, err = client.Jobs().RawJob(jobs[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err)) + return 1 + } + } + + // Print the contents of the job + c.Ui.Output(job) + return 0 +} diff --git a/command/inspect_test.go b/command/inspect_test.go new file mode 100644 index 00000000000..ba7539abe99 --- /dev/null +++ b/command/inspect_test.go @@ -0,0 +1,46 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func TestInspectCommand_Implements(t *testing.T) { + var _ cli.Command = &InspectCommand{} +} + +func TestInspectCommand_Fails(t *testing.T) { + srv, _, url := testServer(t, nil) + defer srv.Stop() + + ui := new(cli.MockUi) + cmd := &InspectCommand{Meta: Meta{Ui: ui}} + + // Fails on misuse + if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 { + t.Fatalf("expected exit code 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) { + t.Fatalf("expected help output, got: %s", out) + } + ui.ErrorWriter.Reset() + + // Fails on non-existent job ID + if code := cmd.Run([]string{"-address=" + url, "nope"}); code != 1 { + t.Fatalf("expect exit 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, "No job(s) with prefix or id") { + t.Fatalf("expect not found error, got: %s", out) + } + ui.ErrorWriter.Reset() + + // Fails on connection failure + if code := cmd.Run([]string{"-address=nope", "nope"}); code != 1 { + t.Fatalf("expected exit code 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error inspecting job") { + t.Fatalf("expected failed query error, got: %s", out) + } +} diff --git a/commands.go b/commands.go index c77c8482a9c..7a6709d12cb 100644 --- a/commands.go +++ b/commands.go @@ -87,7 +87,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Meta: meta, }, nil }, - + "inspect": func() (cli.Command, error) { + return &command.InspectCommand{ + Meta: meta, + }, nil + }, "node-drain": func() (cli.Command, error) { return &command.NodeDrainCommand{ Meta: meta, From eec0e3e9d6cec723c33d866a6f9511e6206c27b3 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 21 Mar 2016 13:00:14 -0700 Subject: [PATCH 2/2] inspect/stop uses prefix search --- command/inspect.go | 54 ++++++++++++++++++++++------------------------ command/stop.go | 53 +++++++++++++++++++++------------------------ 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/command/inspect.go b/command/inspect.go index 03f27f6e5d4..a3843016445 100644 --- a/command/inspect.go +++ b/command/inspect.go @@ -50,36 +50,34 @@ func (c *InspectCommand) Run(args []string) int { } // Check if the job exists - job, _, err := client.Jobs().RawJob(jobID, nil) + jobs, _, err := client.Jobs().PrefixList(jobID) if err != nil { - jobs, _, err := client.Jobs().PrefixList(jobID) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err)) - return 1 - } - if len(jobs) == 0 { - c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) - return 1 - } - if len(jobs) > 1 { - out := make([]string, len(jobs)+1) - out[0] = "ID|Type|Priority|Status" - for i, job := range jobs { - out[i+1] = fmt.Sprintf("%s|%s|%d|%s", - job.ID, - job.Type, - job.Priority, - job.Status) - } - c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out))) - return 0 - } - // Prefix lookup matched a single job - job, _, err = client.Jobs().RawJob(jobs[0].ID, nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err)) - return 1 + c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err)) + return 1 + } + if len(jobs) == 0 { + c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) + return 1 + } + if len(jobs) > 1 { + out := make([]string, len(jobs)+1) + out[0] = "ID|Type|Priority|Status" + for i, job := range jobs { + out[i+1] = fmt.Sprintf("%s|%s|%d|%s", + job.ID, + job.Type, + job.Priority, + job.Status) } + c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out))) + return 0 + } + + // Prefix lookup matched a single job + job, _, err := client.Jobs().RawJob(jobs[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error inspecting job: %s", err)) + return 1 } // Print the contents of the job diff --git a/command/stop.go b/command/stop.go index dfea7ffbee5..b970ca847d4 100644 --- a/command/stop.go +++ b/command/stop.go @@ -75,36 +75,33 @@ func (c *StopCommand) Run(args []string) int { } // Check if the job exists - job, _, err := client.Jobs().Info(jobID, nil) + jobs, _, err := client.Jobs().PrefixList(jobID) if err != nil { - jobs, _, err := client.Jobs().PrefixList(jobID) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) - return 1 - } - if len(jobs) == 0 { - c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) - return 1 - } - if len(jobs) > 1 { - out := make([]string, len(jobs)+1) - out[0] = "ID|Type|Priority|Status" - for i, job := range jobs { - out[i+1] = fmt.Sprintf("%s|%s|%d|%s", - job.ID, - job.Type, - job.Priority, - job.Status) - } - c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out))) - return 0 - } - // Prefix lookup matched a single job - job, _, err = client.Jobs().Info(jobs[0].ID, nil) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) - return 1 + c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) + return 1 + } + if len(jobs) == 0 { + c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) + return 1 + } + if len(jobs) > 1 { + out := make([]string, len(jobs)+1) + out[0] = "ID|Type|Priority|Status" + for i, job := range jobs { + out[i+1] = fmt.Sprintf("%s|%s|%d|%s", + job.ID, + job.Type, + job.Priority, + job.Status) } + c.Ui.Output(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", formatList(out))) + return 0 + } + // Prefix lookup matched a single job + job, _, err := client.Jobs().Info(jobs[0].ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error deregistering job: %s", err)) + return 1 } // Invoke the stop