1package tfconfig
2
3import (
4	"io/ioutil"
5	"strings"
6
7	legacyhcl "github.com/hashicorp/hcl"
8	legacyast "github.com/hashicorp/hcl/hcl/ast"
9)
10
11func loadModuleLegacyHCL(dir string) (*Module, Diagnostics) {
12	// This implementation is intentionally more quick-and-dirty than the
13	// main loader. In particular, it doesn't bother to keep careful track
14	// of multiple error messages because we always fall back on returning
15	// the main parser's error message if our fallback parsing produces
16	// an error, and thus the errors here are not seen by the end-caller.
17	mod := newModule(dir)
18
19	primaryPaths, diags := dirFiles(dir)
20	if diags.HasErrors() {
21		return mod, diagnosticsHCL(diags)
22	}
23
24	for _, filename := range primaryPaths {
25		src, err := ioutil.ReadFile(filename)
26		if err != nil {
27			return mod, diagnosticsErrorf("Error reading %s: %s", filename, err)
28		}
29
30		hclRoot, err := legacyhcl.Parse(string(src))
31		if err != nil {
32			return mod, diagnosticsErrorf("Error parsing %s: %s", filename, err)
33		}
34
35		list, ok := hclRoot.Node.(*legacyast.ObjectList)
36		if !ok {
37			return mod, diagnosticsErrorf("Error parsing %s: no root object", filename)
38		}
39
40		for _, item := range list.Filter("terraform").Items {
41			if len(item.Keys) > 0 {
42				item = &legacyast.ObjectItem{
43					Val: &legacyast.ObjectType{
44						List: &legacyast.ObjectList{
45							Items: []*legacyast.ObjectItem{item},
46						},
47					},
48				}
49			}
50
51			type TerraformBlock struct {
52				RequiredVersion string `hcl:"required_version"`
53			}
54			var block TerraformBlock
55			err = legacyhcl.DecodeObject(&block, item.Val)
56			if err != nil {
57				return nil, diagnosticsErrorf("terraform block: %s", err)
58			}
59
60			if block.RequiredVersion != "" {
61				mod.RequiredCore = append(mod.RequiredCore, block.RequiredVersion)
62			}
63		}
64
65		if vars := list.Filter("variable"); len(vars.Items) > 0 {
66			vars = vars.Children()
67			type VariableBlock struct {
68				Type        string `hcl:"type"`
69				Default     interface{}
70				Description string
71				Fields      []string `hcl:",decodedFields"`
72			}
73
74			for _, item := range vars.Items {
75				unwrapLegacyHCLObjectKeysFromJSON(item, 1)
76
77				if len(item.Keys) != 1 {
78					return nil, diagnosticsErrorf("variable block at %s has no label", item.Pos())
79				}
80
81				name := item.Keys[0].Token.Value().(string)
82
83				var block VariableBlock
84				err := legacyhcl.DecodeObject(&block, item.Val)
85				if err != nil {
86					return nil, diagnosticsErrorf("invalid variable block at %s: %s", item.Pos(), err)
87				}
88
89				// Clean up legacy HCL decoding ambiguity by unwrapping list of maps
90				if ms, ok := block.Default.([]map[string]interface{}); ok {
91					def := make(map[string]interface{})
92					for _, m := range ms {
93						for k, v := range m {
94							def[k] = v
95						}
96					}
97					block.Default = def
98				}
99
100				v := &Variable{
101					Name:        name,
102					Type:        block.Type,
103					Description: block.Description,
104					Default:     block.Default,
105					Pos:         sourcePosLegacyHCL(item.Pos(), filename),
106				}
107				if _, exists := mod.Variables[name]; exists {
108					return nil, diagnosticsErrorf("duplicate variable block for %q", name)
109				}
110				mod.Variables[name] = v
111
112			}
113		}
114
115		if outputs := list.Filter("output"); len(outputs.Items) > 0 {
116			outputs = outputs.Children()
117			type OutputBlock struct {
118				Description string
119			}
120
121			for _, item := range outputs.Items {
122				unwrapLegacyHCLObjectKeysFromJSON(item, 1)
123
124				if len(item.Keys) != 1 {
125					return nil, diagnosticsErrorf("output block at %s has no label", item.Pos())
126				}
127
128				name := item.Keys[0].Token.Value().(string)
129
130				var block OutputBlock
131				err := legacyhcl.DecodeObject(&block, item.Val)
132				if err != nil {
133					return nil, diagnosticsErrorf("invalid output block at %s: %s", item.Pos(), err)
134				}
135
136				o := &Output{
137					Name:        name,
138					Description: block.Description,
139					Pos:         sourcePosLegacyHCL(item.Pos(), filename),
140				}
141				if _, exists := mod.Outputs[name]; exists {
142					return nil, diagnosticsErrorf("duplicate output block for %q", name)
143				}
144				mod.Outputs[name] = o
145			}
146		}
147
148		for _, blockType := range []string{"resource", "data"} {
149			if resources := list.Filter(blockType); len(resources.Items) > 0 {
150				resources = resources.Children()
151				type ResourceBlock struct {
152					Provider string
153				}
154
155				for _, item := range resources.Items {
156					unwrapLegacyHCLObjectKeysFromJSON(item, 2)
157
158					if len(item.Keys) != 2 {
159						return nil, diagnosticsErrorf("resource block at %s has wrong label count", item.Pos())
160					}
161
162					typeName := item.Keys[0].Token.Value().(string)
163					name := item.Keys[1].Token.Value().(string)
164					var mode ResourceMode
165					var rMap map[string]*Resource
166					switch blockType {
167					case "resource":
168						mode = ManagedResourceMode
169						rMap = mod.ManagedResources
170					case "data":
171						mode = DataResourceMode
172						rMap = mod.DataResources
173					}
174
175					var block ResourceBlock
176					err := legacyhcl.DecodeObject(&block, item.Val)
177					if err != nil {
178						return nil, diagnosticsErrorf("invalid resource block at %s: %s", item.Pos(), err)
179					}
180
181					var providerName, providerAlias string
182					if dotPos := strings.IndexByte(block.Provider, '.'); dotPos != -1 {
183						providerName = block.Provider[:dotPos]
184						providerAlias = block.Provider[dotPos+1:]
185					} else {
186						providerName = block.Provider
187					}
188					if providerName == "" {
189						providerName = resourceTypeDefaultProviderName(typeName)
190					}
191
192					r := &Resource{
193						Mode: mode,
194						Type: typeName,
195						Name: name,
196						Provider: ProviderRef{
197							Name:  providerName,
198							Alias: providerAlias,
199						},
200						Pos: sourcePosLegacyHCL(item.Pos(), filename),
201					}
202					key := r.MapKey()
203					if _, exists := rMap[key]; exists {
204						return nil, diagnosticsErrorf("duplicate resource block for %q", key)
205					}
206					rMap[key] = r
207				}
208			}
209
210		}
211
212		if moduleCalls := list.Filter("module"); len(moduleCalls.Items) > 0 {
213			moduleCalls = moduleCalls.Children()
214			type ModuleBlock struct {
215				Source  string
216				Version string
217			}
218
219			for _, item := range moduleCalls.Items {
220				unwrapLegacyHCLObjectKeysFromJSON(item, 1)
221
222				if len(item.Keys) != 1 {
223					return nil, diagnosticsErrorf("module block at %s has no label", item.Pos())
224				}
225
226				name := item.Keys[0].Token.Value().(string)
227
228				var block ModuleBlock
229				err := legacyhcl.DecodeObject(&block, item.Val)
230				if err != nil {
231					return nil, diagnosticsErrorf("module block at %s: %s", item.Pos(), err)
232				}
233
234				mc := &ModuleCall{
235					Name:    name,
236					Source:  block.Source,
237					Version: block.Version,
238					Pos:     sourcePosLegacyHCL(item.Pos(), filename),
239				}
240				// it's possible this module call is from an override file
241				if origMod, exists := mod.ModuleCalls[name]; exists {
242					if mc.Source == "" {
243						mc.Source = origMod.Source
244					}
245				}
246				mod.ModuleCalls[name] = mc
247			}
248		}
249
250		if providerConfigs := list.Filter("provider"); len(providerConfigs.Items) > 0 {
251			providerConfigs = providerConfigs.Children()
252			type ProviderBlock struct {
253				Version string
254			}
255
256			for _, item := range providerConfigs.Items {
257				unwrapLegacyHCLObjectKeysFromJSON(item, 1)
258
259				if len(item.Keys) != 1 {
260					return nil, diagnosticsErrorf("provider block at %s has no label", item.Pos())
261				}
262
263				name := item.Keys[0].Token.Value().(string)
264
265				var block ProviderBlock
266				err := legacyhcl.DecodeObject(&block, item.Val)
267				if err != nil {
268					return nil, diagnosticsErrorf("invalid provider block at %s: %s", item.Pos(), err)
269				}
270
271				if block.Version != "" {
272					mod.RequiredProviders[name] = append(mod.RequiredProviders[name], block.Version)
273				}
274
275				// Even if there wasn't an explicit version required, we still
276				// need an entry in our map to signal the unversioned dependency.
277				if _, exists := mod.RequiredProviders[name]; !exists {
278					mod.RequiredProviders[name] = []string{}
279				}
280
281			}
282		}
283	}
284
285	return mod, nil
286}
287
288// unwrapLegacyHCLObjectKeysFromJSON cleans up an edge case that can occur when
289// parsing JSON as input: if we're parsing JSON then directly nested
290// items will show up as additional "keys".
291//
292// For objects that expect a fixed number of keys, this breaks the
293// decoding process. This function unwraps the object into what it would've
294// looked like if it came directly from HCL by specifying the number of keys
295// you expect.
296//
297// Example:
298//
299// { "foo": { "baz": {} } }
300//
301// Will show up with Keys being: []string{"foo", "baz"}
302// when we really just want the first two. This function will fix this.
303func unwrapLegacyHCLObjectKeysFromJSON(item *legacyast.ObjectItem, depth int) {
304	if len(item.Keys) > depth && item.Keys[0].Token.JSON {
305		for len(item.Keys) > depth {
306			// Pop off the last key
307			n := len(item.Keys)
308			key := item.Keys[n-1]
309			item.Keys[n-1] = nil
310			item.Keys = item.Keys[:n-1]
311
312			// Wrap our value in a list
313			item.Val = &legacyast.ObjectType{
314				List: &legacyast.ObjectList{
315					Items: []*legacyast.ObjectItem{
316						&legacyast.ObjectItem{
317							Keys: []*legacyast.ObjectKey{key},
318							Val:  item.Val,
319						},
320					},
321				},
322			}
323		}
324	}
325}
326