github.com/opentofu/opentofu@v1.7.1/internal/cloud/backend_taskStage_policyEvaluation.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package cloud 7 8 import ( 9 "fmt" 10 "strings" 11 12 "github.com/hashicorp/go-tfe" 13 ) 14 15 type policyEvaluationSummary struct { 16 unreachable bool 17 pending int 18 failed int 19 passed int 20 } 21 22 type Symbol rune 23 24 const ( 25 Tick Symbol = '\u2713' 26 Cross Symbol = '\u00d7' 27 Warning Symbol = '\u24be' 28 Arrow Symbol = '\u2192' 29 DownwardArrow Symbol = '\u21b3' 30 ) 31 32 type policyEvaluationSummarizer struct { 33 finished bool 34 cloud *Cloud 35 counter int 36 } 37 38 func newPolicyEvaluationSummarizer(b *Cloud, ts *tfe.TaskStage) taskStageSummarizer { 39 if len(ts.PolicyEvaluations) == 0 { 40 return nil 41 } 42 return &policyEvaluationSummarizer{ 43 finished: false, 44 cloud: b, 45 } 46 } 47 48 func (pes *policyEvaluationSummarizer) Summarize(context *IntegrationContext, output IntegrationOutputWriter, ts *tfe.TaskStage) (bool, *string, error) { 49 if pes.counter == 0 { 50 output.Output("[bold]OPA Policy Evaluation\n") 51 pes.counter++ 52 } 53 54 if pes.finished { 55 return false, nil, nil 56 } 57 58 counts := summarizePolicyEvaluationResults(ts.PolicyEvaluations) 59 60 if counts.pending != 0 { 61 pendingMessage := "Evaluating ... " 62 return true, &pendingMessage, nil 63 } 64 65 if counts.unreachable { 66 output.Output("Skipping policy evaluation.") 67 output.End() 68 return false, nil, nil 69 } 70 71 // Print out the summary 72 if err := pes.taskStageWithPolicyEvaluation(context, output, ts.PolicyEvaluations); err != nil { 73 return false, nil, err 74 } 75 // Mark as finished 76 pes.finished = true 77 78 return false, nil, nil 79 } 80 81 func summarizePolicyEvaluationResults(policyEvaluations []*tfe.PolicyEvaluation) *policyEvaluationSummary { 82 var pendingCount, errCount, passedCount int 83 for _, policyEvaluation := range policyEvaluations { 84 switch policyEvaluation.Status { 85 case "unreachable": 86 return &policyEvaluationSummary{ 87 unreachable: true, 88 } 89 case "running", "pending", "queued": 90 pendingCount++ 91 case "passed": 92 passedCount++ 93 default: 94 // Everything else is a failure 95 errCount++ 96 } 97 } 98 99 return &policyEvaluationSummary{ 100 unreachable: false, 101 pending: pendingCount, 102 failed: errCount, 103 passed: passedCount, 104 } 105 } 106 107 func (pes *policyEvaluationSummarizer) taskStageWithPolicyEvaluation(context *IntegrationContext, output IntegrationOutputWriter, policyEvaluation []*tfe.PolicyEvaluation) error { 108 var result, message string 109 // Currently only one policy evaluation supported : OPA 110 for _, polEvaluation := range policyEvaluation { 111 if polEvaluation.Status == tfe.PolicyEvaluationPassed { 112 message = "[dim] This result means that all OPA policies passed and the protected behavior is allowed" 113 result = fmt.Sprintf("[green]%s", strings.ToUpper(string(tfe.PolicyEvaluationPassed))) 114 if polEvaluation.ResultCount.AdvisoryFailed > 0 { 115 result += " (with advisory)" 116 } 117 } else { 118 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" 119 result = fmt.Sprintf("[red]%s", strings.ToUpper(string(tfe.PolicyEvaluationFailed))) 120 } 121 122 output.Output(fmt.Sprintf("[bold]%c%c Overall Result: %s", Arrow, Arrow, result)) 123 124 output.Output(message) 125 126 total := getPolicyCount(polEvaluation.ResultCount) 127 128 output.Output(fmt.Sprintf("%d policies evaluated\n", total)) 129 130 policyOutcomes, err := pes.cloud.client.PolicySetOutcomes.List(context.StopContext, polEvaluation.ID, nil) 131 if err != nil { 132 return err 133 } 134 135 for i, out := range policyOutcomes.Items { 136 output.Output(fmt.Sprintf("%c Policy set %d: [bold]%s (%d)", Arrow, i+1, out.PolicySetName, len(out.Outcomes))) 137 for _, outcome := range out.Outcomes { 138 output.Output(fmt.Sprintf(" %c Policy name: [bold]%s", DownwardArrow, outcome.PolicyName)) 139 switch outcome.Status { 140 case "passed": 141 output.Output(fmt.Sprintf(" | [green][bold]%c Passed", Tick)) 142 case "failed": 143 if outcome.EnforcementLevel == tfe.EnforcementAdvisory { 144 output.Output(fmt.Sprintf(" | [blue][bold]%c Advisory", Warning)) 145 } else { 146 output.Output(fmt.Sprintf(" | [red][bold]%c Failed", Cross)) 147 } 148 } 149 if outcome.Description != "" { 150 output.Output(fmt.Sprintf(" | [dim]%s", outcome.Description)) 151 } else { 152 output.Output(" | [dim]No description available") 153 } 154 } 155 } 156 } 157 return nil 158 } 159 160 func getPolicyCount(resultCount *tfe.PolicyResultCount) int { 161 return resultCount.AdvisoryFailed + resultCount.MandatoryFailed + resultCount.Errored + resultCount.Passed 162 }