github.com/rohankumardubey/nomad@v0.11.8/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) Name() string { return "job history" } 84 85 func (c *JobHistoryCommand) Run(args []string) int { 86 var json, diff, full bool 87 var tmpl, versionStr string 88 89 flags := c.Meta.FlagSet(c.Name(), FlagSetClient) 90 flags.Usage = func() { c.Ui.Output(c.Help()) } 91 flags.BoolVar(&diff, "p", false, "") 92 flags.BoolVar(&full, "full", false, "") 93 flags.BoolVar(&json, "json", false, "") 94 flags.StringVar(&versionStr, "version", "", "") 95 flags.StringVar(&tmpl, "t", "", "") 96 97 if err := flags.Parse(args); err != nil { 98 return 1 99 } 100 101 // Check that we got exactly one node 102 args = flags.Args() 103 if l := len(args); l < 1 || l > 2 { 104 c.Ui.Error("This command takes one argument: <job>") 105 c.Ui.Error(commandErrorText(c)) 106 return 1 107 } 108 109 if (json || len(tmpl) != 0) && (diff || full) { 110 c.Ui.Error("-json and -t are exclusive with -p and -full") 111 return 1 112 } 113 114 // Get the HTTP client 115 client, err := c.Meta.Client() 116 if err != nil { 117 c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) 118 return 1 119 } 120 121 jobID := args[0] 122 123 // Check if the job exists 124 jobs, _, err := client.Jobs().PrefixList(jobID) 125 if err != nil { 126 c.Ui.Error(fmt.Sprintf("Error listing jobs: %s", err)) 127 return 1 128 } 129 if len(jobs) == 0 { 130 c.Ui.Error(fmt.Sprintf("No job(s) with prefix or id %q found", jobID)) 131 return 1 132 } 133 if len(jobs) > 1 && strings.TrimSpace(jobID) != jobs[0].ID { 134 c.Ui.Error(fmt.Sprintf("Prefix matched multiple jobs\n\n%s", createStatusListOutput(jobs))) 135 return 1 136 } 137 138 // Prefix lookup matched a single job 139 versions, diffs, _, err := client.Jobs().Versions(jobs[0].ID, diff, nil) 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 }