github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/job_status.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/api/contexts"
    11  	"github.com/posener/complete"
    12  )
    13  
    14  const (
    15  	// maxFailedTGs is the maximum number of task groups we show failure reasons
    16  	// for before deferring to eval-status
    17  	maxFailedTGs = 5
    18  )
    19  
    20  type JobStatusCommand struct {
    21  	Meta
    22  	length    int
    23  	evals     bool
    24  	allAllocs bool
    25  	verbose   bool
    26  }
    27  
    28  func (c *JobStatusCommand) Help() string {
    29  	helpText := `
    30  Usage: nomad status [options] <job>
    31  
    32    Display status information about a job. If no job ID is given, a list of all
    33    known jobs will be displayed.
    34  
    35    When ACLs are enabled, this command requires a token with the 'read-job' and
    36    'list-jobs' capabilities for the job's namespace.
    37  
    38  General Options:
    39  
    40    ` + generalOptionsUsage(usageOptsDefault) + `
    41  
    42  Status Options:
    43  
    44    -short
    45      Display short output. Used only when a single job is being
    46      queried, and drops verbose information about allocations.
    47  
    48    -evals
    49      Display the evaluations associated with the job.
    50  
    51    -all-allocs
    52      Display all allocations matching the job ID, including those from an older
    53      instance of the job.
    54  
    55    -verbose
    56      Display full information.
    57  `
    58  	return strings.TrimSpace(helpText)
    59  }
    60  
    61  func (c *JobStatusCommand) Synopsis() string {
    62  	return "Display status information about a job"
    63  }
    64  
    65  func (c *JobStatusCommand) AutocompleteFlags() complete.Flags {
    66  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    67  		complete.Flags{
    68  			"-all-allocs": complete.PredictNothing,
    69  			"-evals":      complete.PredictNothing,
    70  			"-short":      complete.PredictNothing,
    71  			"-verbose":    complete.PredictNothing,
    72  		})
    73  }
    74  
    75  func (c *JobStatusCommand) AutocompleteArgs() complete.Predictor {
    76  	return complete.PredictFunc(func(a complete.Args) []string {
    77  		client, err := c.Meta.Client()
    78  		if err != nil {
    79  			return nil
    80  		}
    81  
    82  		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Jobs, nil)
    83  		if err != nil {
    84  			return []string{}
    85  		}
    86  		return resp.Matches[contexts.Jobs]
    87  	})
    88  }
    89  
    90  func (c *JobStatusCommand) Name() string { return "status" }
    91  
    92  func (c *JobStatusCommand) Run(args []string) int {
    93  	var short bool
    94  
    95  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    96  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    97  	flags.BoolVar(&short, "short", false, "")
    98  	flags.BoolVar(&c.evals, "evals", false, "")
    99  	flags.BoolVar(&c.allAllocs, "all-allocs", false, "")
   100  	flags.BoolVar(&c.verbose, "verbose", false, "")
   101  
   102  	if err := flags.Parse(args); err != nil {
   103  		return 1
   104  	}
   105  
   106  	// Check that we either got no jobs or exactly one.
   107  	args = flags.Args()
   108  	if len(args) > 1 {
   109  		c.Ui.Error("This command takes either no arguments or one: <job>")
   110  		c.Ui.Error(commandErrorText(c))
   111  		return 1
   112  	}
   113  
   114  	// Truncate the id unless full length is requested
   115  	c.length = shortId
   116  	if c.verbose {
   117  		c.length = fullId
   118  	}
   119  
   120  	// Get the HTTP client
   121  	client, err := c.Meta.Client()
   122  	if err != nil {
   123  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   124  		return 1
   125  	}
   126  
   127  	allNamespaces := c.allNamespaces()
   128  
   129  	// Invoke list mode if no job ID.
   130  	if len(args) == 0 {
   131  		jobs, _, err := client.Jobs().ListOptions(nil, nil)
   132  
   133  		if err != nil {
   134  			c.Ui.Error(fmt.Sprintf("Error querying jobs: %s", err))
   135  			return 1
   136  		}
   137  
   138  		if len(jobs) == 0 {
   139  			// No output if we have no jobs
   140  			c.Ui.Output("No running jobs")
   141  		} else {
   142  			c.Ui.Output(createStatusListOutput(jobs, allNamespaces))
   143  		}
   144  		return 0
   145  	}
   146  
   147  	// Try querying the job
   148  	jobID := strings.TrimSpace(args[0])
   149  
   150  	jobs, _, err := client.Jobs().PrefixList(jobID)
   151  	if err != nil {
   152  		c.Ui.Error(fmt.Sprintf("Error querying job: %s", err))
   153  		return 1
   154  	}
   155  	if len(jobs) == 0 {
   156  		c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID))
   157  		return 1
   158  	}
   159  	if len(jobs) > 1 {
   160  		if (jobID != jobs[0].ID) || (allNamespaces && jobs[0].ID == jobs[1].ID) {
   161  			c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs, allNamespaces)))
   162  			return 1
   163  		}
   164  	}
   165  
   166  	// Prefix lookup matched a single job
   167  	q := &api.QueryOptions{Namespace: jobs[0].JobSummary.Namespace}
   168  	job, _, err := client.Jobs().Info(jobs[0].ID, q)
   169  	if err != nil {
   170  		c.Ui.Error(fmt.Sprintf("Error querying job: %s", err))
   171  		return 1
   172  	}
   173  
   174  	periodic := job.IsPeriodic()
   175  	parameterized := job.IsParameterized()
   176  
   177  	// Format the job info
   178  	basic := []string{
   179  		fmt.Sprintf("ID|%s", *job.ID),
   180  		fmt.Sprintf("Name|%s", *job.Name),
   181  		fmt.Sprintf("Submit Date|%s", formatTime(time.Unix(0, *job.SubmitTime))),
   182  		fmt.Sprintf("Type|%s", *job.Type),
   183  		fmt.Sprintf("Priority|%d", *job.Priority),
   184  		fmt.Sprintf("Datacenters|%s", strings.Join(job.Datacenters, ",")),
   185  		fmt.Sprintf("Namespace|%s", *job.Namespace),
   186  		fmt.Sprintf("Status|%s", getStatusString(*job.Status, job.Stop)),
   187  		fmt.Sprintf("Periodic|%v", periodic),
   188  		fmt.Sprintf("Parameterized|%v", parameterized),
   189  	}
   190  
   191  	if job.DispatchIdempotencyToken != nil && *job.DispatchIdempotencyToken != "" {
   192  		basic = append(basic, fmt.Sprintf("Idempotency Token|%v", *job.DispatchIdempotencyToken))
   193  	}
   194  
   195  	if periodic && !parameterized {
   196  		if *job.Stop {
   197  			basic = append(basic, "Next Periodic Launch|none (job stopped)")
   198  		} else {
   199  			location, err := job.Periodic.GetLocation()
   200  			if err == nil {
   201  				now := time.Now().In(location)
   202  				next, err := job.Periodic.Next(now)
   203  				if err == nil {
   204  					basic = append(basic, fmt.Sprintf("Next Periodic Launch|%s",
   205  						fmt.Sprintf("%s (%s from now)",
   206  							formatTime(next), formatTimeDifference(now, next, time.Second))))
   207  				}
   208  			}
   209  		}
   210  	}
   211  
   212  	c.Ui.Output(formatKV(basic))
   213  
   214  	// Exit early
   215  	if short {
   216  		return 0
   217  	}
   218  
   219  	// Print periodic job information
   220  	if periodic && !parameterized {
   221  		if err := c.outputPeriodicInfo(client, job); err != nil {
   222  			c.Ui.Error(err.Error())
   223  			return 1
   224  		}
   225  	} else if parameterized {
   226  		if err := c.outputParameterizedInfo(client, job); err != nil {
   227  			c.Ui.Error(err.Error())
   228  			return 1
   229  		}
   230  	} else {
   231  		if err := c.outputJobInfo(client, job); err != nil {
   232  			c.Ui.Error(err.Error())
   233  			return 1
   234  		}
   235  	}
   236  
   237  	return 0
   238  }
   239  
   240  // outputPeriodicInfo prints information about the passed periodic job. If a
   241  // request fails, an error is returned.
   242  func (c *JobStatusCommand) outputPeriodicInfo(client *api.Client, job *api.Job) error {
   243  	// Output the summary
   244  	if err := c.outputJobSummary(client, job); err != nil {
   245  		return err
   246  	}
   247  
   248  	// Generate the prefix that matches launched jobs from the periodic job.
   249  	prefix := fmt.Sprintf("%s%s", *job.ID, api.JobPeriodicLaunchSuffix)
   250  	children, _, err := client.Jobs().PrefixList(prefix)
   251  	if err != nil {
   252  		return fmt.Errorf("Error querying job: %s", err)
   253  	}
   254  
   255  	if len(children) == 0 {
   256  		c.Ui.Output("\nNo instances of periodic job found")
   257  		return nil
   258  	}
   259  
   260  	out := make([]string, 1)
   261  	out[0] = "ID|Status"
   262  	for _, child := range children {
   263  		// Ensure that we are only showing jobs whose parent is the requested
   264  		// job.
   265  		if child.ParentID != *job.ID {
   266  			continue
   267  		}
   268  
   269  		out = append(out, fmt.Sprintf("%s|%s",
   270  			child.ID,
   271  			child.Status))
   272  	}
   273  
   274  	c.Ui.Output(c.Colorize().Color("\n[bold]Previously Launched Jobs[reset]"))
   275  	c.Ui.Output(formatList(out))
   276  	return nil
   277  }
   278  
   279  // outputParameterizedInfo prints information about a parameterized job. If a
   280  // request fails, an error is returned.
   281  func (c *JobStatusCommand) outputParameterizedInfo(client *api.Client, job *api.Job) error {
   282  	// Output parameterized job details
   283  	c.Ui.Output(c.Colorize().Color("\n[bold]Parameterized Job[reset]"))
   284  	parameterizedJob := make([]string, 3)
   285  	parameterizedJob[0] = fmt.Sprintf("Payload|%s", job.ParameterizedJob.Payload)
   286  	parameterizedJob[1] = fmt.Sprintf("Required Metadata|%v", strings.Join(job.ParameterizedJob.MetaRequired, ", "))
   287  	parameterizedJob[2] = fmt.Sprintf("Optional Metadata|%v", strings.Join(job.ParameterizedJob.MetaOptional, ", "))
   288  	c.Ui.Output(formatKV(parameterizedJob))
   289  
   290  	// Output the summary
   291  	if err := c.outputJobSummary(client, job); err != nil {
   292  		return err
   293  	}
   294  
   295  	// Generate the prefix that matches launched jobs from the parameterized job.
   296  	prefix := fmt.Sprintf("%s%s", *job.ID, api.JobDispatchLaunchSuffix)
   297  	children, _, err := client.Jobs().PrefixList(prefix)
   298  	if err != nil {
   299  		return fmt.Errorf("Error querying job: %s", err)
   300  	}
   301  
   302  	if len(children) == 0 {
   303  		c.Ui.Output("\nNo dispatched instances of parameterized job found")
   304  		return nil
   305  	}
   306  
   307  	out := make([]string, 1)
   308  	out[0] = "ID|Status"
   309  	for _, child := range children {
   310  		// Ensure that we are only showing jobs whose parent is the requested
   311  		// job.
   312  		if child.ParentID != *job.ID {
   313  			continue
   314  		}
   315  
   316  		out = append(out, fmt.Sprintf("%s|%s",
   317  			child.ID,
   318  			child.Status))
   319  	}
   320  
   321  	c.Ui.Output(c.Colorize().Color("\n[bold]Dispatched Jobs[reset]"))
   322  	c.Ui.Output(formatList(out))
   323  	return nil
   324  }
   325  
   326  // outputJobInfo prints information about the passed non-periodic job. If a
   327  // request fails, an error is returned.
   328  func (c *JobStatusCommand) outputJobInfo(client *api.Client, job *api.Job) error {
   329  	var q *api.QueryOptions
   330  	if job.Namespace != nil {
   331  		q = &api.QueryOptions{Namespace: *job.Namespace}
   332  	}
   333  
   334  	// Query the allocations
   335  	jobAllocs, _, err := client.Jobs().Allocations(*job.ID, c.allAllocs, q)
   336  	if err != nil {
   337  		return fmt.Errorf("Error querying job allocations: %s", err)
   338  	}
   339  
   340  	// Query the evaluations
   341  	jobEvals, _, err := client.Jobs().Evaluations(*job.ID, q)
   342  	if err != nil {
   343  		return fmt.Errorf("Error querying job evaluations: %s", err)
   344  	}
   345  
   346  	latestDeployment, _, err := client.Jobs().LatestDeployment(*job.ID, q)
   347  	if err != nil {
   348  		return fmt.Errorf("Error querying latest job deployment: %s", err)
   349  	}
   350  
   351  	// Output the summary
   352  	if err := c.outputJobSummary(client, job); err != nil {
   353  		return err
   354  	}
   355  
   356  	// Determine latest evaluation with failures whose follow up hasn't
   357  	// completed, this is done while formatting
   358  	var latestFailedPlacement *api.Evaluation
   359  	blockedEval := false
   360  
   361  	// Format the evals
   362  	evals := make([]string, len(jobEvals)+1)
   363  	evals[0] = "ID|Priority|Triggered By|Status|Placement Failures"
   364  	for i, eval := range jobEvals {
   365  		failures, _ := evalFailureStatus(eval)
   366  		evals[i+1] = fmt.Sprintf("%s|%d|%s|%s|%s",
   367  			limit(eval.ID, c.length),
   368  			eval.Priority,
   369  			eval.TriggeredBy,
   370  			eval.Status,
   371  			failures,
   372  		)
   373  
   374  		if eval.Status == "blocked" {
   375  			blockedEval = true
   376  		}
   377  
   378  		if len(eval.FailedTGAllocs) == 0 {
   379  			// Skip evals without failures
   380  			continue
   381  		}
   382  
   383  		if latestFailedPlacement == nil || latestFailedPlacement.CreateIndex < eval.CreateIndex {
   384  			latestFailedPlacement = eval
   385  		}
   386  	}
   387  
   388  	if c.verbose || c.evals {
   389  		c.Ui.Output(c.Colorize().Color("\n[bold]Evaluations[reset]"))
   390  		c.Ui.Output(formatList(evals))
   391  	}
   392  
   393  	if blockedEval && latestFailedPlacement != nil {
   394  		c.outputFailedPlacements(latestFailedPlacement)
   395  	}
   396  
   397  	c.outputReschedulingEvals(client, job, jobAllocs, c.length)
   398  
   399  	if latestDeployment != nil {
   400  		c.Ui.Output(c.Colorize().Color("\n[bold]Latest Deployment[reset]"))
   401  		c.Ui.Output(c.Colorize().Color(c.formatDeployment(client, latestDeployment)))
   402  	}
   403  
   404  	// Format the allocs
   405  	c.Ui.Output(c.Colorize().Color("\n[bold]Allocations[reset]"))
   406  	c.Ui.Output(formatAllocListStubs(jobAllocs, c.verbose, c.length))
   407  	return nil
   408  }
   409  
   410  func (c *JobStatusCommand) formatDeployment(client *api.Client, d *api.Deployment) string {
   411  	// Format the high-level elements
   412  	high := []string{
   413  		fmt.Sprintf("ID|%s", limit(d.ID, c.length)),
   414  		fmt.Sprintf("Status|%s", d.Status),
   415  		fmt.Sprintf("Description|%s", d.StatusDescription),
   416  	}
   417  
   418  	base := formatKV(high)
   419  
   420  	if d.IsMultiregion {
   421  		regions, err := fetchMultiRegionDeployments(client, d)
   422  		if err != nil {
   423  			base += "\n\nError fetching Multiregion deployments\n\n"
   424  		} else if len(regions) > 0 {
   425  			base += "\n\n[bold]Multiregion Deployment[reset]\n"
   426  			base += formatMultiregionDeployment(regions, 8)
   427  		}
   428  	}
   429  
   430  	if len(d.TaskGroups) == 0 {
   431  		return base
   432  	}
   433  	base += "\n\n[bold]Deployed[reset]\n"
   434  	base += formatDeploymentGroups(d, c.length)
   435  	return base
   436  }
   437  
   438  func formatAllocListStubs(stubs []*api.AllocationListStub, verbose bool, uuidLength int) string {
   439  	if len(stubs) == 0 {
   440  		return "No allocations placed"
   441  	}
   442  
   443  	allocs := make([]string, len(stubs)+1)
   444  	if verbose {
   445  		allocs[0] = "ID|Eval ID|Node ID|Node Name|Task Group|Version|Desired|Status|Created|Modified"
   446  		for i, alloc := range stubs {
   447  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%s|%d|%s|%s|%s|%s",
   448  				limit(alloc.ID, uuidLength),
   449  				limit(alloc.EvalID, uuidLength),
   450  				limit(alloc.NodeID, uuidLength),
   451  				alloc.NodeName,
   452  				alloc.TaskGroup,
   453  				alloc.JobVersion,
   454  				alloc.DesiredStatus,
   455  				alloc.ClientStatus,
   456  				formatUnixNanoTime(alloc.CreateTime),
   457  				formatUnixNanoTime(alloc.ModifyTime))
   458  		}
   459  	} else {
   460  		allocs[0] = "ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
   461  		for i, alloc := range stubs {
   462  			now := time.Now()
   463  			createTimePretty := prettyTimeDiff(time.Unix(0, alloc.CreateTime), now)
   464  			modTimePretty := prettyTimeDiff(time.Unix(0, alloc.ModifyTime), now)
   465  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s|%s|%s",
   466  				limit(alloc.ID, uuidLength),
   467  				limit(alloc.NodeID, uuidLength),
   468  				alloc.TaskGroup,
   469  				alloc.JobVersion,
   470  				alloc.DesiredStatus,
   471  				alloc.ClientStatus,
   472  				createTimePretty,
   473  				modTimePretty)
   474  		}
   475  	}
   476  
   477  	return formatList(allocs)
   478  }
   479  
   480  func formatAllocList(allocations []*api.Allocation, verbose bool, uuidLength int) string {
   481  	if len(allocations) == 0 {
   482  		return "No allocations placed"
   483  	}
   484  
   485  	allocs := make([]string, len(allocations)+1)
   486  	if verbose {
   487  		allocs[0] = "ID|Eval ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
   488  		for i, alloc := range allocations {
   489  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s|%s|%s",
   490  				limit(alloc.ID, uuidLength),
   491  				limit(alloc.EvalID, uuidLength),
   492  				limit(alloc.NodeID, uuidLength),
   493  				alloc.TaskGroup,
   494  				*alloc.Job.Version,
   495  				alloc.DesiredStatus,
   496  				alloc.ClientStatus,
   497  				formatUnixNanoTime(alloc.CreateTime),
   498  				formatUnixNanoTime(alloc.ModifyTime))
   499  		}
   500  	} else {
   501  		allocs[0] = "ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
   502  		for i, alloc := range allocations {
   503  			now := time.Now()
   504  			createTimePretty := prettyTimeDiff(time.Unix(0, alloc.CreateTime), now)
   505  			modTimePretty := prettyTimeDiff(time.Unix(0, alloc.ModifyTime), now)
   506  			allocs[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s|%s|%s",
   507  				limit(alloc.ID, uuidLength),
   508  				limit(alloc.NodeID, uuidLength),
   509  				alloc.TaskGroup,
   510  				*alloc.Job.Version,
   511  				alloc.DesiredStatus,
   512  				alloc.ClientStatus,
   513  				createTimePretty,
   514  				modTimePretty)
   515  		}
   516  	}
   517  
   518  	return formatList(allocs)
   519  }
   520  
   521  // outputJobSummary displays the given jobs summary and children job summary
   522  // where appropriate
   523  func (c *JobStatusCommand) outputJobSummary(client *api.Client, job *api.Job) error {
   524  	// Query the summary
   525  	q := &api.QueryOptions{Namespace: *job.Namespace}
   526  	summary, _, err := client.Jobs().Summary(*job.ID, q)
   527  	if err != nil {
   528  		return fmt.Errorf("Error querying job summary: %s", err)
   529  	}
   530  
   531  	if summary == nil {
   532  		return nil
   533  	}
   534  
   535  	periodic := job.IsPeriodic()
   536  	parameterizedJob := job.IsParameterized()
   537  
   538  	// Print the summary
   539  	if !periodic && !parameterizedJob {
   540  		c.Ui.Output(c.Colorize().Color("\n[bold]Summary[reset]"))
   541  		summaries := make([]string, len(summary.Summary)+1)
   542  		summaries[0] = "Task Group|Queued|Starting|Running|Failed|Complete|Lost|Unknown"
   543  		taskGroups := make([]string, 0, len(summary.Summary))
   544  		for taskGroup := range summary.Summary {
   545  			taskGroups = append(taskGroups, taskGroup)
   546  		}
   547  		sort.Strings(taskGroups)
   548  		for idx, taskGroup := range taskGroups {
   549  			tgs := summary.Summary[taskGroup]
   550  			summaries[idx+1] = fmt.Sprintf("%s|%d|%d|%d|%d|%d|%d|%d",
   551  				taskGroup, tgs.Queued, tgs.Starting,
   552  				tgs.Running, tgs.Failed,
   553  				tgs.Complete, tgs.Lost, tgs.Unknown,
   554  			)
   555  		}
   556  		c.Ui.Output(formatList(summaries))
   557  	}
   558  
   559  	// Always display the summary if we are periodic or parameterized, but
   560  	// only display if the summary is non-zero on normal jobs
   561  	if summary.Children != nil && (parameterizedJob || periodic || summary.Children.Sum() > 0) {
   562  		if parameterizedJob {
   563  			c.Ui.Output(c.Colorize().Color("\n[bold]Parameterized Job Summary[reset]"))
   564  		} else {
   565  			c.Ui.Output(c.Colorize().Color("\n[bold]Children Job Summary[reset]"))
   566  		}
   567  		summaries := make([]string, 2)
   568  		summaries[0] = "Pending|Running|Dead"
   569  		summaries[1] = fmt.Sprintf("%d|%d|%d",
   570  			summary.Children.Pending, summary.Children.Running, summary.Children.Dead)
   571  		c.Ui.Output(formatList(summaries))
   572  	}
   573  
   574  	return nil
   575  }
   576  
   577  // outputReschedulingEvals displays eval IDs and time for any
   578  // delayed evaluations by task group
   579  func (c *JobStatusCommand) outputReschedulingEvals(client *api.Client, job *api.Job, allocListStubs []*api.AllocationListStub, uuidLength int) error {
   580  	// Get the most recent alloc ID by task group
   581  
   582  	mostRecentAllocs := make(map[string]*api.AllocationListStub)
   583  	for _, alloc := range allocListStubs {
   584  		a, ok := mostRecentAllocs[alloc.TaskGroup]
   585  		if !ok || alloc.ModifyTime > a.ModifyTime {
   586  			mostRecentAllocs[alloc.TaskGroup] = alloc
   587  		}
   588  	}
   589  
   590  	followUpEvalIds := make(map[string]string)
   591  	for tg, alloc := range mostRecentAllocs {
   592  		if alloc.FollowupEvalID != "" {
   593  			followUpEvalIds[tg] = alloc.FollowupEvalID
   594  		}
   595  	}
   596  
   597  	if len(followUpEvalIds) == 0 {
   598  		return nil
   599  	}
   600  	// Print the reschedule info section
   601  	var delayedEvalInfos []string
   602  
   603  	taskGroups := make([]string, 0, len(followUpEvalIds))
   604  	for taskGroup := range followUpEvalIds {
   605  		taskGroups = append(taskGroups, taskGroup)
   606  	}
   607  	sort.Strings(taskGroups)
   608  	var evalDetails []string
   609  	first := true
   610  	for _, taskGroup := range taskGroups {
   611  		evalID := followUpEvalIds[taskGroup]
   612  		evaluation, _, err := client.Evaluations().Info(evalID, nil)
   613  		// Eval time is not critical output,
   614  		// so don't return it on errors, if its not set, or its already in the past
   615  		if err != nil || evaluation.WaitUntil.IsZero() || time.Now().After(evaluation.WaitUntil) {
   616  			continue
   617  		}
   618  		evalTime := prettyTimeDiff(evaluation.WaitUntil, time.Now())
   619  		if c.verbose {
   620  			if first {
   621  				delayedEvalInfos = append(delayedEvalInfos, "Task Group|Reschedule Policy|Eval ID|Eval Time")
   622  			}
   623  			rp := job.LookupTaskGroup(taskGroup).ReschedulePolicy
   624  			evalDetails = append(evalDetails, fmt.Sprintf("%s|%s|%s|%s", taskGroup, rp.String(), limit(evalID, uuidLength), evalTime))
   625  		} else {
   626  			if first {
   627  				delayedEvalInfos = append(delayedEvalInfos, "Task Group|Eval ID|Eval Time")
   628  			}
   629  			evalDetails = append(evalDetails, fmt.Sprintf("%s|%s|%s", taskGroup, limit(evalID, uuidLength), evalTime))
   630  		}
   631  		first = false
   632  	}
   633  	if len(evalDetails) == 0 {
   634  		return nil
   635  	}
   636  	// Only show this section if there is pending evals
   637  	delayedEvalInfos = append(delayedEvalInfos, evalDetails...)
   638  	c.Ui.Output(c.Colorize().Color("\n[bold]Future Rescheduling Attempts[reset]"))
   639  	c.Ui.Output(formatList(delayedEvalInfos))
   640  	return nil
   641  }
   642  
   643  func (c *JobStatusCommand) outputFailedPlacements(failedEval *api.Evaluation) {
   644  	if failedEval == nil || len(failedEval.FailedTGAllocs) == 0 {
   645  		return
   646  	}
   647  
   648  	c.Ui.Output(c.Colorize().Color("\n[bold]Placement Failure[reset]"))
   649  
   650  	sorted := sortedTaskGroupFromMetrics(failedEval.FailedTGAllocs)
   651  	for i, tg := range sorted {
   652  		if i >= maxFailedTGs {
   653  			break
   654  		}
   655  
   656  		c.Ui.Output(fmt.Sprintf("Task Group %q:", tg))
   657  		metrics := failedEval.FailedTGAllocs[tg]
   658  		c.Ui.Output(formatAllocMetrics(metrics, false, "  "))
   659  		if i != len(sorted)-1 {
   660  			c.Ui.Output("")
   661  		}
   662  	}
   663  
   664  	if len(sorted) > maxFailedTGs {
   665  		trunc := fmt.Sprintf("\nPlacement failures truncated. To see remainder run:\nnomad eval-status %s", failedEval.ID)
   666  		c.Ui.Output(trunc)
   667  	}
   668  }
   669  
   670  // list general information about a list of jobs
   671  func createStatusListOutput(jobs []*api.JobListStub, displayNS bool) string {
   672  	out := make([]string, len(jobs)+1)
   673  	if displayNS {
   674  		out[0] = "ID|Namespace|Type|Priority|Status|Submit Date"
   675  		for i, job := range jobs {
   676  			out[i+1] = fmt.Sprintf("%s|%s|%s|%d|%s|%s",
   677  				job.ID,
   678  				job.JobSummary.Namespace,
   679  				getTypeString(job),
   680  				job.Priority,
   681  				getStatusString(job.Status, &job.Stop),
   682  				formatTime(time.Unix(0, job.SubmitTime)))
   683  		}
   684  	} else {
   685  		out[0] = "ID|Type|Priority|Status|Submit Date"
   686  		for i, job := range jobs {
   687  			out[i+1] = fmt.Sprintf("%s|%s|%d|%s|%s",
   688  				job.ID,
   689  				getTypeString(job),
   690  				job.Priority,
   691  				getStatusString(job.Status, &job.Stop),
   692  				formatTime(time.Unix(0, job.SubmitTime)))
   693  		}
   694  	}
   695  	return formatList(out)
   696  }
   697  
   698  func getTypeString(job *api.JobListStub) string {
   699  	t := job.Type
   700  
   701  	if job.Periodic {
   702  		t += "/periodic"
   703  	}
   704  
   705  	if job.ParameterizedJob {
   706  		t += "/parameterized"
   707  	}
   708  
   709  	return t
   710  }
   711  
   712  func getStatusString(status string, stop *bool) string {
   713  	if stop != nil && *stop {
   714  		return fmt.Sprintf("%s (stopped)", status)
   715  	}
   716  	return status
   717  }