Skip to content

Commit 994457f

Browse files
Add flag to strip refresh output from errored plans
1 parent 2e420e9 commit 994457f

File tree

8 files changed

+120
-49
lines changed

8 files changed

+120
-49
lines changed

cmd/server.go

+5
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const (
142142
SlackTokenFlag = "slack-token"
143143
SSLCertFileFlag = "ssl-cert-file"
144144
SSLKeyFileFlag = "ssl-key-file"
145+
StripRefreshOutputFromErrorsFlag = "strip-refresh-output-from-errors"
145146
RestrictFileList = "restrict-file-list"
146147
TFDistributionFlag = "tf-distribution" // deprecated for DefaultTFDistributionFlag
147148
TFDownloadFlag = "tf-download"
@@ -594,6 +595,10 @@ var boolFlags = map[string]boolFlag{
594595
description: "Silences the posting of allowlist error comments.",
595596
defaultValue: false,
596597
},
598+
StripRefreshOutputFromErrorsFlag: {
599+
description: "Strips state refresh lines from output on plan errors.",
600+
defaultValue: false,
601+
},
597602
DisableMarkdownFoldingFlag: {
598603
description: "Toggle off folding in markdown output.",
599604
defaultValue: false,

cmd/server_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ var testFlags = map[string]interface{}{
141141
SlackTokenFlag: "slack-token",
142142
SSLCertFileFlag: "cert-file",
143143
SSLKeyFileFlag: "key-file",
144+
StripRefreshOutputFromErrorsFlag: false,
144145
RestrictFileList: false,
145146
TFDistributionFlag: "terraform",
146147
TFDownloadFlag: true,

runatlantis.io/docs/server-configuration.md

+12
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,18 @@ This is useful when you have many projects and want to keep the pull request cle
13171317

13181318
Namespace for emitting stats/metrics. See [stats](stats.md) section.
13191319

1320+
### `--strip-refresh-output-from-errors`
1321+
1322+
```bash
1323+
atlantis server --strip-refresh-output-from-errors
1324+
# or
1325+
ATLANTIS_STRIP_REFRESH_OUTPUT_FROM_ERRORS=true
1326+
```
1327+
1328+
Defaults to `false`. Strip "Refreshing state..." messages from plan outputs when the result is an error.
1329+
These messages are always stripped from successful plan output.
1330+
1331+
13201332
### `--tf-distribution`
13211333

13221334
<Badge text="Deprecated" type="warn"/>

server/controllers/events/events_controller_e2e_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -1483,6 +1483,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
14831483
defaultTFVersion,
14841484
statusUpdater,
14851485
asyncTfExec,
1486+
false,
14861487
),
14871488
ShowStepRunner: showStepRunner,
14881489
PolicyCheckStepRunner: policyCheckRunner,

server/core/runtime/plan_step_runner.go

+26-16
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,22 @@ var (
2727
)
2828

2929
type planStepRunner struct {
30-
TerraformExecutor TerraformExec
31-
DefaultTFDistribution terraform.Distribution
32-
DefaultTFVersion *version.Version
33-
CommitStatusUpdater StatusUpdater
34-
AsyncTFExec AsyncTFExec
30+
TerraformExecutor TerraformExec
31+
DefaultTFDistribution terraform.Distribution
32+
DefaultTFVersion *version.Version
33+
CommitStatusUpdater StatusUpdater
34+
AsyncTFExec AsyncTFExec
35+
StripRefreshOutputFromErrors bool
3536
}
3637

37-
func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner {
38+
func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec, stripRefreshOutputFromErrors bool) Runner {
3839
runner := &planStepRunner{
39-
TerraformExecutor: terraformExecutor,
40-
DefaultTFDistribution: defaultTfDistribution,
41-
DefaultTFVersion: defaultTfVersion,
42-
CommitStatusUpdater: commitStatusUpdater,
43-
AsyncTFExec: asyncTFExec,
40+
TerraformExecutor: terraformExecutor,
41+
DefaultTFDistribution: defaultTfDistribution,
42+
DefaultTFVersion: defaultTfVersion,
43+
CommitStatusUpdater: commitStatusUpdater,
44+
AsyncTFExec: asyncTFExec,
45+
StripRefreshOutputFromErrors: stripRefreshOutputFromErrors,
4446
}
4547
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner)
4648
}
@@ -63,7 +65,11 @@ func (p *planStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat
6365
return p.remotePlan(ctx, extraArgs, path, tfDistribution, tfVersion, planFile, envs)
6466
}
6567
if err != nil {
66-
return output, err
68+
if p.StripRefreshOutputFromErrors {
69+
return StripRefreshingFromPlanOutput(output, tfVersion), err
70+
} else {
71+
return output, err
72+
}
6773
}
6874
return p.fmtPlanOutput(output, tfVersion), nil
6975
}
@@ -87,8 +93,14 @@ func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []stri
8793
}
8894
args := p.flatten(argList)
8995
output, err := p.runRemotePlan(ctx, args, path, tfDistribution, tfVersion, envs)
96+
97+
planOutput := StripRefreshingFromPlanOutput(output, tfVersion)
98+
errOutput := output
99+
if p.StripRefreshOutputFromErrors {
100+
errOutput = planOutput
101+
}
90102
if err != nil {
91-
return output, err
103+
return errOutput, err
92104
}
93105

