github.com/hernad/nomad@v1.6.112/command/job_history.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/hernad/nomad/api"
    13  	"github.com/hernad/nomad/api/contexts"
    14  	"github.com/posener/complete"
    15  	"github.com/ryanuber/columnize"
    16  )
    17  
    18  type JobHistoryCommand struct {
    19  	Meta
    20  	formatter DataFormatter
    21  }
    22  
    23  func (c *JobHistoryCommand) Help() string {
    24  	helpText := `
    25  Usage: nomad job history [options] <job>
    26  
    27    History is used to display the known versions of a particular job. The command
    28    can display the diff between job versions and can be useful for understanding
    29    the changes that occurred to the job as well as deciding job versions to revert
    30    to.
    31  
    32    When ACLs are enabled, this command requires a token with the 'read-job'
    33    capability for the job's namespace. The 'list-jobs' capability is required to
    34    run the command with a job prefix instead of the exact job ID.
    35  
    36  General Options:
    37  
    38    ` + generalOptionsUsage(usageOptsDefault) + `
    39  
    40  History Options:
    41  
    42    -p
    43      Display the difference between each job and its predecessor.
    44  
    45    -full
    46      Display the full job definition for each version.
    47  
    48    -version <job version>
    49      Display only the history for the given job version.
    50  
    51    -json
    52      Output the job versions in a JSON format.
    53  
    54    -t
    55      Format and display the job versions using a Go template.
    56  `
    57  	return strings.TrimSpace(helpText)
    58  }
    59  
    60  func (c *JobHistoryCommand) Synopsis() string {
    61  	return "Display all tracked versions of a job"
    62  }
    63  
    64  func (c *JobHistoryCommand) AutocompleteFlags() complete.Flags {
    65  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    66  		complete.Flags{
    67  			"-p":       complete.PredictNothing,
    68  			"-full":    complete.PredictNothing,
    69  			"-version": complete.PredictAnything,
    70  			"-json":    complete.PredictNothing,
    71  			"-t":       complete.PredictAnything,
    72  		})
    73  }
    74  
    75  func (c *JobHistoryCommand) 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 *JobHistoryCommand) Name() string { return "job history" }
    91  
    92  func (c *JobHistoryCommand) Run(args []string) int {
    93  	var json, diff, full bool
    94  	var tmpl, versionStr string
    95  
    96  	flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
    97  	flags.Usage = func() { c.Ui.Output(c.Help()) }
    98  	flags.BoolVar(&diff, "p", false, "")
    99  	flags.BoolVar(&full, "full", false, "")
   100  	flags.BoolVar(&json, "json", false, "")
   101  	flags.StringVar(&versionStr, "version", "", "")
   102  	flags.StringVar(&tmpl, "t", "", "")
   103  
   104  	if err := flags.Parse(args); err != nil {
   105  		return 1
   106  	}
   107  
   108  	// Check that we got exactly one node
   109  	args = flags.Args()
   110  	if l := len(args); l < 1 || l > 2 {
   111  		c.Ui.Error("This command takes one argument: <job>")
   112  		c.Ui.Error(commandErrorText(c))
   113  		return 1
   114  	}
   115  
   116  	if (json || len(tmpl) != 0) && (diff || full) {
   117  		c.Ui.Error("-json and -t are exclusive with -p and -full")
   118  		return 1
   119  	}
   120  
   121  	// Get the HTTP client
   122  	client, err := c.Meta.Client()
   123  	if err != nil {
   124  		c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
   125  		return 1
   126  	}
   127  
   128  	// Check if the job exists
   129  	jobIDPrefix := strings.TrimSpace(args[0])
   130  	jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil)
   131  	if err != nil {
   132  		c.Ui.Error(err.Error())
   133  		return 1
   134  	}
   135  
   136  	q := &api.QueryOptions{Namespace: namespace}
   137  
   138  	// Prefix lookup matched a single job
   139  	versions, diffs, _, err := client.Jobs().Versions(jobID, diff, q)
   140  	if err != nil {
   141  		c.Ui.Error(fmt.Sprintf("Error retrieving job versions: %s", err))
   142  		return 1
   143  	}
   144  
   145  	f, err := DataFormat("json", "")
   146  	if err != nil {
   147  		c.Ui.Error(fmt.Sprintf("Error getting formatter: %s", err))
   148  		return 1
   149  	}
   150  	c.formatter = f
   151  
   152  	if versionStr != "" {
   153  		version, _, err := parseVersion(versionStr)
   154  		if err != nil {
   155  			c.Ui.Error(fmt.Sprintf("Error parsing version value %q: %v", versionStr, err))
   156  			return 1
   157  		}
   158  
   159  		var job *api.Job
   160  		var diff *api.JobDiff
   161  		var nextVersion uint64
   162  		for i, v := range versions {
   163  			if *v.Version != version {
   164  				continue
   165  			}
   166  
   167  			job = v
   168  			if i+1 <= len(diffs) {
   169  				diff = diffs[i]
   170  				nextVersion = *versions[i+1].Version
   171  			}
   172  		}
   173  
   174  		if json || len(tmpl) > 0 {
   175  			out, err := Format(json, tmpl, job)
   176  			if err != nil {
   177  				c.Ui.Error(err.Error())
   178  				return 1
   179  			}
   180  
   181  			c.Ui.Output(out)
   182  			return 0
   183  		}
   184  
   185  		if err := c.formatJobVersion(job, diff, nextVersion, full); err != nil {
   186  			c.Ui.Error(err.Error())
   187  			return 1
   188  		}
   189  
   190  	} else {
   191  		if json || len(tmpl) > 0 {
   192  			out, err := Format(json, tmpl, versions)
   193  			if err != nil {
   194  				c.Ui.Error(err.Error())
   195  				return 1
   196  			}
   197  
   198  			c.Ui.Output(out)
   199  			return 0
   200  		}
   201  
   202  		if err := c.formatJobVersions(versions, diffs, full); err != nil {
   203  			c.Ui.Error(err.Error())
   204  			return 1
   205  		}
   206  	}
   207  
   208  	return 0
   209  }
   210  
   211  // parseVersion parses the version flag and returns the index, whether it
   212  // was set and potentially an error during parsing.
   213  func parseVersion(input string) (uint64, bool, error) {
   214  	if input == "" {
   215  		return 0, false, nil
   216  	}
   217  
   218  	u, err := strconv.ParseUint(input, 10, 64)
   219  	return u, true, err
   220  }
   221  
   222  func (c *JobHistoryCommand) formatJobVersions(versions []*api.Job, diffs []*api.JobDiff, full bool) error {
   223  	vLen := len(versions)
   224  	dLen := len(diffs)
   225  	if dLen != 0 && vLen != dLen+1 {
   226  		return fmt.Errorf("Number of job versions %d doesn't match number of diffs %d", vLen, dLen)
   227  	}
   228  
   229  	for i, version := range versions {
   230  		var diff *api.JobDiff
   231  		var nextVersion uint64
   232  		if i+1 <= dLen {
   233  			diff = diffs[i]
   234  			nextVersion = *versions[i+1].Version
   235  		}
   236  
   237  		if err := c.formatJobVersion(version, diff, nextVersion, full); err != nil {
   238  			return err
   239  		}
   240  
   241  		// Insert a blank
   242  		if i != vLen-1 {
   243  			c.Ui.Output("")
   244  		}
   245  	}
   246  
   247  	return nil
   248  }
   249  
   250  func (c *JobHistoryCommand) formatJobVersion(job *api.Job, diff *api.JobDiff, nextVersion uint64, full bool) error {
   251  	if job == nil {
   252  		return fmt.Errorf("Error printing job history for non-existing job or job version")
   253  	}
   254  
   255  	basic := []string{
   256  		fmt.Sprintf("Version|%d", *job.Version),
   257  		fmt.Sprintf("Stable|%v", *job.Stable),
   258  		fmt.Sprintf("Submit Date|%v", formatTime(time.Unix(0, *job.SubmitTime))),
   259  	}
   260  
   261  	if diff != nil {
   262  		//diffStr := fmt.Sprintf("Difference between version %d and %d:", *job.Version, nextVersion)
   263  		basic = append(basic, fmt.Sprintf("Diff|\n%s", strings.TrimSpace(formatJobDiff(diff, false))))
   264  	}
   265  
   266  	if full {
   267  		out, err := c.formatter.TransformData(job)
   268  		if err != nil {
   269  			return fmt.Errorf("Error formatting the data: %s", err)
   270  		}
   271  
   272  		basic = append(basic, fmt.Sprintf("Full|JSON Job:\n%s", out))
   273  	}
   274  
   275  	columnConf := columnize.DefaultConfig()
   276  	columnConf.Glue = " = "
   277  	columnConf.NoTrim = true
   278  	output := columnize.Format(basic, columnConf)
   279  
   280  	c.Ui.Output(c.Colorize().Color(output))
   281  	return nil
   282  }