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