Skip to content

feat: Add flag to strip refresh output from errored plans #5448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

<Badge text="Deprecated" type="warn"/>
Expand Down
1 change: 1 addition & 0 deletions server/controllers/events/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
defaultTFVersion,
statusUpdater,
asyncTfExec,
false,
),
ShowStepRunner: showStepRunner,
PolicyCheckStepRunner: policyCheckRunner,
Expand Down
42 changes: 26 additions & 16 deletions server/core/runtime/plan_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -258,7 +269,6 @@ func StripRefreshingFromPlanOutput(output string, tfVersion *version.Version) st
finalIndex = i
}
}

if finalIndex != 0 {
output = strings.Join(lines[finalIndex+1:], "\n")
}
Expand Down
55 changes: 48 additions & 7 deletions server/core/runtime/plan_step_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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](),
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand Down Expand Up @@ -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: ".",
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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: ".",
Expand Down
2 changes: 1 addition & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
51 changes: 26 additions & 25 deletions server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading