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 }