terraform/configs/configupgrade/analysis_expr.go

172 lines
6.8 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) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return cty.DynamicVal, 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
}
}