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