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