mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
05caff2ca3
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
386 lines
12 KiB
Go
386 lines
12 KiB
Go
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 {
|
|
if cd, isCD := srcDiag.(contextualFromConfigBody); isCD {
|
|
ret[i] = cd.ElaborateFromConfigBody(body, addr)
|
|
} else {
|
|
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
|
|
}
|
|
|
|
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[:]
|
|
final := d.attrPath[len(d.attrPath)-1]
|
|
|
|
// 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
|
|
}
|
|
|
|
// Process index separately
|
|
idxStep, hasIdx := final.(cty.IndexStep)
|
|
if hasIdx {
|
|
final = d.attrPath[len(d.attrPath)-2]
|
|
traverse = d.attrPath[:len(d.attrPath)-1]
|
|
}
|
|
|
|
// 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.
|
|
if len(traverse) > 1 {
|
|
body = traversePathSteps(traverse, body)
|
|
}
|
|
|
|
// Default is to indicate a missing item in the deepest body we reached
|
|
// while traversing.
|
|
subject := SourceRangeFromHCL(body.MissingItemRange())
|
|
ret.subject = &subject
|
|
|
|
// Once we get here, "final" should be a GetAttr step that maps to an
|
|
// attribute in our current body.
|
|
finalStep, isAttr := final.(cty.GetAttrStep)
|
|
if !isAttr {
|
|
return &ret
|
|
}
|
|
|
|
content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{
|
|
Name: finalStep.Name,
|
|
Required: true,
|
|
},
|
|
},
|
|
})
|
|
if contentDiags.HasErrors() {
|
|
return &ret
|
|
}
|
|
|
|
if attr, ok := content.Attributes[finalStep.Name]; ok {
|
|
hclRange := attr.Expr.Range()
|
|
if hasIdx {
|
|
// Try to be more precise by finding index range
|
|
hclRange = hclRangeFromIndexStepAndAttribute(idxStep, attr)
|
|
}
|
|
subject = SourceRangeFromHCL(hclRange)
|
|
ret.subject = &subject
|
|
}
|
|
|
|
return &ret
|
|
}
|
|
|
|
func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body {
|
|
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() {
|
|
return body
|
|
}
|
|
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
|
|
continue
|
|
}
|
|
|
|
switch indexType {
|
|
case cty.NilType: // no index at all
|
|
if len(filtered) != 1 {
|
|
return body
|
|
}
|
|
body = filtered[0].Body
|
|
case cty.Number:
|
|
var idx int
|
|
err := gocty.FromCtyValue(indexVal, &idx)
|
|
if err != nil || idx >= len(filtered) {
|
|
return body
|
|
}
|
|
body = filtered[idx].Body
|
|
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.
|
|
return body
|
|
}
|
|
body = block.Body
|
|
default:
|
|
// Should never happen, because only string and numeric indices
|
|
// are supported by cty collections.
|
|
return body
|
|
}
|
|
|
|
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.
|
|
return body
|
|
}
|
|
}
|
|
return body
|
|
}
|
|
|
|
func hclRangeFromIndexStepAndAttribute(idxStep cty.IndexStep, attr *hcl.Attribute) hcl.Range {
|
|
switch idxStep.Key.Type() {
|
|
case cty.Number:
|
|
var idx int
|
|
err := gocty.FromCtyValue(idxStep.Key, &idx)
|
|
items, diags := hcl.ExprList(attr.Expr)
|
|
if diags.HasErrors() {
|
|
return attr.Expr.Range()
|
|
}
|
|
if err != nil || idx >= len(items) {
|
|
return attr.NameRange
|
|
}
|
|
return items[idx].Range()
|
|
case cty.String:
|
|
pairs, diags := hcl.ExprMap(attr.Expr)
|
|
if diags.HasErrors() {
|
|
return attr.Expr.Range()
|
|
}
|
|
stepKey := idxStep.Key.AsString()
|
|
for _, kvPair := range pairs {
|
|
key, diags := kvPair.Key.Value(nil)
|
|
if diags.HasErrors() {
|
|
return attr.Expr.Range()
|
|
}
|
|
if key.AsString() == stepKey {
|
|
startRng := kvPair.Value.StartRange()
|
|
return startRng
|
|
}
|
|
}
|
|
return attr.NameRange
|
|
}
|
|
return attr.Expr.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,
|
|
},
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|