From b208da644a5521f35785b0c4ddb33433e97db1c1 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 12 May 2016 17:17:02 -0700 Subject: [PATCH 1/9] initial implementation of nomad plan --- command/meta.go | 13 +++ command/plan.go | 238 ++++++++++++++++++++++++++++++++++++++++++++++++ commands.go | 7 ++ 3 files changed, 258 insertions(+) create mode 100644 command/plan.go diff --git a/command/meta.go b/command/meta.go index f983be975a7..38120545d1a 100644 --- a/command/meta.go +++ b/command/meta.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/nomad/api" "github.com/mitchellh/cli" + "github.com/mitchellh/colorstring" ) const ( @@ -38,6 +39,9 @@ type Meta struct { // These are set by the command line flags. flagAddress string + + // Whether to not-colorize output + noColor bool } // FlagSet returns a FlagSet with the common flags that every @@ -51,6 +55,7 @@ func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet { // client connectivity options. if fs&FlagSetClient != 0 { f.StringVar(&m.flagAddress, "address", "", "") + f.BoolVar(&m.noColor, "no-color", false, "") } // Create an io.Writer that writes to our UI properly for errors. @@ -82,6 +87,14 @@ func (m *Meta) Client() (*api.Client, error) { return api.NewClient(config) } +func (m *Meta) Colorize() *colorstring.Colorize { + return &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Disable: m.noColor, + Reset: true, + } +} + // generalOptionsUsage returns the help string for the global options. func generalOptionsUsage() string { helpText := ` diff --git a/command/plan.go b/command/plan.go new file mode 100644 index 00000000000..bbffa9239f1 --- /dev/null +++ b/command/plan.go @@ -0,0 +1,238 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/jobspec" + "github.com/hashicorp/nomad/scheduler" + "github.com/mitchellh/colorstring" +) + +type PlanCommand struct { + Meta + color *colorstring.Colorize +} + +func (c *PlanCommand) Help() string { + helpText := ` +Usage: nomad plan [options] + + +General Options: + + ` + generalOptionsUsage() + ` + +Run Options: + + -diff + Defaults to true, but can be toggled off to omit diff output. + + -no-color + Disable colored output. +` + return strings.TrimSpace(helpText) +} + +func (c *PlanCommand) Synopsis() string { + return "Dry-run a job update to determine its effects" +} + +func (c *PlanCommand) Run(args []string) int { + var diff bool + + flags := c.Meta.FlagSet("plan", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&diff, "diff", true, "") + + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got exactly one job + args = flags.Args() + if len(args) != 1 { + c.Ui.Error(c.Help()) + return 1 + } + file := args[0] + + // Parse the job file + job, err := jobspec.ParseFile(file) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error parsing job file %s: %s", file, err)) + return 1 + } + + // Initialize any fields that need to be. + job.InitFields() + + // Check that the job is valid + if err := job.Validate(); err != nil { + c.Ui.Error(fmt.Sprintf("Error validating job: %s", err)) + return 1 + } + + // Convert it to something we can use + apiJob, err := convertStructJob(job) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error converting job: %s", err)) + return 1 + } + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Submit the job + resp, _, err := client.Jobs().Plan(apiJob, diff, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error during plan: %s", err)) + return 1 + } + + if diff { + c.Ui.Output(c.Colorize().Color(formatJobDiff(resp.Diff))) + } + + return 0 +} + +func formatJobDiff(job *api.JobDiff) string { + out := fmt.Sprintf("%s[bold]Job: %q\n", getDiffString(job.Type), job.ID) + + for _, field := range job.Fields { + out += fmt.Sprintf("%s\n", formatFieldDiff(field, "")) + } + + for _, object := range job.Objects { + out += fmt.Sprintf("%s\n", formatObjectDiff(object, "")) + } + + for _, tg := range job.TaskGroups { + out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg)) + } + + return out +} + +func formatTaskGroupDiff(tg *api.TaskGroupDiff) string { + out := fmt.Sprintf("%s[bold]Task Group: %q", getDiffString(tg.Type), tg.Name) + + // Append the updates + if l := len(tg.Updates); l > 0 { + updates := make([]string, 0, l) + for updateType, count := range tg.Updates { + var color string + switch updateType { + case scheduler.UpdateTypeIgnore: + case scheduler.UpdateTypeCreate: + color = "[green]" + case scheduler.UpdateTypeDestroy: + color = "[red]" + case scheduler.UpdateTypeMigrate: + color = "[blue]" + case scheduler.UpdateTypeInplaceUpdate: + color = "[light_yellow]" + case scheduler.UpdateTypeDestructiveUpdate: + color = "[yellow]" + } + updates = append(updates, fmt.Sprintf("[reset]%s%d %s", color, count, updateType)) + } + out += fmt.Sprintf(" (%s[reset])\n", strings.Join(updates, ", ")) + } else { + out += "[reset]\n" + } + + for _, field := range tg.Fields { + out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) + } + + for _, object := range tg.Objects { + out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) + } + + for _, task := range tg.Tasks { + out += fmt.Sprintf("%s\n", formatTaskDiff(task)) + } + + return out +} + +func formatTaskDiff(task *api.TaskDiff) string { + out := fmt.Sprintf(" %s[bold]Task: %q", getDiffString(task.Type), task.Name) + if len(task.Annotations) != 0 { + out += fmt.Sprintf(" [reset](%s)\n", strings.Join(task.Annotations, ", ")) + } else { + out += "\n" + } + + if task.Type != "Edited" { + return out + } + + for _, field := range task.Fields { + out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) + } + + for _, object := range task.Objects { + out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) + } + + return out +} + +func formatFieldDiff(diff *api.FieldDiff, prefix string) string { + switch diff.Type { + case "Added": + return fmt.Sprintf("%s%s%s: %q", prefix, getDiffString(diff.Type), diff.Name, diff.New) + case "Deleted": + return fmt.Sprintf("%s%s%s: %q", prefix, getDiffString(diff.Type), diff.Name, diff.Old) + case "Edited": + return fmt.Sprintf("%s%s%s: %q => %q", prefix, getDiffString(diff.Type), diff.Name, diff.Old, diff.New) + default: + return fmt.Sprintf("%s%s: %q", prefix, diff.Name, diff.New) + } +} + +func formatObjectDiff(diff *api.ObjectDiff, prefix string) string { + diffChar := getDiffString(diff.Type) + out := fmt.Sprintf("%s%s%s {\n", prefix, diffChar, diff.Name) + + newPrefix := prefix + " " + numFields := len(diff.Fields) + numObjects := len(diff.Objects) + haveObjects := numObjects != 0 + for i, field := range diff.Fields { + out += formatFieldDiff(field, newPrefix) + if i+1 != numFields || haveObjects { + out += "\n" + } + } + + for i, object := range diff.Objects { + out += formatObjectDiff(object, newPrefix) + if i+1 != numObjects { + out += "\n" + } + } + + return fmt.Sprintf("%s\n%s%s}", out, prefix, diffChar) +} + +func getDiffString(diffType string) string { + switch diffType { + case "Added": + return "[green]+[reset] " + case "Deleted": + return "[red]-[reset] " + case "Edited": + return "[light_yellow]+/-[reset] " + default: + return "" + } +} diff --git a/commands.go b/commands.go index c675c232a9d..b402f429bc8 100644 --- a/commands.go +++ b/commands.go @@ -89,6 +89,13 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Meta: meta, }, nil }, + + "plan": func() (cli.Command, error) { + return &command.PlanCommand{ + Meta: meta, + }, nil + }, + "run": func() (cli.Command, error) { return &command.RunCommand{ Meta: meta, From 764d622d9a82a97570204b49c0a161f1585c9405 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 12 May 2016 21:25:14 -0700 Subject: [PATCH 2/9] little better output --- command/plan.go | 64 +++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/command/plan.go b/command/plan.go index bbffa9239f1..1156f9bffb9 100644 --- a/command/plan.go +++ b/command/plan.go @@ -96,7 +96,7 @@ func (c *PlanCommand) Run(args []string) int { } if diff { - c.Ui.Output(c.Colorize().Color(formatJobDiff(resp.Diff))) + c.Ui.Output(c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff)))) } return 0 @@ -105,12 +105,14 @@ func (c *PlanCommand) Run(args []string) int { func formatJobDiff(job *api.JobDiff) string { out := fmt.Sprintf("%s[bold]Job: %q\n", getDiffString(job.Type), job.ID) - for _, field := range job.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, "")) - } + if job.Type == "Edited" { + for _, field := range job.Fields { + out += fmt.Sprintf("%s\n", formatFieldDiff(field, "")) + } - for _, object := range job.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, "")) + for _, object := range job.Objects { + out += fmt.Sprintf("%s\n", formatObjectDiff(object, "")) + } } for _, tg := range job.TaskGroups { @@ -148,12 +150,14 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff) string { out += "[reset]\n" } - for _, field := range tg.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) - } + if tg.Type == "Edited" { + for _, field := range tg.Fields { + out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) + } - for _, object := range tg.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) + for _, object := range tg.Objects { + out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) + } } for _, task := range tg.Tasks { @@ -166,42 +170,50 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff) string { func formatTaskDiff(task *api.TaskDiff) string { out := fmt.Sprintf(" %s[bold]Task: %q", getDiffString(task.Type), task.Name) if len(task.Annotations) != 0 { - out += fmt.Sprintf(" [reset](%s)\n", strings.Join(task.Annotations, ", ")) - } else { - out += "\n" + out += fmt.Sprintf(" [reset](%s)", strings.Join(task.Annotations, ", ")) } if task.Type != "Edited" { return out + } else { + out += "\n" } - for _, field := range task.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) - } + if task.Type == "Edited" { + for _, field := range task.Fields { + out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) + } - for _, object := range task.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) + for _, object := range task.Objects { + out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) + } } return out } func formatFieldDiff(diff *api.FieldDiff, prefix string) string { + out := prefix switch diff.Type { case "Added": - return fmt.Sprintf("%s%s%s: %q", prefix, getDiffString(diff.Type), diff.Name, diff.New) + out += fmt.Sprintf("%s%s: %q", getDiffString(diff.Type), diff.Name, diff.New) case "Deleted": - return fmt.Sprintf("%s%s%s: %q", prefix, getDiffString(diff.Type), diff.Name, diff.Old) + out += fmt.Sprintf("%s%s: %q", getDiffString(diff.Type), diff.Name, diff.Old) case "Edited": - return fmt.Sprintf("%s%s%s: %q => %q", prefix, getDiffString(diff.Type), diff.Name, diff.Old, diff.New) + out += fmt.Sprintf("%s%s: %q => %q", getDiffString(diff.Type), diff.Name, diff.Old, diff.New) default: - return fmt.Sprintf("%s%s: %q", prefix, diff.Name, diff.New) + out += fmt.Sprintf("%s: %q", diff.Name, diff.New) } + + if len(diff.Annotations) != 0 { + out += fmt.Sprintf(" (%s)", strings.Join(diff.Annotations, ", ")) + } + + return out } func formatObjectDiff(diff *api.ObjectDiff, prefix string) string { - diffChar := getDiffString(diff.Type) - out := fmt.Sprintf("%s%s%s {\n", prefix, diffChar, diff.Name) + out := fmt.Sprintf("%s%s%s {\n", prefix, getDiffString(diff.Type), diff.Name) newPrefix := prefix + " " numFields := len(diff.Fields) @@ -221,7 +233,7 @@ func formatObjectDiff(diff *api.ObjectDiff, prefix string) string { } } - return fmt.Sprintf("%s\n%s%s}", out, prefix, diffChar) + return fmt.Sprintf("%s\n%s}", out, prefix) } func getDiffString(diffType string) string { From dce120669a643df995f9b154a0f030cc6b389404 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Fri, 13 May 2016 12:38:12 -0700 Subject: [PATCH 3/9] verbose output --- command/plan.go | 57 +++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/command/plan.go b/command/plan.go index 1156f9bffb9..dc459126506 100644 --- a/command/plan.go +++ b/command/plan.go @@ -31,6 +31,10 @@ Run Options: -no-color Disable colored output. + + -verbose + Increased diff verbosity + ` return strings.TrimSpace(helpText) } @@ -40,11 +44,12 @@ func (c *PlanCommand) Synopsis() string { } func (c *PlanCommand) Run(args []string) int { - var diff bool + var diff, verbose bool flags := c.Meta.FlagSet("plan", FlagSetClient) flags.Usage = func() { c.Ui.Output(c.Help()) } flags.BoolVar(&diff, "diff", true, "") + flags.BoolVar(&verbose, "verbose", false, "") if err := flags.Parse(args); err != nil { return 1 @@ -96,33 +101,33 @@ func (c *PlanCommand) Run(args []string) int { } if diff { - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff)))) + c.Ui.Output(c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose)))) } return 0 } -func formatJobDiff(job *api.JobDiff) string { +func formatJobDiff(job *api.JobDiff, verbose bool) string { out := fmt.Sprintf("%s[bold]Job: %q\n", getDiffString(job.Type), job.ID) - if job.Type == "Edited" { + if job.Type == "Edited" || verbose { for _, field := range job.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, "")) + out += fmt.Sprintf("%s\n", formatFieldDiff(field, "", verbose)) } for _, object := range job.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, "")) + out += fmt.Sprintf("%s\n", formatObjectDiff(object, "", verbose)) } } for _, tg := range job.TaskGroups { - out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg)) + out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, verbose)) } return out } -func formatTaskGroupDiff(tg *api.TaskGroupDiff) string { +func formatTaskGroupDiff(tg *api.TaskGroupDiff, verbose bool) string { out := fmt.Sprintf("%s[bold]Task Group: %q", getDiffString(tg.Type), tg.Name) // Append the updates @@ -139,7 +144,7 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff) string { case scheduler.UpdateTypeMigrate: color = "[blue]" case scheduler.UpdateTypeInplaceUpdate: - color = "[light_yellow]" + color = "[cyan]" case scheduler.UpdateTypeDestructiveUpdate: color = "[yellow]" } @@ -150,49 +155,49 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff) string { out += "[reset]\n" } - if tg.Type == "Edited" { + if tg.Type == "Edited" || verbose { for _, field := range tg.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) + out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ", verbose)) } for _, object := range tg.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) + out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ", verbose)) } } for _, task := range tg.Tasks { - out += fmt.Sprintf("%s\n", formatTaskDiff(task)) + out += fmt.Sprintf("%s\n", formatTaskDiff(task, verbose)) } return out } -func formatTaskDiff(task *api.TaskDiff) string { +func formatTaskDiff(task *api.TaskDiff, verbose bool) string { out := fmt.Sprintf(" %s[bold]Task: %q", getDiffString(task.Type), task.Name) if len(task.Annotations) != 0 { out += fmt.Sprintf(" [reset](%s)", strings.Join(task.Annotations, ", ")) } - if task.Type != "Edited" { + if task.Type == "None" { + return out + } else if (task.Type == "Deleted" || task.Type == "Added") && !verbose { return out } else { out += "\n" } - if task.Type == "Edited" { - for _, field := range task.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ")) - } + for _, field := range task.Fields { + out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ", verbose)) + } - for _, object := range task.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ")) - } + for _, object := range task.Objects { + out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ", verbose)) } return out } -func formatFieldDiff(diff *api.FieldDiff, prefix string) string { +func formatFieldDiff(diff *api.FieldDiff, prefix string, verbose bool) string { out := prefix switch diff.Type { case "Added": @@ -212,7 +217,7 @@ func formatFieldDiff(diff *api.FieldDiff, prefix string) string { return out } -func formatObjectDiff(diff *api.ObjectDiff, prefix string) string { +func formatObjectDiff(diff *api.ObjectDiff, prefix string, verbose bool) string { out := fmt.Sprintf("%s%s%s {\n", prefix, getDiffString(diff.Type), diff.Name) newPrefix := prefix + " " @@ -220,14 +225,14 @@ func formatObjectDiff(diff *api.ObjectDiff, prefix string) string { numObjects := len(diff.Objects) haveObjects := numObjects != 0 for i, field := range diff.Fields { - out += formatFieldDiff(field, newPrefix) + out += formatFieldDiff(field, newPrefix, verbose) if i+1 != numFields || haveObjects { out += "\n" } } for i, object := range diff.Objects { - out += formatObjectDiff(object, newPrefix) + out += formatObjectDiff(object, newPrefix, verbose) if i+1 != numObjects { out += "\n" } From 9277a3bfeacad91ecefcee4df1e0198ced71b382 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Fri, 13 May 2016 16:29:32 -0700 Subject: [PATCH 4/9] better colors and verify help text --- command/plan.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/command/plan.go b/command/plan.go index dc459126506..f600402febe 100644 --- a/command/plan.go +++ b/command/plan.go @@ -10,6 +10,16 @@ import ( "github.com/mitchellh/colorstring" ) +const ( + casHelp = `To submit the job with version verification run: + +nomad run -verify %d %s + +When running the job with the verify flag, the job will only be run if the server side +version matches the the verify index returned. If the index has changed, another user has +modified the job and the plan's results are potentially invalid.` +) + type PlanCommand struct { Meta color *colorstring.Colorize @@ -101,12 +111,50 @@ func (c *PlanCommand) Run(args []string) int { } if diff { - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose)))) + c.Ui.Output(fmt.Sprintf("%s\n", + c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose))))) } + c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]")) + c.Ui.Output(c.Colorize().Color(formatDryRun(resp.CreatedEvals))) + + c.Ui.Output(c.Colorize().Color(formatCas(resp.Cas, file))) return 0 } +func formatCas(cas uint64, jobName string) string { + help := fmt.Sprintf(casHelp, cas, jobName) + out := fmt.Sprintf("[reset][bold]Job Verify Index: %d[reset]\n%s", cas, help) + return out +} + +func formatDryRun(evals []*api.Evaluation) string { + // "- All tasks successfully allocated." bold and green + + var rolling *api.Evaluation + var blocked *api.Evaluation + for _, eval := range evals { + if eval.TriggeredBy == "rolling-update" { + rolling = eval + } else if eval.Status == "blocked" { + blocked = eval + } + } + + var out string + if blocked == nil { + out = "[bold][green] - All tasks successfully allocated.[reset]\n" + } else { + out = "[bold][yellow] - WARNING: Failed to place all allocations.[reset]\n" + } + + if rolling != nil { + out += fmt.Sprintf("[green] - Rolling update, next evaluation will be in %s.\n", rolling.Wait) + } + + return out +} + func formatJobDiff(job *api.JobDiff, verbose bool) string { out := fmt.Sprintf("%s[bold]Job: %q\n", getDiffString(job.Type), job.ID) @@ -175,7 +223,7 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff, verbose bool) string { func formatTaskDiff(task *api.TaskDiff, verbose bool) string { out := fmt.Sprintf(" %s[bold]Task: %q", getDiffString(task.Type), task.Name) if len(task.Annotations) != 0 { - out += fmt.Sprintf(" [reset](%s)", strings.Join(task.Annotations, ", ")) + out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations)) } if task.Type == "None" { @@ -210,13 +258,39 @@ func formatFieldDiff(diff *api.FieldDiff, prefix string, verbose bool) string { out += fmt.Sprintf("%s: %q", diff.Name, diff.New) } - if len(diff.Annotations) != 0 { - out += fmt.Sprintf(" (%s)", strings.Join(diff.Annotations, ", ")) + // Color the annotations where possible + if l := len(diff.Annotations); l != 0 { + out += fmt.Sprintf(" (%s)", colorAnnotations(diff.Annotations)) } return out } +func colorAnnotations(annotations []string) string { + l := len(annotations) + if l == 0 { + return "" + } + + colored := make([]string, l) + for i, annotation := range annotations { + switch annotation { + case "forces create": + colored[i] = fmt.Sprintf("[green]%s[reset]", annotation) + case "forces destroy": + colored[i] = fmt.Sprintf("[red]%s[reset]", annotation) + case "forces in-place update": + colored[i] = fmt.Sprintf("[cyan]%s[reset]", annotation) + case "forces create/destroy update": + colored[i] = fmt.Sprintf("[yellow]%s[reset]", annotation) + default: + colored[i] = annotation + } + } + + return strings.Join(colored, ", ") +} + func formatObjectDiff(diff *api.ObjectDiff, prefix string, verbose bool) string { out := fmt.Sprintf("%s%s%s {\n", prefix, getDiffString(diff.Type), diff.Name) From 4d30607bfbeb3f686e7bbcdf702183ec26d17912 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 16 May 2016 22:58:13 -0700 Subject: [PATCH 5/9] Alligned properly --- command/plan.go | 211 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 154 insertions(+), 57 deletions(-) diff --git a/command/plan.go b/command/plan.go index f600402febe..90d8956e1ca 100644 --- a/command/plan.go +++ b/command/plan.go @@ -11,13 +11,14 @@ import ( ) const ( - casHelp = `To submit the job with version verification run: + jobModifyIndexHelp = `To submit the job with version verification run: nomad run -verify %d %s -When running the job with the verify flag, the job will only be run if the server side -version matches the the verify index returned. If the index has changed, another user has -modified the job and the plan's results are potentially invalid.` +When running the job with the verify flag, the job will only be run if the +server side version matches the the job modify index returned. If the index has +changed, another user has modified the job and the plan's results are +potentially invalid.` ) type PlanCommand struct { @@ -118,13 +119,13 @@ func (c *PlanCommand) Run(args []string) int { c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]")) c.Ui.Output(c.Colorize().Color(formatDryRun(resp.CreatedEvals))) - c.Ui.Output(c.Colorize().Color(formatCas(resp.Cas, file))) + c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, file))) return 0 } -func formatCas(cas uint64, jobName string) string { - help := fmt.Sprintf(casHelp, cas, jobName) - out := fmt.Sprintf("[reset][bold]Job Verify Index: %d[reset]\n%s", cas, help) +func formatJobModifyIndex(jobModifyIndex uint64, jobName string) string { + help := fmt.Sprintf(jobModifyIndexHelp, jobModifyIndex, jobName) + out := fmt.Sprintf("[reset][bold]Job Modify Index: %d[reset]\n%s", jobModifyIndex, help) return out } @@ -156,27 +157,51 @@ func formatDryRun(evals []*api.Evaluation) string { } func formatJobDiff(job *api.JobDiff, verbose bool) string { - out := fmt.Sprintf("%s[bold]Job: %q\n", getDiffString(job.Type), job.ID) + marker, _ := getDiffString(job.Type) + out := fmt.Sprintf("%s[bold]Job: %q\n", marker, job.ID) + longestField, longestMarker := getLongestPrefixes(job.Fields, job.Objects) + for _, tg := range job.TaskGroups { + if _, l := getDiffString(tg.Type); l > longestMarker { + longestMarker = l + } + } + + subStartPrefix := "" if job.Type == "Edited" || verbose { for _, field := range job.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, "", verbose)) + _, mLength := getDiffString(field.Type) + kPrefix := longestMarker - mLength + vPrefix := longestField - len(field.Name) + out += fmt.Sprintf("%s\n", formatFieldDiff( + field, + subStartPrefix, + strings.Repeat(" ", kPrefix), + strings.Repeat(" ", vPrefix))) } for _, object := range job.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, "", verbose)) + _, mLength := getDiffString(object.Type) + kPrefix := longestMarker - mLength + out += fmt.Sprintf("%s\n", formatObjectDiff( + object, + subStartPrefix, + strings.Repeat(" ", kPrefix))) } } for _, tg := range job.TaskGroups { - out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, verbose)) + _, mLength := getDiffString(tg.Type) + kPrefix := longestMarker - mLength + out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, strings.Repeat(" ", kPrefix), verbose)) } return out } -func formatTaskGroupDiff(tg *api.TaskGroupDiff, verbose bool) string { - out := fmt.Sprintf("%s[bold]Task Group: %q", getDiffString(tg.Type), tg.Name) +func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) string { + marker, _ := getDiffString(tg.Type) + out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, tgPrefix, tg.Name) // Append the updates if l := len(tg.Updates); l > 0 { @@ -203,25 +228,48 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff, verbose bool) string { out += "[reset]\n" } + longestField, longestMarker := getLongestPrefixes(tg.Fields, tg.Objects) + for _, task := range tg.Tasks { + if _, l := getDiffString(task.Type); l > longestMarker { + longestMarker = l + } + } + + subStartPrefix := strings.Repeat(" ", len(tgPrefix)+2) if tg.Type == "Edited" || verbose { for _, field := range tg.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ", verbose)) + _, mLength := getDiffString(field.Type) + kPrefix := longestMarker - mLength + vPrefix := longestField - len(field.Name) + out += fmt.Sprintf("%s\n", formatFieldDiff( + field, + subStartPrefix, + strings.Repeat(" ", kPrefix), + strings.Repeat(" ", vPrefix))) } for _, object := range tg.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ", verbose)) + _, mLength := getDiffString(object.Type) + kPrefix := longestMarker - mLength + out += fmt.Sprintf("%s\n", formatObjectDiff( + object, + subStartPrefix, + strings.Repeat(" ", kPrefix))) } } for _, task := range tg.Tasks { - out += fmt.Sprintf("%s\n", formatTaskDiff(task, verbose)) + _, mLength := getDiffString(task.Type) + prefix := strings.Repeat(" ", (longestMarker - mLength)) + out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose)) } return out } -func formatTaskDiff(task *api.TaskDiff, verbose bool) string { - out := fmt.Sprintf(" %s[bold]Task: %q", getDiffString(task.Type), task.Name) +func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix string, verbose bool) string { + marker, _ := getDiffString(task.Type) + out := fmt.Sprintf("%s%s%s[bold]Task: %q", startPrefix, marker, taskPrefix, task.Name) if len(task.Annotations) != 0 { out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations)) } @@ -234,28 +282,43 @@ func formatTaskDiff(task *api.TaskDiff, verbose bool) string { out += "\n" } + subStartPrefix := strings.Repeat(" ", len(startPrefix)+2) + longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects) for _, field := range task.Fields { - out += fmt.Sprintf("%s\n", formatFieldDiff(field, " ", verbose)) + _, mLength := getDiffString(field.Type) + kPrefix := longestMarker - mLength + vPrefix := longestField - len(field.Name) + out += fmt.Sprintf("%s\n", formatFieldDiff( + field, + subStartPrefix, + strings.Repeat(" ", kPrefix), + strings.Repeat(" ", vPrefix))) } for _, object := range task.Objects { - out += fmt.Sprintf("%s\n", formatObjectDiff(object, " ", verbose)) + _, mLength := getDiffString(object.Type) + kPrefix := longestMarker - mLength + out += fmt.Sprintf("%s\n", formatObjectDiff( + object, + subStartPrefix, + strings.Repeat(" ", kPrefix))) } return out } -func formatFieldDiff(diff *api.FieldDiff, prefix string, verbose bool) string { - out := prefix +func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix string) string { + marker, _ := getDiffString(diff.Type) + out := fmt.Sprintf("%s%s%s%s: %s", startPrefix, marker, keyPrefix, diff.Name, valuePrefix) switch diff.Type { case "Added": - out += fmt.Sprintf("%s%s: %q", getDiffString(diff.Type), diff.Name, diff.New) + out += fmt.Sprintf("%q", diff.New) case "Deleted": - out += fmt.Sprintf("%s%s: %q", getDiffString(diff.Type), diff.Name, diff.Old) + out += fmt.Sprintf("%q", diff.Old) case "Edited": - out += fmt.Sprintf("%s%s: %q => %q", getDiffString(diff.Type), diff.Name, diff.Old, diff.New) + out += fmt.Sprintf("%q => %q", diff.Old, diff.New) default: - out += fmt.Sprintf("%s: %q", diff.Name, diff.New) + out += fmt.Sprintf("%q", diff.New) } // Color the annotations where possible @@ -266,64 +329,98 @@ func formatFieldDiff(diff *api.FieldDiff, prefix string, verbose bool) string { return out } -func colorAnnotations(annotations []string) string { - l := len(annotations) - if l == 0 { - return "" +func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (longestField, longestMarker int) { + for _, field := range fields { + if l := len(field.Name); l > longestField { + longestField = l + } + if _, l := getDiffString(field.Type); l > longestMarker { + longestMarker = l + } } - - colored := make([]string, l) - for i, annotation := range annotations { - switch annotation { - case "forces create": - colored[i] = fmt.Sprintf("[green]%s[reset]", annotation) - case "forces destroy": - colored[i] = fmt.Sprintf("[red]%s[reset]", annotation) - case "forces in-place update": - colored[i] = fmt.Sprintf("[cyan]%s[reset]", annotation) - case "forces create/destroy update": - colored[i] = fmt.Sprintf("[yellow]%s[reset]", annotation) - default: - colored[i] = annotation + for _, obj := range objects { + if _, l := getDiffString(obj.Type); l > longestMarker { + longestMarker = l } } - - return strings.Join(colored, ", ") + return longestField, longestMarker } -func formatObjectDiff(diff *api.ObjectDiff, prefix string, verbose bool) string { - out := fmt.Sprintf("%s%s%s {\n", prefix, getDiffString(diff.Type), diff.Name) +func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix string) string { + marker, _ := getDiffString(diff.Type) + out := fmt.Sprintf("%s%s%s%s {\n", startPrefix, marker, keyPrefix, diff.Name) - newPrefix := prefix + " " + // Determine the length of the longest name and longest diff marker to + // properly align names and values + longestField, longestMarker := getLongestPrefixes(diff.Fields, diff.Objects) + subStartPrefix := strings.Repeat(" ", len(startPrefix)+2) numFields := len(diff.Fields) numObjects := len(diff.Objects) haveObjects := numObjects != 0 for i, field := range diff.Fields { - out += formatFieldDiff(field, newPrefix, verbose) + _, mLength := getDiffString(field.Type) + kPrefix := longestMarker - mLength + vPrefix := longestField - len(field.Name) + out += formatFieldDiff( + field, + subStartPrefix, + strings.Repeat(" ", kPrefix), + strings.Repeat(" ", vPrefix)) + + // Avoid a dangling new line if i+1 != numFields || haveObjects { out += "\n" } } for i, object := range diff.Objects { - out += formatObjectDiff(object, newPrefix, verbose) + _, mLength := getDiffString(object.Type) + kPrefix := longestMarker - mLength + out += formatObjectDiff(object, subStartPrefix, strings.Repeat(" ", kPrefix)) + + // Avoid a dangling new line if i+1 != numObjects { out += "\n" } } - return fmt.Sprintf("%s\n%s}", out, prefix) + return fmt.Sprintf("%s\n%s}", out, startPrefix) } -func getDiffString(diffType string) string { +func getDiffString(diffType string) (string, int) { switch diffType { case "Added": - return "[green]+[reset] " + return "[green]+[reset] ", 2 case "Deleted": - return "[red]-[reset] " + return "[red]-[reset] ", 2 case "Edited": - return "[light_yellow]+/-[reset] " + return "[light_yellow]+/-[reset] ", 4 default: + return "", 0 + } +} + +func colorAnnotations(annotations []string) string { + l := len(annotations) + if l == 0 { return "" } + + colored := make([]string, l) + for i, annotation := range annotations { + switch annotation { + case "forces create": + colored[i] = fmt.Sprintf("[green]%s[reset]", annotation) + case "forces destroy": + colored[i] = fmt.Sprintf("[red]%s[reset]", annotation) + case "forces in-place update": + colored[i] = fmt.Sprintf("[cyan]%s[reset]", annotation) + case "forces create/destroy update": + colored[i] = fmt.Sprintf("[yellow]%s[reset]", annotation) + default: + colored[i] = annotation + } + } + + return strings.Join(colored, ", ") } From 74339714b47adf010c056f16ffd30d89a855081b Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 17 May 2016 12:06:14 -0700 Subject: [PATCH 6/9] comments, refactor and help string --- command/plan.go | 228 ++++++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 105 deletions(-) diff --git a/command/plan.go b/command/plan.go index 90d8956e1ca..0b7fc51c5e1 100644 --- a/command/plan.go +++ b/command/plan.go @@ -30,6 +30,20 @@ func (c *PlanCommand) Help() string { helpText := ` Usage: nomad plan [options] + Plan invokes a dry-run of the scheduler to determine the effects of submitting + either a new or updated version of a job. The plan will not result in any + changes to the cluster but gives insight into whether the job could be run + successfully and how it would affect existing allocations. + + A job modify index is returned with the plan. This value can be used when + submitting the job using "nomad run -verify", which will check that the job + was not modified between the plan and run command before invoking the + scheduler. This ensures that the plan reflects the same modifications to the + job as the run. + + An annotated diff between the submitted job and the remote state is also + displayed. This diff gives insight onto what the scheduler will attempt to do + and why. General Options: @@ -44,7 +58,7 @@ Run Options: Disable colored output. -verbose - Increased diff verbosity + Increase diff verbosity. ` return strings.TrimSpace(helpText) @@ -111,27 +125,31 @@ func (c *PlanCommand) Run(args []string) int { return 1 } + // Print the diff if not disabled if diff { c.Ui.Output(fmt.Sprintf("%s\n", c.Colorize().Color(strings.TrimSpace(formatJobDiff(resp.Diff, verbose))))) } + // Print the scheduler dry-run output c.Ui.Output(c.Colorize().Color("[bold]Scheduler dry-run:[reset]")) c.Ui.Output(c.Colorize().Color(formatDryRun(resp.CreatedEvals))) + // Print the job index info c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, file))) return 0 } +// formatJobModifyIndex produces a help string that displays the job modify +// index and how to submit a job with it. func formatJobModifyIndex(jobModifyIndex uint64, jobName string) string { help := fmt.Sprintf(jobModifyIndexHelp, jobModifyIndex, jobName) out := fmt.Sprintf("[reset][bold]Job Modify Index: %d[reset]\n%s", jobModifyIndex, help) return out } +// formatDryRun produces a string explaining the results of the dry run. func formatDryRun(evals []*api.Evaluation) string { - // "- All tasks successfully allocated." bold and green - var rolling *api.Evaluation var blocked *api.Evaluation for _, eval := range evals { @@ -156,10 +174,14 @@ func formatDryRun(evals []*api.Evaluation) string { return out } +// formatJobDiff produces an annoted diff of the the job. If verbose mode is +// set, added or deleted task groups and tasks are expanded. func formatJobDiff(job *api.JobDiff, verbose bool) string { marker, _ := getDiffString(job.Type) out := fmt.Sprintf("%s[bold]Job: %q\n", marker, job.ID) + // Determine the longest markers and fields so that the output can be + // properly alligned. longestField, longestMarker := getLongestPrefixes(job.Fields, job.Objects) for _, tg := range job.TaskGroups { if _, l := getDiffString(tg.Type); l > longestMarker { @@ -167,43 +189,35 @@ func formatJobDiff(job *api.JobDiff, verbose bool) string { } } - subStartPrefix := "" + // Only show the job's field and object diffs if the job is edited or + // verbose mode is set. if job.Type == "Edited" || verbose { - for _, field := range job.Fields { - _, mLength := getDiffString(field.Type) - kPrefix := longestMarker - mLength - vPrefix := longestField - len(field.Name) - out += fmt.Sprintf("%s\n", formatFieldDiff( - field, - subStartPrefix, - strings.Repeat(" ", kPrefix), - strings.Repeat(" ", vPrefix))) - } - - for _, object := range job.Objects { - _, mLength := getDiffString(object.Type) - kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatObjectDiff( - object, - subStartPrefix, - strings.Repeat(" ", kPrefix))) + fo := alignedFieldAndObjects(job.Fields, job.Objects, 0, longestField, longestMarker) + out += fo + if len(fo) > 0 { + out += "\n" } } + // Print the task groups for _, tg := range job.TaskGroups { _, mLength := getDiffString(tg.Type) kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, strings.Repeat(" ", kPrefix), verbose)) + out += fmt.Sprintf("%s\n", formatTaskGroupDiff(tg, kPrefix, verbose)) } return out } -func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) string { +// formatTaskGroupDiff produces an annotated diff of a task group. If the +// verbose field is set, the task groups fields and objects are expanded even if +// the full object is an addition or removal. tgPrefix is the number of spaces to prefix +// the output of the task group. +func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix int, verbose bool) string { marker, _ := getDiffString(tg.Type) - out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, tgPrefix, tg.Name) + out := fmt.Sprintf("%s%s[bold]Task Group: %q[reset]", marker, strings.Repeat(" ", tgPrefix), tg.Name) - // Append the updates + // Append the updates and colorize them if l := len(tg.Updates); l > 0 { updates := make([]string, 0, l) for updateType, count := range tg.Updates { @@ -228,6 +242,8 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) s out += "[reset]\n" } + // Determine the longest field and markers so the output is properly + // alligned longestField, longestMarker := getLongestPrefixes(tg.Fields, tg.Objects) for _, task := range tg.Tasks { if _, l := getDiffString(task.Type); l > longestMarker { @@ -235,41 +251,36 @@ func formatTaskGroupDiff(tg *api.TaskGroupDiff, tgPrefix string, verbose bool) s } } - subStartPrefix := strings.Repeat(" ", len(tgPrefix)+2) + // Only show the task groups's field and object diffs if the group is edited or + // verbose mode is set. + subStartPrefix := tgPrefix + 2 if tg.Type == "Edited" || verbose { - for _, field := range tg.Fields { - _, mLength := getDiffString(field.Type) - kPrefix := longestMarker - mLength - vPrefix := longestField - len(field.Name) - out += fmt.Sprintf("%s\n", formatFieldDiff( - field, - subStartPrefix, - strings.Repeat(" ", kPrefix), - strings.Repeat(" ", vPrefix))) - } - - for _, object := range tg.Objects { - _, mLength := getDiffString(object.Type) - kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatObjectDiff( - object, - subStartPrefix, - strings.Repeat(" ", kPrefix))) + fo := alignedFieldAndObjects(tg.Fields, tg.Objects, subStartPrefix, longestField, longestMarker) + out += fo + if len(fo) > 0 { + out += "\n" } } + // Output the tasks for _, task := range tg.Tasks { _, mLength := getDiffString(task.Type) - prefix := strings.Repeat(" ", (longestMarker - mLength)) + prefix := longestMarker - mLength out += fmt.Sprintf("%s\n", formatTaskDiff(task, subStartPrefix, prefix, verbose)) } return out } -func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix string, verbose bool) string { +// formatTaskDiff produces an annotated diff of a task. If the verbose field is +// set, the tasks fields and objects are expanded even if the full object is an +// addition or removal. startPrefix is the number of spaces to prefix the output of +// the task and taskPrefix is the number of spaces to put betwen the marker and +// task name output. +func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix int, verbose bool) string { marker, _ := getDiffString(task.Type) - out := fmt.Sprintf("%s%s%s[bold]Task: %q", startPrefix, marker, taskPrefix, task.Name) + out := fmt.Sprintf("%s%s%s[bold]Task: %q", + strings.Repeat(" ", startPrefix), marker, strings.Repeat(" ", taskPrefix), task.Name) if len(task.Annotations) != 0 { out += fmt.Sprintf(" [reset](%s)", colorAnnotations(task.Annotations)) } @@ -277,39 +288,46 @@ func formatTaskDiff(task *api.TaskDiff, startPrefix, taskPrefix string, verbose if task.Type == "None" { return out } else if (task.Type == "Deleted" || task.Type == "Added") && !verbose { + // Exit early if the job was not edited and it isn't verbose output return out } else { out += "\n" } - subStartPrefix := strings.Repeat(" ", len(startPrefix)+2) + subStartPrefix := startPrefix + 2 longestField, longestMarker := getLongestPrefixes(task.Fields, task.Objects) - for _, field := range task.Fields { - _, mLength := getDiffString(field.Type) - kPrefix := longestMarker - mLength - vPrefix := longestField - len(field.Name) - out += fmt.Sprintf("%s\n", formatFieldDiff( - field, - subStartPrefix, - strings.Repeat(" ", kPrefix), - strings.Repeat(" ", vPrefix))) - } + out += alignedFieldAndObjects(task.Fields, task.Objects, subStartPrefix, longestField, longestMarker) + return out +} - for _, object := range task.Objects { - _, mLength := getDiffString(object.Type) - kPrefix := longestMarker - mLength - out += fmt.Sprintf("%s\n", formatObjectDiff( - object, - subStartPrefix, - strings.Repeat(" ", kPrefix))) - } +// formatObjectDiff produces an annotated diff of an object. startPrefix is the +// number of spaces to prefix the output of the object and keyPrefix is the number +// of spaces to put betwen the marker and object name output. +func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix int) string { + start := strings.Repeat(" ", startPrefix) + marker, _ := getDiffString(diff.Type) + out := fmt.Sprintf("%s%s%s%s {\n", start, marker, strings.Repeat(" ", keyPrefix), diff.Name) - return out + // Determine the length of the longest name and longest diff marker to + // properly align names and values + longestField, longestMarker := getLongestPrefixes(diff.Fields, diff.Objects) + subStartPrefix := startPrefix + 2 + out += alignedFieldAndObjects(diff.Fields, diff.Objects, subStartPrefix, longestField, longestMarker) + return fmt.Sprintf("%s\n%s}", out, start) } -func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix string) string { +// formatFieldDiff produces an annotated diff of a field. startPrefix is the +// number of spaces to prefix the output of the field, keyPrefix is the number +// of spaces to put betwen the marker and field name output and valuePrefix is +// the number of spaces to put infront of the value for aligning values. +func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix int) string { marker, _ := getDiffString(diff.Type) - out := fmt.Sprintf("%s%s%s%s: %s", startPrefix, marker, keyPrefix, diff.Name, valuePrefix) + out := fmt.Sprintf("%s%s%s%s: %s", + strings.Repeat(" ", startPrefix), + marker, strings.Repeat(" ", keyPrefix), + diff.Name, + strings.Repeat(" ", valuePrefix)) + switch diff.Type { case "Added": out += fmt.Sprintf("%q", diff.New) @@ -329,43 +347,20 @@ func formatFieldDiff(diff *api.FieldDiff, startPrefix, keyPrefix, valuePrefix st return out } -func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (longestField, longestMarker int) { - for _, field := range fields { - if l := len(field.Name); l > longestField { - longestField = l - } - if _, l := getDiffString(field.Type); l > longestMarker { - longestMarker = l - } - } - for _, obj := range objects { - if _, l := getDiffString(obj.Type); l > longestMarker { - longestMarker = l - } - } - return longestField, longestMarker -} - -func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix string) string { - marker, _ := getDiffString(diff.Type) - out := fmt.Sprintf("%s%s%s%s {\n", startPrefix, marker, keyPrefix, diff.Name) +// alignedFieldAndObjects is a helper method that prints fields and objects +// properly aligned. +func alignedFieldAndObjects(fields []*api.FieldDiff, objects []*api.ObjectDiff, + startPrefix, longestField, longestMarker int) string { - // Determine the length of the longest name and longest diff marker to - // properly align names and values - longestField, longestMarker := getLongestPrefixes(diff.Fields, diff.Objects) - subStartPrefix := strings.Repeat(" ", len(startPrefix)+2) - numFields := len(diff.Fields) - numObjects := len(diff.Objects) + var out string + numFields := len(fields) + numObjects := len(objects) haveObjects := numObjects != 0 - for i, field := range diff.Fields { + for i, field := range fields { _, mLength := getDiffString(field.Type) kPrefix := longestMarker - mLength vPrefix := longestField - len(field.Name) - out += formatFieldDiff( - field, - subStartPrefix, - strings.Repeat(" ", kPrefix), - strings.Repeat(" ", vPrefix)) + out += formatFieldDiff(field, startPrefix, kPrefix, vPrefix) // Avoid a dangling new line if i+1 != numFields || haveObjects { @@ -373,10 +368,10 @@ func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix string) strin } } - for i, object := range diff.Objects { + for i, object := range objects { _, mLength := getDiffString(object.Type) kPrefix := longestMarker - mLength - out += formatObjectDiff(object, subStartPrefix, strings.Repeat(" ", kPrefix)) + out += formatObjectDiff(object, startPrefix, kPrefix) // Avoid a dangling new line if i+1 != numObjects { @@ -384,9 +379,30 @@ func formatObjectDiff(diff *api.ObjectDiff, startPrefix, keyPrefix string) strin } } - return fmt.Sprintf("%s\n%s}", out, startPrefix) + return out +} + +// getLongestPrefixes takes a list of fields and objects and determines the +// longest field name and the longest marker. +func getLongestPrefixes(fields []*api.FieldDiff, objects []*api.ObjectDiff) (longestField, longestMarker int) { + for _, field := range fields { + if l := len(field.Name); l > longestField { + longestField = l + } + if _, l := getDiffString(field.Type); l > longestMarker { + longestMarker = l + } + } + for _, obj := range objects { + if _, l := getDiffString(obj.Type); l > longestMarker { + longestMarker = l + } + } + return longestField, longestMarker } +// getDiffString returns a colored diff marker and the length of the string +// without color annotations. func getDiffString(diffType string) (string, int) { switch diffType { case "Added": @@ -400,6 +416,8 @@ func getDiffString(diffType string) (string, int) { } } +// colorAnnotations returns a comma concatonated list of the annotations where +// the annotations are colored where possible. func colorAnnotations(annotations []string) string { l := len(annotations) if l == 0 { From 75e8f28f8feee9743e77e9a5d1285e5471794c31 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 17 May 2016 13:32:47 -0700 Subject: [PATCH 7/9] tests --- command/plan.go | 15 +++---- command/plan_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 command/plan_test.go diff --git a/command/plan.go b/command/plan.go index 0b7fc51c5e1..677bfcfb85b 100644 --- a/command/plan.go +++ b/command/plan.go @@ -13,9 +13,9 @@ import ( const ( jobModifyIndexHelp = `To submit the job with version verification run: -nomad run -verify %d %s +nomad run -check-index %d %s -When running the job with the verify flag, the job will only be run if the +When running the job with the check-index flag, the job will only be run if the server side version matches the the job modify index returned. If the index has changed, another user has modified the job and the plan's results are potentially invalid.` @@ -36,14 +36,12 @@ Usage: nomad plan [options] successfully and how it would affect existing allocations. A job modify index is returned with the plan. This value can be used when - submitting the job using "nomad run -verify", which will check that the job + submitting the job using "nomad run -check-index", which will check that the job was not modified between the plan and run command before invoking the - scheduler. This ensures that the plan reflects the same modifications to the - job as the run. + scheduler. This ensures the job has not been modified since the plan. - An annotated diff between the submitted job and the remote state is also - displayed. This diff gives insight onto what the scheduler will attempt to do - and why. + A structured diff between the local and remote job is displayed to + give insight into what the scheduler will attempt to do and why. General Options: @@ -59,7 +57,6 @@ Run Options: -verbose Increase diff verbosity. - ` return strings.TrimSpace(helpText) } diff --git a/command/plan_test.go b/command/plan_test.go new file mode 100644 index 00000000000..af53826d506 --- /dev/null +++ b/command/plan_test.go @@ -0,0 +1,103 @@ +package command + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func TestPlanCommand_Implements(t *testing.T) { + var _ cli.Command = &RunCommand{} +} + +func TestPlanCommand_Fails(t *testing.T) { + ui := new(cli.MockUi) + cmd := &PlanCommand{Meta: Meta{Ui: ui}} + + // Fails on misuse + if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 { + t.Fatalf("expected exit code 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) { + t.Fatalf("expected help output, got: %s", out) + } + ui.ErrorWriter.Reset() + + // Fails when specified file does not exist + if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 1 { + t.Fatalf("expect exit 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error parsing") { + t.Fatalf("expect parsing error, got: %s", out) + } + ui.ErrorWriter.Reset() + + // Fails on invalid HCL + fh1, err := ioutil.TempFile("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh1.Name()) + if _, err := fh1.WriteString("nope"); err != nil { + t.Fatalf("err: %s", err) + } + if code := cmd.Run([]string{fh1.Name()}); code != 1 { + t.Fatalf("expect exit 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error parsing") { + t.Fatalf("expect parsing error, got: %s", err) + } + ui.ErrorWriter.Reset() + + // Fails on invalid job spec + fh2, err := ioutil.TempFile("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh2.Name()) + if _, err := fh2.WriteString(`job "job1" {}`); err != nil { + t.Fatalf("err: %s", err) + } + if code := cmd.Run([]string{fh2.Name()}); code != 1 { + t.Fatalf("expect exit 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error validating") { + t.Fatalf("expect validation error, got: %s", out) + } + ui.ErrorWriter.Reset() + + // Fails on connection failure (requires a valid job) + fh3, err := ioutil.TempFile("", "nomad") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(fh3.Name()) + _, err = fh3.WriteString(` +job "job1" { + type = "service" + datacenters = [ "dc1" ] + group "group1" { + count = 1 + task "task1" { + driver = "exec" + resources = { + cpu = 1000 + disk = 150 + memory = 512 + } + } + } +}`) + if err != nil { + t.Fatalf("err: %s", err) + } + if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 1 { + t.Fatalf("expected exit code 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") { + t.Fatalf("expected failed query error, got: %s", out) + } +} From 5a4500c9ddfc4f7ac20228fa2c43661948ad1a84 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 24 May 2016 18:22:46 -0700 Subject: [PATCH 8/9] vendor colorstring --- .../github.com/mitchellh/colorstring/LICENSE | 21 ++ .../mitchellh/colorstring/README.md | 30 +++ .../mitchellh/colorstring/colorstring.go | 244 ++++++++++++++++++ vendor/vendor.json | 6 + 4 files changed, 301 insertions(+) create mode 100644 vendor/github.com/mitchellh/colorstring/LICENSE create mode 100644 vendor/github.com/mitchellh/colorstring/README.md create mode 100644 vendor/github.com/mitchellh/colorstring/colorstring.go diff --git a/vendor/github.com/mitchellh/colorstring/LICENSE b/vendor/github.com/mitchellh/colorstring/LICENSE new file mode 100644 index 00000000000..22985159044 --- /dev/null +++ b/vendor/github.com/mitchellh/colorstring/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/colorstring/README.md b/vendor/github.com/mitchellh/colorstring/README.md new file mode 100644 index 00000000000..0654d454dea --- /dev/null +++ b/vendor/github.com/mitchellh/colorstring/README.md @@ -0,0 +1,30 @@ +# colorstring [![Build Status](https://travis-ci.org/mitchellh/colorstring.svg)](https://travis-ci.org/mitchellh/colorstring) + +colorstring is a [Go](http://www.golang.org) library for outputting colored +strings to a console using a simple inline syntax in your string to specify +the color to print as. + +For example, the string `[blue]hello [red]world` would output the text +"hello world" in two colors. The API of colorstring allows for easily disabling +colors, adding aliases, etc. + +## Installation + +Standard `go get`: + +``` +$ go get github.com/mitchellh/colorstring +``` + +## Usage & Example + +For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/colorstring). + +Usage is easy enough: + +```go +colorstring.Println("[blue]Hello [red]World!") +``` + +Additionally, the `Colorize` struct can be used to set options such as +custom colors, color disabling, etc. diff --git a/vendor/github.com/mitchellh/colorstring/colorstring.go b/vendor/github.com/mitchellh/colorstring/colorstring.go new file mode 100644 index 00000000000..3de5b241d90 --- /dev/null +++ b/vendor/github.com/mitchellh/colorstring/colorstring.go @@ -0,0 +1,244 @@ +// colorstring provides functions for colorizing strings for terminal +// output. +package colorstring + +import ( + "bytes" + "fmt" + "io" + "regexp" + "strings" +) + +// Color colorizes your strings using the default settings. +// +// Strings given to Color should use the syntax `[color]` to specify the +// color for text following. For example: `[blue]Hello` will return "Hello" +// in blue. See DefaultColors for all the supported colors and attributes. +// +// If an unrecognized color is given, it is ignored and assumed to be part +// of the string. For example: `[hi]world` will result in "[hi]world". +// +// A color reset is appended to the end of every string. This will reset +// the color of following strings when you output this text to the same +// terminal session. +// +// If you want to customize any of this behavior, use the Colorize struct. +func Color(v string) string { + return def.Color(v) +} + +// ColorPrefix returns the color sequence that prefixes the given text. +// +// This is useful when wrapping text if you want to inherit the color +// of the wrapped text. For example, "[green]foo" will return "[green]". +// If there is no color sequence, then this will return "". +func ColorPrefix(v string) string { + return def.ColorPrefix(v) +} + +// Colorize colorizes your strings, giving you the ability to customize +// some of the colorization process. +// +// The options in Colorize can be set to customize colorization. If you're +// only interested in the defaults, just use the top Color function directly, +// which creates a default Colorize. +type Colorize struct { + // Colors maps a color string to the code for that color. The code + // is a string so that you can use more complex colors to set foreground, + // background, attributes, etc. For example, "boldblue" might be + // "1;34" + Colors map[string]string + + // If true, color attributes will be ignored. This is useful if you're + // outputting to a location that doesn't support colors and you just + // want the strings returned. + Disable bool + + // Reset, if true, will reset the color after each colorization by + // adding a reset code at the end. + Reset bool +} + +// Color colorizes a string according to the settings setup in the struct. +// +// For more details on the syntax, see the top-level Color function. +func (c *Colorize) Color(v string) string { + matches := parseRe.FindAllStringIndex(v, -1) + if len(matches) == 0 { + return v + } + + result := new(bytes.Buffer) + colored := false + m := []int{0, 0} + for _, nm := range matches { + // Write the text in between this match and the last + result.WriteString(v[m[1]:nm[0]]) + m = nm + + var replace string + if code, ok := c.Colors[v[m[0]+1:m[1]-1]]; ok { + colored = true + + if !c.Disable { + replace = fmt.Sprintf("\033[%sm", code) + } + } else { + replace = v[m[0]:m[1]] + } + + result.WriteString(replace) + } + result.WriteString(v[m[1]:]) + + if colored && c.Reset && !c.Disable { + // Write the clear byte at the end + result.WriteString("\033[0m") + } + + return result.String() +} + +// ColorPrefix returns the first color sequence that exists in this string. +// +// For example: "[green]foo" would return "[green]". If no color sequence +// exists, then "" is returned. This is especially useful when wrapping +// colored texts to inherit the color of the wrapped text. +func (c *Colorize) ColorPrefix(v string) string { + return prefixRe.FindString(strings.TrimSpace(v)) +} + +// DefaultColors are the default colors used when colorizing. +// +// If the color is surrounded in underscores, such as "_blue_", then that +// color will be used for the background color. +var DefaultColors map[string]string + +func init() { + DefaultColors = map[string]string{ + // Default foreground/background colors + "default": "39", + "_default_": "49", + + // Foreground colors + "black": "30", + "red": "31", + "green": "32", + "yellow": "33", + "blue": "34", + "magenta": "35", + "cyan": "36", + "light_gray": "37", + "dark_gray": "90", + "light_red": "91", + "light_green": "92", + "light_yellow": "93", + "light_blue": "94", + "light_magenta": "95", + "light_cyan": "96", + "white": "97", + + // Background colors + "_black_": "40", + "_red_": "41", + "_green_": "42", + "_yellow_": "43", + "_blue_": "44", + "_magenta_": "45", + "_cyan_": "46", + "_light_gray_": "47", + "_dark_gray_": "100", + "_light_red_": "101", + "_light_green_": "102", + "_light_yellow_": "103", + "_light_blue_": "104", + "_light_magenta_": "105", + "_light_cyan_": "106", + "_white_": "107", + + // Attributes + "bold": "1", + "dim": "2", + "underline": "4", + "blink_slow": "5", + "blink_fast": "6", + "invert": "7", + "hidden": "8", + + // Reset to reset everything to their defaults + "reset": "0", + "reset_bold": "21", + } + + def = Colorize{ + Colors: DefaultColors, + Reset: true, + } +} + +var def Colorize +var parseReRaw = `\[[a-z0-9_-]+\]` +var parseRe = regexp.MustCompile(`(?i)` + parseReRaw) +var prefixRe = regexp.MustCompile(`^(?i)(` + parseReRaw + `)+`) + +// Print is a convenience wrapper for fmt.Print with support for color codes. +// +// Print formats using the default formats for its operands and writes to +// standard output with support for color codes. Spaces are added between +// operands when neither is a string. It returns the number of bytes written +// and any write error encountered. +func Print(a string) (n int, err error) { + return fmt.Print(Color(a)) +} + +// Println is a convenience wrapper for fmt.Println with support for color +// codes. +// +// Println formats using the default formats for its operands and writes to +// standard output with support for color codes. Spaces are always added +// between operands and a newline is appended. It returns the number of bytes +// written and any write error encountered. +func Println(a string) (n int, err error) { + return fmt.Println(Color(a)) +} + +// Printf is a convenience wrapper for fmt.Printf with support for color codes. +// +// Printf formats according to a format specifier and writes to standard output +// with support for color codes. It returns the number of bytes written and any +// write error encountered. +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(Color(format), a...) +} + +// Fprint is a convenience wrapper for fmt.Fprint with support for color codes. +// +// Fprint formats using the default formats for its operands and writes to w +// with support for color codes. Spaces are added between operands when neither +// is a string. It returns the number of bytes written and any write error +// encountered. +func Fprint(w io.Writer, a string) (n int, err error) { + return fmt.Fprint(w, Color(a)) +} + +// Fprintln is a convenience wrapper for fmt.Fprintln with support for color +// codes. +// +// Fprintln formats using the default formats for its operands and writes to w +// with support for color codes. Spaces are always added between operands and a +// newline is appended. It returns the number of bytes written and any write +// error encountered. +func Fprintln(w io.Writer, a string) (n int, err error) { + return fmt.Fprintln(w, Color(a)) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf with support for color +// codes. +// +// Fprintf formats according to a format specifier and writes to w with support +// for color codes. It returns the number of bytes written and any write error +// encountered. +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, Color(format), a...) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 066964e9265..f96367a3095 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -483,6 +483,12 @@ "path": "github.com/mitchellh/cli", "revision": "cb6853d606ea4a12a15ac83cc43503df99fd28fb" }, + { + "checksumSHA1": "ttEN1Aupb7xpPMkQLqb3tzLFdXs=", + "path": "github.com/mitchellh/colorstring", + "revision": "8631ce90f28644f54aeedcb3e389a85174e067d1", + "revisionTime": "2015-09-17T21:48:07Z" + }, { "path": "github.com/mitchellh/copystructure", "revision": "80adcec1955ee4e97af357c30dee61aadcc02c10" From 0a6c5ba084bdf4406b2f90457b148303bdb0a121 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Tue, 24 May 2016 18:53:46 -0700 Subject: [PATCH 9/9] meta flag test --- command/meta_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/meta_test.go b/command/meta_test.go index 979ce1161ed..366d339eb97 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -18,7 +18,7 @@ func TestMeta_FlagSet(t *testing.T) { }, { FlagSetClient, - []string{"address"}, + []string{"address", "no-color"}, }, }