diff --git a/command/dispatch.go b/command/dispatch.go new file mode 100644 index 000000000..816152dee --- /dev/null +++ b/command/dispatch.go @@ -0,0 +1,115 @@ +package command + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + flaghelper "github.com/hashicorp/nomad/helper/flag-helpers" + "github.com/jrasell/levant/levant" + "github.com/jrasell/levant/logging" +) + +// DispatchCommand is the command implementation that allows users to +// dispatch a Nomad job. +type DispatchCommand struct { + args []string + Meta +} + +// Help provides the help information for the dispatch command. +func (c *DispatchCommand) Help() string { + helpText := ` +Usage: levant dispatch [options] [input source] + + Dispatch creates an instance of a parameterized job. A data payload to the + dispatched instance can be provided via stdin by using "-" or by specifying a + path to a file. Metadata can be supplied by using the meta flag one or more + times. + +General Options: + + -address= + The Nomad HTTP API address including port which Levant will use to make + calls. + + -log-level= + Specify the verbosity level of Levant's logs. Valid values include DEBUG, + INFO, and WARN, in decreasing order of verbosity. The default is INFO. + +Dispatch Options: + + -meta = + Meta takes a key/value pair separated by "=". The metadata key will be + merged into the job's metadata. The job may define a default value for the + key which is overridden when dispatching. The flag can be provided more + than once to inject multiple metadata key/value pairs. Arbitrary keys are + not allowed. The parameterized job must allow the key to be merged. +` + return strings.TrimSpace(helpText) +} + +// Synopsis is provides a brief summary of the dispatch command. +func (c *DispatchCommand) Synopsis() string { + return "Dispatch an instance of a parameterized job" +} + +// Run triggers a run of the Levant dispatch functions. +func (c *DispatchCommand) Run(args []string) int { + + var meta []string + var addr, logLevel string + + flags := c.Meta.FlagSet("dispatch", FlagSetVars) + flags.Usage = func() { c.UI.Output(c.Help()) } + flags.Var((*flaghelper.StringFlag)(&meta), "meta", "") + flags.StringVar(&addr, "address", "", "") + flags.StringVar(&logLevel, "log-level", "INFO", "") + + if err := flags.Parse(args); err != nil { + return 1 + } + + args = flags.Args() + if l := len(args); l < 1 || l > 2 { + c.UI.Error(c.Help()) + return 1 + } + + logging.SetLevel(logLevel) + + job := args[0] + var payload []byte + var readErr error + + if len(args) == 2 { + switch args[1] { + case "-": + payload, readErr = ioutil.ReadAll(os.Stdin) + default: + payload, readErr = ioutil.ReadFile(args[1]) + } + if readErr != nil { + c.UI.Error(fmt.Sprintf("Error reading input data: %v", readErr)) + return 1 + } + } + + metaMap := make(map[string]string, len(meta)) + for _, m := range meta { + split := strings.SplitN(m, "=", 2) + if len(split) != 2 { + c.UI.Error(fmt.Sprintf("Error parsing meta value: %v", m)) + return 1 + } + metaMap[split[0]] = split[1] + } + + success := levant.TriggerDispatch(job, metaMap, payload, addr) + if !success { + return 1 + } + + return 0 +} diff --git a/commands.go b/commands.go index 859f7c213..aec5ea0fa 100644 --- a/commands.go +++ b/commands.go @@ -31,6 +31,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "dispatch": func() (cli.Command, error) { + return &command.DispatchCommand{ + Meta: meta, + }, nil + }, "render": func() (cli.Command, error) { return &command.RenderCommand{ Meta: meta, diff --git a/levant/dispatch.go b/levant/dispatch.go new file mode 100644 index 000000000..8f9a29268 --- /dev/null +++ b/levant/dispatch.go @@ -0,0 +1,70 @@ +package levant + +import ( + nomad "github.com/hashicorp/nomad/api" + "github.com/jrasell/levant/levant/structs" + "github.com/jrasell/levant/logging" +) + +// TriggerDispatch provides the main entry point into a Levant dispatch and +// is used to setup the clients before triggering the dispatch process. +func TriggerDispatch(job string, metaMap map[string]string, payload []byte, address string) bool { + + client, err := newNomadClient(address) + if err != nil { + logging.Error("levant/dispatch: unable to setup Levant dispatch: %v", err) + return false + } + + // TODO: Potential refactor so that dispatch does not need to use the + // levantDeployment object. Requires client refactor. + dep := &levantDeployment{} + dep.config = &structs.Config{} + dep.nomad = client + + success := dep.dispatch(job, metaMap, payload) + if !success { + logging.Error("levant/dispatch: dispatch of job %v failed", job) + return false + } + + logging.Info("levant/dispatch: dispatch of job %v successful", job) + return true +} + +// dispatch triggers a new instance of a parameterized job of the job +// resulting in a Nomad job which is monitored to determine the eventual +// state. +func (l *levantDeployment) dispatch(job string, metaMap map[string]string, payload []byte) bool { + + // Initiate the dispatch with the passed meta parameters. + eval, _, err := l.nomad.Jobs().Dispatch(job, metaMap, payload, nil) + if err != nil { + logging.Error("levant/dispatch: %v", err) + return false + } + + logging.Info("levant/dispatch: triggering dispatch against job %s", job) + + // If we didn't get an EvaluationID then we cannot continue. + if eval.EvalID == "" { + logging.Error("levant/dispatch: dispatched job %s did not return evaluation", job) + return false + } + + // In order to correctly run the jobStatusChecker we need to correctly + // assign the dispatched job ID/Name based on the invoked job. + l.config.Job = &nomad.Job{} + l.config.Job.ID = &eval.DispatchedJobID + l.config.Job.Name = &eval.DispatchedJobID + + // Perform the evaluation inspection to ensure to check for any possible + // errors in triggering the dispatch job. + err = l.evaluationInspector(&eval.EvalID) + if err != nil { + logging.Error("levant/dispatch: %v", err) + return false + } + + return l.jobStatusChecker(&eval.EvalID) +}