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