github.com/adityamillind98/nomad@v0.11.8/command/job_plan.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "sort" 6 "strings" 7 "time" 8 9 "github.com/hashicorp/nomad/api" 10 "github.com/hashicorp/nomad/scheduler" 11 "github.com/posener/complete" 12 ) 13 14 const ( 15 jobModifyIndexHelp = `To submit the job with version verification run: 16 17 nomad job run -check-index %d %s 18 19 When running the job with the check-index flag, the job will only be run if the 20 job modify index given matches the server-side version. If the index has 21 changed, another user has modified the job and the plan's results are 22 potentially invalid.` 23 24 // preemptionDisplayThreshold is an upper bound used to limit and summarize 25 // the details of preempted jobs in the output 26 preemptionDisplayThreshold = 10 27 ) 28 29 type JobPlanCommand struct { 30 Meta 31 JobGetter 32 } 33 34 func (c *JobPlanCommand) Help() string { 35 helpText := ` 36 Usage: nomad job plan [options] <path> 37 Alias: nomad plan 38 39 Plan invokes a dry-run of the scheduler to determine the effects of submitting 40 either a new or updated version of a job. The plan will not result in any 41 changes to the cluster but gives insight into whether the job could be run 42 successfully and how it would affect existing allocations. 43 44 If the supplied path is "-", the jobfile is read from stdin. Otherwise 45 it is read from the file at the supplied path or downloaded and 46 read from URL specified. 47 48 A job modify index is returned with the plan. This value can be used when 49 submitting the job using "nomad run -check-index", which will check that the job 50 was not modified between the plan and run command before invoking the 51 scheduler. This ensures the job has not been modified since the plan. 52 53 A structured diff between the local and remote job is displayed to 54 give insight into what the scheduler will attempt to do and why. 55 56 If the job has specified the region, the -region flag and NOMAD_REGION 57 environment variable are overridden and the job's region is used. 58 59 Plan will return one of the following exit codes: 60 * 0: No allocations created or destroyed. 61 * 1: Allocations created or destroyed. 62 * 255: Error determining plan results. 63 64 General Options: 65 66 ` + generalOptionsUsage() + ` 67 68 Plan Options: 69 70 -diff 71 Determines whether the diff between the remote job and planned job is shown. 72 Defaults to true. 73 74 -policy-override 75 Sets the flag to force override any soft mandatory Sentinel policies. 76 77 -verbose 78 Increase diff verbosity. 79 ` 80 return strings.TrimSpace(helpText) 81 } 82 83 func (c *JobPlanCommand) Synopsis() string { 84 return "Dry-run a job update to determine its effects" 85 } 86 87 func (c *JobPlanCommand) AutocompleteFlags() complete.Flags { 88 return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), 89 complete.Flags{ 90 "-diff": complete.PredictNothing, 91 "-policy-override": complete.PredictNothing, 92 "-verbose": complete.PredictNothing, 93 }) 94 } 95 96 func (c *JobPlanCommand) AutocompleteArgs() complete.Predictor { 97 return complete.PredictOr(complete.PredictFiles("*.nomad"), complete.PredictFiles("*.hcl")) 98 } 99 100 func (c *JobPlanCommand) Name() string { return "job plan" } 101 func (c *JobPlanCommand) Run(args []string) int { 102 var diff, policyOverride, verbose bool 103 104 flags := c.Meta.FlagSet(c.Name(), FlagSetClient) 105 flags.Usage = func() { c.Ui.Output(c.Help()) } 106 flags.BoolVar(&diff, "diff", true, "") 107 flags.BoolVar(&policyOverride, "policy-override", false, "") 108 flags.BoolVar(&verbose, "verbose", false, "") 109 110 if err := flags.Parse(args); err != nil { 111 return 255 112 } 113 114 // Check that we got exactly one job 115 args = flags.Args() 116 if len(args) != 1 { 117 c.Ui.Error("This command takes one argument: <path>") 118 c.Ui.Error(commandErrorText(c)) 119 return 255 120 } 121 122 path := args[0] 123 // Get Job struct from Jobfile 124 job, err := c.JobGetter.ApiJob(args[0]) 125 if err != nil { 126 c.Ui.Error(fmt.Sprintf("Error getting job struct: %s", err)) 127 return 255 128 } 129 130 // Get the HTTP client 131 client, err := c.Meta.Client() 132 if err != nil { 133 c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) 134 return 255 135 } 136 137 // Force the region to be that of the job. 138 if r := job.Region; r != nil { 139 client.SetRegion(*r) 140 } 141 142 // Force the namespace to be that of the job. 143 if n := job.Namespace; n != nil { 144 client.SetNamespace(*n) 145 } 146 147 // Setup the options 148 opts := &api.PlanOptions{} 149 if diff { 150 opts.Diff = true 151 } 152 if policyOverride { 153 opts.PolicyOverride = true 154 } 155 156 // Submit the job 157 resp, _, err := client.Jobs().PlanOpts(job, opts, nil) 158 if err != nil { 159 c.Ui.Error(fmt.Sprintf("Error during plan: %s", err)) 160 return 255 161 } 162 163 // Print the diff if not disabled 164 if diff { 165 c.Ui.Output(fmt.Sprintf("%s\n", 166 c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose))))) 167 } 168 169 // Print the scheduler dry-run output 170 c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]")) 171 c.Ui.Output(c.Colorize().Color(formatDryRun(resp, job))) 172 c.Ui.Output("") 173 174 // Print any warnings if there are any 175 if resp.Warnings != "" { 176 c.Ui.Output( 177 c.Colorize().Color(fmt.Sprintf("[bold][yellow]Job Warnings:\n%s[reset]\n", resp.Warnings))) 178 } 179 180 // Print preemptions if there are any 181 if resp.Annotations != nil && len(resp.Annotations.PreemptedAllocs) > 0 { 182 c.addPreemptions(resp) 183 } 184 185 // Print the job index info 186 c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, path))) 187 return getExitCode(resp) 188 } 189 190 // addPreemptions shows details about preempted allocations 191 func (c *JobPlanCommand) addPreemptions(resp *api.JobPlanResponse) { 192 c.Ui.Output(c.Colorize().Color("[bold][yellow]Preemptions:\n[reset]")) 193 if len(resp.Annotations.PreemptedAllocs) < preemptionDisplayThreshold { 194 var allocs []string 195 allocs = append(allocs, fmt.Sprintf("Alloc ID|Job ID|Task Group")) 196 for _, alloc := range resp.Annotations.PreemptedAllocs { 197 allocs = append(allocs, fmt.Sprintf("%s|%s|%s", alloc.ID, alloc.JobID, alloc.TaskGroup)) 198 } 199 c.Ui.Output(formatList(allocs)) 200 return 201 } 202 // Display in a summary format if the list is too large 203 // Group by job type and job ids 204 allocDetails := make(map[string]map[namespaceIdPair]int) 205 numJobs := 0 206 for _, alloc := range resp.Annotations.PreemptedAllocs { 207 id := namespaceIdPair{alloc.JobID, alloc.Namespace} 208 countMap := allocDetails[alloc.JobType] 209 if countMap == nil { 210 countMap = make(map[namespaceIdPair]int) 211 } 212 cnt, ok := countMap[id] 213 if !ok { 214 // First time we are seeing this job, increment counter 215 numJobs++ 216 } 217 countMap[id] = cnt + 1 218 allocDetails[alloc.JobType] = countMap 219 } 220 221 // Show counts grouped by job ID if its less than a threshold 222 var output []string 223 if numJobs < preemptionDisplayThreshold { 224 output = append(output, fmt.Sprintf("Job ID|Namespace|Job Type|Preemptions")) 225 for jobType, jobCounts := range allocDetails { 226 for jobId, count := range jobCounts { 227 output = append(output, fmt.Sprintf("%s|%s|%s|%d", jobId.id, jobId.namespace, jobType, count)) 228 } 229 } 230 } else { 231 // Show counts grouped by job type 232 output = append(output, fmt.Sprintf("Job Type|Preemptions")) 233 for jobType, jobCounts := range allocDetails { 234 total := 0 235 for _, count := range jobCounts { 236 total += count 237 } 238 output = append(output, fmt.Sprintf("%s|%d", jobType, total)) 239 } 240 } 241 c.Ui.Output(formatList(output)) 242 243 } 244 245 type namespaceIdPair struct { 246 id string 247 namespace string 248 } 249 250 // getExitCode returns 0: 251 // * 0: No allocations created or destroyed. 252 // * 1: Allocations created or destroyed. 253 func getExitCode(resp *api.JobPlanResponse) int { 254 // Check for changes 255 for _, d := range resp.Annotations.DesiredTGUpdates { 256 if d.Stop+d.Place+d.Migrate+d.DestructiveUpdate+d.Canary > 0 { 257 return 1 258 } 259 } 260 261 return 0 262 } 263 264 // formatJobModifyIndex produces a help string that displays the job modify 265 // index and how to submit a job with it. 266 func formatJobModifyIndex(jobModifyIndex uint64, jobName string) string { 267 help := fmt.Sprintf(jobModifyIndexHelp, jobModifyIndex, jobName) 268 out := fmt.Sprintf("[reset][bold]Job Modify Index: %d[reset]\n%s", jobModifyIndex, help) 269 return out 270 } 271 272 // formatDryRun produces a string explaining the results of the dry run. 273 func formatDryRun(resp *api.JobPlanResponse, job *api.Job) string { 274 var rolling *api.Evaluation 275 for _, eval := range resp.CreatedEvals { 276 if eval.TriggeredBy == "rolling-update" { 277 rolling = eval 278 } 279 } 280 281 var out string 282 if len(resp.FailedTGAllocs) == 0 { 283 out = "[bold][green]- All tasks successfully allocated.[reset]\n" 284 } else { 285 // Change the output depending on if we are a system job or not 286 if job.Type != nil && *job.Type == "system" { 287 out = "[bold][yellow]- WARNING: Failed to place allocations on all nodes.[reset]\n" 288 } else { 289 out = "[bold][yellow]- WARNING: Failed to place all allocations.[reset]\n" 290 } 291 sorted := sortedTaskGroupFromMetrics(resp.FailedTGAllocs) 292 for _, tg := range sorted { 293 metrics := resp.FailedTGAllocs[tg] 294 295 noun := "allocation" 296 if metrics.CoalescedFailures > 0 { 297 noun += "s" 298 } 299 out += fmt.Sprintf("%s[yellow]Task Group %q (failed to place %d %s):\n[reset]", strings.Repeat(" ", 2), tg, metrics.CoalescedFailures+1, noun) 300 out += fmt.Sprintf("[yellow]%s[reset]\n\n", formatAllocMetrics(metrics, false, strings.Repeat(" ", 4))) 301 } 302 if rolling == nil { 303 out = strings.TrimSuffix(out, "\n") 304 } 305 } 306 307 if rolling != nil { 308 out += fmt.Sprintf("[green]- Rolling update, next evaluation will be in %s.\n", rolling.Wait) 309 } 310 311 if next := resp.NextPeriodicLaunch; !next.IsZero() && !job.IsParameterized() { 312 loc, err := job.Periodic.GetLocation() 313 if err != nil { 314 out += fmt.Sprintf("[yellow]- Invalid time zone: %v", err) 315 } else { 316 now := time.Now().In(loc) 317 out += fmt.Sprintf("[green]- If submitted now, next periodic launch would be at %s (%s from now).\n", 318 formatTime(next), formatTimeDifference(now, next, time.Second)) 319 } 320 } 321 322 out = strings.TrimSuffix(out, "\n") 323 return out 324 } 325 326 // formatJobDiff produces an annotated diff of the job. If verbose mode is 327 // set, added or deleted task groups and tasks are expanded. 328 func formatJobDiff(job *api.JobDiff, verbose bool) string { 329 marker, _ := getDiffString(job.Type) 330 out := fmt.Sprintf("%s[bold]Job: %q\n", marker, job.ID) 331 332 // Determine the longest markers and fields so that the output can be 333 // properly aligned. 334 longestField, longestMarker := getLongestPrefixes(job.Fields, job.Objects) 335 for _, tg := range job.TaskGroups { 336 if _, l := getDiffString(tg.Type); l > longestMarker { 337 longestMarker = l 338 } 339 } 340 341 // Only show the job's field and object diffs if the job is edited or 342 // verbose mode is set. 343 if job.Type == "Edited" || verbose { 344 fo := alignedFieldAndObjects(job.Fields, job.Objects, 0, longestField, longestMarker) 345 out += fo 346 if len(fo) > 0 { 347 out += "\n" 348 } 349 } 350 351 // Print the task groups 352 for _, tg := range job.TaskGroups { 353 _, mLength := getDiffString(tg.Type) 354 kPrefix := longestMarker - mLength 355 out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, kPrefix, verbose)) 356 } 357 358 return out 359 } 360 361 // formatTaskGroupDiff produces an annotated diff of a task group. If the 362 // verbose field is set, the task groups fields and objects are expanded even if 363 // the full object is an addition or removal. tgPrefix is the number of spaces to prefix 364 // the output of the task group. 365 func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix int, verbose bool) string { 366 marker, _ := getDiffString(tg.Type) 367 out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, strings.Repeat(" ", tgPrefix), tg.Name) 368 369 // Append the updates and colorize them 370 if l := len(tg.Updates); l > 0 { 371 order := make([]string, 0, l) 372 for updateType := range tg.Updates { 373 order = append(order, updateType) 374 } 375 376 sort.Strings(order) 377 updates := make([]string, 0, l) 378 for _, updateType := range order { 379 count := tg.Updates[updateType] 380 var color string 381 switch updateType { 382 case scheduler.UpdateTypeIgnore: 383 case scheduler.UpdateTypeCreate: 384 color = "[green]" 385 case scheduler.UpdateTypeDestroy: 386 color = "[red]" 387 case scheduler.UpdateTypeMigrate: 388 color = "[blue]" 389 case scheduler.UpdateTypeInplaceUpdate: 390 color = "[cyan]" 391 case scheduler.UpdateTypeDestructiveUpdate: 392 color = "[yellow]" 393 case scheduler.UpdateTypeCanary: 394 color = "[light_yellow]" 395 } 396 updates = append(updates, fmt.Sprintf("[reset]%s%d %s", color, count, updateType)) 397 } 398 out += fmt.Sprintf(" (%s[reset])\n", strings.Join(updates, ", ")) 399 } else { 400 out += "[reset]\n" 401 } 402 403 // Determine the longest field and markers so the output is properly 404 // aligned 405 longestField, longestMarker := getLongestPrefixes(tg.Fields, tg.Objects) 406 for _, task := range tg.Tasks { 407 if _, l := getDiffString(task.Type); l > longestMarker { 408 longestMarker = l 409 } 410 } 411 412 // Only show the task groups's field and object diffs if the group is edited or 413 // verbose mode is set. 414 subStartPrefix := tgPrefix + 2 415 if tg.Type == "Edited" || verbose { 416 fo := alignedFieldAndObjects(tg.Fields, tg.Objects, subStartPrefix, longestField, longestMarker) 417 out += fo 418 if len(fo) > 0 { 419 out += "\n" 420 } 421 } 422 423 // Output the tasks 424 for _, task := range tg.Tasks { 425 _, mLength := getDiffString(task.Type) 426 prefix := longestMarker - mLength 427 out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose)) 428 } 429 430 return out 431 } 432 433 // formatTaskDiff produces an annotated diff of a task. If the verbose field is 434 // set, the tasks fields and objects are expanded even if the full object is an 435 // addition or removal. startPrefix is the number of spaces to prefix the output of 436 // the task and taskPrefix is the number of spaces to put between the marker and 437 // task name output. 438 func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix int, verbose bool) string { 439 marker, _ := getDiffString(task.Type) 440 out := fmt.Sprintf("%s%s%s[bold]Task: %q", 441 strings.Repeat(" ", startPrefix), marker, strings.Repeat(" ", taskPrefix), task.Name) 442 if len(task.Annotations) != 0 { 443 out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations)) 444 } 445 446 if task.Type == "None" { 447 return out 448 } else if (task.Type == "Deleted" || task.Type == "Added") && !verbose { 449 // Exit early if the job was not edited and it isn't verbose output 450 return out 451 } else { 452 out += "\n" 453 } 454 455 subStartPrefix := startPrefix + 2 456 longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects) 457 out += alignedFieldAndObjects(task.Fields, task.Objects, subStartPrefix, longestField, longestMarker) 458 return out 459 } 460 461 // formatObjectDiff produces an annotated diff of an object. startPrefix is the 462 // number of spaces to prefix the output of the object and keyPrefix is the number 463 // of spaces to put between the marker and object name output. 464 func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix int) string { 465 start := strings.Repeat(" ", startPrefix) 466 marker, markerLen := getDiffString(diff.Type) 467 out := fmt.Sprintf("%s%s%s%s {\n", start, marker, strings.Repeat(" ", keyPrefix), diff.Name) 468 469 // Determine the length of the longest name and longest diff marker to 470 // properly align names and values 471 longestField, longestMarker := getLongestPrefixes(diff.Fields, diff.Objects) 472 subStartPrefix := startPrefix + keyPrefix + 2 473 out += alignedFieldAndObjects(diff.Fields, diff.Objects, subStartPrefix, longestField, longestMarker) 474 475 endprefix := strings.Repeat(" ", startPrefix+markerLen+keyPrefix) 476 return fmt.Sprintf("%s\n%s}", out, endprefix) 477 } 478 479 // formatFieldDiff produces an annotated diff of a field. startPrefix is the 480 // number of spaces to prefix the output of the field, keyPrefix is the number 481 // of spaces to put between the marker and field name output and valuePrefix is 482 // the number of spaces to put infront of the value for aligning values. 483 func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix int) string { 484 marker, _ := getDiffString(diff.Type) 485 out := fmt.Sprintf("%s%s%s%s: %s", 486 strings.Repeat(" ", startPrefix), 487 marker, strings.Repeat(" ", keyPrefix), 488 diff.Name, 489 strings.Repeat(" ", valuePrefix)) 490 491 switch diff.Type { 492 case "Added": 493 out += fmt.Sprintf("%q", diff.New) 494 case "Deleted": 495 out += fmt.Sprintf("%q", diff.Old) 496 case "Edited": 497 out += fmt.Sprintf("%q => %q", diff.Old, diff.New) 498 default: 499 out += fmt.Sprintf("%q", diff.New) 500 } 501 502 // Color the annotations where possible 503 if l := len(diff.Annotations); l != 0 { 504 out += fmt.Sprintf(" (%s)", colorAnnotations(diff.Annotations)) 505 } 506 507 return out 508 } 509 510 // alignedFieldAndObjects is a helper method that prints fields and objects 511 // properly aligned. 512 func alignedFieldAndObjects(fields []*api.FieldDiff, objects []*api.ObjectDiff, 513 startPrefix, longestField, longestMarker int) string { 514 515 var out string 516 numFields := len(fields) 517 numObjects := len(objects) 518 haveObjects := numObjects != 0 519 for i, field := range fields { 520 _, mLength := getDiffString(field.Type) 521 kPrefix := longestMarker - mLength 522 vPrefix := longestField - len(field.Name) 523 out += formatFieldDiff(field, startPrefix, kPrefix, vPrefix) 524 525 // Avoid a dangling new line 526 if i+1 != numFields || haveObjects { 527 out += "\n" 528 } 529 } 530 531 for i, object := range objects { 532 _, mLength := getDiffString(object.Type) 533 kPrefix := longestMarker - mLength 534 out += formatObjectDiff(object, startPrefix, kPrefix) 535 536 // Avoid a dangling new line 537 if i+1 != numObjects { 538 out += "\n" 539 } 540 } 541 542 return out 543 } 544 545 // getLongestPrefixes takes a list of fields and objects and determines the 546 // longest field name and the longest marker. 547 func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (longestField, longestMarker int) { 548 for _, field := range fields { 549 if l := len(field.Name); l > longestField { 550 longestField = l 551 } 552 if _, l := getDiffString(field.Type); l > longestMarker { 553 longestMarker = l 554 } 555 } 556 for _, obj := range objects { 557 if _, l := getDiffString(obj.Type); l > longestMarker { 558 longestMarker = l 559 } 560 } 561 return longestField, longestMarker 562 } 563 564 // getDiffString returns a colored diff marker and the length of the string 565 // without color annotations. 566 func getDiffString(diffType string) (string, int) { 567 switch diffType { 568 case "Added": 569 return "[green]+[reset] ", 2 570 case "Deleted": 571 return "[red]-[reset] ", 2 572 case "Edited": 573 return "[light_yellow]+/-[reset] ", 4 574 default: 575 return "", 0 576 } 577 } 578 579 // colorAnnotations returns a comma concatenated list of the annotations where 580 // the annotations are colored where possible. 581 func colorAnnotations(annotations []string) string { 582 l := len(annotations) 583 if l == 0 { 584 return "" 585 } 586 587 colored := make([]string, l) 588 for i, annotation := range annotations { 589 switch annotation { 590 case "forces create": 591 colored[i] = fmt.Sprintf("[green]%s[reset]", annotation) 592 case "forces destroy": 593 colored[i] = fmt.Sprintf("[red]%s[reset]", annotation) 594 case "forces in-place update": 595 colored[i] = fmt.Sprintf("[cyan]%s[reset]", annotation) 596 case "forces create/destroy update": 597 colored[i] = fmt.Sprintf("[yellow]%s[reset]", annotation) 598 default: 599 colored[i] = annotation 600 } 601 } 602 603 return strings.Join(colored, ", ") 604 }