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 }