github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/command/plan.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/hashicorp/nomad/api"
    10  	"github.com/hashicorp/nomad/scheduler"
    11  	"github.com/mitchellh/colorstring"
    12  	"github.com/posener/complete"
    13  )
    14  
    15  const (
    16  	jobModifyIndexHelp = `To submit the job with version verification run:
    17  
    18  nomad run -check-index %d %s
    19  
    20  When running the job with the check-index flag, the job will only be run if the
    21  server side version matches the job modify index returned. If the index has
    22  changed, another user has modified the job and the plan's results are
    23  potentially invalid.`
    24  )
    25  
    26  type PlanCommand struct {
    27  	Meta
    28  	JobGetter
    29  	color *colorstring.Colorize
    30  }
    31  
    32  func (c *PlanCommand) Help() string {
    33  	helpText := `
    34  Usage: nomad plan [options] <path>
    35  
    36    Plan invokes a dry-run of the scheduler to determine the effects of submitting
    37    either a new or updated version of a job. The plan will not result in any
    38    changes to the cluster but gives insight into whether the job could be run
    39    successfully and how it would affect existing allocations.
    40  
    41    If the supplied path is "-", the jobfile is read from stdin. Otherwise
    42    it is read from the file at the supplied path or downloaded and
    43    read from URL specified.
    44  
    45    A job modify index is returned with the plan. This value can be used when
    46    submitting the job using "nomad run -check-index", which will check that the job
    47    was not modified between the plan and run command before invoking the
    48    scheduler. This ensures the job has not been modified since the plan.
    49  
    50    A structured diff between the local and remote job is displayed to
    51    give insight into what the scheduler will attempt to do and why.
    52  
    53    If the job has specified the region, the -region flag and NOMAD_REGION
    54    environment variable are overridden and the job's region is used.
    55  
    56    Plan will return one of the following exit codes:
    57      * 0: No allocations created or destroyed.
    58      * 1: Allocations created or destroyed.
    59      * 255: Error determining plan results.
    60  
    61  General Options:
    62  
    63    ` + generalOptionsUsage() + `
    64  
    65  Plan Options:
    66  
    67    -diff
    68      Determines whether the diff between the remote job and planned job is shown.
    69      Defaults to true.
    70  
    71    -verbose
    72      Increase diff verbosity.
    73  `
    74  	return strings.TrimSpace(helpText)
    75  }
    76  
    77  func (c *PlanCommand) Synopsis() string {
    78  	return "Dry-run a job update to determine its effects"
    79  }
    80  
    81  func (c *PlanCommand) AutocompleteFlags() complete.Flags {
    82  	return nil
    83  }
    84  
    85  func (c *PlanCommand) AutocompleteArgs() complete.Predictor {
    86  	return complete.PredictOr(complete.PredictFiles("*.nomad"), complete.PredictFiles("*.hcl"))
    87  }
    88  
    89  func (c *PlanCommand) Run(args []string) int {
    90  	var diff, verbose bool
    91  
    92  	flags := c.Meta.FlagSet("plan", FlagSetClient)
    93  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    94  	flags.BoolVar(&diff, "diff", true, "")
    95  	flags.BoolVar(&verbose, "verbose", false, "")
    96  
    97  	if err := flags.Parse(args); err != nil {
    98  		return 255
    99  	}
   100  
   101  	// Check that we got exactly one job
   102  	args = flags.Args()
   103  	if len(args) != 1 {
   104  		c.Ui.Error(c.Help())
   105  		return 255
   106  	}
   107  
   108  	path := args[0]
   109  	// Get Job struct from Jobfile
   110  	job, err := c.JobGetter.ApiJob(args[0])
   111  	if err != nil {
   112  		c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err))
   113  		return 255
   114  	}
   115  
   116  	// Get the HTTP client
   117  	client, err := c.Meta.Client()
   118  	if err != nil {
   119  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   120  		return 255
   121  	}
   122  
   123  	// Force the region to be that of the job.
   124  	if r := job.Region; r != nil {
   125  		client.SetRegion(*r)
   126  	}
   127  
   128  	// Submit the job
   129  	resp, _, err := client.Jobs().Plan(job, diff, nil)
   130  	if err != nil {
   131  		c.Ui.Error(fmt.Sprintf("Error during plan: %s", err))
   132  		return 255
   133  	}
   134  
   135  	// Print the diff if not disabled
   136  	if diff {
   137  		c.Ui.Output(fmt.Sprintf("%s\n",
   138  			c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose)))))
   139  	}
   140  
   141  	// Print the scheduler dry-run output
   142  	c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]"))
   143  	c.Ui.Output(c.Colorize().Color(formatDryRun(resp, job)))
   144  	c.Ui.Output("")
   145  
   146  	// Print any warnings if there are any
   147  	if resp.Warnings != "" {
   148  		c.Ui.Output(
   149  			c.Colorize().Color(fmt.Sprintf("[bold][yellow]Job Warnings:\n%s[reset]\n", resp.Warnings)))
   150  	}
   151  
   152  	// Print the job index info
   153  	c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, path)))
   154  	return getExitCode(resp)
   155  }
   156  
   157  // getExitCode returns 0:
   158  // * 0: No allocations created or destroyed.
   159  // * 1: Allocations created or destroyed.
   160  func getExitCode(resp *api.JobPlanResponse) int {
   161  	// Check for changes
   162  	for _, d := range resp.Annotations.DesiredTGUpdates {
   163  		if d.Stop+d.Place+d.Migrate+d.DestructiveUpdate+d.Canary > 0 {
   164  			return 1
   165  		}
   166  	}
   167  
   168  	return 0
   169  }
   170  
   171  // formatJobModifyIndex produces a help string that displays the job modify
   172  // index and how to submit a job with it.
   173  func formatJobModifyIndex(jobModifyIndex uint64, jobName string) string {
   174  	help := fmt.Sprintf(jobModifyIndexHelp, jobModifyIndex, jobName)
   175  	out := fmt.Sprintf("[reset][bold]Job Modify Index: %d[reset]\n%s", jobModifyIndex, help)
   176  	return out
   177  }
   178  
   179  // formatDryRun produces a string explaining the results of the dry run.
   180  func formatDryRun(resp *api.JobPlanResponse, job *api.Job) string {
   181  	var rolling *api.Evaluation
   182  	for _, eval := range resp.CreatedEvals {
   183  		if eval.TriggeredBy == "rolling-update" {
   184  			rolling = eval
   185  		}
   186  	}
   187  
   188  	var out string
   189  	if len(resp.FailedTGAllocs) == 0 {
   190  		out = "[bold][green]- All tasks successfully allocated.[reset]\n"
   191  	} else {
   192  		// Change the output depending on if we are a system job or not
   193  		if job.Type != nil && *job.Type == "system" {
   194  			out = "[bold][yellow]- WARNING: Failed to place allocations on all nodes.[reset]\n"
   195  		} else {
   196  			out = "[bold][yellow]- WARNING: Failed to place all allocations.[reset]\n"
   197  		}
   198  		sorted := sortedTaskGroupFromMetrics(resp.FailedTGAllocs)
   199  		for _, tg := range sorted {
   200  			metrics := resp.FailedTGAllocs[tg]
   201  
   202  			noun := "allocation"
   203  			if metrics.CoalescedFailures > 0 {
   204  				noun += "s"
   205  			}
   206  			out += fmt.Sprintf("%s[yellow]Task Group %q (failed to place %d %s):\n[reset]", strings.Repeat(" ", 2), tg, metrics.CoalescedFailures+1, noun)
   207  			out += fmt.Sprintf("[yellow]%s[reset]\n\n", formatAllocMetrics(metrics, false, strings.Repeat(" ", 4)))
   208  		}
   209  		if rolling == nil {
   210  			out = strings.TrimSuffix(out, "\n")
   211  		}
   212  	}
   213  
   214  	if rolling != nil {
   215  		out += fmt.Sprintf("[green]- Rolling update, next evaluation will be in %s.\n", rolling.Wait)
   216  	}
   217  
   218  	if next := resp.NextPeriodicLaunch; !next.IsZero() && !job.IsParameterized() {
   219  		loc, err := job.Periodic.GetLocation()
   220  		if err != nil {
   221  			out += fmt.Sprintf("[yellow]- Invalid time zone: %v", err)
   222  		} else {
   223  			now := time.Now().In(loc)
   224  			out += fmt.Sprintf("[green]- If submitted now, next periodic launch would be at %s (%s from now).\n",
   225  				formatTime(next), formatTimeDifference(now, next, time.Second))
   226  		}
   227  	}
   228  
   229  	out = strings.TrimSuffix(out, "\n")
   230  	return out
   231  }
   232  
   233  // formatJobDiff produces an annoted diff of the job. If verbose mode is
   234  // set, added or deleted task groups and tasks are expanded.
   235  func formatJobDiff(job *api.JobDiff, verbose bool) string {
   236  	marker, _ := getDiffString(job.Type)
   237  	out := fmt.Sprintf("%s[bold]Job: %q\n", marker, job.ID)
   238  
   239  	// Determine the longest markers and fields so that the output can be
   240  	// properly aligned.
   241  	longestField, longestMarker := getLongestPrefixes(job.Fields, job.Objects)
   242  	for _, tg := range job.TaskGroups {
   243  		if _, l := getDiffString(tg.Type); l > longestMarker {
   244  			longestMarker = l
   245  		}
   246  	}
   247  
   248  	// Only show the job's field and object diffs if the job is edited or
   249  	// verbose mode is set.
   250  	if job.Type == "Edited" || verbose {
   251  		fo := alignedFieldAndObjects(job.Fields, job.Objects, 0, longestField, longestMarker)
   252  		out += fo
   253  		if len(fo) > 0 {
   254  			out += "\n"
   255  		}
   256  	}
   257  
   258  	// Print the task groups
   259  	for _, tg := range job.TaskGroups {
   260  		_, mLength := getDiffString(tg.Type)
   261  		kPrefix := longestMarker - mLength
   262  		out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, kPrefix, verbose))
   263  	}
   264  
   265  	return out
   266  }
   267  
   268  // formatTaskGroupDiff produces an annotated diff of a task group. If the
   269  // verbose field is set, the task groups fields and objects are expanded even if
   270  // the full object is an addition or removal. tgPrefix is the number of spaces to prefix
   271  // the output of the task group.
   272  func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix int, verbose bool) string {
   273  	marker, _ := getDiffString(tg.Type)
   274  	out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, strings.Repeat(" ", tgPrefix), tg.Name)
   275  
   276  	// Append the updates and colorize them
   277  	if l := len(tg.Updates); l > 0 {
   278  		order := make([]string, 0, l)
   279  		for updateType := range tg.Updates {
   280  			order = append(order, updateType)
   281  		}
   282  
   283  		sort.Strings(order)
   284  		updates := make([]string, 0, l)
   285  		for _, updateType := range order {
   286  			count := tg.Updates[updateType]
   287  			var color string
   288  			switch updateType {
   289  			case scheduler.UpdateTypeIgnore:
   290  			case scheduler.UpdateTypeCreate:
   291  				color = "[green]"
   292  			case scheduler.UpdateTypeDestroy:
   293  				color = "[red]"
   294  			case scheduler.UpdateTypeMigrate:
   295  				color = "[blue]"
   296  			case scheduler.UpdateTypeInplaceUpdate:
   297  				color = "[cyan]"
   298  			case scheduler.UpdateTypeDestructiveUpdate:
   299  				color = "[yellow]"
   300  			case scheduler.UpdateTypeCanary:
   301  				color = "[light_yellow]"
   302  			}
   303  			updates = append(updates, fmt.Sprintf("[reset]%s%d %s", color, count, updateType))
   304  		}
   305  		out += fmt.Sprintf(" (%s[reset])\n", strings.Join(updates, ", "))
   306  	} else {
   307  		out += "[reset]\n"
   308  	}
   309  
   310  	// Determine the longest field and markers so the output is properly
   311  	// aligned
   312  	longestField, longestMarker := getLongestPrefixes(tg.Fields, tg.Objects)
   313  	for _, task := range tg.Tasks {
   314  		if _, l := getDiffString(task.Type); l > longestMarker {
   315  			longestMarker = l
   316  		}
   317  	}
   318  
   319  	// Only show the task groups's field and object diffs if the group is edited or
   320  	// verbose mode is set.
   321  	subStartPrefix := tgPrefix + 2
   322  	if tg.Type == "Edited" || verbose {
   323  		fo := alignedFieldAndObjects(tg.Fields, tg.Objects, subStartPrefix, longestField, longestMarker)
   324  		out += fo
   325  		if len(fo) > 0 {
   326  			out += "\n"
   327  		}
   328  	}
   329  
   330  	// Output the tasks
   331  	for _, task := range tg.Tasks {
   332  		_, mLength := getDiffString(task.Type)
   333  		prefix := longestMarker - mLength
   334  		out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose))
   335  	}
   336  
   337  	return out
   338  }
   339  
   340  // formatTaskDiff produces an annotated diff of a task. If the verbose field is
   341  // set, the tasks fields and objects are expanded even if the full object is an
   342  // addition or removal. startPrefix is the number of spaces to prefix the output of
   343  // the task and taskPrefix is the number of spaces to put between the marker and
   344  // task name output.
   345  func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix int, verbose bool) string {
   346  	marker, _ := getDiffString(task.Type)
   347  	out := fmt.Sprintf("%s%s%s[bold]Task: %q",
   348  		strings.Repeat(" ", startPrefix), marker, strings.Repeat(" ", taskPrefix), task.Name)
   349  	if len(task.Annotations) != 0 {
   350  		out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations))
   351  	}
   352  
   353  	if task.Type == "None" {
   354  		return out
   355  	} else if (task.Type == "Deleted" || task.Type == "Added") && !verbose {
   356  		// Exit early if the job was not edited and it isn't verbose output
   357  		return out
   358  	} else {
   359  		out += "\n"
   360  	}
   361  
   362  	subStartPrefix := startPrefix + 2
   363  	longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects)
   364  	out += alignedFieldAndObjects(task.Fields, task.Objects, subStartPrefix, longestField, longestMarker)
   365  	return out
   366  }
   367  
   368  // formatObjectDiff produces an annotated diff of an object. startPrefix is the
   369  // number of spaces to prefix the output of the object and keyPrefix is the number
   370  // of spaces to put between the marker and object name output.
   371  func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix int) string {
   372  	start := strings.Repeat(" ", startPrefix)
   373  	marker, markerLen := getDiffString(diff.Type)
   374  	out := fmt.Sprintf("%s%s%s%s {\n", start, marker, strings.Repeat(" ", keyPrefix), diff.Name)
   375  
   376  	// Determine the length of the longest name and longest diff marker to
   377  	// properly align names and values
   378  	longestField, longestMarker := getLongestPrefixes(diff.Fields, diff.Objects)
   379  	subStartPrefix := startPrefix + keyPrefix + 2
   380  	out += alignedFieldAndObjects(diff.Fields, diff.Objects, subStartPrefix, longestField, longestMarker)
   381  
   382  	endprefix := strings.Repeat(" ", startPrefix+markerLen+keyPrefix)
   383  	return fmt.Sprintf("%s\n%s}", out, endprefix)
   384  }
   385  
   386  // formatFieldDiff produces an annotated diff of a field. startPrefix is the
   387  // number of spaces to prefix the output of the field, keyPrefix is the number
   388  // of spaces to put between the marker and field name output and valuePrefix is
   389  // the number of spaces to put infront of the value for aligning values.
   390  func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix int) string {
   391  	marker, _ := getDiffString(diff.Type)
   392  	out := fmt.Sprintf("%s%s%s%s: %s",
   393  		strings.Repeat(" ", startPrefix),
   394  		marker, strings.Repeat(" ", keyPrefix),
   395  		diff.Name,
   396  		strings.Repeat(" ", valuePrefix))
   397  
   398  	switch diff.Type {
   399  	case "Added":
   400  		out += fmt.Sprintf("%q", diff.New)
   401  	case "Deleted":
   402  		out += fmt.Sprintf("%q", diff.Old)
   403  	case "Edited":
   404  		out += fmt.Sprintf("%q => %q", diff.Old, diff.New)
   405  	default:
   406  		out += fmt.Sprintf("%q", diff.New)
   407  	}
   408  
   409  	// Color the annotations where possible
   410  	if l := len(diff.Annotations); l != 0 {
   411  		out += fmt.Sprintf(" (%s)", colorAnnotations(diff.Annotations))
   412  	}
   413  
   414  	return out
   415  }
   416  
   417  // alignedFieldAndObjects is a helper method that prints fields and objects
   418  // properly aligned.
   419  func alignedFieldAndObjects(fields []*api.FieldDiff, objects []*api.ObjectDiff,
   420  	startPrefix, longestField, longestMarker int) string {
   421  
   422  	var out string
   423  	numFields := len(fields)
   424  	numObjects := len(objects)
   425  	haveObjects := numObjects != 0
   426  	for i, field := range fields {
   427  		_, mLength := getDiffString(field.Type)
   428  		kPrefix := longestMarker - mLength
   429  		vPrefix := longestField - len(field.Name)
   430  		out += formatFieldDiff(field, startPrefix, kPrefix, vPrefix)
   431  
   432  		// Avoid a dangling new line
   433  		if i+1 != numFields || haveObjects {
   434  			out += "\n"
   435  		}
   436  	}
   437  
   438  	for i, object := range objects {
   439  		_, mLength := getDiffString(object.Type)
   440  		kPrefix := longestMarker - mLength
   441  		out += formatObjectDiff(object, startPrefix, kPrefix)
   442  
   443  		// Avoid a dangling new line
   444  		if i+1 != numObjects {
   445  			out += "\n"
   446  		}
   447  	}
   448  
   449  	return out
   450  }
   451  
   452  // getLongestPrefixes takes a list  of fields and objects and determines the
   453  // longest field name and the longest marker.
   454  func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (longestField, longestMarker int) {
   455  	for _, field := range fields {
   456  		if l := len(field.Name); l > longestField {
   457  			longestField = l
   458  		}
   459  		if _, l := getDiffString(field.Type); l > longestMarker {
   460  			longestMarker = l
   461  		}
   462  	}
   463  	for _, obj := range objects {
   464  		if _, l := getDiffString(obj.Type); l > longestMarker {
   465  			longestMarker = l
   466  		}
   467  	}
   468  	return longestField, longestMarker
   469  }
   470  
   471  // getDiffString returns a colored diff marker and the length of the string
   472  // without color annotations.
   473  func getDiffString(diffType string) (string, int) {
   474  	switch diffType {
   475  	case "Added":
   476  		return "[green]+[reset] ", 2
   477  	case "Deleted":
   478  		return "[red]-[reset] ", 2
   479  	case "Edited":
   480  		return "[light_yellow]+/-[reset] ", 4
   481  	default:
   482  		return "", 0
   483  	}
   484  }
   485  
   486  // colorAnnotations returns a comma concatonated list of the annotations where
   487  // the annotations are colored where possible.
   488  func colorAnnotations(annotations []string) string {
   489  	l := len(annotations)
   490  	if l == 0 {
   491  		return ""
   492  	}
   493  
   494  	colored := make([]string, l)
   495  	for i, annotation := range annotations {
   496  		switch annotation {
   497  		case "forces create":
   498  			colored[i] = fmt.Sprintf("[green]%s[reset]", annotation)
   499  		case "forces destroy":
   500  			colored[i] = fmt.Sprintf("[red]%s[reset]", annotation)
   501  		case "forces in-place update":
   502  			colored[i] = fmt.Sprintf("[cyan]%s[reset]", annotation)
   503  		case "forces create/destroy update":
   504  			colored[i] = fmt.Sprintf("[yellow]%s[reset]", annotation)
   505  		default:
   506  			colored[i] = annotation
   507  		}
   508  	}
   509  
   510  	return strings.Join(colored, ", ")
   511  }