172 lines
6.8 KiB
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
|
|
}
|
|
}
|