mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
493 lines
14 KiB
493 lines
14 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package tfdiags
|
|
|
|
import (
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/gocty"
|
|
)
|
|
|
|
// The "contextual" family of diagnostics are designed to allow separating
|
|
// the detection of a problem from placing that problem in context. For
|
|
// example, some code that is validating an object extracted from configuration
|
|
// may not have access to the configuration that generated it, but can still
|
|
// report problems within that object which the caller can then place in
|
|
// context by calling IsConfigBody on the returned diagnostics.
|
|
//
|
|
// When contextual diagnostics are used, the documentation for a method must
|
|
// be very explicit about what context is implied for any diagnostics returned,
|
|
// to help ensure the expected result.
|
|
|
|
// contextualFromConfig is an interface type implemented by diagnostic types
|
|
// that can elaborate themselves when given information about the configuration
|
|
// body they are embedded in, as well as the runtime address associated with
|
|
// that configuration.
|
|
//
|
|
// Usually this entails extracting source location information in order to
|
|
// populate the "Subject" range.
|
|
type contextualFromConfigBody interface {
|
|
ElaborateFromConfigBody(hcl.Body, string) Diagnostic
|
|
}
|
|
|
|
// InConfigBody returns a copy of the receiver with any config-contextual
|
|
// diagnostics elaborated in the context of the given body. An optional address
|
|
// argument may be added to indicate which instance of the configuration the
|
|
// error related to.
|
|
func (diags Diagnostics) InConfigBody(body hcl.Body, addr string) Diagnostics {
|
|
if len(diags) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ret := make(Diagnostics, len(diags))
|
|
for i, srcDiag := range diags {
|
|
switch diag := srcDiag.(type) {
|
|
case contextualFromConfigBody:
|
|
ret[i] = diag.ElaborateFromConfigBody(body, addr)
|
|
case overriddenDiagnostic:
|
|
if cd, isCD := diag.original.(contextualFromConfigBody); isCD {
|
|
newOriginal := cd.ElaborateFromConfigBody(body, addr)
|
|
ret[i] = &overriddenDiagnostic{
|
|
original: newOriginal,
|
|
severity: diag.severity,
|
|
extra: diag.extra,
|
|
}
|
|
}
|
|
default:
|
|
ret[i] = srcDiag
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// AttributeValue returns a diagnostic about an attribute value in an implied current
|
|
// configuration context. This should be returned only from functions whose
|
|
// interface specifies a clear configuration context that this will be
|
|
// resolved in.
|
|
//
|
|
// The given path is relative to the implied configuration context. To describe
|
|
// a top-level attribute, it should be a single-element cty.Path with a
|
|
// cty.GetAttrStep. It's assumed that the path is returning into a structure
|
|
// that would be produced by our conventions in the configschema package; it
|
|
// may return unexpected results for structures that can't be represented by
|
|
// configschema.
|
|
//
|
|
// Since mapping attribute paths back onto configuration is an imprecise
|
|
// operation (e.g. dynamic block generation may cause the same block to be
|
|
// evaluated multiple times) the diagnostic detail should include the attribute
|
|
// name and other context required to help the user understand what is being
|
|
// referenced in case the identified source range is not unique.
|
|
//
|
|
// The returned attribute will not have source location information until
|
|
// context is applied to the containing diagnostics using diags.InConfigBody.
|
|
// After context is applied, the source location is the value assigned to the
|
|
// named attribute, or the containing body's "missing item range" if no
|
|
// value is present.
|
|
func AttributeValue(severity Severity, summary, detail string, attrPath cty.Path) Diagnostic {
|
|
return &attributeDiagnostic{
|
|
diagnosticBase: diagnosticBase{
|
|
severity: severity,
|
|
summary: summary,
|
|
detail: detail,
|
|
},
|
|
attrPath: attrPath,
|
|
}
|
|
}
|
|
|
|
// GetAttribute extracts an attribute cty.Path from a diagnostic if it contains
|
|
// one. Normally this is not accessed directly, and instead the config body is
|
|
// added to the Diagnostic to create a more complete message for the user. In
|
|
// some cases however, we may want to know just the name of the attribute that
|
|
// generated the Diagnostic message.
|
|
// This returns a nil cty.Path if it does not exist in the Diagnostic.
|
|
func GetAttribute(d Diagnostic) cty.Path {
|
|
if d, ok := d.(*attributeDiagnostic); ok {
|
|
return d.attrPath
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ Diagnostic = &attributeDiagnostic{}
|
|
var _ ComparableDiagnostic = &attributeDiagnostic{}
|
|
|
|
type attributeDiagnostic struct {
|
|
diagnosticBase
|
|
attrPath cty.Path
|
|
subject *SourceRange // populated only after ElaborateFromConfigBody
|
|
}
|
|
|
|
// ElaborateFromConfigBody finds the most accurate possible source location
|
|
// for a diagnostic's attribute path within the given body.
|
|
//
|
|
// Backing out from a path back to a source location is not always entirely
|
|
// possible because we lose some information in the decoding process, so
|
|
// if an exact position cannot be found then the returned diagnostic will
|
|
// refer to a position somewhere within the containing body, which is assumed
|
|
// to be better than no location at all.
|
|
//
|
|
// If possible it is generally better to report an error at a layer where
|
|
// source location information is still available, for more accuracy. This
|
|
// is not always possible due to system architecture, so this serves as a
|
|
// "best effort" fallback behavior for such situations.
|
|
func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string) Diagnostic {
|
|
// don't change an existing address
|
|
if d.address == "" {
|
|
d.address = addr
|
|
}
|
|
|
|
if len(d.attrPath) < 1 {
|
|
// Should never happen, but we'll allow it rather than crashing.
|
|
return d
|
|
}
|
|
|
|
if d.subject != nil {
|
|
// Don't modify an already-elaborated diagnostic.
|
|
return d
|
|
}
|
|
|
|
ret := *d
|
|
|
|
// This function will often end up re-decoding values that were already
|
|
// decoded by an earlier step. This is non-ideal but is architecturally
|
|
// more convenient than arranging for source location information to be
|
|
// propagated to every place in Terraform, and this happens only in the
|
|
// presence of errors where performance isn't a concern.
|
|
|
|
traverse := d.attrPath[:]
|
|
|
|
// Index should never be the first step
|
|
// as indexing of top blocks (such as resources & data sources)
|
|
// is handled elsewhere
|
|
if _, isIdxStep := traverse[0].(cty.IndexStep); isIdxStep {
|
|
subject := SourceRangeFromHCL(body.MissingItemRange())
|
|
ret.subject = &subject
|
|
return &ret
|
|
}
|
|
|
|
// If we have more than one step after removing index
|
|
// then we'll first try to traverse to a child body
|
|
// corresponding to the requested path.
|
|
remaining := traverse
|
|
if len(traverse) > 1 {
|
|
body, remaining = getDeepestBodyFromPath(body, traverse)
|
|
}
|
|
|
|
// Default is to indicate a missing item in the deepest body we reached
|
|
// while traversing.
|
|
subject := SourceRangeFromHCL(rangeOfDeepestAttributeValueFromPath(body, remaining))
|
|
ret.subject = &subject
|
|
|
|
return &ret
|
|
}
|
|
|
|
func (d *attributeDiagnostic) Equals(otherDiag ComparableDiagnostic) bool {
|
|
od, ok := otherDiag.(*attributeDiagnostic)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if d.severity != od.severity {
|
|
return false
|
|
}
|
|
if d.summary != od.summary {
|
|
return false
|
|
}
|
|
if d.detail != od.detail {
|
|
return false
|
|
}
|
|
if !d.attrPath.Equals(od.attrPath) {
|
|
return false
|
|
}
|
|
|
|
// address can differ between and after expansion
|
|
// even though it represents the same attribute
|
|
// so we avoid comparing it here
|
|
|
|
return sourceRangeEquals(d.subject, od.subject)
|
|
}
|
|
|
|
func getDeepestBodyFromPath(body hcl.Body, traverse []cty.PathStep) (hcl.Body, []cty.PathStep) {
|
|
lastProcessedIndex := -1
|
|
|
|
LOOP:
|
|
for i := 0; i < len(traverse); i++ {
|
|
step := traverse[i]
|
|
|
|
switch tStep := step.(type) {
|
|
case cty.GetAttrStep:
|
|
var next cty.PathStep
|
|
if i < (len(traverse) - 1) {
|
|
next = traverse[i+1]
|
|
}
|
|
|
|
// Will be indexing into our result here?
|
|
var indexType cty.Type
|
|
var indexVal cty.Value
|
|
if nextIndex, ok := next.(cty.IndexStep); ok {
|
|
indexVal = nextIndex.Key
|
|
indexType = indexVal.Type()
|
|
i++ // skip over the index on subsequent iterations
|
|
}
|
|
|
|
var blockLabelNames []string
|
|
if indexType == cty.String {
|
|
// Map traversal means we expect one label for the key.
|
|
blockLabelNames = []string{"key"}
|
|
}
|
|
|
|
// For intermediate steps we expect to be referring to a child
|
|
// block, so we'll attempt decoding under that assumption.
|
|
content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: tStep.Name,
|
|
LabelNames: blockLabelNames,
|
|
},
|
|
},
|
|
})
|
|
if contentDiags.HasErrors() {
|
|
break LOOP
|
|
}
|
|
filtered := make([]*hcl.Block, 0, len(content.Blocks))
|
|
for _, block := range content.Blocks {
|
|
if block.Type == tStep.Name {
|
|
filtered = append(filtered, block)
|
|
}
|
|
}
|
|
if len(filtered) == 0 {
|
|
// Step doesn't refer to a block
|
|
break LOOP
|
|
}
|
|
|
|
switch indexType {
|
|
case cty.NilType: // no index at all
|
|
if len(filtered) != 1 {
|
|
break LOOP
|
|
}
|
|
body = filtered[0].Body
|
|
lastProcessedIndex = i
|
|
case cty.Number:
|
|
var idx int
|
|
err := gocty.FromCtyValue(indexVal, &idx)
|
|
if err != nil || idx >= len(filtered) {
|
|
break LOOP
|
|
}
|
|
body = filtered[idx].Body
|
|
lastProcessedIndex = i
|
|
case cty.String:
|
|
key := indexVal.AsString()
|
|
var block *hcl.Block
|
|
for _, candidate := range filtered {
|
|
if candidate.Labels[0] == key {
|
|
block = candidate
|
|
break
|
|
}
|
|
}
|
|
if block == nil {
|
|
// No block with this key, so we'll just indicate a
|
|
// missing item in the containing block.
|
|
break LOOP
|
|
}
|
|
body = block.Body
|
|
lastProcessedIndex = i
|
|
default:
|
|
// Should never happen, because only string and numeric indices
|
|
// are supported by cty collections.
|
|
break LOOP
|
|
}
|
|
|
|
default:
|
|
// For any other kind of step, we'll just return our current body
|
|
// as the subject and accept that this is a little inaccurate.
|
|
break LOOP
|
|
}
|
|
}
|
|
return body, traverse[lastProcessedIndex+1:]
|
|
}
|
|
|
|
func rangeOfDeepestAttributeValueFromPath(body hcl.Body, traverse cty.Path) hcl.Range {
|
|
if len(traverse) == 0 {
|
|
return body.MissingItemRange()
|
|
}
|
|
// First we need to use the first traverse item to get the final attribute
|
|
// expression.
|
|
current, rest := traverse[0], traverse[1:]
|
|
|
|
currentGetAttr, ok := current.(cty.GetAttrStep)
|
|
if !ok {
|
|
// If the remaining basis is not an attribute access something went wrong.
|
|
// We can't do anything better than returning the bodies missing item range.
|
|
return body.MissingItemRange()
|
|
}
|
|
|
|
content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: currentGetAttr.Name,
|
|
Required: true,
|
|
},
|
|
},
|
|
})
|
|
if contentDiags.HasErrors() {
|
|
return body.MissingItemRange()
|
|
}
|
|
attr, ok := content.Attributes[currentGetAttr.Name]
|
|
if !ok {
|
|
// We could not find the attribute, this should have emitted a diag above, but just in case
|
|
return body.MissingItemRange()
|
|
}
|
|
|
|
return RangeForExpressionAtPath(attr.Expr, rest)
|
|
}
|
|
|
|
func RangeForExpressionAtPath(expression hcl.Expression, path cty.Path) hcl.Range {
|
|
// Now we need to loop through the rest of the path and progressively introspect
|
|
// the HCL expression.
|
|
currentExpr := expression
|
|
|
|
STEP_ITERATION:
|
|
for _, step := range path {
|
|
// We treat cty.IndexStep[type=String] and cty.GetAttrStep the same, so we just
|
|
// need to deal with list indexes first
|
|
if idxStep, ok := step.(cty.IndexStep); ok && idxStep.Key.Type() == cty.Number {
|
|
var idx int
|
|
err := gocty.FromCtyValue(idxStep.Key, &idx)
|
|
items, diags := hcl.ExprList(currentExpr)
|
|
if diags.HasErrors() {
|
|
return currentExpr.Range()
|
|
}
|
|
if err != nil || idx >= len(items) {
|
|
return currentExpr.Range()
|
|
}
|
|
currentExpr = items[idx]
|
|
continue STEP_ITERATION
|
|
}
|
|
|
|
var stepKey string
|
|
switch s := step.(type) {
|
|
case cty.GetAttrStep:
|
|
stepKey = s.Name
|
|
case cty.IndexStep:
|
|
stepKey = s.Key.AsString()
|
|
default: // should not happen
|
|
return currentExpr.Range()
|
|
}
|
|
|
|
pairs, diags := hcl.ExprMap(currentExpr)
|
|
if diags.HasErrors() {
|
|
return currentExpr.Range()
|
|
}
|
|
|
|
for _, kvPair := range pairs {
|
|
key, diags := kvPair.Key.Value(nil)
|
|
if diags.HasErrors() {
|
|
return currentExpr.Range()
|
|
}
|
|
if key.AsString() == stepKey {
|
|
currentExpr = kvPair.Value
|
|
continue STEP_ITERATION
|
|
}
|
|
}
|
|
// If we could not find the item return early
|
|
return currentExpr.Range()
|
|
}
|
|
|
|
return currentExpr.Range()
|
|
}
|
|
|
|
func (d *attributeDiagnostic) Source() Source {
|
|
return Source{
|
|
Subject: d.subject,
|
|
}
|
|
}
|
|
|
|
// WholeContainingBody returns a diagnostic about the body that is an implied
|
|
// current configuration context. This should be returned only from
|
|
// functions whose interface specifies a clear configuration context that this
|
|
// will be resolved in.
|
|
//
|
|
// The returned attribute will not have source location information until
|
|
// context is applied to the containing diagnostics using diags.InConfigBody.
|
|
// After context is applied, the source location is currently the missing item
|
|
// range of the body. In future, this may change to some other suitable
|
|
// part of the containing body.
|
|
func WholeContainingBody(severity Severity, summary, detail string) Diagnostic {
|
|
return &wholeBodyDiagnostic{
|
|
diagnosticBase: diagnosticBase{
|
|
severity: severity,
|
|
summary: summary,
|
|
detail: detail,
|
|
},
|
|
}
|
|
}
|
|
|
|
var _ Diagnostic = &wholeBodyDiagnostic{}
|
|
var _ ComparableDiagnostic = &wholeBodyDiagnostic{}
|
|
|
|
type wholeBodyDiagnostic struct {
|
|
diagnosticBase
|
|
subject *SourceRange // populated only after ElaborateFromConfigBody
|
|
}
|
|
|
|
func (d *wholeBodyDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string) Diagnostic {
|
|
// don't change an existing address
|
|
if d.address == "" {
|
|
d.address = addr
|
|
}
|
|
|
|
if d.subject != nil {
|
|
// Don't modify an already-elaborated diagnostic.
|
|
return d
|
|
}
|
|
|
|
ret := *d
|
|
rng := SourceRangeFromHCL(body.MissingItemRange())
|
|
ret.subject = &rng
|
|
return &ret
|
|
}
|
|
|
|
func (d *wholeBodyDiagnostic) Source() Source {
|
|
return Source{
|
|
Subject: d.subject,
|
|
}
|
|
}
|
|
|
|
func (d *wholeBodyDiagnostic) Equals(otherDiag ComparableDiagnostic) bool {
|
|
od, ok := otherDiag.(*wholeBodyDiagnostic)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if d.severity != od.severity {
|
|
return false
|
|
}
|
|
if d.summary != od.summary {
|
|
return false
|
|
}
|
|
if d.detail != od.detail {
|
|
return false
|
|
}
|
|
|
|
// address can differ between and after expansion
|
|
// even though it represents the same attribute
|
|
// so we avoid comparing it here
|
|
|
|
return sourceRangeEquals(d.subject, od.subject)
|
|
}
|
|
|
|
func sourceRangeEquals(l, r *SourceRange) bool {
|
|
if l == nil || r == nil {
|
|
return l == r
|
|
}
|
|
if l.Filename != r.Filename {
|
|
return false
|
|
}
|
|
if l.Start.Byte != r.Start.Byte {
|
|
return false
|
|
}
|
|
if l.End.Byte != r.End.Byte {
|
|
return false
|
|
}
|
|
return true
|
|
}
|