github.com/dkerwin/nomad@v0.3.3-0.20160525181927-74554135514b/command/plan.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/hashicorp/nomad/api" 8 "github.com/hashicorp/nomad/jobspec" 9 "github.com/hashicorp/nomad/scheduler" 10 "github.com/mitchellh/colorstring" 11 ) 12 13 const ( 14 jobModifyIndexHelp = `To submit the job with version verification run: 15 16 nomad run -check-index %d %s 17 18 When running the job with the check-index flag, the job will only be run if the 19 server side version matches the the job modify index returned. If the index has 20 changed, another user has modified the job and the plan's results are 21 potentially invalid.` 22 ) 23 24 type PlanCommand struct { 25 Meta 26 color *colorstring.Colorize 27 } 28 29 func (c *PlanCommand) Help() string { 30 helpText := ` 31 Usage: nomad plan [options] <file> 32 33 Plan invokes a dry-run of the scheduler to determine the effects of submitting 34 either a new or updated version of a job. The plan will not result in any 35 changes to the cluster but gives insight into whether the job could be run 36 successfully and how it would affect existing allocations. 37 38 A job modify index is returned with the plan. This value can be used when 39 submitting the job using "nomad run -check-index", which will check that the job 40 was not modified between the plan and run command before invoking the 41 scheduler. This ensures the job has not been modified since the plan. 42 43 A structured diff between the local and remote job is displayed to 44 give insight into what the scheduler will attempt to do and why. 45 46 General Options: 47 48 ` + generalOptionsUsage() + ` 49 50 Run Options: 51 52 -diff 53 Defaults to true, but can be toggled off to omit diff output. 54 55 -no-color 56 Disable colored output. 57 58 -verbose 59 Increase diff verbosity. 60 ` 61 return strings.TrimSpace(helpText) 62 } 63 64 func (c *PlanCommand) Synopsis() string { 65 return "Dry-run a job update to determine its effects" 66 } 67 68 func (c *PlanCommand) Run(args []string) int { 69 var diff, verbose bool 70 71 flags := c.Meta.FlagSet("plan", FlagSetClient) 72 flags.Usage = func() { c.Ui.Output(c.Help()) } 73 flags.BoolVar(&diff, "diff", true, "") 74 flags.BoolVar(&verbose, "verbose", false, "") 75 76 if err := flags.Parse(args); err != nil { 77 return 1 78 } 79 80 // Check that we got exactly one job 81 args = flags.Args() 82 if len(args) != 1 { 83 c.Ui.Error(c.Help()) 84 return 1 85 } 86 file := args[0] 87 88 // Parse the job file 89 job, err := jobspec.ParseFile(file) 90 if err != nil { 91 c.Ui.Error(fmt.Sprintf("Error parsing job file %s: %s", file, err)) 92 return 1 93 } 94 95 // Initialize any fields that need to be. 96 job.InitFields() 97 98 // Check that the job is valid 99 if err := job.Validate(); err != nil { 100 c.Ui.Error(fmt.Sprintf("Error validating job: %s", err)) 101 return 1 102 } 103 104 // Convert it to something we can use 105 apiJob, err := convertStructJob(job) 106 if err != nil { 107 c.Ui.Error(fmt.Sprintf("Error converting job: %s", err)) 108 return 1 109 } 110 111 // Get the HTTP client 112 client, err := c.Meta.Client() 113 if err != nil { 114 c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) 115 return 1 116 } 117 118 // Submit the job 119 resp, _, err := client.Jobs().Plan(apiJob, diff, nil) 120 if err != nil { 121 c.Ui.Error(fmt.Sprintf("Error during plan: %s", err)) 122 return 1 123 } 124 125 // Print the diff if not disabled 126 if diff { 127 c.Ui.Output(fmt.Sprintf("%s\n", 128 c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose))))) 129 } 130 131 // Print the scheduler dry-run output 132 c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]")) 133 c.Ui.Output(c.Colorize().Color(formatDryRun(resp.CreatedEvals))) 134 135 // Print the job index info 136 c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, file))) 137 return 0 138 } 139 140 // formatJobModifyIndex produces a help string that displays the job modify 141 // index and how to submit a job with it. 142 func formatJobModifyIndex(jobModifyIndex uint64, jobName string) string { 143 help := fmt.Sprintf(jobModifyIndexHelp, jobModifyIndex, jobName) 144 out := fmt.Sprintf("[reset][bold]Job Modify Index: %d[reset]\n%s", jobModifyIndex, help) 145 return out 146 } 147 148 // formatDryRun produces a string explaining the results of the dry run. 149 func formatDryRun(evals []*api.Evaluation) string { 150 var rolling *api.Evaluation 151 var blocked *api.Evaluation 152 for _, eval := range evals { 153 if eval.TriggeredBy == "rolling-update" { 154 rolling = eval 155 } else if eval.Status == "blocked" { 156 blocked = eval 157 } 158 } 159 160 var out string 161 if blocked == nil { 162 out = "[bold][green] - All tasks successfully allocated.[reset]\n" 163 } else { 164 out = "[bold][yellow] - WARNING: Failed to place all allocations.[reset]\n" 165 } 166 167 if rolling != nil { 168 out += fmt.Sprintf("[green] - Rolling update, next evaluation will be in %s.\n", rolling.Wait) 169 } 170 171 return out 172 } 173 174 // formatJobDiff produces an annoted diff of the the job. If verbose mode is 175 // set, added or deleted task groups and tasks are expanded. 176 func formatJobDiff(job *api.JobDiff, verbose bool) string { 177 marker, _ := getDiffString(job.Type) 178 out := fmt.Sprintf("%s[bold]Job: %q\n", marker, job.ID) 179 180 // Determine the longest markers and fields so that the output can be 181 // properly alligned. 182 longestField, longestMarker := getLongestPrefixes(job.Fields, job.Objects) 183 for _, tg := range job.TaskGroups { 184 if _, l := getDiffString(tg.Type); l > longestMarker { 185 longestMarker = l 186 } 187 } 188 189 // Only show the job's field and object diffs if the job is edited or 190 // verbose mode is set. 191 if job.Type == "Edited" || verbose { 192 fo := alignedFieldAndObjects(job.Fields, job.Objects, 0, longestField, longestMarker) 193 out += fo 194 if len(fo) > 0 { 195 out += "\n" 196 } 197 } 198 199 // Print the task groups 200 for _, tg := range job.TaskGroups { 201 _, mLength := getDiffString(tg.Type) 202 kPrefix := longestMarker - mLength 203 out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, kPrefix, verbose)) 204 } 205 206 return out 207 } 208 209 // formatTaskGroupDiff produces an annotated diff of a task group. If the 210 // verbose field is set, the task groups fields and objects are expanded even if 211 // the full object is an addition or removal. tgPrefix is the number of spaces to prefix 212 // the output of the task group. 213 func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix int, verbose bool) string { 214 marker, _ := getDiffString(tg.Type) 215 out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, strings.Repeat(" ", tgPrefix), tg.Name) 216 217 // Append the updates and colorize them 218 if l := len(tg.Updates); l > 0 { 219 updates := make([]string, 0, l) 220 for updateType, count := range tg.Updates { 221 var color string 222 switch updateType { 223 case scheduler.UpdateTypeIgnore: 224 case scheduler.UpdateTypeCreate: 225 color = "[green]" 226 case scheduler.UpdateTypeDestroy: 227 color = "[red]" 228 case scheduler.UpdateTypeMigrate: 229 color = "[blue]" 230 case scheduler.UpdateTypeInplaceUpdate: 231 color = "[cyan]" 232 case scheduler.UpdateTypeDestructiveUpdate: 233 color = "[yellow]" 234 } 235 updates = append(updates, fmt.Sprintf("[reset]%s%d %s", color, count, updateType)) 236 } 237 out += fmt.Sprintf(" (%s[reset])\n", strings.Join(updates, ", ")) 238 } else { 239 out += "[reset]\n" 240 } 241 242 // Determine the longest field and markers so the output is properly 243 // alligned 244 longestField, longestMarker := getLongestPrefixes(tg.Fields, tg.Objects) 245 for _, task := range tg.Tasks { 246 if _, l := getDiffString(task.Type); l > longestMarker { 247 longestMarker = l 248 } 249 } 250 251 // Only show the task groups's field and object diffs if the group is edited or 252 // verbose mode is set. 253 subStartPrefix := tgPrefix + 2 254 if tg.Type == "Edited" || verbose { 255 fo := alignedFieldAndObjects(tg.Fields, tg.Objects, subStartPrefix, longestField, longestMarker) 256 out += fo 257 if len(fo) > 0 { 258 out += "\n" 259 } 260 } 261 262 // Output the tasks 263 for _, task := range tg.Tasks { 264 _, mLength := getDiffString(task.Type) 265 prefix := longestMarker - mLength 266 out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose)) 267 } 268 269 return out 270 } 271 272 // formatTaskDiff produces an annotated diff of a task. If the verbose field is 273 // set, the tasks fields and objects are expanded even if the full object is an 274 // addition or removal. startPrefix is the number of spaces to prefix the output of 275 // the task and taskPrefix is the number of spaces to put betwen the marker and 276 // task name output. 277 func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix int, verbose bool) string { 278 marker, _ := getDiffString(task.Type) 279 out := fmt.Sprintf("%s%s%s[bold]Task: %q", 280 strings.Repeat(" ", startPrefix), marker, strings.Repeat(" ", taskPrefix), task.Name) 281 if len(task.Annotations) != 0 { 282 out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations)) 283 } 284 285 if task.Type == "None" { 286 return out 287 } else if (task.Type == "Deleted" || task.Type == "Added") && !verbose { 288 // Exit early if the job was not edited and it isn't verbose output 289 return out 290 } else { 291 out += "\n" 292 } 293 294 subStartPrefix := startPrefix + 2 295 longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects) 296 out += alignedFieldAndObjects(task.Fields, task.Objects, subStartPrefix, longestField, longestMarker) 297 return out 298 } 299 300 // formatObjectDiff produces an annotated diff of an object. startPrefix is the 301 // number of spaces to prefix the output of the object and keyPrefix is the number 302 // of spaces to put betwen the marker and object name output. 303 func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix int) string { 304 start := strings.Repeat(" ", startPrefix) 305 marker, _ := getDiffString(diff.Type) 306 out := fmt.Sprintf("%s%s%s%s {\n", start, marker, strings.Repeat(" ", keyPrefix), diff.Name) 307 308 // Determine the length of the longest name and longest diff marker to 309 // properly align names and values 310 longestField, longestMarker := getLongestPrefixes(diff.Fields, diff.Objects) 311 subStartPrefix := startPrefix + 2 312 out += alignedFieldAndObjects(diff.Fields, diff.Objects, subStartPrefix, longestField, longestMarker) 313 return fmt.Sprintf("%s\n%s}", out, start) 314 } 315 316 // formatFieldDiff produces an annotated diff of a field. startPrefix is the 317 // number of spaces to prefix the output of the field, keyPrefix is the number 318 // of spaces to put betwen the marker and field name output and valuePrefix is 319 // the number of spaces to put infront of the value for aligning values. 320 func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix int) string { 321 marker, _ := getDiffString(diff.Type) 322 out := fmt.Sprintf("%s%s%s%s: %s", 323 strings.Repeat(" ", startPrefix), 324 marker, strings.Repeat(" ", keyPrefix), 325 diff.Name, 326 strings.Repeat(" ", valuePrefix)) 327 328 switch diff.Type { 329 case "Added": 330 out += fmt.Sprintf("%q", diff.New) 331 case "Deleted": 332 out += fmt.Sprintf("%q", diff.Old) 333 case "Edited": 334 out += fmt.Sprintf("%q => %q", diff.Old, diff.New) 335 default: 336 out += fmt.Sprintf("%q", diff.New) 337 } 338 339 // Color the annotations where possible 340 if l := len(diff.Annotations); l != 0 { 341 out += fmt.Sprintf(" (%s)", colorAnnotations(diff.Annotations)) 342 } 343 344 return out 345 } 346 347 // alignedFieldAndObjects is a helper method that prints fields and objects 348 // properly aligned. 349 func alignedFieldAndObjects(fields []*api.FieldDiff, objects []*api.ObjectDiff, 350 startPrefix, longestField, longestMarker int) string { 351 352 var out string 353 numFields := len(fields) 354 numObjects := len(objects) 355 haveObjects := numObjects != 0 356 for i, field := range fields { 357 _, mLength := getDiffString(field.Type) 358 kPrefix := longestMarker - mLength 359 vPrefix := longestField - len(field.Name) 360 out += formatFieldDiff(field, startPrefix, kPrefix, vPrefix) 361 362 // Avoid a dangling new line 363 if i+1 != numFields || haveObjects { 364 out += "\n" 365 } 366 } 367 368 for i, object := range objects { 369 _, mLength := getDiffString(object.Type) 370 kPrefix := longestMarker - mLength 371 out += formatObjectDiff(object, startPrefix, kPrefix) 372 373 // Avoid a dangling new line 374 if i+1 != numObjects { 375 out += "\n" 376 } 377 } 378 379 return out 380 } 381 382 // getLongestPrefixes takes a list of fields and objects and determines the 383 // longest field name and the longest marker. 384 func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (longestField, longestMarker int) { 385 for _, field := range fields { 386 if l := len(field.Name); l > longestField { 387 longestField = l 388 } 389 if _, l := getDiffString(field.Type); l > longestMarker { 390 longestMarker = l 391 } 392 } 393 for _, obj := range objects { 394 if _, l := getDiffString(obj.Type); l > longestMarker { 395 longestMarker = l 396 } 397 } 398 return longestField, longestMarker 399 } 400 401 // getDiffString returns a colored diff marker and the length of the string 402 // without color annotations. 403 func getDiffString(diffType string) (string, int) { 404 switch diffType { 405 case "Added": 406 return "[green]+[reset] ", 2 407 case "Deleted": 408 return "[red]-[reset] ", 2 409 case "Edited": 410 return "[light_yellow]+/-[reset] ", 4 411 default: 412 return "", 0 413 } 414 } 415 416 // colorAnnotations returns a comma concatonated list of the annotations where 417 // the annotations are colored where possible. 418 func colorAnnotations(annotations []string) string { 419 l := len(annotations) 420 if l == 0 { 421 return "" 422 } 423 424 colored := make([]string, l) 425 for i, annotation := range annotations { 426 switch annotation { 427 case "forces create": 428 colored[i] = fmt.Sprintf("[green]%s[reset]", annotation) 429 case "forces destroy": 430 colored[i] = fmt.Sprintf("[red]%s[reset]", annotation) 431 case "forces in-place update": 432 colored[i] = fmt.Sprintf("[cyan]%s[reset]", annotation) 433 case "forces create/destroy update": 434 colored[i] = fmt.Sprintf("[yellow]%s[reset]", annotation) 435 default: 436 colored[i] = annotation 437 } 438 } 439 440 return strings.Join(colored, ", ") 441 }