Skip to content

Commit

Permalink
Add plan command to allow running a plan whilst using templating.
Browse files Browse the repository at this point in the history
When performing larger job changes; Nomad plan provides useful
insights into what changes will happen. Previously Levant would run
a plan during the deploy command run, but would move straight onto
the deploy part. The plan command addition means you can render a
templated job and see what changes will be made without triggering
a deployment.

Closes #229
  • Loading branch information
jrasell committed Sep 18, 2018
1 parent 85f932d commit d404126
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 91 deletions.
59 changes: 38 additions & 21 deletions command/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"strings"

nomad "github.com/hashicorp/nomad/api"

"github.com/jrasell/levant/helper"
"github.com/jrasell/levant/levant"
"github.com/jrasell/levant/levant/structs"
Expand Down Expand Up @@ -91,38 +90,44 @@ func (c *DeployCommand) Synopsis() string {
func (c *DeployCommand) Run(args []string) int {

var err error
var addr string
config := &structs.Config{}
var level, format string

config := &levant.DeployConfig{
Client: &structs.ClientConfig{},
Deploy: &structs.DeployConfig{},
Plan: &structs.PlanConfig{},
Template: &structs.TemplateConfig{},
}

flags := c.Meta.FlagSet("deploy", FlagSetVars)
flags.Usage = func() { c.UI.Output(c.Help()) }

flags.StringVar(&config.Addr, "address", "", "")
flags.BoolVar(&config.AllowStale, "allow-stale", false, "")
flags.IntVar(&config.Canary, "canary-auto-promote", 0, "")
flags.StringVar(&addr, "consul-address", "", "")
flags.BoolVar(&config.ForceBatch, "force-batch", false, "")
flags.BoolVar(&config.ForceCount, "force-count", false, "")
flags.BoolVar(&config.IgnoreNoChanges, "ignore-no-changes", false, "")
flags.StringVar(&config.LogLevel, "log-level", "INFO", "")
flags.StringVar(&config.LogFormat, "log-format", "HUMAN", "")
flags.Var((*helper.FlagStringSlice)(&config.VariableFiles), "var-file", "")
flags.StringVar(&config.Client.Addr, "address", "", "")
flags.BoolVar(&config.Client.AllowStale, "allow-stale", false, "")
flags.IntVar(&config.Deploy.Canary, "canary-auto-promote", 0, "")
flags.StringVar(&config.Client.ConsulAddr, "consul-address", "", "")
flags.BoolVar(&config.Deploy.ForceBatch, "force-batch", false, "")
flags.BoolVar(&config.Deploy.ForceCount, "force-count", false, "")
flags.BoolVar(&config.Plan.IgnoreNoChanges, "ignore-no-changes", false, "")
flags.StringVar(&level, "log-level", "INFO", "")
flags.StringVar(&format, "log-format", "HUMAN", "")
flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "")

if err = flags.Parse(args); err != nil {
return 1
}

args = flags.Args()

if err = logging.SetupLogger(config.LogLevel, config.LogFormat); err != nil {
if err = logging.SetupLogger(level, format); err != nil {
c.UI.Error(err.Error())
return 1
}

if len(args) == 1 {
config.TemplateFile = args[0]
config.Template.TemplateFile = args[0]
} else if len(args) == 0 {
if config.TemplateFile = helper.GetDefaultTmplFile(); config.TemplateFile == "" {
if config.Template.TemplateFile = helper.GetDefaultTmplFile(); config.Template.TemplateFile == "" {
c.UI.Error(c.Help())
c.UI.Error("\nERROR: Template arg missing and no default template found")
return 1
Expand All @@ -132,26 +137,38 @@ func (c *DeployCommand) Run(args []string) int {
return 1
}

config.Job, err = template.RenderJob(config.TemplateFile, config.VariableFiles, addr, &c.Meta.flagVars)
config.Template.Job, err = template.RenderJob(config.Template.TemplateFile,
config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars)
if err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
return 1
}

if config.Canary > 0 {
if err = c.checkCanaryAutoPromote(config.Job, config.Canary); err != nil {
if config.Deploy.Canary > 0 {
if err = c.checkCanaryAutoPromote(config.Template.Job, config.Deploy.Canary); err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
return 1
}
}

if config.ForceBatch {
if err = c.checkForceBatch(config.Job, config.ForceBatch); err != nil {
if config.Deploy.ForceBatch {
if err = c.checkForceBatch(config.Template.Job, config.Deploy.ForceBatch); err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
return 1
}
}

p := levant.PlanConfig{
Client: config.Client,
Plan: config.Plan,
Template: config.Template,
}

planSuccess := levant.TriggerPlan(&p)
if !planSuccess {
return 1
}

success := levant.TriggerDeployment(config, nil)
if !success {
return 1
Expand Down
140 changes: 140 additions & 0 deletions command/plan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package command

import (
"fmt"
"strings"

"github.com/jrasell/levant/helper"
"github.com/jrasell/levant/levant"
"github.com/jrasell/levant/levant/structs"
"github.com/jrasell/levant/logging"
"github.com/jrasell/levant/template"
)

// PlanCommand is the command implementation that allows users to plan a
// Nomad job based on passed templates and variables.
type PlanCommand struct {
Meta
}

// Help provides the help information for the plan command.
func (c *PlanCommand) Help() string {
helpText := `
Usage: levant plan [options] [TEMPLATE]
Perform a Nomad plan based on input templates and variable files. The plan
command supports passing variables individually on the command line. Multiple
commands can be passed in the format of -var 'key=value'. Variables passed
via the command line take precedence over the same variable declared within
a passed variable file.
Arguments:
TEMPLATE nomad job template
If no argument is given we look for a single *.nomad file
General Options:
-address=<http_address>
The Nomad HTTP API address including port which Levant will use to make
calls.
-allow-stale
Allow stale consistency mode for requests into nomad.
-consul-address=<addr>
The Consul host and port to use when making Consul KeyValue lookups for
template rendering.
-force-count
Use the taskgroup count from the Nomad jobfile instead of the count that
is currently set in a running job.
-ignore-no-changes
By default if no changes are detected when running a plan Levant will
exit with a status 1 to indicate there are no changes. This behaviour
can be changed using this flag so that Levant will exit cleanly ensuring CD
pipelines don't fail when no changes are detected.
-log-level=<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.
-log-format=<format>
Specify the format of Levant's logs. Valid values are HUMAN or JSON. The
default is HUMAN.
-var-file=<file>
Used in conjunction with the -job-file will plan a templated job against your
Nomad cluster. You can repeat this flag multiple times to supply multiple var-files.
[default: levant.(json|yaml|yml|tf)]
`
return strings.TrimSpace(helpText)
}

// Synopsis is provides a brief summary of the plan command.
func (c *PlanCommand) Synopsis() string {
return "Render and perform a Nomad job plan from a template"
}

// Run triggers a run of the Levant template and plan functions.
func (c *PlanCommand) Run(args []string) int {

var err error
var level, format string
config := &levant.PlanConfig{
Client: &structs.ClientConfig{},
Plan: &structs.PlanConfig{},
Template: &structs.TemplateConfig{},
}

flags := c.Meta.FlagSet("plan", FlagSetVars)
flags.Usage = func() { c.UI.Output(c.Help()) }

flags.StringVar(&config.Client.Addr, "address", "", "")
flags.BoolVar(&config.Client.AllowStale, "allow-stale", false, "")
flags.StringVar(&config.Client.ConsulAddr, "consul-address", "", "")
flags.BoolVar(&config.Plan.IgnoreNoChanges, "ignore-no-changes", false, "")
flags.StringVar(&level, "log-level", "INFO", "")
flags.StringVar(&format, "log-format", "HUMAN", "")
flags.Var((*helper.FlagStringSlice)(&config.Template.VariableFiles), "var-file", "")

if err = flags.Parse(args); err != nil {
return 1
}

args = flags.Args()

if err = logging.SetupLogger(level, format); err != nil {
c.UI.Error(err.Error())
return 1
}

if len(args) == 1 {
config.Template.TemplateFile = args[0]
} else if len(args) == 0 {
if config.Template.TemplateFile = helper.GetDefaultTmplFile(); config.Template.TemplateFile == "" {
c.UI.Error(c.Help())
c.UI.Error("\nERROR: Template arg missing and no default template found")
return 1
}
} else {
c.UI.Error(c.Help())
return 1
}

config.Template.Job, err = template.RenderJob(config.Template.TemplateFile,
config.Template.VariableFiles, config.Client.ConsulAddr, &c.Meta.flagVars)

if err != nil {
c.UI.Error(fmt.Sprintf("[ERROR] levant/command: %v", err))
return 1
}

success := levant.TriggerPlan(config)
if !success {
return 1
}

return 0
}
5 changes: 5 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"plan": func() (cli.Command, error) {
return &command.PlanCommand{
Meta: meta,
}, nil
},
"render": func() (cli.Command, error) {
return &command.RenderCommand{
Meta: meta,
Expand Down
Loading

0 comments on commit d404126

Please sign in to comment.