]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package tfconfig |
2 | ||
3 | import ( | |
4 | "io/ioutil" | |
5 | "strings" | |
6 | ||
7 | legacyhcl "github.com/hashicorp/hcl" | |
8 | legacyast "github.com/hashicorp/hcl/hcl/ast" | |
9 | ) | |
10 | ||
11 | func 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. | |
303 | func 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 | } |