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) } } fqn := addrs.NewLegacyProvider(name) log.Printf("[TRACE] Provider block requires provider %q", fqn.LegacyString()) m.Providers[fqn] = 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) } } var fqn addrs.Provider if providerKey == "" { fqn = rAddr.DefaultProvider() } else { // ProviderDependencies only need to know the provider FQN // strip any alias from the providerKey parts := strings.Split(providerKey, ".") fqn = addrs.NewLegacyProvider(parts[0]) } log.Printf("[TRACE] Resource block for %s requires provider %q", rAddr, fqn) if _, exists := m.Providers[fqn]; !exists { m.Providers[fqn] = moduledeps.ProviderDependency{ Reason: moduledeps.ProviderDependencyImplicit, } } ret.ResourceProviderType[rAddr] = fqn.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 fqn, fn := range providerFactories { log.Printf("[TRACE] Fetching schema from provider %q", fqn.LegacyString()) provider, err := fn() if err != nil { return nil, fmt.Errorf("failed to load provider %q: %s", fqn.LegacyString(), 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[fqn.LegacyString()] = 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 }