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