diff --git a/CHANGELOG.md b/CHANGELOG.md index 40cd51ff..eb69c64a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,8 +17,10 @@ IMPROVEMENTS: * cli: Add flags to configure Nomad API client [[GH-213](https://github.com/hashicorp/nomad-pack/pull/213)] * template: Add support for custom Spew configurations. [[GH-220](https://github.com/hashicorp/nomad-pack/pull/220)] * template: Create a `my` alias for the current pack [[GH-221](https://github.com/hashicorp/nomad-pack/pull/221)] +* cli: Add flags to override exit codes on `plan` command [[GH-236](https://github.com/hashicorp/nomad-pack/pull/236)] * cli: Add environment variables to configure Nomad API client [[GH-230](https://github.com/hashicorp/nomad-pack/pull/230)] + ## 0.0.1-techpreview2 (February 07, 2022) FEATURES: diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 415ada77..6729b96d 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -185,6 +185,75 @@ func TestJobPlanConflictingNonPackJob(t *testing.T) { }) } +func TestJobPlanOverrideExitCodes(t *testing.T) { + httpTest(t, WithDefaultConfig(), func(s *agent.TestAgent) { + // Plan against empty - should be makes-changes + result := runTestPackCmd(t, s, []string{ + "plan", + "--exit-code-makes-changes=91", + "--exit-code-no-changes=90", + "--exit-code-error=92", + getTestPackPath(testPack), + }) + require.Empty(t, result.cmdErr.String(), "cmdErr should be empty, but was %q", result.cmdErr.String()) + require.Contains(t, result.cmdOut.String(), "Plan succeeded\n") + require.Equal(t, 91, result.exitCode) // Should return exit-code-makes-changes + + // Register non pack job + err := NomadRun(s, getTestNomadJobPath(testPack)) + require.NoError(t, err) + + // Now try to register the pack, should make error + result = runTestPackCmd(t, s, []string{ + "plan", + "--exit-code-makes-changes=91", + "--exit-code-no-changes=90", + "--exit-code-error=92", + getTestPackPath(testPack), + }) + require.Empty(t, result.cmdErr.String(), "cmdErr should be empty, but was %q", result.cmdErr.String()) + require.Contains(t, result.cmdOut.String(), job.ErrExistsNonPack{JobID: testPack}.Error()) + require.Equal(t, 92, result.exitCode) // Should exit-code-error + + err = NomadPurge(s, testPack) + require.NoError(t, err) + + isGone := func() bool { + _, err = NomadJobStatus(s, testPack) + if err != nil { + return err.Error() == "job not found" + } + return false + } + require.Eventually(t, isGone, 10*time.Second, 500*time.Millisecond) + + result = runTestPackCmd(t, s, []string{"run", getTestPackPath(testPack)}) + require.Empty(t, result.cmdErr.String(), "cmdErr should be empty, but was %q", result.cmdErr.String()) + require.Contains(t, result.cmdOut.String(), "") + require.Equal(t, 0, result.exitCode) // Should return 0 + isStarted := func() bool { + j, err := NomadJobStatus(s, testPack) + if err != nil { + return false + } + return j.GetStatus() == "running" + } + require.Eventually(t, isStarted, 10*time.Second, 500*time.Millisecond) + + // Plan against deployed - should be no-changes + result = runTestPackCmd(t, s, []string{ + "plan", + "--exit-code-makes-changes=91", + "--exit-code-no-changes=90", + "--exit-code-error=92", + getTestPackPath(testPack), + }) + require.Empty(t, result.cmdErr.String(), "cmdErr should be empty, but was %q", result.cmdErr.String()) + require.Contains(t, result.cmdOut.String(), "Plan succeeded\n") + require.Equal(t, 90, result.exitCode) // Should return exit-code-no-changes + }) +} + func TestJobStop(t *testing.T) { httpTestParallel(t, WithDefaultConfig(), func(s *agent.TestAgent) { expectGoodPackDeploy(t, runTestPackCmd(t, s, []string{"run", getTestPackPath(testPack)})) @@ -906,6 +975,18 @@ func NomadRun(s *agent.TestAgent, path string, opts ...v1.ClientOption) error { return nil } +func NomadJobStatus(s *agent.TestAgent, jobname string, opts ...v1.ClientOption) (*client.Job, error) { + c, err := NewTestClient(s, opts...) + if err != nil { + return nil, err + } + resp, _, err := c.Jobs().GetJob(c.QueryOpts().Ctx(), jobname) + if err != nil { + return nil, err + } + return resp, nil +} + func NomadStop(s *agent.TestAgent, jobname string, opts ...v1.ClientOption) error { c, err := NewTestClient(s, opts...) diff --git a/internal/cli/plan.go b/internal/cli/plan.go index c2f7d5d0..b381ff39 100644 --- a/internal/cli/plan.go +++ b/internal/cli/plan.go @@ -11,11 +11,18 @@ import ( type PlanCommand struct { *baseCommand - packConfig *cache.PackConfig - jobConfig *job.CLIConfig + packConfig *cache.PackConfig + jobConfig *job.CLIConfig + exitCodeNoChanges int + exitCodeChanges int + exitCodeError int } func (c *PlanCommand) Run(args []string) int { + c.exitCodeNoChanges = 0 + c.exitCodeChanges = 1 + c.exitCodeError = 255 + c.cmdKey = "plan" // Add cmdKey here to print out helpUsageMessage on Init error // Initialize. If we fail, we just exit since Init handles the UI. if err := c.Init( @@ -35,7 +42,7 @@ func (c *PlanCommand) Run(args []string) int { // verify packs exist before planning jobs if err := cache.VerifyPackExists(c.packConfig, errorContext, c.ui); err != nil { - return 255 + return c.exitCodeError } // If no deploymentName set default to pack@ref @@ -45,7 +52,7 @@ func (c *PlanCommand) Run(args []string) int { client, err := c.getAPIClient() if err != nil { c.ui.ErrorWithContext(err, "failed to initialize client", errorContext.GetAll()...) - return 255 + return c.exitCodeError } packManager := generatePackManager(c.baseCommand, client, c.packConfig) @@ -53,14 +60,14 @@ func (c *PlanCommand) Run(args []string) int { // load pack r, err := renderPack(packManager, c.baseCommand.ui, errorContext) if err != nil { - return 255 + return c.exitCodeError } // Commands that render templates are required to render at least one // parent template. if r.LenParentRenders() < 1 { c.ui.ErrorWithContext(errors.ErrNoTemplatesRendered, "no templates rendered", errorContext.GetAll()...) - return 255 + return c.exitCodeError } depConfig := runner.Config{ @@ -75,7 +82,7 @@ func (c *PlanCommand) Run(args []string) int { jobRunner, err := generateRunner(client, "job", c.jobConfig, &depConfig) if err != nil { c.ui.ErrorWithContext(err, "failed to generate deployer", errorContext.GetAll()...) - return 255 + return c.exitCodeError } // Set the rendered templates on the job deployer. @@ -87,7 +94,7 @@ func (c *PlanCommand) Run(args []string) int { validateErr.Context.Append(errorContext) c.ui.ErrorWithContext(validateErr.Err, validateErr.Subject, validateErr.Context.GetAll()...) } - return 255 + return c.exitCodeError } if canonicalizeErrs := jobRunner.CanonicalizeTemplates(); canonicalizeErrs != nil { @@ -95,14 +102,14 @@ func (c *PlanCommand) Run(args []string) int { canonicalizeErr.Context.Append(errorContext) c.ui.ErrorWithContext(canonicalizeErr.Err, canonicalizeErr.Subject, canonicalizeErr.Context.GetAll()...) } - return 1 + return c.exitCodeError } if conflictErrs := jobRunner.CheckForConflicts(errorContext); conflictErrs != nil { for _, conflictErr := range conflictErrs { c.ui.ErrorWithContext(conflictErr.Err, conflictErr.Subject, conflictErr.Context.GetAll()...) } - return 255 + return c.exitCodeError } planExitCode, planErrs := jobRunner.PlanDeployment(c.ui, errorContext) @@ -113,7 +120,18 @@ func (c *PlanCommand) Run(args []string) int { if planExitCode < 2 { c.ui.Success("Plan succeeded") } - return planExitCode + + // Map planExitCode to replacement values. + switch planExitCode { + case 0: + return c.exitCodeNoChanges + case 1: + return c.exitCodeChanges + case 255: + return c.exitCodeError + default: // protect from unexpected new exit codes. + return planExitCode + } } func (c *PlanCommand) Flags() *flag.Sets { @@ -177,6 +195,26 @@ func (c *PlanCommand) Flags() *flag.Sets { }, Shorthand: "v", }) + f.IntVar(&flag.IntVar{ + Name: "exit-code-no-changes", + Target: &c.exitCodeNoChanges, + Default: 0, + Usage: `Override exit code returned when the plan shown no changes.`, + }) + + f.IntVar(&flag.IntVar{ + Name: "exit-code-makes-changes", + Target: &c.exitCodeChanges, + Default: 1, + Usage: `Override exit code returned when the plan shows changes.`, + }) + + f.IntVar(&flag.IntVar{ + Name: "exit-code-error", + Target: &c.exitCodeError, + Default: 255, + Usage: `Override exit code returned when there is an error.`, + }) }) }