github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/tfdiags/contextual.go (about)

     1  package tfdiags
     2  
     3  import (
     4  	"github.com/hashicorp/hcl/v2"
     5  	"github.com/zclconf/go-cty/cty"
     6  	"github.com/zclconf/go-cty/cty/gocty"
     7  )
     8  
     9  // The "contextual" family of diagnostics are designed to allow separating
    10  // the detection of a problem from placing that problem in context. For
    11  // example, some code that is validating an object extracted from configuration
    12  // may not have access to the configuration that generated it, but can still
    13  // report problems within that object which the caller can then place in
    14  // context by calling IsConfigBody on the returned diagnostics.
    15  //
    16  // When contextual diagnostics are used, the documentation for a method must
    17  // be very explicit about what context is implied for any diagnostics returned,
    18  // to help ensure the expected result.
    19  
    20  // contextualFromConfig is an interface type implemented by diagnostic types
    21  // that can elaborate themselves when given information about the configuration
    22  // body they are embedded in.
    23  //
    24  // Usually this entails extracting source location information in order to
    25  // populate the "Subject" range.
    26  type contextualFromConfigBody interface {
    27  	ElaborateFromConfigBody(hcl.Body) Diagnostic
    28  }
    29  
    30  // InConfigBody returns a copy of the receiver with any config-contextual
    31  // diagnostics elaborated in the context of the given body.
    32  func (d Diagnostics) InConfigBody(body hcl.Body) Diagnostics {
    33  	if len(d) == 0 {
    34  		return nil
    35  	}
    36  
    37  	ret := make(Diagnostics, len(d))
    38  	for i, srcDiag := range d {
    39  		if cd, isCD := srcDiag.(contextualFromConfigBody); isCD {
    40  			ret[i] = cd.ElaborateFromConfigBody(body)
    41  		} else {
    42  			ret[i] = srcDiag
    43  		}
    44  	}
    45  
    46  	return ret
    47  }
    48  
    49  // AttributeValue returns a diagnostic about an attribute value in an implied current
    50  // configuration context. This should be returned only from functions whose
    51  // interface specifies a clear configuration context that this will be
    52  // resolved in.
    53  //
    54  // The given path is relative to the implied configuration context. To describe
    55  // a top-level attribute, it should be a single-element cty.Path with a
    56  // cty.GetAttrStep. It's assumed that the path is returning into a structure
    57  // that would be produced by our conventions in the configschema package; it
    58  // may return unexpected results for structures that can't be represented by
    59  // configschema.
    60  //
    61  // Since mapping attribute paths back onto configuration is an imprecise
    62  // operation (e.g. dynamic block generation may cause the same block to be
    63  // evaluated multiple times) the diagnostic detail should include the attribute
    64  // name and other context required to help the user understand what is being
    65  // referenced in case the identified source range is not unique.
    66  //
    67  // The returned attribute will not have source location information until
    68  // context is applied to the containing diagnostics using diags.InConfigBody.
    69  // After context is applied, the source location is the value assigned to the
    70  // named attribute, or the containing body's "missing item range" if no
    71  // value is present.
    72  func AttributeValue(severity Severity, summary, detail string, attrPath cty.Path) Diagnostic {
    73  	return &attributeDiagnostic{
    74  		diagnosticBase: diagnosticBase{
    75  			severity: severity,
    76  			summary:  summary,
    77  			detail:   detail,
    78  		},
    79  		attrPath: attrPath,
    80  	}
    81  }
    82  
    83  // GetAttribute extracts an attribute cty.Path from a diagnostic if it contains
    84  // one. Normally this is not accessed directly, and instead the config body is
    85  // added to the Diagnostic to create a more complete message for the user. In
    86  // some cases however, we may want to know just the name of the attribute that
    87  // generated the Diagnostic message.
    88  // This returns a nil cty.Path if it does not exist in the Diagnostic.
    89  func GetAttribute(d Diagnostic) cty.Path {
    90  	if d, ok := d.(*attributeDiagnostic); ok {
    91  		return d.attrPath
    92  	}
    93  	return nil
    94  }
    95  
    96  type attributeDiagnostic struct {
    97  	diagnosticBase
    98  	attrPath cty.Path
    99  	subject  *SourceRange // populated only after ElaborateFromConfigBody
   100  }
   101  
   102  // ElaborateFromConfigBody finds the most accurate possible source location
   103  // for a diagnostic's attribute path within the given body.
   104  //
   105  // Backing out from a path back to a source location is not always entirely
   106  // possible because we lose some information in the decoding process, so
   107  // if an exact position cannot be found then the returned diagnostic will
   108  // refer to a position somewhere within the containing body, which is assumed
   109  // to be better than no location at all.
   110  //
   111  // If possible it is generally better to report an error at a layer where
   112  // source location information is still available, for more accuracy. This
   113  // is not always possible due to system architecture, so this serves as a
   114  // "best effort" fallback behavior for such situations.
   115  func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body) Diagnostic {
   116  	if len(d.attrPath) < 1 {
   117  		// Should never happen, but we'll allow it rather than crashing.
   118  		return d
   119  	}
   120  
   121  	if d.subject != nil {
   122  		// Don't modify an already-elaborated diagnostic.
   123  		return d
   124  	}
   125  
   126  	ret := *d
   127  
   128  	// This function will often end up re-decoding values that were already
   129  	// decoded by an earlier step. This is non-ideal but is architecturally
   130  	// more convenient than arranging for source location information to be
   131  	// propagated to every place in Terraform, and this happens only in the
   132  	// presence of errors where performance isn't a concern.
   133  
   134  	traverse := d.attrPath[:]
   135  	final := d.attrPath[len(d.attrPath)-1]
   136  
   137  	// Index should never be the first step
   138  	// as indexing of top blocks (such as resources & data sources)
   139  	// is handled elsewhere
   140  	if _, isIdxStep := traverse[0].(cty.IndexStep); isIdxStep {
   141  		subject := SourceRangeFromHCL(body.MissingItemRange())
   142  		ret.subject = &subject
   143  		return &ret
   144  	}
   145  
   146  	// Process index separately
   147  	idxStep, hasIdx := final.(cty.IndexStep)
   148  	if hasIdx {
   149  		final = d.attrPath[len(d.attrPath)-2]
   150  		traverse = d.attrPath[:len(d.attrPath)-1]
   151  	}
   152  
   153  	// If we have more than one step after removing index
   154  	// then we'll first try to traverse to a child body
   155  	// corresponding to the requested path.
   156  	if len(traverse) > 1 {
   157  		body = traversePathSteps(traverse, body)
   158  	}
   159  
   160  	// Default is to indicate a missing item in the deepest body we reached
   161  	// while traversing.
   162  	subject := SourceRangeFromHCL(body.MissingItemRange())
   163  	ret.subject = &subject
   164  
   165  	// Once we get here, "final" should be a GetAttr step that maps to an
   166  	// attribute in our current body.
   167  	finalStep, isAttr := final.(cty.GetAttrStep)
   168  	if !isAttr {
   169  		return &ret
   170  	}
   171  
   172  	content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
   173  		Attributes: []hcl.AttributeSchema{
   174  			{
   175  				Name:     finalStep.Name,
   176  				Required: true,
   177  			},
   178  		},
   179  	})
   180  	if contentDiags.HasErrors() {
   181  		return &ret
   182  	}
   183  
   184  	if attr, ok := content.Attributes[finalStep.Name]; ok {
   185  		hclRange := attr.Expr.Range()
   186  		if hasIdx {
   187  			// Try to be more precise by finding index range
   188  			hclRange = hclRangeFromIndexStepAndAttribute(idxStep, attr)
   189  		}
   190  		subject = SourceRangeFromHCL(hclRange)
   191  		ret.subject = &subject
   192  	}
   193  
   194  	return &ret
   195  }
   196  
   197  func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body {
   198  	for i := 0; i < len(traverse); i++ {
   199  		step := traverse[i]
   200  
   201  		switch tStep := step.(type) {
   202  		case cty.GetAttrStep:
   203  
   204  			var next cty.PathStep
   205  			if i < (len(traverse) - 1) {
   206  				next = traverse[i+1]
   207  			}
   208  
   209  			// Will be indexing into our result here?
   210  			var indexType cty.Type
   211  			var indexVal cty.Value
   212  			if nextIndex, ok := next.(cty.IndexStep); ok {
   213  				indexVal = nextIndex.Key
   214  				indexType = indexVal.Type()
   215  				i++ // skip over the index on subsequent iterations
   216  			}
   217  
   218  			var blockLabelNames []string
   219  			if indexType == cty.String {
   220  				// Map traversal means we expect one label for the key.
   221  				blockLabelNames = []string{"key"}
   222  			}
   223  
   224  			// For intermediate steps we expect to be referring to a child
   225  			// block, so we'll attempt decoding under that assumption.
   226  			content, _, contentDiags := body.PartialContent(&hcl.BodySchema{
   227  				Blocks: []hcl.BlockHeaderSchema{
   228  					{
   229  						Type:       tStep.Name,
   230  						LabelNames: blockLabelNames,
   231  					},
   232  				},
   233  			})
   234  			if contentDiags.HasErrors() {
   235  				return body
   236  			}
   237  			filtered := make([]*hcl.Block, 0, len(content.Blocks))
   238  			for _, block := range content.Blocks {
   239  				if block.Type == tStep.Name {
   240  					filtered = append(filtered, block)
   241  				}
   242  			}
   243  			if len(filtered) == 0 {
   244  				// Step doesn't refer to a block
   245  				continue
   246  			}
   247  
   248  			switch indexType {
   249  			case cty.NilType: // no index at all
   250  				if len(filtered) != 1 {
   251  					return body
   252  				}
   253  				body = filtered[0].Body
   254  			case cty.Number:
   255  				var idx int
   256  				err := gocty.FromCtyValue(indexVal, &idx)
   257  				if err != nil || idx >= len(filtered) {
   258  					return body
   259  				}
   260  				body = filtered[idx].Body
   261  			case cty.String:
   262  				key := indexVal.AsString()
   263  				var block *hcl.Block
   264  				for _, candidate := range filtered {
   265  					if candidate.Labels[0] == key {
   266  						block = candidate
   267  						break
   268  					}
   269  				}
   270  				if block == nil {
   271  					// No block with this key, so we'll just indicate a
   272  					// missing item in the containing block.
   273  					return body
   274  				}
   275  				body = block.Body
   276  			default:
   277  				// Should never happen, because only string and numeric indices
   278  				// are supported by cty collections.
   279  				return body
   280  			}
   281  
   282  		default:
   283  			// For any other kind of step, we'll just return our current body
   284  			// as the subject and accept that this is a little inaccurate.
   285  			return body
   286  		}
   287  	}
   288  	return body
   289  }
   290  
   291  func hclRangeFromIndexStepAndAttribute(idxStep cty.IndexStep, attr *hcl.Attribute) hcl.Range {
   292  	switch idxStep.Key.Type() {
   293  	case cty.Number:
   294  		var idx int
   295  		err := gocty.FromCtyValue(idxStep.Key, &idx)
   296  		items, diags := hcl.ExprList(attr.Expr)
   297  		if diags.HasErrors() {
   298  			return attr.Expr.Range()
   299  		}
   300  		if err != nil || idx >= len(items) {
   301  			return attr.NameRange
   302  		}
   303  		return items[idx].Range()
   304  	case cty.String:
   305  		pairs, diags := hcl.ExprMap(attr.Expr)
   306  		if diags.HasErrors() {
   307  			return attr.Expr.Range()
   308  		}
   309  		stepKey := idxStep.Key.AsString()
   310  		for _, kvPair := range pairs {
   311  			key, err := kvPair.Key.Value(nil)
   312  			if err != nil {
   313  				return attr.Expr.Range()
   314  			}
   315  			if key.AsString() == stepKey {
   316  				startRng := kvPair.Value.StartRange()
   317  				return startRng
   318  			}
   319  		}
   320  		return attr.NameRange
   321  	}
   322  	return attr.Expr.Range()
   323  }
   324  
   325  func (d *attributeDiagnostic) Source() Source {
   326  	return Source{
   327  		Subject: d.subject,
   328  	}
   329  }
   330  
   331  // WholeContainingBody returns a diagnostic about the body that is an implied
   332  // current configuration context. This should be returned only from
   333  // functions whose interface specifies a clear configuration context that this
   334  // will be resolved in.
   335  //
   336  // The returned attribute will not have source location information until
   337  // context is applied to the containing diagnostics using diags.InConfigBody.
   338  // After context is applied, the source location is currently the missing item
   339  // range of the body. In future, this may change to some other suitable
   340  // part of the containing body.
   341  func WholeContainingBody(severity Severity, summary, detail string) Diagnostic {
   342  	return &wholeBodyDiagnostic{
   343  		diagnosticBase: diagnosticBase{
   344  			severity: severity,
   345  			summary:  summary,
   346  			detail:   detail,
   347  		},
   348  	}
   349  }
   350  
   351  type wholeBodyDiagnostic struct {
   352  	diagnosticBase
   353  	subject *SourceRange // populated only after ElaborateFromConfigBody
   354  }
   355  
   356  func (d *wholeBodyDiagnostic) ElaborateFromConfigBody(body hcl.Body) Diagnostic {
   357  	if d.subject != nil {
   358  		// Don't modify an already-elaborated diagnostic.
   359  		return d
   360  	}
   361  
   362  	ret := *d
   363  	rng := SourceRangeFromHCL(body.MissingItemRange())
   364  	ret.subject = &rng
   365  	return &ret
   366  }
   367  
   368  func (d *wholeBodyDiagnostic) Source() Source {
   369  	return Source{
   370  		Subject: d.subject,
   371  	}
   372  }