blob: a34e5925ba23de6db185c6179d0641103ace443b [file]
// 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
}