blob: 86ffdf11dd32203db57ff5b7560feafb905041a6 [file] [log] [blame]
package tfconfig
import (
"io/ioutil"
"strings"
legacyhcl "github.com/hashicorp/hcl"
legacyast "github.com/hashicorp/hcl/hcl/ast"
)
func loadModuleLegacyHCL(dir string) (*Module, Diagnostics) {
// This implementation is intentionally more quick-and-dirty than the
// main loader. In particular, it doesn't bother to keep careful track
// of multiple error messages because we always fall back on returning
// the main parser's error message if our fallback parsing produces
// an error, and thus the errors here are not seen by the end-caller.
mod := newModule(dir)
primaryPaths, diags := dirFiles(dir)
if diags.HasErrors() {
return mod, diagnosticsHCL(diags)
}
for _, filename := range primaryPaths {
src, err := ioutil.ReadFile(filename)
if err != nil {
return mod, diagnosticsErrorf("Error reading %s: %s", filename, err)
}
hclRoot, err := legacyhcl.Parse(string(src))
if err != nil {
return mod, diagnosticsErrorf("Error parsing %s: %s", filename, err)
}
list, ok := hclRoot.Node.(*legacyast.ObjectList)
if !ok {
return mod, diagnosticsErrorf("Error parsing %s: no root object", filename)
}
for _, item := range list.Filter("terraform").Items {
if len(item.Keys) > 0 {
item = &legacyast.ObjectItem{
Val: &legacyast.ObjectType{
List: &legacyast.ObjectList{
Items: []*legacyast.ObjectItem{item},
},
},
}
}
type TerraformBlock struct {
RequiredVersion string `hcl:"required_version"`
}
var block TerraformBlock
err = legacyhcl.DecodeObject(&block, item.Val)
if err != nil {
return nil, diagnosticsErrorf("terraform block: %s", err)
}
if block.RequiredVersion != "" {
mod.RequiredCore = append(mod.RequiredCore, block.RequiredVersion)
}
}
if vars := list.Filter("variable"); len(vars.Items) > 0 {
vars = vars.Children()
type VariableBlock struct {
Type string `hcl:"type"`
Default interface{}
Description string
Fields []string `hcl:",decodedFields"`
}
for _, item := range vars.Items {
unwrapLegacyHCLObjectKeysFromJSON(item, 1)
if len(item.Keys) != 1 {
return nil, diagnosticsErrorf("variable block at %s has no label", item.Pos())
}
name := item.Keys[0].Token.Value().(string)
var block VariableBlock
err := legacyhcl.DecodeObject(&block, item.Val)
if err != nil {
return nil, diagnosticsErrorf("invalid variable block at %s: %s", item.Pos(), err)
}
// Clean up legacy HCL decoding ambiguity by unwrapping list of maps
if ms, ok := block.Default.([]map[string]interface{}); ok {
def := make(map[string]interface{})
for _, m := range ms {
for k, v := range m {
def[k] = v
}
}
block.Default = def
}
v := &Variable{
Name: name,
Type: block.Type,
Description: block.Description,
Default: block.Default,
Pos: sourcePosLegacyHCL(item.Pos(), filename),
}
if _, exists := mod.Variables[name]; exists {
return nil, diagnosticsErrorf("duplicate variable block for %q", name)
}
mod.Variables[name] = v
}
}
if outputs := list.Filter("output"); len(outputs.Items) > 0 {
outputs = outputs.Children()
type OutputBlock struct {
Description string
}
for _, item := range outputs.Items {
unwrapLegacyHCLObjectKeysFromJSON(item, 1)
if len(item.Keys) != 1 {
return nil, diagnosticsErrorf("output block at %s has no label", item.Pos())
}
name := item.Keys[0].Token.Value().(string)
var block OutputBlock
err := legacyhcl.DecodeObject(&block, item.Val)
if err != nil {
return nil, diagnosticsErrorf("invalid output block at %s: %s", item.Pos(), err)
}
o := &Output{
Name: name,
Description: block.Description,
Pos: sourcePosLegacyHCL(item.Pos(), filename),
}
if _, exists := mod.Outputs[name]; exists {
return nil, diagnosticsErrorf("duplicate output block for %q", name)
}
mod.Outputs[name] = o
}
}
for _, blockType := range []string{"resource", "data"} {
if resources := list.Filter(blockType); len(resources.Items) > 0 {
resources = resources.Children()
type ResourceBlock struct {
Provider string
}
for _, item := range resources.Items {
unwrapLegacyHCLObjectKeysFromJSON(item, 2)
if len(item.Keys) != 2 {
return nil, diagnosticsErrorf("resource block at %s has wrong label count", item.Pos())
}
typeName := item.Keys[0].Token.Value().(string)
name := item.Keys[1].Token.Value().(string)
var mode ResourceMode
var rMap map[string]*Resource
switch blockType {
case "resource":
mode = ManagedResourceMode
rMap = mod.ManagedResources
case "data":
mode = DataResourceMode
rMap = mod.DataResources
}
var block ResourceBlock
err := legacyhcl.DecodeObject(&block, item.Val)
if err != nil {
return nil, diagnosticsErrorf("invalid resource block at %s: %s", item.Pos(), err)
}
var providerName, providerAlias string
if dotPos := strings.IndexByte(block.Provider, '.'); dotPos != -1 {
providerName = block.Provider[:dotPos]
providerAlias = block.Provider[dotPos+1:]
} else {
providerName = block.Provider
}
if providerName == "" {
providerName = resourceTypeDefaultProviderName(typeName)
}
r := &Resource{
Mode: mode,
Type: typeName,
Name: name,
Provider: ProviderRef{
Name: providerName,
Alias: providerAlias,
},
Pos: sourcePosLegacyHCL(item.Pos(), filename),
}
key := r.MapKey()
if _, exists := rMap[key]; exists {
return nil, diagnosticsErrorf("duplicate resource block for %q", key)
}
rMap[key] = r
}
}
}
if moduleCalls := list.Filter("module"); len(moduleCalls.Items) > 0 {
moduleCalls = moduleCalls.Children()
type ModuleBlock struct {
Source string
Version string
}
for _, item := range moduleCalls.Items {
unwrapLegacyHCLObjectKeysFromJSON(item, 1)
if len(item.Keys) != 1 {
return nil, diagnosticsErrorf("module block at %s has no label", item.Pos())
}
name := item.Keys[0].Token.Value().(string)
var block ModuleBlock
err := legacyhcl.DecodeObject(&block, item.Val)
if err != nil {
return nil, diagnosticsErrorf("module block at %s: %s", item.Pos(), err)
}
mc := &ModuleCall{
Name: name,
Source: block.Source,
Version: block.Version,
Pos: sourcePosLegacyHCL(item.Pos(), filename),
}
// it's possible this module call is from an override file
if origMod, exists := mod.ModuleCalls[name]; exists {
if mc.Source == "" {
mc.Source = origMod.Source
}
}
mod.ModuleCalls[name] = mc
}
}
if providerConfigs := list.Filter("provider"); len(providerConfigs.Items) > 0 {
providerConfigs = providerConfigs.Children()
type ProviderBlock struct {
Version string
}
for _, item := range providerConfigs.Items {
unwrapLegacyHCLObjectKeysFromJSON(item, 1)
if len(item.Keys) != 1 {
return nil, diagnosticsErrorf("provider block at %s has no label", item.Pos())
}
name := item.Keys[0].Token.Value().(string)
var block ProviderBlock
err := legacyhcl.DecodeObject(&block, item.Val)
if err != nil {
return nil, diagnosticsErrorf("invalid provider block at %s: %s", item.Pos(), err)
}
if block.Version != "" {
mod.RequiredProviders[name] = append(mod.RequiredProviders[name], block.Version)
}
// Even if there wasn't an explicit version required, we still
// need an entry in our map to signal the unversioned dependency.
if _, exists := mod.RequiredProviders[name]; !exists {
mod.RequiredProviders[name] = []string{}
}
}
}
}
return mod, nil
}
// unwrapLegacyHCLObjectKeysFromJSON cleans up an edge case that can occur when
// parsing JSON as input: if we're parsing JSON then directly nested
// items will show up as additional "keys".
//
// For objects that expect a fixed number of keys, this breaks the
// decoding process. This function unwraps the object into what it would've
// looked like if it came directly from HCL by specifying the number of keys
// you expect.
//
// Example:
//
// { "foo": { "baz": {} } }
//
// Will show up with Keys being: []string{"foo", "baz"}
// when we really just want the first two. This function will fix this.
func unwrapLegacyHCLObjectKeysFromJSON(item *legacyast.ObjectItem, depth int) {
if len(item.Keys) > depth && item.Keys[0].Token.JSON {
for len(item.Keys) > depth {
// Pop off the last key
n := len(item.Keys)
key := item.Keys[n-1]
item.Keys[n-1] = nil
item.Keys = item.Keys[:n-1]
// Wrap our value in a list
item.Val = &legacyast.ObjectType{
List: &legacyast.ObjectList{
Items: []*legacyast.ObjectItem{
&legacyast.ObjectItem{
Keys: []*legacyast.ObjectKey{key},
Val: item.Val,
},
},
},
}
}
}
}