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