9 "github.com/hashicorp/hcl2/hcl"
12 // LoadModule reads the directory at the given path and attempts to interpret
13 // it as a Terraform module.
14 func LoadModule(dir string) (*Module, Diagnostics) {
16 // For broad compatibility here we actually have two separate loader
17 // codepaths. The main one uses the new HCL parser and API and is intended
18 // for configurations from Terraform 0.12 onwards (though will work for
19 // many older configurations too), but we'll also fall back on one that
20 // uses the _old_ HCL implementation so we can deal with some edge-cases
21 // that are not valid in new HCL.
23 module, diags := loadModule(dir)
24 if diags.HasErrors() {
25 // Try using the legacy HCL parser and see if we fare better.
26 legacyModule, legacyDiags := loadModuleLegacyHCL(dir)
27 if !legacyDiags.HasErrors() {
28 legacyModule.init(legacyDiags)
29 return legacyModule, legacyDiags
37 // IsModuleDir checks if the given path contains terraform configuration files.
38 // This allows the caller to decide how to handle directories that do not have tf files.
39 func IsModuleDir(dir string) bool {
40 primaryPaths, _ := dirFiles(dir)
41 if len(primaryPaths) == 0 {
47 func (m *Module) init(diags Diagnostics) {
48 // Fill in any additional provider requirements that are implied by
49 // resource configurations, to avoid the caller from needing to apply
50 // this logic itself. Implied requirements don't have version constraints,
51 // but we'll make sure the requirement value is still non-nil in this
52 // case so callers can easily recognize it.
53 for _, r := range m.ManagedResources {
54 if _, exists := m.RequiredProviders[r.Provider.Name]; !exists {
55 m.RequiredProviders[r.Provider.Name] = []string{}
58 for _, r := range m.DataResources {
59 if _, exists := m.RequiredProviders[r.Provider.Name]; !exists {
60 m.RequiredProviders[r.Provider.Name] = []string{}
64 // We redundantly also reference the diagnostics from inside the module
65 // object, primarily so that we can easily included in JSON-serialized
66 // versions of the module object.
70 func dirFiles(dir string) (primary []string, diags hcl.Diagnostics) {
71 infos, err := ioutil.ReadDir(dir)
73 diags = append(diags, &hcl.Diagnostic{
74 Severity: hcl.DiagError,
75 Summary: "Failed to read module directory",
76 Detail: fmt.Sprintf("Module directory %s does not exist or cannot be read.", dir),
82 for _, info := range infos {
84 // We only care about files
90 if ext == "" || isIgnoredFile(name) {
94 baseName := name[:len(name)-len(ext)] // strip extension
95 isOverride := baseName == "override" || strings.HasSuffix(baseName, "_override")
97 fullPath := filepath.Join(dir, name)
99 override = append(override, fullPath)
101 primary = append(primary, fullPath)
105 // We are assuming that any _override files will be logically named,
106 // and processing the files in alphabetical order. Primaries first, then overrides.
107 primary = append(primary, override...)
112 // fileExt returns the Terraform configuration extension of the given
113 // path, or a blank string if it is not a recognized extension.
114 func fileExt(path string) string {
115 if strings.HasSuffix(path, ".tf") {
117 } else if strings.HasSuffix(path, ".tf.json") {
124 // isIgnoredFile returns true if the given filename (which must not have a
125 // directory path ahead of it) should be ignored as e.g. an editor swap file.
126 func isIgnoredFile(name string) bool {
127 return strings.HasPrefix(name, ".") || // Unix-like hidden files
128 strings.HasSuffix(name, "~") || // vim
129 strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#") // emacs