github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/command/job_history.go (about)

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