mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-16 11:42:58 -06:00
8490fc36f7
Prior to v0.12 Terraform was liberal about these and allowed them to mismatch, but now it's important to get this right so that resources and resource instances can be used directly as object values, and so we'll fix up any sloppy existing references so things keep working as expected. This is particularly important for the pattern of using count to create conditional resources, since previously the "true" case would create one instance and Terraform would accept an unindexed reference to that.
228 lines
7.3 KiB
Go
228 lines
7.3 KiB
Go
package configupgrade
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
hcl1 "github.com/hashicorp/hcl"
|
|
hcl1ast "github.com/hashicorp/hcl/hcl/ast"
|
|
hcl1parser "github.com/hashicorp/hcl/hcl/parser"
|
|
hcl1token "github.com/hashicorp/hcl/hcl/token"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/configs/configschema"
|
|
"github.com/hashicorp/terraform/moduledeps"
|
|
"github.com/hashicorp/terraform/plugin/discovery"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
// analysis is a container for the various different information gathered
|
|
// by Upgrader.analyze.
|
|
type analysis struct {
|
|
ProviderSchemas map[string]*terraform.ProviderSchema
|
|
ProvisionerSchemas map[string]*configschema.Block
|
|
ResourceProviderType map[addrs.Resource]string
|
|
ResourceHasCount map[addrs.Resource]bool
|
|
}
|
|
|
|
// analyze processes the configuration files included inside the receiver
|
|
// and returns an assortment of information required to make decisions during
|
|
// a configuration upgrade.
|
|
func (u *Upgrader) analyze(ms ModuleSources) (*analysis, error) {
|
|
ret := &analysis{
|
|
ProviderSchemas: make(map[string]*terraform.ProviderSchema),
|
|
ProvisionerSchemas: make(map[string]*configschema.Block),
|
|
ResourceProviderType: make(map[addrs.Resource]string),
|
|
ResourceHasCount: make(map[addrs.Resource]bool),
|
|
}
|
|
|
|
m := &moduledeps.Module{
|
|
Providers: make(moduledeps.Providers),
|
|
}
|
|
|
|
// This is heavily based on terraform.ModuleTreeDependencies but
|
|
// differs in that it works directly with the HCL1 AST rather than
|
|
// the legacy config structs (and can thus outlive those) and that
|
|
// it only works on one module at a time, and so doesn't need to
|
|
// recurse into child calls.
|
|
for name, src := range ms {
|
|
if ext := fileExt(name); ext != ".tf" {
|
|
continue
|
|
}
|
|
|
|
log.Printf("[TRACE] configupgrade: Analyzing %q", name)
|
|
|
|
f, err := hcl1parser.Parse(src)
|
|
if err != nil {
|
|
// If we encounter a syntax error then we'll just skip for now
|
|
// and assume that we'll catch this again when we do the upgrade.
|
|
// If not, we'll break the upgrade step of renaming .tf files to
|
|
// .tf.json if they seem to be JSON syntax.
|
|
log.Printf("[ERROR] Failed to parse %q: %s", name, err)
|
|
continue
|
|
}
|
|
|
|
list, ok := f.Node.(*hcl1ast.ObjectList)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
|
|
}
|
|
|
|
if providersList := list.Filter("provider"); len(providersList.Items) > 0 {
|
|
providerObjs := providersList.Children()
|
|
for _, providerObj := range providerObjs.Items {
|
|
if len(providerObj.Keys) != 1 {
|
|
return nil, fmt.Errorf("provider block has wrong number of labels")
|
|
}
|
|
name := providerObj.Keys[0].Token.Value().(string)
|
|
|
|
var listVal *hcl1ast.ObjectList
|
|
if ot, ok := providerObj.Val.(*hcl1ast.ObjectType); ok {
|
|
listVal = ot.List
|
|
} else {
|
|
return nil, fmt.Errorf("provider %q: must be a block", name)
|
|
}
|
|
|
|
var versionStr string
|
|
if a := listVal.Filter("version"); len(a.Items) > 0 {
|
|
err := hcl1.DecodeObject(&versionStr, a.Items[0].Val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading version for provider %q: %s", name, err)
|
|
}
|
|
}
|
|
var constraints discovery.Constraints
|
|
if versionStr != "" {
|
|
constraints, err = discovery.ConstraintStr(versionStr).Parse()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error parsing version for provider %q: %s", name, err)
|
|
}
|
|
}
|
|
|
|
var alias string
|
|
if a := listVal.Filter("alias"); len(a.Items) > 0 {
|
|
err := hcl1.DecodeObject(&alias, a.Items[0].Val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading alias for provider %q: %s", name, err)
|
|
}
|
|
}
|
|
|
|
inst := moduledeps.ProviderInstance(name)
|
|
if alias != "" {
|
|
inst = moduledeps.ProviderInstance(name + "." + alias)
|
|
}
|
|
log.Printf("[TRACE] Provider block requires provider %q", inst)
|
|
m.Providers[inst] = moduledeps.ProviderDependency{
|
|
Constraints: constraints,
|
|
Reason: moduledeps.ProviderDependencyExplicit,
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
resourceConfigsList := list.Filter("resource")
|
|
dataResourceConfigsList := list.Filter("data")
|
|
// list.Filter annoyingly strips off the key used for matching,
|
|
// so we'll put it back here so we can distinguish our two types
|
|
// of blocks below.
|
|
for _, obj := range resourceConfigsList.Items {
|
|
obj.Keys = append([]*hcl1ast.ObjectKey{
|
|
{Token: hcl1token.Token{Type: hcl1token.IDENT, Text: "resource"}},
|
|
}, obj.Keys...)
|
|
}
|
|
for _, obj := range dataResourceConfigsList.Items {
|
|
obj.Keys = append([]*hcl1ast.ObjectKey{
|
|
{Token: hcl1token.Token{Type: hcl1token.IDENT, Text: "data"}},
|
|
}, obj.Keys...)
|
|
}
|
|
// Now we can merge the two lists together, since we can distinguish
|
|
// them just by their keys[0].
|
|
resourceConfigsList.Items = append(resourceConfigsList.Items, dataResourceConfigsList.Items...)
|
|
|
|
resourceObjs := resourceConfigsList.Children()
|
|
for _, resourceObj := range resourceObjs.Items {
|
|
if len(resourceObj.Keys) != 3 {
|
|
return nil, fmt.Errorf("resource or data block has wrong number of labels")
|
|
}
|
|
typeName := resourceObj.Keys[1].Token.Value().(string)
|
|
name := resourceObj.Keys[2].Token.Value().(string)
|
|
rAddr := addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: typeName,
|
|
Name: name,
|
|
}
|
|
if resourceObj.Keys[0].Token.Value() == "data" {
|
|
rAddr.Mode = addrs.DataResourceMode
|
|
}
|
|
|
|
var listVal *hcl1ast.ObjectList
|
|
if ot, ok := resourceObj.Val.(*hcl1ast.ObjectType); ok {
|
|
listVal = ot.List
|
|
} else {
|
|
return nil, fmt.Errorf("config for %q must be a block", rAddr)
|
|
}
|
|
|
|
if o := listVal.Filter("count"); len(o.Items) > 0 {
|
|
ret.ResourceHasCount[rAddr] = true
|
|
} else {
|
|
ret.ResourceHasCount[rAddr] = false
|
|
}
|
|
|
|
var providerKey string
|
|
if o := listVal.Filter("provider"); len(o.Items) > 0 {
|
|
err := hcl1.DecodeObject(&providerKey, o.Items[0].Val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading provider for resource %s: %s", rAddr, err)
|
|
}
|
|
}
|
|
|
|
if providerKey == "" {
|
|
providerKey = rAddr.DefaultProviderConfig().StringCompact()
|
|
}
|
|
|
|
inst := moduledeps.ProviderInstance(providerKey)
|
|
log.Printf("[TRACE] Resource block for %s requires provider %q", rAddr, inst)
|
|
if _, exists := m.Providers[inst]; !exists {
|
|
m.Providers[inst] = moduledeps.ProviderDependency{
|
|
Reason: moduledeps.ProviderDependencyImplicit,
|
|
}
|
|
}
|
|
ret.ResourceProviderType[rAddr] = inst.Type()
|
|
}
|
|
}
|
|
}
|
|
|
|
providerFactories, err := u.Providers.ResolveProviders(m.PluginRequirements())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error resolving providers: %s", err)
|
|
}
|
|
|
|
for name, fn := range providerFactories {
|
|
log.Printf("[TRACE] Fetching schema from provider %q", name)
|
|
provider, err := fn()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load provider %q: %s", name, err)
|
|
}
|
|
|
|
resp := provider.GetSchema()
|
|
if resp.Diagnostics.HasErrors() {
|
|
return nil, resp.Diagnostics.Err()
|
|
}
|
|
|
|
schema := &terraform.ProviderSchema{
|
|
Provider: resp.Provider.Block,
|
|
ResourceTypes: map[string]*configschema.Block{},
|
|
DataSources: map[string]*configschema.Block{},
|
|
}
|
|
for t, s := range resp.ResourceTypes {
|
|
schema.ResourceTypes[t] = s.Block
|
|
}
|
|
for t, s := range resp.DataSources {
|
|
schema.DataSources[t] = s.Block
|
|
}
|
|
ret.ProviderSchemas[name] = schema
|
|
}
|
|
|
|
// TODO: Also ProvisionerSchemas
|
|
|
|
return ret, nil
|
|
}
|