open-nomad/command/plan.go

475 lines
14 KiB
Go
Raw Normal View History

2016-05-13 00:17:02 +00:00
package command
import (
"fmt"
"sort"
2016-05-13 00:17:02 +00:00
"strings"
2016-06-15 22:26:58 +00:00
"time"
2016-05-13 00:17:02 +00:00
"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
2016-05-17 20:32:47 +00:00
nomad run -check-index %d %s
2016-05-13 23:29:32 +00:00
2016-05-17 20:32:47 +00:00
When running the job with the check-index flag, the job will only be run if the
2016-05-17 05:58:13 +00:00
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>
2016-05-17 19:06:14 +00:00
Plan invokes a dry-run of the scheduler to determine the effects of submitting
either a new or updated version of a job. The plan will not result in any
changes to the cluster but gives insight into whether the job could be run
successfully and how it would affect existing allocations.
A job modify index is returned with the plan. This value can be used when
2016-05-17 20:32:47 +00:00
submitting the job using "nomad run -check-index", which will check that the job
2016-05-17 19:06:14 +00:00
was not modified between the plan and run command before invoking the
2016-05-17 20:32:47 +00:00
scheduler. This ensures the job has not been modified since the plan.
2016-05-17 19:06:14 +00:00
2016-05-17 20:32:47 +00:00
A structured diff between the local and remote job is displayed to
give insight into what the scheduler will attempt to do and why.
2016-05-13 00:17:02 +00:00
If the job has specified the region, the -region flag and NOMAD_REGION
2016-06-07 18:33:55 +00:00
environment variable are overridden and the the job's region is used.
2016-05-13 00:17:02 +00:00
General Options:
` + generalOptionsUsage() + `
Plan Options:
2016-05-13 00:17:02 +00:00
-diff
2016-06-17 00:23:49 +00:00
Determines whether the diff between the remote job and planned job is shown.
Defaults to true.
2016-05-13 19:38:12 +00:00
-verbose
2016-05-17 19:06:14 +00:00
Increase 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
}
// Force the region to be that of the job.
if r := job.Region; r != "" {
client.SetRegion(r)
}
2016-05-13 00:17:02 +00:00
// 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
}
2016-05-17 19:06:14 +00:00
// Print the diff if not disabled
2016-05-13 00:17:02 +00:00
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-17 19:06:14 +00:00
// Print the scheduler dry-run output
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)))
c.Ui.Output("")
2016-05-13 23:29:32 +00:00
2016-05-17 19:06:14 +00:00
// Print the job index info
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 19:06:14 +00:00
// formatJobModifyIndex produces a help string that displays the job modify
// index and how to submit a job with it.
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
}
2016-05-17 19:06:14 +00:00
// formatDryRun produces a string explaining the results of the dry run.
func formatDryRun(resp *api.JobPlanResponse) string {
2016-05-13 23:29:32 +00:00
var rolling *api.Evaluation
for _, eval := range resp.CreatedEvals {
2016-05-13 23:29:32 +00:00
if eval.TriggeredBy == "rolling-update" {
rolling = eval
}
}
var out string
if len(resp.FailedTGAllocs) == 0 {
out = "[bold][green]- All tasks successfully allocated.[reset]\n"
2016-05-13 23:29:32 +00:00
} else {
out = "[bold][yellow]- WARNING: Failed to place all allocations.[reset]\n"
sorted := sortedTaskGroupFromMetrics(resp.FailedTGAllocs)
for _, tg := range sorted {
metrics := resp.FailedTGAllocs[tg]
noun := "allocation"
if metrics.CoalescedFailures > 0 {
noun += "s"
}
out += fmt.Sprintf("%s[yellow]Task Group %q (failed to place %d %s):\n[reset]", strings.Repeat(" ", 2), tg, metrics.CoalescedFailures+1, noun)
out += fmt.Sprintf("[yellow]%s[reset]\n\n", formatAllocMetrics(metrics, false, strings.Repeat(" ", 4)))
}
if rolling == nil {
out = strings.TrimSuffix(out, "\n")
}
2016-05-13 23:29:32 +00:00
}
if rolling != nil {
out += fmt.Sprintf("[green]- Rolling update, next evaluation will be in %s.\n", rolling.Wait)
2016-05-13 23:29:32 +00:00
}
2016-06-15 22:26:58 +00:00
if next := resp.NextPeriodicLaunch; !next.IsZero() {
out += fmt.Sprintf("[green]- If submitted now, next periodic launch would be at %s (%s from now).\n",
formatTime(next), formatTimeDifference(time.Now().UTC(), next, time.Second))
}
out = strings.TrimSuffix(out, "\n")
2016-05-13 23:29:32 +00:00
return out
}
2016-05-17 19:06:14 +00:00
// formatJobDiff produces an annoted diff of the the job. If verbose mode is
// set, added or deleted task groups and tasks are expanded.
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 19:06:14 +00:00
// Determine the longest markers and fields so that the output can be
2016-06-16 23:17:17 +00:00
// properly aligned.
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
}
}
2016-05-17 19:06:14 +00:00
// Only show the job's field and object diffs if the job is edited or
// verbose mode is set.
2016-05-13 19:38:12 +00:00
if job.Type == "Edited" || verbose {
2016-05-17 19:06:14 +00:00
fo := alignedFieldAndObjects(job.Fields, job.Objects, 0, longestField, longestMarker)
out += fo
if len(fo) > 0 {
out += "\n"
2016-05-13 04:25:14 +00:00
}
2016-05-13 00:17:02 +00:00
}
2016-05-17 19:06:14 +00:00
// Print the task groups
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
2016-05-17 19:06:14 +00:00
out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, kPrefix, verbose))
2016-05-13 00:17:02 +00:00
}
return out
}
2016-05-17 19:06:14 +00:00
// formatTaskGroupDiff produces an annotated diff of a task group. If the
// verbose field is set, the task groups fields and objects are expanded even if
// the full object is an addition or removal. tgPrefix is the number of spaces to prefix
// the output of the task group.
func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix int, verbose bool) string {
2016-05-17 05:58:13 +00:00
marker, _ := getDiffString(tg.Type)
2016-05-17 19:06:14 +00:00
out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, strings.Repeat(" ", tgPrefix), tg.Name)
2016-05-13 00:17:02 +00:00
2016-05-17 19:06:14 +00:00
// Append the updates and colorize them
2016-05-13 00:17:02 +00:00
if l := len(tg.Updates); l > 0 {
order := make([]string, 0, l)
for updateType := range tg.Updates {
order = append(order, updateType)
}
sort.Strings(order)
2016-05-13 00:17:02 +00:00
updates := make([]string, 0, l)
for _, updateType := range order {
count := tg.Updates[updateType]
2016-05-13 00:17:02 +00:00
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 19:06:14 +00:00
// Determine the longest field and markers so the output is properly
2016-06-16 23:17:17 +00:00
// aligned
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
}
}
2016-05-17 19:06:14 +00:00
// Only show the task groups's field and object diffs if the group is edited or
// verbose mode is set.
subStartPrefix := tgPrefix + 2
2016-05-13 19:38:12 +00:00
if tg.Type == "Edited" || verbose {
2016-05-17 19:06:14 +00:00
fo := alignedFieldAndObjects(tg.Fields, tg.Objects, subStartPrefix, longestField, longestMarker)
out += fo
if len(fo) > 0 {
out += "\n"
2016-05-13 04:25:14 +00:00
}
2016-05-13 00:17:02 +00:00
}
2016-05-17 19:06:14 +00:00
// Output the tasks
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)
2016-05-17 19:06:14 +00:00
prefix := longestMarker - mLength
2016-05-17 05:58:13 +00:00
out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose))
2016-05-13 00:17:02 +00:00
}
return out
}
2016-05-17 19:06:14 +00:00
// formatTaskDiff produces an annotated diff of a task. If the verbose field is
// set, the tasks fields and objects are expanded even if the full object is an
// addition or removal. startPrefix is the number of spaces to prefix the output of
2016-06-16 23:17:17 +00:00
// the task and taskPrefix is the number of spaces to put between the marker and
2016-05-17 19:06:14 +00:00
// task name output.
func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix int, verbose bool) string {
2016-05-17 05:58:13 +00:00
marker, _ := getDiffString(task.Type)
2016-05-17 19:06:14 +00:00
out := fmt.Sprintf("%s%s%s[bold]Task: %q",
strings.Repeat(" ", startPrefix), marker, strings.Repeat(" ", 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-17 19:06:14 +00:00
// Exit early if the job was not edited and it isn't verbose output
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 19:06:14 +00:00
subStartPrefix := startPrefix + 2
2016-05-17 05:58:13 +00:00
longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects)
2016-05-17 19:06:14 +00:00
out += alignedFieldAndObjects(task.Fields, task.Objects, subStartPrefix, longestField, longestMarker)
return out
}
2016-05-13 00:17:02 +00:00
2016-05-17 19:06:14 +00:00
// formatObjectDiff produces an annotated diff of an object. startPrefix is the
// number of spaces to prefix the output of the object and keyPrefix is the number
2016-06-16 23:17:17 +00:00
// of spaces to put between the marker and object name output.
2016-05-17 19:06:14 +00:00
func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix int) string {
start := strings.Repeat(" ", startPrefix)
marker, _ := getDiffString(diff.Type)
out := fmt.Sprintf("%s%s%s%s {\n", start, marker, strings.Repeat(" ", keyPrefix), diff.Name)
2016-05-13 00:17:02 +00:00
2016-05-17 19:06:14 +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 := startPrefix + 2
out += alignedFieldAndObjects(diff.Fields, diff.Objects, subStartPrefix, longestField, longestMarker)
return fmt.Sprintf("%s\n%s}", out, start)
2016-05-13 00:17:02 +00:00
}
2016-05-17 19:06:14 +00:00
// formatFieldDiff produces an annotated diff of a field. startPrefix is the
// number of spaces to prefix the output of the field, keyPrefix is the number
2016-06-16 23:17:17 +00:00
// of spaces to put between the marker and field name output and valuePrefix is
2016-05-17 19:06:14 +00:00
// the number of spaces to put infront of the value for aligning values.
func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix int) string {
2016-05-17 05:58:13 +00:00
marker, _ := getDiffString(diff.Type)
2016-05-17 19:06:14 +00:00
out := fmt.Sprintf("%s%s%s%s: %s",
strings.Repeat(" ", startPrefix),
marker, strings.Repeat(" ", keyPrefix),
diff.Name,
strings.Repeat(" ", 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 19:06:14 +00:00
// alignedFieldAndObjects is a helper method that prints fields and objects
// properly aligned.
func alignedFieldAndObjects(fields []*api.FieldDiff, objects []*api.ObjectDiff,
startPrefix, longestField, longestMarker int) string {
2016-05-13 00:17:02 +00:00
2016-05-17 19:06:14 +00:00
var out string
numFields := len(fields)
numObjects := len(objects)
2016-05-13 00:17:02 +00:00
haveObjects := numObjects != 0
2016-05-17 19:06:14 +00:00
for i, field := range fields {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(field.Type)
kPrefix := longestMarker - mLength
vPrefix := longestField - len(field.Name)
2016-05-17 19:06:14 +00:00
out += formatFieldDiff(field, startPrefix, kPrefix, vPrefix)
2016-05-17 05:58:13 +00:00
// Avoid a dangling new line
2016-05-13 00:17:02 +00:00
if i+1 != numFields || haveObjects {
out += "\n"
}
}
2016-05-17 19:06:14 +00:00
for i, object := range objects {
2016-05-17 05:58:13 +00:00
_, mLength := getDiffString(object.Type)
kPrefix := longestMarker - mLength
2016-05-17 19:06:14 +00:00
out += formatObjectDiff(object, startPrefix, kPrefix)
2016-05-17 05:58:13 +00:00
// Avoid a dangling new line
2016-05-13 00:17:02 +00:00
if i+1 != numObjects {
out += "\n"
}
}
2016-05-17 19:06:14 +00:00
return out
}
// getLongestPrefixes takes a list of fields and objects and determines the
// longest field name and the longest marker.
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
}
}
for _, obj := range objects {
if _, l := getDiffString(obj.Type); l > longestMarker {
longestMarker = l
}
}
return longestField, longestMarker
2016-05-13 00:17:02 +00:00
}
2016-05-17 19:06:14 +00:00
// getDiffString returns a colored diff marker and the length of the string
// without color annotations.
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
}
}
2016-05-17 19:06:14 +00:00
// colorAnnotations returns a comma concatonated list of the annotations where
// the annotations are colored where possible.
2016-05-17 05:58:13 +00:00
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
}