github.com/dkerwin/nomad@v0.3.3-0.20160525181927-74554135514b/command/plan.go (about)

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