287 lines
9.0 KiB
Go
287 lines
9.0 KiB
Go
package configupgrade
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
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
|
|
VariableTypes map[string]string
|
|
ModuleDir string
|
|
}
|
|
|
|
// 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),
|
|
VariableTypes: make(map[string]string),
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
if variablesList := list.Filter("variable"); len(variablesList.Items) > 0 {
|
|
variableObjs := variablesList.Children()
|
|
for _, variableObj := range variableObjs.Items {
|
|
if len(variableObj.Keys) != 1 {
|
|
return nil, fmt.Errorf("variable block has wrong number of labels")
|
|
}
|
|
name := variableObj.Keys[0].Token.Value().(string)
|
|
|
|
var listVal *hcl1ast.ObjectList
|
|
if ot, ok := variableObj.Val.(*hcl1ast.ObjectType); ok {
|
|
listVal = ot.List
|
|
} else {
|
|
return nil, fmt.Errorf("variable %q: must be a block", name)
|
|
}
|
|
|
|
var typeStr string
|
|
if a := listVal.Filter("type"); len(a.Items) > 0 {
|
|
err := hcl1.DecodeObject(&typeStr, a.Items[0].Val)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading type for variable %q: %s", name, err)
|
|
}
|
|
} else if a := listVal.Filter("default"); len(a.Items) > 0 {
|
|
switch a.Items[0].Val.(type) {
|
|
case *hcl1ast.ObjectType:
|
|
typeStr = "map"
|
|
case *hcl1ast.ListType:
|
|
typeStr = "list"
|
|
default:
|
|
typeStr = "string"
|
|
}
|
|
} else {
|
|
typeStr = "string"
|
|
}
|
|
|
|
ret.VariableTypes[name] = strings.TrimSpace(typeStr)
|
|
}
|
|
}
|
|
}
|
|
|
|
providerFactories, errs := u.Providers.ResolveProviders(m.PluginRequirements())
|
|
if len(errs) > 0 {
|
|
var errorsMsg string
|
|
for _, err := range errs {
|
|
errorsMsg += fmt.Sprintf("\n- %s", err)
|
|
}
|
|
return nil, fmt.Errorf("error resolving providers:\n%s", errorsMsg)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
for name, fn := range u.Provisioners {
|
|
log.Printf("[TRACE] Fetching schema from provisioner %q", name)
|
|
provisioner, err := fn()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load provisioner %q: %s", name, err)
|
|
}
|
|
|
|
resp := provisioner.GetSchema()
|
|
if resp.Diagnostics.HasErrors() {
|
|
return nil, resp.Diagnostics.Err()
|
|
}
|
|
|
|
ret.ProvisionerSchemas[name] = resp.Provisioner
|
|
}
|
|
|
|
return ret, nil
|
|
}
|