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