94106
// 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
99111
// plan. To ensure that what gets applied is the plan we printed to the PR,
100112
// during the apply phase, we diff the output we stored in the fake
101113
// planfile with the pending apply output.
102-
planOutput := StripRefreshingFromPlanOutput(output, tfVersion)
103114

104115
// We also prepend our own remote ops header to the file so during apply we
105116
// know this is a remote apply.
106117
err = os.WriteFile(planFile, []byte(remoteOpsHeader+planOutput), 0600)
107118
if err != nil {
108-
return output, errors.Wrap(err, "unable to create planfile for remote ops")
119+
return errOutput, errors.Wrap(err, "unable to create planfile for remote ops")
109120
}
110121

111122
return p.fmtPlanOutput(output, tfVersion), nil
@@ -258,7 +269,6 @@ func StripRefreshingFromPlanOutput(output string, tfVersion *version.Version) st
258269
finalIndex = i
259270
}
260271
}
261-
262272
if finalIndex != 0 {
263273
output = strings.Join(lines[finalIndex+1:], "\n")
264274
}

server/core/runtime/plan_step_runner_test.go

+48-7
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
4343
// Using version >= 0.10 here so we don't expect any env commands.
4444
tfVersion, _ := version.NewVersion("0.10.0")
4545
logger := logging.NewNoopLogger(t)
46-
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
46+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false)
4747

