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