diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dc0e7af75..d716cdfdab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ENHANCEMENTS: * `terraform console`: Now has basic support for multi-line input in interactive mode. ([#34822](https://github.com/hashicorp/terraform/pull/34822)) If an entered line contains opening paretheses/etc that are not closed, Terraform will await another line of input to complete the expression. This initial implementation is primarily intended to support pasting in multi-line expressions from elsewhere, rather than for manual multi-line editing, so the interactive editing support is currently limited. +* `cli`: Updates the Terraform CLI output to show logical separation between OPA and Sentinel policy evaluations BUG FIXES: diff --git a/internal/cloud/backend_taskStage_policyEvaluation.go b/internal/cloud/backend_taskStage_policyEvaluation.go index 1eb2b9e58c..360b3235d7 100644 --- a/internal/cloud/backend_taskStage_policyEvaluation.go +++ b/internal/cloud/backend_taskStage_policyEvaluation.go @@ -45,7 +45,8 @@ func newPolicyEvaluationSummarizer(b *Cloud, ts *tfe.TaskStage) taskStageSummari func (pes *policyEvaluationSummarizer) Summarize(context *IntegrationContext, output IntegrationOutputWriter, ts *tfe.TaskStage) (bool, *string, error) { if pes.counter == 0 { - output.Output("[bold]OPA Policy Evaluation\n") + output.Output("------------------------------------------------------------------------\n") + output.Output("[bold]Policy Evaluations\n") pes.counter++ } @@ -103,20 +104,27 @@ func summarizePolicyEvaluationResults(policyEvaluations []*tfe.PolicyEvaluation) } func (pes *policyEvaluationSummarizer) taskStageWithPolicyEvaluation(context *IntegrationContext, output IntegrationOutputWriter, policyEvaluation []*tfe.PolicyEvaluation) error { - var result, message string + var result, message, kind string // Currently only one policy evaluation supported : OPA for _, polEvaluation := range policyEvaluation { + if polEvaluation.PolicyKind == "opa" { + kind = "OPA" + } else { + kind = "Sentinel" + } if polEvaluation.Status == tfe.PolicyEvaluationPassed { - message = "[dim] This result means that all OPA policies passed and the protected behavior is allowed" + message = fmt.Sprintf("[dim] This result means that all %s policies passed and the protected behavior is allowed", kind) result = fmt.Sprintf("[green]%s", strings.ToUpper(string(tfe.PolicyEvaluationPassed))) if polEvaluation.ResultCount.AdvisoryFailed > 0 { result += " (with advisory)" } } else { - message = "[dim] This result means that one or more OPA policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules" + message = fmt.Sprintf("[dim] This result means that one or more %s policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules", kind) result = fmt.Sprintf("[red]%s", strings.ToUpper(string(tfe.PolicyEvaluationFailed))) } + output.Output("--------------------------------\n") + output.Output(fmt.Sprintf("[bold]%s Policy Evaluation\n", kind)) output.Output(fmt.Sprintf("[bold]%c%c Overall Result: %s", Arrow, Arrow, result)) output.Output(message) diff --git a/internal/cloud/backend_taskStage_policyEvaluation_test.go b/internal/cloud/backend_taskStage_policyEvaluation_test.go index 51dfc6c92a..4e5fcdbdff 100644 --- a/internal/cloud/backend_taskStage_policyEvaluation_test.go +++ b/internal/cloud/backend_taskStage_policyEvaluation_test.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/go-tfe" ) -func TestCloud_runTaskStageWithPolicyEvaluation(t *testing.T) { +func TestCloud_runTaskStageWithOPAPolicyEvaluation(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -27,7 +27,7 @@ func TestCloud_runTaskStageWithPolicyEvaluation(t *testing.T) { taskStage: func() *tfe.TaskStage { ts := &tfe.TaskStage{} ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ - {ID: "pol-pass", ResultCount: &tfe.PolicyResultCount{Passed: 1}, Status: "passed"}, + {ID: "pol-pass", ResultCount: &tfe.PolicyResultCount{Passed: 1}, Status: "passed", PolicyKind: "opa"}, } return ts }, @@ -40,7 +40,7 @@ func TestCloud_runTaskStageWithPolicyEvaluation(t *testing.T) { taskStage: func() *tfe.TaskStage { ts := &tfe.TaskStage{} ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ - {ID: "pol-fail", ResultCount: &tfe.PolicyResultCount{MandatoryFailed: 1}, Status: "failed"}, + {ID: "pol-fail", ResultCount: &tfe.PolicyResultCount{MandatoryFailed: 1}, Status: "failed", PolicyKind: "opa"}, } return ts }, @@ -53,7 +53,7 @@ func TestCloud_runTaskStageWithPolicyEvaluation(t *testing.T) { taskStage: func() *tfe.TaskStage { ts := &tfe.TaskStage{} ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ - {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{AdvisoryFailed: 1}, Status: "failed"}, + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{AdvisoryFailed: 1}, Status: "failed", PolicyKind: "opa"}, } return ts }, @@ -66,7 +66,96 @@ func TestCloud_runTaskStageWithPolicyEvaluation(t *testing.T) { taskStage: func() *tfe.TaskStage { ts := &tfe.TaskStage{} ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ - {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{Errored: 1}, Status: "unreachable"}, + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{Errored: 1}, Status: "unreachable", PolicyKind: "opa"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"Skipping policy evaluation."}, + isError: false, + }, + } + + for _, c := range cases { + c.writer.output.Reset() + trs := policyEvaluationSummarizer{ + cloud: b, + } + c.context.Poll(0, 0, func(i int) (bool, error) { + cont, _, _ := trs.Summarize(c.context, c.writer, c.taskStage()) + if cont { + return true, nil + } + + output := c.writer.output.String() + for _, expected := range c.expectedOutputs { + if !strings.Contains(output, expected) { + t.Fatalf("Expected output to contain '%s' but it was:\n\n%s", expected, output) + } + } + return false, nil + }) + } +} + +func TestCloud_runTaskStageWithSentinelPolicyEvaluation(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + integrationContext, writer := newMockIntegrationContext(b, t) + + cases := map[string]struct { + taskStage func() *tfe.TaskStage + context *IntegrationContext + writer *testIntegrationOutput + expectedOutputs []string + isError bool + }{ + "all-succeeded": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "pol-pass", ResultCount: &tfe.PolicyResultCount{Passed: 1}, Status: "passed", PolicyKind: "sentinel"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]Sentinel Policy Evaluation\n\n│ [bold]→→ Overall Result: [green]PASSED\n│ [dim] This result means that all Sentinel policies passed and the protected behavior is allowed\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-passes (1)\n│ ↳ Policy name: [bold]policy-pass\n│ | [green][bold]✓ Passed\n│ | [dim]This policy will pass\n"}, + isError: false, + }, + "mandatory-failed": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "pol-fail", ResultCount: &tfe.PolicyResultCount{MandatoryFailed: 1}, Status: "failed", PolicyKind: "sentinel"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]→→ Overall Result: [red]FAILED\n│ [dim] This result means that one or more Sentinel policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-fails (1)\n│ ↳ Policy name: [bold]policy-fail\n│ | [red][bold]× Failed\n│ | [dim]This policy will fail"}, + isError: true, + }, + "advisory-failed": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{AdvisoryFailed: 1}, Status: "failed", PolicyKind: "sentinel"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]Sentinel Policy Evaluation\n\n│ [bold]→→ Overall Result: [red]FAILED\n│ [dim] This result means that one or more Sentinel policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-fails (1)\n│ ↳ Policy name: [bold]policy-fail\n│ | [blue][bold]Ⓘ Advisory\n│ | [dim]This policy will fail"}, + isError: false, + }, + "unreachable": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{Errored: 1}, Status: "unreachable", PolicyKind: "sentinel"}, } return ts }, diff --git a/internal/cloud/backend_taskStages.go b/internal/cloud/backend_taskStages.go index d0400c6262..a16875864d 100644 --- a/internal/cloud/backend_taskStages.go +++ b/internal/cloud/backend_taskStages.go @@ -114,6 +114,9 @@ func (b *Cloud) runTaskStage(ctx *IntegrationContext, output IntegrationOutputWr errs = e } if ok { + if b.CLI != nil { + b.CLI.Output("------------------------------------------------------------------------") + } return true, nil } case tfe.TaskStageCanceled, tfe.TaskStageErrored, tfe.TaskStageFailed: @@ -122,6 +125,9 @@ func (b *Cloud) runTaskStage(ctx *IntegrationContext, output IntegrationOutputWr errs = e } if ok { + if b.CLI != nil { + b.CLI.Output("------------------------------------------------------------------------") + } return true, nil } return false, fmt.Errorf("Task Stage %s.", stage.Status) @@ -137,6 +143,9 @@ func (b *Cloud) runTaskStage(ctx *IntegrationContext, output IntegrationOutputWr if err != nil { errs = errors.Join(errs, err) } else { + if b.CLI != nil { + b.CLI.Output("------------------------------------------------------------------------") + } return cont, nil } case tfe.TaskStageUnreachable: @@ -170,9 +179,13 @@ func processSummarizers(ctx *IntegrationContext, output IntegrationOutputWriter, } func (b *Cloud) processStageOverrides(context *IntegrationContext, output IntegrationOutputWriter, taskStageID string) (bool, error) { + if b.CLI != nil { + b.CLI.Output("--------------------------------\n") + b.CLI.Output(b.Colorize().Color(fmt.Sprintf("%c%c [bold]Override", Arrow, Arrow))) + } opts := &terraform.InputOpts{ Id: fmt.Sprintf("%c%c [bold]Override", Arrow, Arrow), - Query: "\nDo you want to override the failed policy check?", + Query: "\nDo you want to override the failed policies?", Description: "Only 'override' will be accepted to override.", } runURL := fmt.Sprintf(taskStageHeader, b.Hostname, b.Organization, context.Op.Workspace, context.Run.ID)