4848
expPlanArgs := []string{"plan",
4949
"-input=false",
@@ -104,7 +104,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
104104
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
105105
tfVersion, _ := version.NewVersion("0.10.0")
106106
logger := logging.NewNoopLogger(t)
107-
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
107+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false)
108108
ctx := command.ProjectContext{
109109
Log: logger,
110110
Workspace: "default",
@@ -185,7 +185,7 @@ Terraform will perform the following actions:
185185
mockDownloader := mocks.NewMockDownloader()
186186
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
187187
tfVersion, _ := version.NewVersion("0.10.0")
188-
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
188+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false)
189189
When(terraform.RunCommandWithVersion(
190190
Any[command.ProjectContext](),
191191
Any[string](),
@@ -238,7 +238,7 @@ func TestRun_OutputOnErr(t *testing.T) {
238238
mockDownloader := mocks.NewMockDownloader()
239239
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
240240
tfVersion, _ := version.NewVersion("0.10.0")
241-
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
241+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false)
242242
expOutput := "expected output"
243243
expErrMsg := "error!"
244244
When(terraform.RunCommandWithVersion(
@@ -265,6 +265,47 @@ func TestRun_OutputOnErr(t *testing.T) {
265265
Equals(t, expOutput, actOutput)
266266
}
267267

268+
// Test that we strip refresh output from errors if configured to do so.
269+
func TestRun_StripRefreshOutputOnErr(t *testing.T) {
270+
RegisterMockTestingT(t)
271+
terraform := tfclientmocks.NewMockClient()
272+
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
273+
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
274+
mockDownloader := mocks.NewMockDownloader()
275+
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
276+
tfVersion, _ := version.NewVersion("0.14.0")
277+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, true)
278+
tfOutput := `null_resource.hi: Refreshing state... (ID: 217661332516885645)
279+
null_resource.hi[1]: Refreshing state... (ID: 6064510335076839362)
280+
281+
An execution plan has been generated and is shown below.`
282+
strippedOutput := `
283+
An execution plan has been generated and is shown below.`
284+
expErrMsg := "error!"
285+
When(terraform.RunCommandWithVersion(
286+
Any[command.ProjectContext](),
287+
Any[string](),
288+
Any[[]string](),
289+
Any[map[string]string](),
290+
Any[tf.Distribution](),
291+
Any[*version.Version](),
292+
Any[string]())).
293+
Then(func(params []Param) ReturnValues {
294+
// This code allows us to return different values depending on the
295+
// tf command being run while still using the wildcard matchers above.
296+
tfArgs := params[2].([]string)
297+
if stringSliceEquals(tfArgs, []string{"workspace", "show"}) {
298+
return []ReturnValue{"default\n", nil}
299+
} else if tfArgs[0] == "plan" {
300+
return []ReturnValue{tfOutput, errors.New(expErrMsg)}
301+
}
302+
return []ReturnValue{"", errors.New("unexpected call to RunCommandWithVersion")}
303+
})
304+
actOutput, actErr := s.Run(command.ProjectContext{Workspace: "default"}, nil, "", map[string]string(nil))
305+
ErrEquals(t, expErrMsg, actErr)
306+
Equals(t, strippedOutput, actOutput)
307+
}
308+
268309
// Test that if we're using 0.12, we don't set the optional -var atlantis_repo_name
269310
// flags because in >= 0.12 you can't set -var flags if those variables aren't
270311
// being used.
@@ -314,7 +355,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
314355
mockDownloader := mocks.NewMockDownloader()
315356
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
316357
tfVersion, _ := version.NewVersion(c.tfVersion)
317-
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
358+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false)
318359
ctx := command.ProjectContext{
319360
Workspace: "default",
320361
RepoRelDir: ".",
@@ -406,7 +447,7 @@ locally at this time.
406447
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
407448
tfVersion, _ := version.NewVersion(c.tfVersion)
408449
asyncTf := &remotePlanMock{}
409-
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf)
450+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf, false)
410451
absProjectPath := t.TempDir()
411452

412453
// First, terraform workspace gets run.
@@ -603,7 +644,7 @@ func TestPlanStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) {
603644
mockDownloader := mocks.NewMockDownloader()
604645
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
605646
tfVersion, _ := version.NewVersion(c.tfVersion)
606-
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
647+
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec, false)
607648
ctx := command.ProjectContext{
608649
Workspace: "default",
609650
RepoRelDir: ".",

server/server.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
703703
DefaultTFDistribution: defaultTfDistribution,
704704
DefaultTFVersion: defaultTfVersion,
705705
},
706-
PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient),
706+
PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient, userConfig.StripRefreshOutputFromErrors),
707707
ShowStepRunner: showStepRunner,
708708
PolicyCheckStepRunner: policyCheckStepRunner,
709709
ApplyStepRunner: &runtime.ApplyStepRunner{

server/user_config.go

+26-25
Original file line numberDiff line numberDiff line change
@@ -106,31 +106,32 @@ type UserConfig struct {
106106
SilenceVCSStatusNoPlans bool `mapstructure:"silence-vcs-status-no-plans"`
107107
// SilenceVCSStatusNoProjects is whether autoplan should set commit status if no projects
108108
// are found.
109-
SilenceVCSStatusNoProjects bool `mapstructure:"silence-vcs-status-no-projects"`
110-
SilenceAllowlistErrors bool `mapstructure:"silence-allowlist-errors"`
111-
SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"`
112-
SlackToken string `mapstructure:"slack-token"`
113-
SSLCertFile string `mapstructure:"ssl-cert-file"`
114-
SSLKeyFile string `mapstructure:"ssl-key-file"`
115-
RestrictFileList bool `mapstructure:"restrict-file-list"`
116-
TFDistribution string `mapstructure:"tf-distribution"` // deprecated in favor of DefaultTFDistribution
117-
TFDownload bool `mapstructure:"tf-download"`
118-
TFDownloadURL string `mapstructure:"tf-download-url"`
119-
TFEHostname string `mapstructure:"tfe-hostname"`
120-
TFELocalExecutionMode bool `mapstructure:"tfe-local-execution-mode"`
121-
TFEToken string `mapstructure:"tfe-token"`
122-
VarFileAllowlist string `mapstructure:"var-file-allowlist"`
123-
VCSStatusName string `mapstructure:"vcs-status-name"`
124-
DefaultTFDistribution string `mapstructure:"default-tf-distribution"`
125-
DefaultTFVersion string `mapstructure:"default-tf-version"`
126-
Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"`
127-
WebhookHttpHeaders string `mapstructure:"webhook-http-headers"`
128-
WebBasicAuth bool `mapstructure:"web-basic-auth"`
129-
WebUsername string `mapstructure:"web-username"`
130-
WebPassword string `mapstructure:"web-password"`
131-
WriteGitCreds bool `mapstructure:"write-git-creds"`
132-
WebsocketCheckOrigin bool `mapstructure:"websocket-check-origin"`
133-
UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"`
109+
SilenceVCSStatusNoProjects bool `mapstructure:"silence-vcs-status-no-projects"`
110+
SilenceAllowlistErrors bool `mapstructure:"silence-allowlist-errors"`
111+
SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"`
112+
SlackToken string `mapstructure:"slack-token"`
113+
SSLCertFile string `mapstructure:"ssl-cert-file"`
114+
SSLKeyFile string `mapstructure:"ssl-key-file"`
115+
StripRefreshOutputFromErrors bool `mapstructure:"strip-refresh-output-from-errors"`
116+
RestrictFileList bool `mapstructure:"restrict-file-list"`
117+
TFDistribution string `mapstructure:"tf-distribution"` // deprecated in favor of DefaultTFDistribution
118+
TFDownload bool `mapstructure:"tf-download"`
119+
TFDownloadURL string `mapstructure:"tf-download-url"`
120+
TFEHostname string `mapstructure:"tfe-hostname"`
121+
TFELocalExecutionMode bool `mapstructure:"tfe-local-execution-mode"`
122+
TFEToken string `mapstructure:"tfe-token"`
123+
VarFileAllowlist string `mapstructure:"var-file-allowlist"`
124+
VCSStatusName string `mapstructure:"vcs-status-name"`
125+
DefaultTFDistribution string `mapstructure:"default-tf-distribution"`
126+
DefaultTFVersion string `mapstructure:"default-tf-version"`
127+
Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"`
128+
WebhookHttpHeaders string `mapstructure:"webhook-http-headers"`
129+
WebBasicAuth bool `mapstructure:"web-basic-auth"`
130+
WebUsername string `mapstructure:"web-username"`
131+
WebPassword string `mapstructure:"web-password"`
132+
WriteGitCreds bool `mapstructure:"write-git-creds"`
133+
WebsocketCheckOrigin bool `mapstructure:"websocket-check-origin"`
134+
UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"`
134135
}
135136

136137
// ToAllowCommandNames parse AllowCommands into a slice of CommandName

0 commit comments

Comments
 (0)