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