mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
d9603d5bc5
By collecting information about the input variables during analysis, we can return approximate type information for any references to those variables in expressions. Since Terraform 0.11 allowed maps of maps and lists of lists in certain circumstances even though this was documented as forbidden, we conservatively return collection types whose element types are unknown here, which allows us to do shallow inference on them but will cause us to get an incomplete result if any operations are performed on elements of the list or map value.
168 lines
6.6 KiB
Go
168 lines
6.6 KiB
Go
package configupgrade
|
|
|
|
import (
|
|
"log"
|
|
|
|
hcl2 "github.com/hashicorp/hcl2/hcl"
|
|
hcl2syntax "github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/lang"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
)
|
|
|
|
// InferExpressionType attempts to determine a result type for the given
|
|
// expression source code, which should already have been upgraded to new
|
|
// expression syntax.
|
|
//
|
|
// If self is non-nil, it will determine the meaning of the special "self"
|
|
// reference.
|
|
//
|
|
// If such an inference isn't possible, either because of limitations of
|
|
// static analysis or because of errors in the expression, the result is
|
|
// cty.DynamicPseudoType indicating "unknown".
|
|
func (an *analysis) InferExpressionType(src []byte, self addrs.Referenceable) cty.Type {
|
|
expr, diags := hcl2syntax.ParseExpression(src, "", hcl2.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
// If there's a syntax error then analysis is impossible.
|
|
return cty.DynamicPseudoType
|
|
}
|
|
|
|
data := analysisData{an}
|
|
scope := &lang.Scope{
|
|
Data: data,
|
|
SelfAddr: self,
|
|
PureOnly: false,
|
|
BaseDir: ".",
|
|
}
|
|
val, _ := scope.EvalExpr(expr, cty.DynamicPseudoType)
|
|
|
|
// Value will be cty.DynamicVal if either inference was impossible or
|
|
// if there was an error, leading to cty.DynamicPseudoType here.
|
|
return val.Type()
|
|
}
|
|
|
|
// analysisData is an implementation of lang.Data that returns unknown values
|
|
// of suitable types in order to achieve approximate dynamic analysis of
|
|
// expression result types, which we need for some upgrade rules.
|
|
//
|
|
// Unlike a usual implementation of this interface, this one never returns
|
|
// errors and will instead just return cty.DynamicVal if it can't produce
|
|
// an exact type for any reason. This can then allow partial upgrading to
|
|
// proceed and the caller can emit warning comments for ambiguous situations.
|
|
//
|
|
// N.B.: Source ranges in the data methods are meaningless, since they are
|
|
// just relative to the byte array passed to InferExpressionType, not to
|
|
// any real input file.
|
|
type analysisData struct {
|
|
an *analysis
|
|
}
|
|
|
|
var _ lang.Data = (*analysisData)(nil)
|
|
|
|
func (d analysisData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics {
|
|
// This implementation doesn't do any static validation.
|
|
return nil
|
|
}
|
|
|
|
func (d analysisData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
// All valid count attributes are numbers
|
|
return cty.UnknownVal(cty.Number), nil
|
|
}
|
|
|
|
func (d analysisData) GetResourceInstance(instAddr addrs.ResourceInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
log.Printf("[TRACE] configupgrade: Determining type for %s", instAddr)
|
|
addr := instAddr.Resource
|
|
|
|
// Our analysis pass should've found a suitable schema for every resource
|
|
// type in the module.
|
|
providerType, ok := d.an.ResourceProviderType[addr]
|
|
if !ok {
|
|
// Should not be possible, since analysis visits every resource block.
|
|
log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a provider type for %s", addr)
|
|
return cty.DynamicVal, nil
|
|
}
|
|
providerSchema, ok := d.an.ProviderSchemas[providerType]
|
|
if !ok {
|
|
// Should not be possible, since analysis loads schema for every provider.
|
|
log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a provider schema for for %q", providerType)
|
|
return cty.DynamicVal, nil
|
|
}
|
|
schema, _ := providerSchema.SchemaForResourceAddr(addr)
|
|
if schema == nil {
|
|
// Should not be possible, since analysis loads schema for every provider.
|
|
log.Printf("[TRACE] configupgrade: analysis.GetResourceInstance doesn't have a schema for for %s", addr)
|
|
return cty.DynamicVal, nil
|
|
}
|
|
|
|
objTy := schema.ImpliedType()
|
|
|
|
// We'll emulate the normal evaluator's behavor of deciding whether to
|
|
// return a list or a single object type depending on whether count is
|
|
// set and whether an instance key is given in the address.
|
|
if d.an.ResourceHasCount[addr] {
|
|
if instAddr.Key == addrs.NoKey {
|
|
log.Printf("[TRACE] configupgrade: %s refers to counted instance without a key, so result is a list of %#v", instAddr, objTy)
|
|
return cty.UnknownVal(cty.List(objTy)), nil
|
|
}
|
|
log.Printf("[TRACE] configupgrade: %s refers to counted instance with a key, so result is single object", instAddr)
|
|
return cty.UnknownVal(objTy), nil
|
|
}
|
|
|
|
if instAddr.Key != addrs.NoKey {
|
|
log.Printf("[TRACE] configupgrade: %s refers to non-counted instance with a key, which is invalid", instAddr)
|
|
return cty.DynamicVal, nil
|
|
}
|
|
log.Printf("[TRACE] configupgrade: %s refers to non-counted instance without a key, so result is single object", instAddr)
|
|
return cty.UnknownVal(objTy), nil
|
|
}
|
|
|
|
func (d analysisData) GetLocalValue(addrs.LocalValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
// We can only predict these in general by recursively evaluating their
|
|
// expressions, which creates some undesirable complexity here so for
|
|
// now we'll just skip analyses with locals and see if this complexity
|
|
// is warranted with real-world testing.
|
|
return cty.DynamicVal, nil
|
|
}
|
|
|
|
func (d analysisData) GetModuleInstance(addrs.ModuleCallInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
// We only work on one module at a time during upgrading, so we have no
|
|
// information about the outputs of a child module.
|
|
return cty.DynamicVal, nil
|
|
}
|
|
|
|
func (d analysisData) GetModuleInstanceOutput(addrs.ModuleCallOutput, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
// We only work on one module at a time during upgrading, so we have no
|
|
// information about the outputs of a child module.
|
|
return cty.DynamicVal, nil
|
|
}
|
|
|
|
func (d analysisData) GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
// All valid path attributes are strings
|
|
return cty.UnknownVal(cty.String), nil
|
|
}
|
|
|
|
func (d analysisData) GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
// All valid "terraform" attributes are strings
|
|
return cty.UnknownVal(cty.String), nil
|
|
}
|
|
|
|
func (d analysisData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
|
// TODO: Collect shallow type information (list vs. map vs. string vs. unknown)
|
|
// in analysis and then return a similarly-approximate type here.
|
|
log.Printf("[TRACE] configupgrade: Determining type for %s", addr)
|
|
name := addr.Name
|
|
typeName := d.an.VariableTypes[name]
|
|
switch typeName {
|
|
case "list":
|
|
return cty.UnknownVal(cty.List(cty.DynamicPseudoType)), nil
|
|
case "map":
|
|
return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), nil
|
|
case "string":
|
|
return cty.UnknownVal(cty.String), nil
|
|
default:
|
|
return cty.DynamicVal, nil
|
|
}
|
|
}
|