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