diff --git a/command/plan.go b/command/plan.go index 4b404cfd48e..d1844be1617 100644 --- a/command/plan.go +++ b/command/plan.go @@ -56,6 +56,11 @@ Usage: nomad plan [options] If the job has specified the region, the -region flag and NOMAD_REGION environment variable are overridden and the the job's region is used. + Plan will return one of the following exit codes: + * 0: No allocations created or destroyed. + * 1: Allocations created or destroyed. + * 255: Error determining plan results. + General Options: ` + generalOptionsUsage() + ` @@ -85,14 +90,14 @@ func (c *PlanCommand) Run(args []string) int { flags.BoolVar(&verbose, "verbose", false, "") if err := flags.Parse(args); err != nil { - return 1 + return 255 } // Check that we got exactly one job args = flags.Args() if len(args) != 1 { c.Ui.Error(c.Help()) - return 1 + return 255 } // Read the Jobfile @@ -112,7 +117,7 @@ func (c *PlanCommand) Run(args []string) int { defer file.Close() if err != nil { c.Ui.Error(fmt.Sprintf("Error opening file %q: %v", path, err)) - return 1 + return 255 } f = file } @@ -121,7 +126,7 @@ func (c *PlanCommand) Run(args []string) int { job, err := jobspec.Parse(f) if err != nil { c.Ui.Error(fmt.Sprintf("Error parsing job file %s: %v", path, err)) - return 1 + return 255 } // Initialize any fields that need to be. @@ -130,21 +135,21 @@ func (c *PlanCommand) Run(args []string) int { // Check that the job is valid if err := job.Validate(); err != nil { c.Ui.Error(fmt.Sprintf("Error validating job: %s", err)) - return 1 + return 255 } // 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 + return 255 } // Get the HTTP client client, err := c.Meta.Client() if err != nil { c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) - return 1 + return 255 } // Force the region to be that of the job. @@ -156,7 +161,7 @@ func (c *PlanCommand) Run(args []string) int { resp, _, err := client.Jobs().Plan(apiJob, diff, nil) if err != nil { c.Ui.Error(fmt.Sprintf("Error during plan: %s", err)) - return 1 + return 255 } // Print the diff if not disabled @@ -172,6 +177,20 @@ func (c *PlanCommand) Run(args []string) int { // Print the job index info c.Ui.Output(c.Colorize().Color(formatJobModifyIndex(resp.JobModifyIndex, path))) + return getExitCode(resp) +} + +// getExitCode returns 0: +// * 0: No allocations created or destroyed. +// * 1: Allocations created or destroyed. +func getExitCode(resp *api.JobPlanResponse) int { + // Check for changes + for _, d := range resp.Annotations.DesiredTGUpdates { + if d.Stop+d.Place+d.Migrate+d.DestructiveUpdate > 0 { + return 1 + } + } + return 0 } diff --git a/command/plan_test.go b/command/plan_test.go index 2187ac931da..3ec790dba0f 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -18,7 +18,7 @@ func TestPlanCommand_Fails(t *testing.T) { cmd := &PlanCommand{Meta: Meta{Ui: ui}} // Fails on misuse - if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 { + if code := cmd.Run([]string{"some", "bad", "args"}); code != 255 { t.Fatalf("expected exit code 1, got: %d", code) } if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) { @@ -27,7 +27,7 @@ func TestPlanCommand_Fails(t *testing.T) { ui.ErrorWriter.Reset() // Fails when specified file does not exist - if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 1 { + if code := cmd.Run([]string{"/unicorns/leprechauns"}); code != 255 { t.Fatalf("expect exit 1, got: %d", code) } if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error opening") { @@ -44,7 +44,7 @@ func TestPlanCommand_Fails(t *testing.T) { if _, err := fh1.WriteString("nope"); err != nil { t.Fatalf("err: %s", err) } - if code := cmd.Run([]string{fh1.Name()}); code != 1 { + if code := cmd.Run([]string{fh1.Name()}); code != 255 { t.Fatalf("expect exit 1, got: %d", code) } if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error parsing") { @@ -61,7 +61,7 @@ func TestPlanCommand_Fails(t *testing.T) { if _, err := fh2.WriteString(`job "job1" {}`); err != nil { t.Fatalf("err: %s", err) } - if code := cmd.Run([]string{fh2.Name()}); code != 1 { + if code := cmd.Run([]string{fh2.Name()}); code != 255 { t.Fatalf("expect exit 1, got: %d", code) } if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error validating") { @@ -94,7 +94,7 @@ job "job1" { if err != nil { t.Fatalf("err: %s", err) } - if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 1 { + if code := cmd.Run([]string{"-address=nope", fh3.Name()}); code != 255 { t.Fatalf("expected exit code 1, got: %d", code) } if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error during plan") { @@ -135,7 +135,7 @@ job "job1" { }() args := []string{"-"} - if code := cmd.Run(args); code != 1 { + if code := cmd.Run(args); code != 255 { t.Fatalf("expected exit code 1, got %d: %q", code, ui.ErrorWriter.String()) } diff --git a/scheduler/annotate.go b/scheduler/annotate.go index 66c405fd630..05f768b41bd 100644 --- a/scheduler/annotate.go +++ b/scheduler/annotate.go @@ -159,17 +159,12 @@ func annotateTask(diff *structs.TaskDiff, parent *structs.TaskGroupDiff) { } } - // All changes to primitive fields result in a destructive update. + // All changes to primitive fields result in a destructive update except + // KillTimeout destructive := false - if len(diff.Fields) != 0 { - destructive = true - } - - // Changes that can be done in-place are log configs, services and - // constraints. - for _, oDiff := range diff.Objects { - switch oDiff.Name { - case "LogConfig", "Service", "Constraint": + for _, fDiff := range diff.Fields { + switch fDiff.Name { + case "KillTimeout": continue default: destructive = true @@ -177,6 +172,20 @@ func annotateTask(diff *structs.TaskDiff, parent *structs.TaskGroupDiff) { } } + // Object changes that can be done in-place are log configs, services, + // constraints. + if !destructive { + for _, oDiff := range diff.Objects { + switch oDiff.Name { + case "LogConfig", "Service", "Constraint": + continue + default: + destructive = true + break + } + } + } + if destructive { diff.Annotations = append(diff.Annotations, AnnotationForcesDestructiveUpdate) } else { diff --git a/scheduler/annotate_test.go b/scheduler/annotate_test.go index 9ffcac2e8e0..ec9d3bc2552 100644 --- a/scheduler/annotate_test.go +++ b/scheduler/annotate_test.go @@ -347,6 +347,21 @@ func TestAnnotateTask(t *testing.T) { Parent: &structs.TaskGroupDiff{Type: structs.DiffTypeEdited}, Desired: AnnotationForcesInplaceUpdate, }, + { + Diff: &structs.TaskDiff{ + Type: structs.DiffTypeEdited, + Fields: []*structs.FieldDiff{ + { + Type: structs.DiffTypeEdited, + Name: "KillTimeout", + Old: "200", + New: "2000000", + }, + }, + }, + Parent: &structs.TaskGroupDiff{Type: structs.DiffTypeEdited}, + Desired: AnnotationForcesInplaceUpdate, + }, // Task deleted new parent { Diff: &structs.TaskDiff{ diff --git a/website/source/docs/commands/plan.html.md.erb b/website/source/docs/commands/plan.html.md.erb index 7afdadd8788..cafd3895296 100644 --- a/website/source/docs/commands/plan.html.md.erb +++ b/website/source/docs/commands/plan.html.md.erb @@ -36,6 +36,12 @@ give insight into what the scheduler will attempt to do and why. If the job has specified the region, the `-region` flag and `NOMAD_REGION` environment variable are overridden and the the job's region is used. +Plan will return one of the following exit codes: + + * 0: No allocations created or destroyed. + * 1: Allocations created or destroyed. + * 255: Error determining plan results. + ## General Options <%= general_options_usage %>