diff --git a/backend/remote/backend_apply_test.go b/backend/remote/backend_apply_test.go index f68dea0cc..5b2698a94 100644 --- a/backend/remote/backend_apply_test.go +++ b/backend/remote/backend_apply_test.go @@ -338,6 +338,9 @@ func TestRemote_applyNoChanges(t *testing.T) { if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { t.Fatalf("expected no changes in plan summery: %s", output) } + if !strings.Contains(output, "Sentinel Result: true") { + t.Fatalf("expected policy check result in output: %s", output) + } } func TestRemote_applyNoApprove(t *testing.T) { @@ -771,7 +774,7 @@ func TestRemote_applyPolicyPass(t *testing.T) { t.Fatalf("expected plan summery in output: %s", output) } if !strings.Contains(output, "Sentinel Result: true") { - t.Fatalf("expected polic check result in output: %s", output) + t.Fatalf("expected policy check result in output: %s", output) } if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { t.Fatalf("expected apply summery in output: %s", output) diff --git a/backend/remote/backend_common.go b/backend/remote/backend_common.go index 03ba2b733..b1f6f4c07 100644 --- a/backend/remote/backend_common.go +++ b/backend/remote/backend_common.go @@ -260,7 +260,7 @@ func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Ope switch pc.Status { case tfe.PolicyPasses: - if (op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { + if (r.HasChanges && op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { b.CLI.Output("\n------------------------------------------------------------------------") } continue diff --git a/backend/remote/backend_plan.go b/backend/remote/backend_plan.go index 8c48d83c6..16eeb19c5 100644 --- a/backend/remote/backend_plan.go +++ b/backend/remote/backend_plan.go @@ -266,10 +266,10 @@ func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, return r, generalError("Failed to retrieve run", err) } - // Return if there are no changes or the run errored. We return - // without an error, even if the run errored, as the error is - // already displayed by the output of the remote run. - if !r.HasChanges || r.Status == tfe.RunErrored { + // Return if the run errored. We return without an error, even + // if the run errored, as the error is already displayed by the + // output of the remote run. + if r.Status == tfe.RunErrored { return r, nil } diff --git a/backend/remote/backend_plan_test.go b/backend/remote/backend_plan_test.go index 60c052d50..403af271b 100644 --- a/backend/remote/backend_plan_test.go +++ b/backend/remote/backend_plan_test.go @@ -287,6 +287,36 @@ func TestRemote_planNoConfig(t *testing.T) { } } +func TestRemote_planNoChanges(t *testing.T) { + b := testBackendDefault(t) + + op, configCleanup := testOperationApply(t, "./test-fixtures/plan-no-changes") + defer configCleanup() + + op.Workspace = backend.DefaultStateName + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backend.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if !run.PlanEmpty { + t.Fatalf("expected plan to be empty") + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { + t.Fatalf("expected no changes in plan summery: %s", output) + } + if !strings.Contains(output, "Sentinel Result: true") { + t.Fatalf("expected policy check result in output: %s", output) + } +} + func TestRemote_planForceLocal(t *testing.T) { // Set TF_FORCE_LOCAL_BACKEND so the remote backend will use // the local backend with itself as embedded backend. @@ -551,7 +581,7 @@ func TestRemote_planPolicyPass(t *testing.T) { t.Fatalf("expected plan summery in output: %s", output) } if !strings.Contains(output, "Sentinel Result: true") { - t.Fatalf("expected polic check result in output: %s", output) + t.Fatalf("expected policy check result in output: %s", output) } } diff --git a/backend/remote/test-fixtures/apply-no-changes/policy.log b/backend/remote/test-fixtures/apply-no-changes/policy.log new file mode 100644 index 000000000..b0cb1e598 --- /dev/null +++ b/backend/remote/test-fixtures/apply-no-changes/policy.log @@ -0,0 +1,12 @@ +Sentinel Result: true + +This result means that Sentinel policies returned true and the protected +behavior is allowed by Sentinel policies. + +1 policies evaluated. + +## Policy 1: Passthrough.sentinel (soft-mandatory) + +Result: true + +TRUE - Passthrough.sentinel:1:1 - Rule "main" diff --git a/backend/remote/test-fixtures/plan-no-changes/main.tf b/backend/remote/test-fixtures/plan-no-changes/main.tf new file mode 100644 index 000000000..3911a2a9b --- /dev/null +++ b/backend/remote/test-fixtures/plan-no-changes/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/backend/remote/test-fixtures/plan-no-changes/plan.log b/backend/remote/test-fixtures/plan-no-changes/plan.log new file mode 100644 index 000000000..704168151 --- /dev/null +++ b/backend/remote/test-fixtures/plan-no-changes/plan.log @@ -0,0 +1,17 @@ +Terraform v0.11.7 + +Configuring remote state backend... +Initializing Terraform configuration... +Refreshing Terraform state in-memory prior to plan... +The refreshed state will be used to calculate this plan, but will not be +persisted to local or remote state storage. + +null_resource.hello: Refreshing state... (ID: 8657651096157629581) + +------------------------------------------------------------------------ + +No changes. Infrastructure is up-to-date. + +This means that Terraform did not detect any differences between your +configuration and real physical resources that exist. As a result, no +actions need to be performed. diff --git a/backend/remote/test-fixtures/plan-no-changes/policy.log b/backend/remote/test-fixtures/plan-no-changes/policy.log new file mode 100644 index 000000000..b0cb1e598 --- /dev/null +++ b/backend/remote/test-fixtures/plan-no-changes/policy.log @@ -0,0 +1,12 @@ +Sentinel Result: true + +This result means that Sentinel policies returned true and the protected +behavior is allowed by Sentinel policies. + +1 policies evaluated. + +## Policy 1: Passthrough.sentinel (soft-mandatory) + +Result: true + +TRUE - Passthrough.sentinel:1:1 - Rule "main"