| // Copyright IBM Corp. 2014, 2026 |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package policy |
| |
| import ( |
| "github.com/hashicorp/hcl/v2" |
| "github.com/hashicorp/terraform/internal/policy/proto" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // PolicyExtra is a wrapper for policy information that can be used as a diagnostic extra. |
| // It also combines all the different extra information in the original proto diagnostic. |
| type PolicyExtra struct { |
| Policy |
| Severity hcl.DiagnosticSeverity |
| Result EvaluateResult |
| |
| // EnforceIndex is the index of the enforce block that generated this diagnostic. |
| // This is only set if the diagnostic was generated by an enforce block. |
| EnforceIndex *int32 |
| |
| Snippet *proto.Snippet |
| Range *proto.RangeExtra |
| ExpressionValues []*proto.ExpressionValue |
| Attribute cty.Path |
| } |
| |
| // DiagsFromProto converts a slice of proto.Diagnostic to tfdiags.Diagnostics, while wrapping |
| // the policy information in the diagnostic extra info. |
| func DiagsFromProto(protoDiags []*proto.Diagnostic, policy *Policy) Diagnostics { |
| var ret Diagnostics |
| if len(protoDiags) == 0 { |
| return ret |
| } |
| diags := proto.ToHCLDiagnostics(protoDiags) |
| for _, diag := range diags { |
| ret = append(ret, newPolicyDiagnostic(diag, policy)) |
| } |
| |
| return ret |
| } |
| |
| func newPolicyDiagnostic(diag *hcl.Diagnostic, policy *Policy) Diagnostic { |
| // policy diags are represented as hcl diagnostics, with the diagnostic containing |
| // source information related to the policy files. We convert them to a terraform diag here. |
| hclDiag := tfdiags.FromHCL(diag) |
| return Diagnostic{original: hclDiag, extra: policyExtra(diag, policy)} |
| } |
| |
| func policyExtra(diag *hcl.Diagnostic, policy *Policy) *PolicyExtra { |
| diagExtra := &PolicyExtra{Severity: diag.Severity} |
| if policy != nil { |
| diagExtra.Policy = *policy |
| } |
| if extra := extraInfo[*proto.EvaluateResultExtra](diag.Extra); extra != nil { |
| diagExtra.Result = ResultFromProto(extra.EvaluateResult) |
| } |
| if extra := extraInfo[*proto.SnippetExtra](diag.Extra); extra != nil { |
| diagExtra.Snippet = extra.Snippet |
| } |
| if extra := extraInfo[*proto.RangeExtra](diag.Extra); extra != nil { |
| diagExtra.Range = extra |
| } |
| if extra := extraInfo[*proto.ExpressionValuesExtra](diag.Extra); extra != nil { |
| diagExtra.ExpressionValues = extra.ExpressionValues |
| } |
| if extra := extraInfo[*proto.AttributeExtra](diag.Extra); extra != nil { |
| diagExtra.Attribute = extra.Attribute |
| } |
| |
| // if we have no policy information, we can use the little we have within the diagnostic itself. |
| if extra := extraInfo[*proto.PolicyExtra](diag.Extra); extra != nil && policy == nil { |
| diagExtra.Policy = Policy{ |
| PolicySetName: extra.PolicySet.Name, |
| Directory: extra.PolicySet.Path, |
| } |
| } |
| |
| return diagExtra |
| } |
| |
| func extraInfo[T any](extra any) T { |
| if extra, ok := extra.(T); ok { |
| return extra |
| } |
| return tfdiags.ExtraInfoNext[T](extra) |
| } |
| |
| // Diagnostic contains the diagnostic produced by the policy engine, as well as |
| // extra information within it. |
| type Diagnostic struct { |
| original tfdiags.Diagnostic |
| extra *PolicyExtra |
| |
| // localRange is the range of the terraform object related to the policy being evaluated |
| localRange *hcl.Range |
| } |
| |
| type Diagnostics []Diagnostic |
| |
| func (d Diagnostics) HasErrors() bool { |
| for _, diag := range d { |
| if diag.Severity() == tfdiags.Error { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // WithLocalRange returns a copy of the response with the local range set to the given value. |
| // This value typically comes from the range of the terraform object being evaluated. |
| func (d Diagnostics) WithLocalRange(localRange *hcl.Range) Diagnostics { |
| if len(d) == 0 || localRange == nil { |
| return d |
| } |
| for i := range d { |
| d[i].localRange = localRange |
| } |
| return d |
| } |
| |
| func (o Diagnostic) Severity() tfdiags.Severity { |
| return o.original.Severity() |
| } |
| |
| func (o Diagnostic) Description() tfdiags.Description { |
| return o.original.Description() |
| } |
| |
| // Source returns the local terraform source information. |
| // The policy source information should be obtained separately from the policy extra information. |
| func (o Diagnostic) Source() tfdiags.Source { |
| var ret tfdiags.Source |
| if o.localRange != nil { |
| rng := tfdiags.SourceRangeFromHCL(*o.localRange) |
| ret.Subject = &rng |
| // TODO: get exact context |
| ret.Context = &rng |
| } |
| return ret |
| } |
| |
| func (o Diagnostic) FromExpr() *tfdiags.FromExpr { |
| return o.original.FromExpr() |
| } |
| |
| func (o Diagnostic) ExtraInfo() any { |
| return o.extra |
| } |
| |
| // WithLocalRange sets the local terraform range, which will be used as the diagnostic's source information |
| func (o Diagnostic) WithLocalRange(rng *hcl.Range) Diagnostic { |
| o.localRange = rng |
| return o |
| } |
| |
| func NewErrorDiagnostic(summary, detail string, result EvaluateResult) Diagnostic { |
| return Diagnostic{ |
| original: tfdiags.Sourceless(tfdiags.Error, summary, detail), |
| extra: &PolicyExtra{Severity: hcl.DiagError, Result: result}, |
| } |
| } |
| |
| func (diags Diagnostics) AsTerraformDiags() tfdiags.Diagnostics { |
| var ret tfdiags.Diagnostics |
| for _, diag := range diags { |
| ret = ret.Append(diag) |
| } |
| return ret |
| } |