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