diff --git a/command/plan.go b/command/plan.go index 90d8956e1..0b7fc51c5 100644 --- a/command/plan.go +++ b/command/plan.go @@ -30,6 +30,20 @@ func (c *PlanCommand) Help() string { helpText := ` Usage: nomad plan [options] + 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 + submitting the job using "nomad run -verify", which will check that the job + was not modified between the plan and run command before invoking the + scheduler. This ensures that the plan reflects the same modifications to the + job as the run. + + An annotated diff between the submitted job and the remote state is also + displayed. This diff gives insight onto what the scheduler will attempt to do + and why. General Options: @@ -44,7 +58,7 @@ Run Options: Disable colored output. -verbose - Increased diff verbosity + Increase diff verbosity. ` return strings.TrimSpace(helpText) @@ -111,27 +125,31 @@ func (c *PlanCommand) Run(args []string) int { return 1 } + // Print the diff if not disabled if diff { c.Ui.Output(fmt.Sprintf("%s\n", c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose))))) } + // Print the scheduler dry-run output c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]")) c.Ui.Output(c.Colorize().Color(formatDryRun(resp.CreatedEvals))) + // Print the job index info c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, file))) return 0 } +// formatJobModifyIndex produces a help string that displays the job modify +// index and how to submit a job with it. 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) return out } +// formatDryRun produces a string explaining the results of the dry run. 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 { @@ -156,10 +174,14 @@ func formatDryRun(evals []*api.Evaluation) string { return out } +// formatJobDiff produces an annoted diff of the the job. If verbose mode is +// set, added or deleted task groups and tasks are expanded. func formatJobDiff(job *api.JobDiff, verbose bool) string { marker, _ := getDiffString(job.Type) out := fmt.Sprintf("%s[bold]Job: %q\n", marker, job.ID) + // Determine the longest markers and fields so that the output can be + // properly alligned. longestField, longestMarker := getLongestPrefixes(job.Fields, job.Objects) for _, tg := range job.TaskGroups { if _, l := getDiffString(tg.Type); l > longestMarker { @@ -167,43 +189,35 @@ func formatJobDiff(job *api.JobDiff, verbose bool) string { } } - subStartPrefix := "" + // Only show the job's field and object diffs if the job is edited or + // verbose mode is set. if job.Type == "Edited" || verbose { - for _, field := range job.Fields { - _, 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))) - } - - for _, object := range job.Objects { - _, mLength := getDiffString(object.Type) - kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatObjectDiff( - object, - subStartPrefix, - strings.Repeat(" ", kPrefix))) + fo := alignedFieldAndObjects(job.Fields, job.Objects, 0, longestField, longestMarker) + out += fo + if len(fo) > 0 { + out += "\n" } } + // Print the task groups for _, tg := range job.TaskGroups { _, mLength := getDiffString(tg.Type) kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, strings.Repeat(" ", kPrefix), verbose)) + out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, kPrefix, verbose)) } return out } -func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) string { +// 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 { marker, _ := getDiffString(tg.Type) - out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, tgPrefix, tg.Name) + out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, strings.Repeat(" ", tgPrefix), tg.Name) - // Append the updates + // Append the updates and colorize them if l := len(tg.Updates); l > 0 { updates := make([]string, 0, l) for updateType, count := range tg.Updates { @@ -228,6 +242,8 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) s out += "[reset]\n" } + // Determine the longest field and markers so the output is properly + // alligned longestField, longestMarker := getLongestPrefixes(tg.Fields, tg.Objects) for _, task := range tg.Tasks { if _, l := getDiffString(task.Type); l > longestMarker { @@ -235,41 +251,36 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) s } } - subStartPrefix := strings.Repeat(" ", len(tgPrefix)+2) + // Only show the task groups's field and object diffs if the group is edited or + // verbose mode is set. + subStartPrefix := tgPrefix + 2 if tg.Type == "Edited" || verbose { - for _, field := range tg.Fields { - _, 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))) - } - - for _, object := range tg.Objects { - _, mLength := getDiffString(object.Type) - kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatObjectDiff( - object, - subStartPrefix, - strings.Repeat(" ", kPrefix))) + fo := alignedFieldAndObjects(tg.Fields, tg.Objects, subStartPrefix, longestField, longestMarker) + out += fo + if len(fo) > 0 { + out += "\n" } } + // Output the tasks for _, task := range tg.Tasks { _, mLength := getDiffString(task.Type) - prefix := strings.Repeat(" ", (longestMarker - mLength)) + prefix := longestMarker - mLength out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose)) } return out } -func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix string, verbose bool) string { +// 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 +// the task and taskPrefix is the number of spaces to put betwen the marker and +// task name output. +func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix int, verbose bool) string { marker, _ := getDiffString(task.Type) - out := fmt.Sprintf("%s%s%s[bold]Task: %q", startPrefix, marker, taskPrefix, task.Name) + out := fmt.Sprintf("%s%s%s[bold]Task: %q", + strings.Repeat(" ", startPrefix), marker, strings.Repeat(" ", taskPrefix), task.Name) if len(task.Annotations) != 0 { out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations)) } @@ -277,39 +288,46 @@ func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix string, verbose if task.Type == "None" { return out } else if (task.Type == "Deleted" || task.Type == "Added") && !verbose { + // Exit early if the job was not edited and it isn't verbose output return out } else { out += "\n" } - subStartPrefix := strings.Repeat(" ", len(startPrefix)+2) + subStartPrefix := startPrefix + 2 longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects) - for _, field := range task.Fields { - _, 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))) - } - - for _, object := range task.Objects { - _, mLength := getDiffString(object.Type) - kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatObjectDiff( - object, - subStartPrefix, - strings.Repeat(" ", kPrefix))) - } - + out += alignedFieldAndObjects(task.Fields, task.Objects, subStartPrefix, longestField, longestMarker) return out } -func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix string) string { +// 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 +// of spaces to put betwen the marker and object name output. +func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix int) string { + start := strings.Repeat(" ", startPrefix) marker, _ := getDiffString(diff.Type) - out := fmt.Sprintf("%s%s%s%s: %s", startPrefix, marker, keyPrefix, diff.Name, valuePrefix) + out := fmt.Sprintf("%s%s%s%s {\n", start, marker, strings.Repeat(" ", keyPrefix), diff.Name) + + // 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) +} + +// 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 +// of spaces to put betwen the marker and field name output and valuePrefix is +// the number of spaces to put infront of the value for aligning values. +func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix int) string { + marker, _ := getDiffString(diff.Type) + out := fmt.Sprintf("%s%s%s%s: %s", + strings.Repeat(" ", startPrefix), + marker, strings.Repeat(" ", keyPrefix), + diff.Name, + strings.Repeat(" ", valuePrefix)) + switch diff.Type { case "Added": out += fmt.Sprintf("%q", diff.New) @@ -329,6 +347,43 @@ func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix st return out } +// 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 { + + var out string + numFields := len(fields) + numObjects := len(objects) + haveObjects := numObjects != 0 + for i, field := range fields { + _, mLength := getDiffString(field.Type) + kPrefix := longestMarker - mLength + vPrefix := longestField - len(field.Name) + out += formatFieldDiff(field, startPrefix, kPrefix, vPrefix) + + // Avoid a dangling new line + if i+1 != numFields || haveObjects { + out += "\n" + } + } + + for i, object := range objects { + _, mLength := getDiffString(object.Type) + kPrefix := longestMarker - mLength + out += formatObjectDiff(object, startPrefix, kPrefix) + + // Avoid a dangling new line + if i+1 != numObjects { + out += "\n" + } + } + + 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 { @@ -346,47 +401,8 @@ func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (lon return longestField, longestMarker } -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) - - // 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) - numFields := len(diff.Fields) - numObjects := len(diff.Objects) - haveObjects := numObjects != 0 - for i, field := range diff.Fields { - _, 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 - if i+1 != numFields || haveObjects { - out += "\n" - } - } - - for i, object := range diff.Objects { - _, mLength := getDiffString(object.Type) - kPrefix := longestMarker - mLength - out += formatObjectDiff(object, subStartPrefix, strings.Repeat(" ", kPrefix)) - - // Avoid a dangling new line - if i+1 != numObjects { - out += "\n" - } - } - - return fmt.Sprintf("%s\n%s}", out, startPrefix) -} - +// getDiffString returns a colored diff marker and the length of the string +// without color annotations. func getDiffString(diffType string) (string, int) { switch diffType { case "Added": @@ -400,6 +416,8 @@ func getDiffString(diffType string) (string, int) { } } +// colorAnnotations returns a comma concatonated list of the annotations where +// the annotations are colored where possible. func colorAnnotations(annotations []string) string { l := len(annotations) if l == 0 {