diff --git a/cmd/server.go b/cmd/server.go index 9c6ade1001..bb87f24e2a 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -142,6 +142,7 @@ const ( SlackTokenFlag = "slack-token" SSLCertFileFlag = "ssl-cert-file" SSLKeyFileFlag = "ssl-key-file" + StripRefreshOutputFromErrorsFlag = "strip-refresh-output-from-errors" RestrictFileList = "restrict-file-list" TFDistributionFlag = "tf-distribution" // deprecated for DefaultTFDistributionFlag TFDownloadFlag = "tf-download" @@ -594,6 +595,10 @@ var boolFlags = map[string]boolFlag{ description: "Silences the posting of allowlist error comments.", defaultValue: false, }, + StripRefreshOutputFromErrorsFlag: { + description: "Strips state refresh lines from output on plan errors.", + defaultValue: false, + }, DisableMarkdownFoldingFlag: { description: "Toggle off folding in markdown output.", defaultValue: false, diff --git a/cmd/server_test.go b/cmd/server_test.go index ea73de2905..6d7cc505bc 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -141,6 +141,7 @@ var testFlags = map[string]interface{}{ SlackTokenFlag: "slack-token", SSLCertFileFlag: "cert-file", SSLKeyFileFlag: "key-file", + StripRefreshOutputFromErrorsFlag: false, RestrictFileList: false, TFDistributionFlag: "terraform", TFDownloadFlag: true, diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 1e658a2440..17ca0943a9 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -1317,6 +1317,17 @@ This is useful when you have many projects and want to keep the pull request cle Namespace for emitting stats/metrics. See [stats](stats.md) section. +### `--strip-refresh-output-from-errors` + + ```bash + atlantis server --strip-refresh-output-from-errors + # or + ATLANTIS_STRIP_REFRESH_OUTPUT_FROM_ERRORS=true + ``` + + Defaults to `false`. Strip "Refreshing state..." messages from plan outputs when the result is an error. + These messages are always stripped from successful plan output. + ### `--tf-distribution` diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index d06b237b9f..3e70c96bc3 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1483,6 +1483,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers defaultTFVersion, statusUpdater, asyncTfExec, + false, ), ShowStepRunner: showStepRunner, PolicyCheckStepRunner: policyCheckRunner, diff --git a/server/core/runtime/plan_step_runner.go b/server/core/runtime/plan_step_runner.go index b3fc491351..faba29a3e2 100644 --- a/server/core/runtime/plan_step_runner.go +++ b/server/core/runtime/plan_step_runner.go @@ -27,20 +27,22 @@ var ( ) type planStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFDistribution terraform.Distribution - DefaultTFVersion *version.Version - CommitStatusUpdater StatusUpdater - AsyncTFExec AsyncTFExec + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version + CommitStatusUpdater StatusUpdater + AsyncTFExec AsyncTFExec + StripRefreshOutputFromErrors bool } -func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner { +func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec, stripRefreshOutputFromErrors bool) Runner { runner := &planStepRunner{ - TerraformExecutor: terraformExecutor, - DefaultTFDistribution: defaultTfDistribution, - DefaultTFVersion: defaultTfVersion, - CommitStatusUpdater: commitStatusUpdater, - AsyncTFExec: asyncTFExec, + TerraformExecutor: terraformExecutor, + DefaultTFDistribution: defaultTfDistribution, + DefaultTFVersion: defaultTfVersion, + CommitStatusUpdater: commitStatusUpdater, + AsyncTFExec: asyncTFExec, + StripRefreshOutputFromErrors: stripRefreshOutputFromErrors, } return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner) } @@ -63,7 +65,11 @@ func (p *planStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat return p.remotePlan(ctx, extraArgs, path, tfDistribution, tfVersion, planFile, envs) } if err != nil { - return output, err + if p.StripRefreshOutputFromErrors { + return StripRefreshingFromPlanOutput(output, tfVersion), err + } else { + return output, err + } } return p.fmtPlanOutput(output, tfVersion), nil } @@ -87,8 +93,14 @@ func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []stri } args := p.flatten(argList) output, err := p.runRemotePlan(ctx, args, path, tfDistribution, tfVersion, envs) + + planOutput := StripRefreshingFromPlanOutput(output, tfVersion) + errOutput := output + if p.StripRefreshOutputFromErrors { + errOutput = planOutput + } if err != nil { - return output, err + return errOutput, err } // If using remote ops, we create our own "fake" planfile with the @@ -99,13 +111,12 @@ func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []stri // plan. To ensure that what gets applied is the plan we printed to the PR, // during the apply phase, we diff the output we stored in the fake // planfile with the pending apply output. - planOutput := StripRefreshingFromPlanOutput(output, tfVersion) // We also prepend our own remote ops header to the file so during apply we // know this is a remote apply. err = os.WriteFile(planFile, []byte(remoteOpsHeader+planOutput), 0600) if err != nil { - return output, errors.Wrap(err, "unable to create planfile for remote ops") + return errOutput, errors.Wrap(err, "unable to create planfile for remote ops") } return p.fmtPlanOutput(output, tfVersion), nil @@ -258,7 +269,6 @@ func StripRefreshingFromPlanOutput(output string, tfVersion *version.Version) st finalIndex = i } } - if finalIndex != 0 { output = strings.Join(lines[finalIndex+1:], "\n") } diff --git a/server/core/runtime/plan_step_runner_test.go b/server/core/runtime/plan_step_runner_test.go index 6a16b03e3f..7463093da6 100644 --- a/server/core/runtime/plan_step_runner_test.go +++ b/server/core/runtime/plan_step_runner_test.go @@ -43,7 +43,7 @@ func TestRun_AddsEnvVarFile(t *testing.T) { // Using version >= 0.10 here so we don't expect any env commands. tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) - s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false) expPlanArgs := []string{"plan", "-input=false", @@ -104,7 +104,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) - s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false) ctx := command.ProjectContext{ Log: logger, Workspace: "default", @@ -185,7 +185,7 @@ Terraform will perform the following actions: mockDownloader := mocks.NewMockDownloader() tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") - s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false) When(terraform.RunCommandWithVersion( Any[command.ProjectContext](), Any[string](), @@ -238,7 +238,7 @@ func TestRun_OutputOnErr(t *testing.T) { mockDownloader := mocks.NewMockDownloader() tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") - s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false) expOutput := "expected output" expErrMsg := "error!" When(terraform.RunCommandWithVersion( @@ -265,6 +265,47 @@ func TestRun_OutputOnErr(t *testing.T) { Equals(t, expOutput, actOutput) } +// Test that we strip refresh output from errors if configured to do so. +func TestRun_StripRefreshOutputOnErr(t *testing.T) { + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + commitStatusUpdater := runtimemocks.NewMockStatusUpdater() + asyncTfExec := runtimemocks.NewMockAsyncTFExec() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + tfVersion, _ := version.NewVersion("0.14.0") + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, true) + tfOutput := `null_resource.hi: Refreshing state... (ID: 217661332516885645) +null_resource.hi[1]: Refreshing state... (ID: 6064510335076839362) + +An execution plan has been generated and is shown below.` + strippedOutput := ` +An execution plan has been generated and is shown below.` + expErrMsg := "error!" + When(terraform.RunCommandWithVersion( + Any[command.ProjectContext](), + Any[string](), + Any[[]string](), + Any[map[string]string](), + Any[tf.Distribution](), + Any[*version.Version](), + Any[string]())). + Then(func(params []Param) ReturnValues { + // This code allows us to return different values depending on the + // tf command being run while still using the wildcard matchers above. + tfArgs := params[2].([]string) + if stringSliceEquals(tfArgs, []string{"workspace", "show"}) { + return []ReturnValue{"default\n", nil} + } else if tfArgs[0] == "plan" { + return []ReturnValue{tfOutput, errors.New(expErrMsg)} + } + return []ReturnValue{"", errors.New("unexpected call to RunCommandWithVersion")} + }) + actOutput, actErr := s.Run(command.ProjectContext{Workspace: "default"}, nil, "", map[string]string(nil)) + ErrEquals(t, expErrMsg, actErr) + Equals(t, strippedOutput, actOutput) +} + // Test that if we're using 0.12, we don't set the optional -var atlantis_repo_name // flags because in >= 0.12 you can't set -var flags if those variables aren't // being used. @@ -314,7 +355,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { mockDownloader := mocks.NewMockDownloader() tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) - s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false) ctx := command.ProjectContext{ Workspace: "default", RepoRelDir: ".", @@ -406,7 +447,7 @@ locally at this time. tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) asyncTf := &remotePlanMock{} - s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf, false) absProjectPath := t.TempDir() // First, terraform workspace gets run. @@ -603,7 +644,7 @@ func TestPlanStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) { mockDownloader := mocks.NewMockDownloader() tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) - s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false) ctx := command.ProjectContext{ Workspace: "default", RepoRelDir: ".", diff --git a/server/server.go b/server/server.go index 3028c51529..cfbd0ea80f 100644 --- a/server/server.go +++ b/server/server.go @@ -703,7 +703,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { DefaultTFDistribution: defaultTfDistribution, DefaultTFVersion: defaultTfVersion, }, - PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient), + PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient, userConfig.StripRefreshOutputFromErrors), ShowStepRunner: showStepRunner, PolicyCheckStepRunner: policyCheckStepRunner, ApplyStepRunner: &runtime.ApplyStepRunner{ diff --git a/server/user_config.go b/server/user_config.go index 787c6a49e0..b88b244bb9 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -106,31 +106,32 @@ type UserConfig struct { SilenceVCSStatusNoPlans bool `mapstructure:"silence-vcs-status-no-plans"` // SilenceVCSStatusNoProjects is whether autoplan should set commit status if no projects // are found. - SilenceVCSStatusNoProjects bool `mapstructure:"silence-vcs-status-no-projects"` - SilenceAllowlistErrors bool `mapstructure:"silence-allowlist-errors"` - SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"` - SlackToken string `mapstructure:"slack-token"` - SSLCertFile string `mapstructure:"ssl-cert-file"` - SSLKeyFile string `mapstructure:"ssl-key-file"` - RestrictFileList bool `mapstructure:"restrict-file-list"` - TFDistribution string `mapstructure:"tf-distribution"` // deprecated in favor of DefaultTFDistribution - TFDownload bool `mapstructure:"tf-download"` - TFDownloadURL string `mapstructure:"tf-download-url"` - TFEHostname string `mapstructure:"tfe-hostname"` - TFELocalExecutionMode bool `mapstructure:"tfe-local-execution-mode"` - TFEToken string `mapstructure:"tfe-token"` - VarFileAllowlist string `mapstructure:"var-file-allowlist"` - VCSStatusName string `mapstructure:"vcs-status-name"` - DefaultTFDistribution string `mapstructure:"default-tf-distribution"` - DefaultTFVersion string `mapstructure:"default-tf-version"` - Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"` - WebhookHttpHeaders string `mapstructure:"webhook-http-headers"` - WebBasicAuth bool `mapstructure:"web-basic-auth"` - WebUsername string `mapstructure:"web-username"` - WebPassword string `mapstructure:"web-password"` - WriteGitCreds bool `mapstructure:"write-git-creds"` - WebsocketCheckOrigin bool `mapstructure:"websocket-check-origin"` - UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"` + SilenceVCSStatusNoProjects bool `mapstructure:"silence-vcs-status-no-projects"` + SilenceAllowlistErrors bool `mapstructure:"silence-allowlist-errors"` + SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"` + SlackToken string `mapstructure:"slack-token"` + SSLCertFile string `mapstructure:"ssl-cert-file"` + SSLKeyFile string `mapstructure:"ssl-key-file"` + StripRefreshOutputFromErrors bool `mapstructure:"strip-refresh-output-from-errors"` + RestrictFileList bool `mapstructure:"restrict-file-list"` + TFDistribution string `mapstructure:"tf-distribution"` // deprecated in favor of DefaultTFDistribution + TFDownload bool `mapstructure:"tf-download"` + TFDownloadURL string `mapstructure:"tf-download-url"` + TFEHostname string `mapstructure:"tfe-hostname"` + TFELocalExecutionMode bool `mapstructure:"tfe-local-execution-mode"` + TFEToken string `mapstructure:"tfe-token"` + VarFileAllowlist string `mapstructure:"var-file-allowlist"` + VCSStatusName string `mapstructure:"vcs-status-name"` + DefaultTFDistribution string `mapstructure:"default-tf-distribution"` + DefaultTFVersion string `mapstructure:"default-tf-version"` + Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"` + WebhookHttpHeaders string `mapstructure:"webhook-http-headers"` + WebBasicAuth bool `mapstructure:"web-basic-auth"` + WebUsername string `mapstructure:"web-username"` + WebPassword string `mapstructure:"web-password"` + WriteGitCreds bool `mapstructure:"write-git-creds"` + WebsocketCheckOrigin bool `mapstructure:"websocket-check-origin"` + UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"` } // ToAllowCommandNames parse AllowCommands into a slice of CommandName