open-nomad/command/plan.go

427 lines
11 KiB
Go
Raw Normal View History

2016-05-13 00:17:02 +00:00
package command
import (
"fmt"
"strings"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/jobspec"
"github.com/hashicorp/nomad/scheduler"
"github.com/mitchellh/colorstring"
)
2016-05-13 23:29:32 +00:00
const (
2016-05-17 05:58:13 +00:00
jobModifyIndexHelp = `To submit the job with version verification run:
2016-05-13 23:29:32 +00:00
nomad run -verify %d %s
2016-05-17 05:58:13 +00:00
When running the job with the verify flag, the job will only be run if the
server side version matches the the job modify index returned. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.`
2016-05-13 23:29:32 +00:00
)
2016-05-13 00:17:02 +00:00
type PlanCommand struct {
Meta
color *colorstring.Colorize
}
func (c *PlanCommand) Help() string {
helpText := `
Usage: nomad plan [options] <file>
General Options:
` + generalOptionsUsage() + `
Run Options:
-diff
Defaults to true, but can be toggled off to omit diff output.
-no-color
Disable colored output.
2016-05-13 19:38:12 +00:00
-verbose
Increased diff verbosity
2016-05-13 00:17:02 +00:00
`
return strings.TrimSpace(helpText)
}
func (c *PlanCommand) Synopsis() string {
return "Dry-run a job update to determine its effects"
}
func (c *PlanCommand) Run(args []string) int {
2016-05-13 19:38:12 +00:00
var diff, verbose bool
2016-05-13 00:17:02 +00:00
flags := c.Meta.FlagSet("plan", FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&diff, "diff", true, "")
2016-05-13 19:38:12 +00:00
flags.BoolVar(&verbose, "verbose", false, "")
2016-05-13 00:17:02 +00:00
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
}
file := args[0]
// Parse the job file
job, err := jobspec.ParseFile(file)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing job file %s: %s", file, err))
return 1
}
// Initialize any fields that need to be.
job.InitFields()
// Check that the job is valid
if err := job.Validate(); err != nil {
c.Ui.Error(fmt.Sprintf("Error validating job: %s", err))
return 1
}
// Convert it to something we can use
apiJob, err := convertStructJob(job)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error converting job: %s", err))
return 1
}
// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
return 1
}
// Submit the job
resp, _, err := client.Jobs().Plan(apiJob, diff, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error during plan: %s", err))
return 1
}
if diff {
2016-05-13 23:29:32 +00:00
c.Ui.Output(fmt.Sprintf("%s\n",
c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose)))))
2016-05-13 00:17:02 +00:00
}
2016-05-13 23:29:32 +00:00
c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]"))
c.Ui.Output(c.Colorize().Color(formatDryRun(resp.CreatedEvals)))
2016-05-17 05:58:13 +00:00
c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, file)))
2016-05-13 00:17:02 +00:00
return 0
}
2016-05-17 05:58:13 +00:00
func formatJobModifyIndex(jobModifyIndex uint64, jobName string) string {
help := fmt.Sprintf(jobModifyIndexHelp, jobModifyIndex, jobName)
out := fmt.Sprintf("[reset][bold]Job Modify Index: %d[reset]\n%s", jobModifyIndex, help)
2016-05-13 23:29:32 +00:00
return out
}
func formatDryRun(evals []*api.Evaluation) string {
// "- All tasks successfully allocated." bold and green
var rolling *api.Evaluation
var blocked *api.Evaluation
for _, eval := range evals {
if eval.TriggeredBy == "rolling-update" {
rolling = eval
} else if eval.Status == "blocked" {
blocked = eval
}
}
var out string
if blocked == nil {
out = "[bold][green] - All tasks successfully allocated.[reset]\n"
} else {
out = "[bold][yellow] - WARNING: Failed to place all allocations.[reset]\n"
}
if rolling != nil {
out += fmt.Sprintf("[green] - Rolling update, next evaluation will be in %s.\n", rolling.Wait)
}
return out
}
2016-05-13 19:38:12 +00:00
func formatJobDiff(job *api.JobDiff, verbose bool) string {
2016-05-17 05:58:13 +00:00
marker, _ := getDiffString(job.Type)
out := fmt.Sprintf("%s[bold]Job: %q\n", marker, job.ID)
2016-05-13 00:17:02 +00:00
2016-05-17 05:58:13 +00:00
longestField, longestMarker := getLongestPrefixes(job.Fields, job.Objects)
for _, tg := range job.TaskGroups {
if _, l := getDiffString(tg.Type); l > longestMarker {
longestMarker = l
}
}
subStartPrefix := ""
2016-05-13 19:38:12 +00:00
if job.Type == "Edited" || verbose {
2016-05-13 04:25:14 +00:00
for _, field := range job.Fields {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(field.Type)
kPrefix := longestMarker - mLength
vPrefix := longestField - len(field.Name)
out += fmt.Sprintf("%s\n", formatFieldDiff(
field,
subStartPrefix,
strings.Repeat(" ", kPrefix),
strings.Repeat(" ", vPrefix)))
2016-05-13 04:25:14 +00:00
}
2016-05-13 00:17:02 +00:00
2016-05-13 04:25:14 +00:00
for _, object := range job.Objects {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(object.Type)
kPrefix := longestMarker - mLength
out += fmt.Sprintf("%s\n", formatObjectDiff(
object,
subStartPrefix,
strings.Repeat(" ", kPrefix)))
2016-05-13 04:25:14 +00:00
}
2016-05-13 00:17:02 +00:00
}
for _, tg := range job.TaskGroups {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(tg.Type)
kPrefix := longestMarker - mLength
out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, strings.Repeat(" ", kPrefix), verbose))
2016-05-13 00:17:02 +00:00
}
return out
}
2016-05-17 05:58:13 +00:00
func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) string {
marker, _ := getDiffString(tg.Type)
out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, tgPrefix, tg.Name)
2016-05-13 00:17:02 +00:00
// Append the updates
if l := len(tg.Updates); l > 0 {
updates := make([]string, 0, l)
for updateType, count := range tg.Updates {
var color string
switch updateType {
case scheduler.UpdateTypeIgnore:
case scheduler.UpdateTypeCreate:
color = "[green]"
case scheduler.UpdateTypeDestroy:
color = "[red]"
case scheduler.UpdateTypeMigrate:
color = "[blue]"
case scheduler.UpdateTypeInplaceUpdate:
2016-05-13 19:38:12 +00:00
color = "[cyan]"
2016-05-13 00:17:02 +00:00
case scheduler.UpdateTypeDestructiveUpdate:
color = "[yellow]"
}
updates = append(updates, fmt.Sprintf("[reset]%s%d %s", color, count, updateType))
}
out += fmt.Sprintf(" (%s[reset])\n", strings.Join(updates, ", "))
} else {
out += "[reset]\n"
}
2016-05-17 05:58:13 +00:00
longestField, longestMarker := getLongestPrefixes(tg.Fields, tg.Objects)
for _, task := range tg.Tasks {
if _, l := getDiffString(task.Type); l > longestMarker {
longestMarker = l
}
}
subStartPrefix := strings.Repeat(" ", len(tgPrefix)+2)
2016-05-13 19:38:12 +00:00
if tg.Type == "Edited" || verbose {
2016-05-13 04:25:14 +00:00
for _, field := range tg.Fields {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(field.Type)
kPrefix := longestMarker - mLength
vPrefix := longestField - len(field.Name)
out += fmt.Sprintf("%s\n", formatFieldDiff(
field,
subStartPrefix,
strings.Repeat(" ", kPrefix),
strings.Repeat(" ", vPrefix)))
2016-05-13 04:25:14 +00:00
}
2016-05-13 00:17:02 +00:00
2016-05-13 04:25:14 +00:00
for _, object := range tg.Objects {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(object.Type)
kPrefix := longestMarker - mLength
out += fmt.Sprintf("%s\n", formatObjectDiff(
object,
subStartPrefix,
strings.Repeat(" ", kPrefix)))
2016-05-13 04:25:14 +00:00
}
2016-05-13 00:17:02 +00:00
}
for _, task := range tg.Tasks {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(task.Type)
prefix := strings.Repeat(" ", (longestMarker - mLength))
out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose))
2016-05-13 00:17:02 +00:00
}
return out
}
2016-05-17 05:58:13 +00:00
func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix string, verbose bool) string {
marker, _ := getDiffString(task.Type)
out := fmt.Sprintf("%s%s%s[bold]Task: %q", startPrefix, marker, taskPrefix, task.Name)
2016-05-13 00:17:02 +00:00
if len(task.Annotations) != 0 {
2016-05-13 23:29:32 +00:00
out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations))
2016-05-13 00:17:02 +00:00
}
2016-05-13 19:38:12 +00:00
if task.Type == "None" {
return out
} else if (task.Type == "Deleted" || task.Type == "Added") && !verbose {
2016-05-13 00:17:02 +00:00
return out
2016-05-13 04:25:14 +00:00
} else {
out += "\n"
2016-05-13 00:17:02 +00:00
}
2016-05-17 05:58:13 +00:00
subStartPrefix := strings.Repeat(" ", len(startPrefix)+2)
longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects)
2016-05-13 19:38:12 +00:00
for _, field := range task.Fields {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(field.Type)
kPrefix := longestMarker - mLength
vPrefix := longestField - len(field.Name)
out += fmt.Sprintf("%s\n", formatFieldDiff(
field,
subStartPrefix,
strings.Repeat(" ", kPrefix),
strings.Repeat(" ", vPrefix)))
2016-05-13 19:38:12 +00:00
}
2016-05-13 00:17:02 +00:00
2016-05-13 19:38:12 +00:00
for _, object := range task.Objects {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(object.Type)
kPrefix := longestMarker - mLength
out += fmt.Sprintf("%s\n", formatObjectDiff(
object,
subStartPrefix,
strings.Repeat(" ", kPrefix)))
2016-05-13 00:17:02 +00:00
}
return out
}
2016-05-17 05:58:13 +00:00
func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix string) string {
marker, _ := getDiffString(diff.Type)
out := fmt.Sprintf("%s%s%s%s: %s", startPrefix, marker, keyPrefix, diff.Name, valuePrefix)
2016-05-13 00:17:02 +00:00
switch diff.Type {
case "Added":
2016-05-17 05:58:13 +00:00
out += fmt.Sprintf("%q", diff.New)
2016-05-13 00:17:02 +00:00
case "Deleted":
2016-05-17 05:58:13 +00:00
out += fmt.Sprintf("%q", diff.Old)
2016-05-13 00:17:02 +00:00
case "Edited":
2016-05-17 05:58:13 +00:00
out += fmt.Sprintf("%q => %q", diff.Old, diff.New)
2016-05-13 00:17:02 +00:00
default:
2016-05-17 05:58:13 +00:00
out += fmt.Sprintf("%q", diff.New)
2016-05-13 00:17:02 +00:00
}
2016-05-13 04:25:14 +00:00
2016-05-13 23:29:32 +00:00
// Color the annotations where possible
if l := len(diff.Annotations); l != 0 {
out += fmt.Sprintf(" (%s)", colorAnnotations(diff.Annotations))
2016-05-13 04:25:14 +00:00
}
return out
2016-05-13 00:17:02 +00:00
}
2016-05-17 05:58:13 +00:00
func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (longestField, longestMarker int) {
for _, field := range fields {
if l := len(field.Name); l > longestField {
longestField = l
}
if _, l := getDiffString(field.Type); l > longestMarker {
longestMarker = l
}
2016-05-13 23:29:32 +00:00
}
2016-05-17 05:58:13 +00:00
for _, obj := range objects {
if _, l := getDiffString(obj.Type); l > longestMarker {
longestMarker = l
2016-05-13 23:29:32 +00:00
}
}
2016-05-17 05:58:13 +00:00
return longestField, longestMarker
2016-05-13 23:29:32 +00:00
}
2016-05-17 05:58:13 +00:00
func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix string) string {
marker, _ := getDiffString(diff.Type)
out := fmt.Sprintf("%s%s%s%s {\n", startPrefix, marker, keyPrefix, diff.Name)
2016-05-13 00:17:02 +00:00
2016-05-17 05:58:13 +00:00
// Determine the length of the longest name and longest diff marker to
// properly align names and values
longestField, longestMarker := getLongestPrefixes(diff.Fields, diff.Objects)
subStartPrefix := strings.Repeat(" ", len(startPrefix)+2)
2016-05-13 00:17:02 +00:00
numFields := len(diff.Fields)
numObjects := len(diff.Objects)
haveObjects := numObjects != 0
for i, field := range diff.Fields {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(field.Type)
kPrefix := longestMarker - mLength
vPrefix := longestField - len(field.Name)
out += formatFieldDiff(
field,
subStartPrefix,
strings.Repeat(" ", kPrefix),
strings.Repeat(" ", vPrefix))
// Avoid a dangling new line
2016-05-13 00:17:02 +00:00
if i+1 != numFields || haveObjects {
out += "\n"
}
}
for i, object := range diff.Objects {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(object.Type)
kPrefix := longestMarker - mLength
out += formatObjectDiff(object, subStartPrefix, strings.Repeat(" ", kPrefix))
// Avoid a dangling new line
2016-05-13 00:17:02 +00:00
if i+1 != numObjects {
out += "\n"
}
}
2016-05-17 05:58:13 +00:00
return fmt.Sprintf("%s\n%s}", out, startPrefix)
2016-05-13 00:17:02 +00:00
}
2016-05-17 05:58:13 +00:00
func getDiffString(diffType string) (string, int) {
2016-05-13 00:17:02 +00:00
switch diffType {
case "Added":
2016-05-17 05:58:13 +00:00
return "[green]+[reset] ", 2
2016-05-13 00:17:02 +00:00
case "Deleted":
2016-05-17 05:58:13 +00:00
return "[red]-[reset] ", 2
2016-05-13 00:17:02 +00:00
case "Edited":
2016-05-17 05:58:13 +00:00
return "[light_yellow]+/-[reset] ", 4
2016-05-13 00:17:02 +00:00
default:
2016-05-17 05:58:13 +00:00
return "", 0
}
}
func colorAnnotations(annotations []string) string {
l := len(annotations)
if l == 0 {
2016-05-13 00:17:02 +00:00
return ""
}
2016-05-17 05:58:13 +00:00
colored := make([]string, l)
for i, annotation := range annotations {
switch annotation {
case "forces create":
colored[i] = fmt.Sprintf("[green]%s[reset]", annotation)
case "forces destroy":
colored[i] = fmt.Sprintf("[red]%s[reset]", annotation)
case "forces in-place update":
colored[i] = fmt.Sprintf("[cyan]%s[reset]", annotation)
case "forces create/destroy update":
colored[i] = fmt.Sprintf("[yellow]%s[reset]", annotation)
default:
colored[i] = annotation
}
}
return strings.Join(colored, ", ")
2016-05-13 00:17:02 +00:00
}