From 3bb508c0b5fb8bfd032195f3198c05f3d1be8db9 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 16 Sep 2015 13:58:33 -0700 Subject: [PATCH] command: start implementing eval monitoring for run --- command/monitor.go | 118 +++++++++++++++++++++++++++++++++++++++++++++ command/run.go | 28 +++++++++-- commands.go | 11 +++-- 3 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 command/monitor.go diff --git a/command/monitor.go b/command/monitor.go new file mode 100644 index 000000000..ed1173303 --- /dev/null +++ b/command/monitor.go @@ -0,0 +1,118 @@ +package command + +import ( + "fmt" + "sync" + "time" + + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/nomad/structs" + "github.com/mitchellh/cli" +) + +const ( + // dateFmt is the format we use when printing the date in + // status update messages during monitoring. + dateFmt = "2006/01/02 15:04:05" +) + +// monitor wraps an evaluation monitor and holds metadata and +// state information. +type monitor struct { + ui cli.Ui + client *api.Client + state *monitorState + + sync.Mutex +} + +// newMonitor returns a new monitor. The returned monitor will +// write output information to the provided ui. +func newMonitor(ui cli.Ui, client *api.Client) *monitor { + return &monitor{ + ui: ui, + client: client, + state: new(monitorState), + } +} + +// output is used to write informational messages to the ui. +func (m *monitor) output(msg string) { + m.ui.Output(fmt.Sprintf("%s %s", time.Now().Format(dateFmt), msg)) +} + +// monitorState is used to store the current "state of the world" +// in the context of monitoring an evaluation. +type monitorState struct { + status string + nodeID string + wait time.Duration +} + +// update is used to update our monitor with new state. It can be +// called whether the passed information is new or not, and will +// only dump update messages when state changes. +func (m *monitor) update(eval *api.Evaluation) { + m.Lock() + defer m.Unlock() + + existing := m.state + + // Create the new state + update := &monitorState{ + status: eval.Status, + nodeID: eval.NodeID, + wait: eval.Wait, + } + defer func() { m.state = update }() + + // Check if the status changed + if existing.status != update.status { + m.output(fmt.Sprintf("Evaluation changed status from %q to %q", + existing.status, eval.Status)) + } + + // Check if the wait time is different + if existing.wait == 0 && update.wait != 0 { + m.output(fmt.Sprintf("Waiting %s before running eval", + eval.Wait)) + } + + // Check if the nodeID changed + if existing.nodeID == "" && update.nodeID != "" { + m.output(fmt.Sprintf("Evaluation was assigned node ID %q", + eval.NodeID)) + } +} + +// monitor is used to start monitoring the given evaluation ID. It +// writes output directly to the monitor's ui, and returns the +// exit code for the command. The return code is 0 if monitoring +// succeeded and exited successfully, or 1 if an error was encountered +// or the eval status was returned as failed. +func (m *monitor) monitor(evalID string) int { + for { + // Check the current state of things + eval, _, err := m.client.Evaluations().Info(evalID, nil) + if err != nil { + m.ui.Error(fmt.Sprintf("Error reading evaluation: %s", err)) + return 1 + } + + // Update the state + m.update(eval) + + // Check if the eval is complete + switch eval.Status { + case structs.EvalStatusComplete: + return 0 + case structs.EvalStatusFailed: + return 1 + } + + // Wait for the next poll + time.Sleep(time.Second) + } + + return 0 +} diff --git a/command/run.go b/command/run.go index 300859946..59caeef57 100644 --- a/command/run.go +++ b/command/run.go @@ -24,7 +24,16 @@ Usage: nomad run [options] General Options: - ` + generalOptionsUsage() + ` + generalOptionsUsage() + ` + +Run Options: + + -monitor + On successful job completion, immediately begin monitoring the + evaluation created by the job registration. This mode will + enter an interactive session where status is printed to the + screen, similar to the "tail" UNIX command. +` return strings.TrimSpace(helpText) } @@ -33,8 +42,12 @@ func (c *RunCommand) Synopsis() string { } func (c *RunCommand) Run(args []string) int { + var monitor bool + flags := c.Meta.FlagSet("run", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&monitor, "monitor", false, "") + if err := flags.Parse(args); err != nil { return 1 } @@ -75,9 +88,16 @@ func (c *RunCommand) Run(args []string) int { return 1 } - c.Ui.Info("Job registered successfully!\n") - c.Ui.Info("JobID = " + job.ID) - c.Ui.Info("EvalID = " + evalID) + // Check if we should enter monitor mode + if monitor { + mon := newMonitor(c.Ui, client) + return mon.monitor(evalID) + } + + // By default just print some info and return + c.Ui.Output("Job registered successfully!\n") + c.Ui.Output("JobID = " + job.ID) + c.Ui.Output("EvalID = " + evalID) return 0 } diff --git a/commands.go b/commands.go index 3916d6eed..de795baf1 100644 --- a/commands.go +++ b/commands.go @@ -17,9 +17,14 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { meta := *metaPtr if meta.Ui == nil { - meta.Ui = &cli.BasicUi{ - Writer: os.Stdout, - ErrorWriter: os.Stderr, + meta.Ui = &cli.PrefixedUi{ + InfoPrefix: "==> ", + OutputPrefix: "", + ErrorPrefix: "", + Ui: &cli.BasicUi{ + Writer: os.Stdout, + ErrorWriter: os.Stderr, + }, } }