github.com/djenriquez/nomad-1@v0.8.1/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 }