github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/internal/initwd/module_install.go (about)

     1  package initwd
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	version "github.com/hashicorp/go-version"
    11  	"github.com/hashicorp/terraform-config-inspect/tfconfig"
    12  	"github.com/hashicorp/terraform/addrs"
    13  	"github.com/hashicorp/terraform/internal/earlyconfig"
    14  	"github.com/hashicorp/terraform/internal/modsdir"
    15  	"github.com/hashicorp/terraform/registry"
    16  	"github.com/hashicorp/terraform/registry/regsrc"
    17  	"github.com/hashicorp/terraform/registry/response"
    18  	"github.com/hashicorp/terraform/tfdiags"
    19  )
    20  
    21  type ModuleInstaller struct {
    22  	modsDir string
    23  	reg     *registry.Client
    24  
    25  	// The keys in moduleVersions are resolved and trimmed registry source
    26  	// addresses and the values are the registry response.
    27  	moduleVersions map[string]*response.ModuleVersions
    28  
    29  	// The keys in moduleVersionsUrl are the moduleVersion struct below and
    30  	// addresses and the values are the download URLs.
    31  	moduleVersionsUrl map[moduleVersion]string
    32  }
    33  
    34  type moduleVersion struct {
    35  	module  string
    36  	version string
    37  }
    38  
    39  func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller {
    40  	return &ModuleInstaller{
    41  		modsDir:           modsDir,
    42  		reg:               reg,
    43  		moduleVersions:    make(map[string]*response.ModuleVersions),
    44  		moduleVersionsUrl: make(map[moduleVersion]string),
    45  	}
    46  }
    47  
    48  // InstallModules analyses the root module in the given directory and installs
    49  // all of its direct and transitive dependencies into the given modules
    50  // directory, which must already exist.
    51  //
    52  // Since InstallModules makes possibly-time-consuming calls to remote services,
    53  // a hook interface is supported to allow the caller to be notified when
    54  // each module is installed and, for remote modules, when downloading begins.
    55  // LoadConfig guarantees that two hook calls will not happen concurrently but
    56  // it does not guarantee any particular ordering of hook calls. This mechanism
    57  // is for UI feedback only and does not give the caller any control over the
    58  // process.
    59  //
    60  // If modules are already installed in the target directory, they will be
    61  // skipped unless their source address or version have changed or unless
    62  // the upgrade flag is set.
    63  //
    64  // InstallModules never deletes any directory, except in the case where it
    65  // needs to replace a directory that is already present with a newly-extracted
    66  // package.
    67  //
    68  // If the returned diagnostics contains errors then the module installation
    69  // may have wholly or partially completed. Modules must be loaded in order
    70  // to find their dependencies, so this function does many of the same checks
    71  // as LoadConfig as a side-effect.
    72  //
    73  // If successful (the returned diagnostics contains no errors) then the
    74  // first return value is the early configuration tree that was constructed by
    75  // the installation process.
    76  func (i *ModuleInstaller) InstallModules(rootDir string, upgrade bool, hooks ModuleInstallHooks) (*earlyconfig.Config, tfdiags.Diagnostics) {
    77  	log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir)
    78  
    79  	rootMod, diags := earlyconfig.LoadModule(rootDir)
    80  	if rootMod == nil {
    81  		return nil, diags
    82  	}
    83  
    84  	manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir)
    85  	if err != nil {
    86  		diags = diags.Append(tfdiags.Sourceless(
    87  			tfdiags.Error,
    88  			"Failed to read modules manifest file",
    89  			fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err),
    90  		))
    91  		return nil, diags
    92  	}
    93  
    94  	getter := reusingGetter{}
    95  	cfg, instDiags := i.installDescendentModules(rootMod, rootDir, manifest, upgrade, hooks, getter)
    96  	diags = append(diags, instDiags...)
    97  
    98  	return cfg, diags
    99  }
   100  
   101  func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, getter reusingGetter) (*earlyconfig.Config, tfdiags.Diagnostics) {
   102  	var diags tfdiags.Diagnostics
   103  
   104  	if hooks == nil {
   105  		// Use our no-op implementation as a placeholder
   106  		hooks = ModuleInstallHooksImpl{}
   107  	}
   108  
   109  	// Create a manifest record for the root module. This will be used if
   110  	// there are any relative-pathed modules in the root.
   111  	manifest[""] = modsdir.Record{
   112  		Key: "",
   113  		Dir: rootDir,
   114  	}
   115  
   116  	cfg, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc(
   117  		func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) {
   118  
   119  			key := manifest.ModuleKey(req.Path)
   120  			instPath := i.packageInstallPath(req.Path)
   121  
   122  			log.Printf("[DEBUG] Module installer: begin %s", key)
   123  
   124  			// First we'll check if we need to upgrade/replace an existing
   125  			// installed module, and delete it out of the way if so.
   126  			replace := upgrade
   127  			if !replace {
   128  				record, recorded := manifest[key]
   129  				switch {
   130  				case !recorded:
   131  					log.Printf("[TRACE] ModuleInstaller: %s is not yet installed", key)
   132  					replace = true
   133  				case record.SourceAddr != req.SourceAddr:
   134  					log.Printf("[TRACE] ModuleInstaller: %s source address has changed from %q to %q", key, record.SourceAddr, req.SourceAddr)
   135  					replace = true
   136  				case record.Version != nil && !req.VersionConstraints.Check(record.Version):
   137  					log.Printf("[TRACE] ModuleInstaller: %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraints)
   138  					replace = true
   139  				}
   140  			}
   141  
   142  			// If we _are_ planning to replace this module, then we'll remove
   143  			// it now so our installation code below won't conflict with any
   144  			// existing remnants.
   145  			if replace {
   146  				if _, recorded := manifest[key]; recorded {
   147  					log.Printf("[TRACE] ModuleInstaller: discarding previous record of %s prior to reinstall", key)
   148  				}
   149  				delete(manifest, key)
   150  				// Deleting a module invalidates all of its descendent modules too.
   151  				keyPrefix := key + "."
   152  				for subKey := range manifest {
   153  					if strings.HasPrefix(subKey, keyPrefix) {
   154  						if _, recorded := manifest[subKey]; recorded {
   155  							log.Printf("[TRACE] ModuleInstaller: also discarding downstream %s", subKey)
   156  						}
   157  						delete(manifest, subKey)
   158  					}
   159  				}
   160  			}
   161  
   162  			record, recorded := manifest[key]
   163  			if !recorded {
   164  				// Clean up any stale cache directory that might be present.
   165  				// If this is a local (relative) source then the dir will
   166  				// not exist, but we'll ignore that.
   167  				log.Printf("[TRACE] ModuleInstaller: cleaning directory %s prior to install of %s", instPath, key)
   168  				err := os.RemoveAll(instPath)
   169  				if err != nil && !os.IsNotExist(err) {
   170  					log.Printf("[TRACE] ModuleInstaller: failed to remove %s: %s", key, err)
   171  					diags = diags.Append(tfdiags.Sourceless(
   172  						tfdiags.Error,
   173  						"Failed to remove local module cache",
   174  						fmt.Sprintf(
   175  							"Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s",
   176  							instPath, err,
   177  						),
   178  					))
   179  					return nil, nil, diags
   180  				}
   181  			} else {
   182  				// If this module is already recorded and its root directory
   183  				// exists then we will just load what's already there and
   184  				// keep our existing record.
   185  				info, err := os.Stat(record.Dir)
   186  				if err == nil && info.IsDir() {
   187  					mod, mDiags := earlyconfig.LoadModule(record.Dir)
   188  					diags = diags.Append(mDiags)
   189  
   190  					log.Printf("[TRACE] ModuleInstaller: Module installer: %s %s already installed in %s", key, record.Version, record.Dir)
   191  					return mod, record.Version, diags
   192  				}
   193  			}
   194  
   195  			// If we get down here then it's finally time to actually install
   196  			// the module. There are some variants to this process depending
   197  			// on what type of module source address we have.
   198  			switch {
   199  
   200  			case isLocalSourceAddr(req.SourceAddr):
   201  				log.Printf("[TRACE] ModuleInstaller: %s has local path %q", key, req.SourceAddr)
   202  				mod, mDiags := i.installLocalModule(req, key, manifest, hooks)
   203  				diags = append(diags, mDiags...)
   204  				return mod, nil, diags
   205  
   206  			case isRegistrySourceAddr(req.SourceAddr):
   207  				addr, err := regsrc.ParseModuleSource(req.SourceAddr)
   208  				if err != nil {
   209  					// Should never happen because isRegistrySourceAddr already validated
   210  					panic(err)
   211  				}
   212  				log.Printf("[TRACE] ModuleInstaller: %s is a registry module at %s", key, addr)
   213  
   214  				mod, v, mDiags := i.installRegistryModule(req, key, instPath, addr, manifest, hooks, getter)
   215  				diags = append(diags, mDiags...)
   216  				return mod, v, diags
   217  
   218  			default:
   219  				log.Printf("[TRACE] ModuleInstaller: %s address %q will be handled by go-getter", key, req.SourceAddr)
   220  
   221  				mod, mDiags := i.installGoGetterModule(req, key, instPath, manifest, hooks, getter)
   222  				diags = append(diags, mDiags...)
   223  				return mod, nil, diags
   224  			}
   225  
   226  		},
   227  	))
   228  	diags = append(diags, cDiags...)
   229  
   230  	err := manifest.WriteSnapshotToDir(i.modsDir)
   231  	if err != nil {
   232  		diags = diags.Append(tfdiags.Sourceless(
   233  			tfdiags.Error,
   234  			"Failed to update module manifest",
   235  			fmt.Sprintf("Unable to write the module manifest file: %s", err),
   236  		))
   237  	}
   238  
   239  	return cfg, diags
   240  }
   241  
   242  func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) {
   243  	var diags tfdiags.Diagnostics
   244  
   245  	parentKey := manifest.ModuleKey(req.Parent.Path)
   246  	parentRecord, recorded := manifest[parentKey]
   247  	if !recorded {
   248  		// This is indicative of a bug rather than a user-actionable error
   249  		panic(fmt.Errorf("missing manifest record for parent module %s", parentKey))
   250  	}
   251  
   252  	if len(req.VersionConstraints) != 0 {
   253  		diags = diags.Append(tfdiags.Sourceless(
   254  			tfdiags.Error,
   255  			"Invalid version constraint",
   256  			fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a relative local path.", req.Name, req.CallPos.Filename, req.CallPos.Line),
   257  		))
   258  	}
   259  
   260  	// For local sources we don't actually need to modify the
   261  	// filesystem at all because the parent already wrote
   262  	// the files we need, and so we just load up what's already here.
   263  	newDir := filepath.Join(parentRecord.Dir, req.SourceAddr)
   264  
   265  	log.Printf("[TRACE] ModuleInstaller: %s uses directory from parent: %s", key, newDir)
   266  	// it is possible that the local directory is a symlink
   267  	newDir, err := filepath.EvalSymlinks(newDir)
   268  	if err != nil {
   269  		diags = diags.Append(tfdiags.Sourceless(
   270  			tfdiags.Error,
   271  			"Unreadable module directory",
   272  			fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()),
   273  		))
   274  	}
   275  
   276  	mod, mDiags := earlyconfig.LoadModule(newDir)
   277  	if mod == nil {
   278  		// nil indicates missing or unreadable directory, so we'll
   279  		// discard the returned diags and return a more specific
   280  		// error message here.
   281  		diags = diags.Append(tfdiags.Sourceless(
   282  			tfdiags.Error,
   283  			"Unreadable module directory",
   284  			fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallPos.Filename, req.CallPos.Line),
   285  		))
   286  	} else {
   287  		diags = diags.Append(mDiags)
   288  	}
   289  
   290  	// Note the local location in our manifest.
   291  	manifest[key] = modsdir.Record{
   292  		Key:        key,
   293  		Dir:        newDir,
   294  		SourceAddr: req.SourceAddr,
   295  	}
   296  	log.Printf("[DEBUG] Module installer: %s installed at %s", key, newDir)
   297  	hooks.Install(key, nil, newDir)
   298  
   299  	return mod, diags
   300  }
   301  
   302  func (i *ModuleInstaller) installRegistryModule(req *earlyconfig.ModuleRequest, key string, instPath string, addr *regsrc.Module, manifest modsdir.Manifest, hooks ModuleInstallHooks, getter reusingGetter) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) {
   303  	var diags tfdiags.Diagnostics
   304  
   305  	hostname, err := addr.SvcHost()
   306  	if err != nil {
   307  		// If it looks like the user was trying to use punycode then we'll generate
   308  		// a specialized error for that case. We require the unicode form of
   309  		// hostname so that hostnames are always human-readable in configuration
   310  		// and punycode can't be used to hide a malicious module hostname.
   311  		if strings.HasPrefix(addr.RawHost.Raw, "xn--") {
   312  			diags = diags.Append(tfdiags.Sourceless(
   313  				tfdiags.Error,
   314  				"Invalid module registry hostname",
   315  				fmt.Sprintf("The hostname portion of the module %q source address (at %s:%d) is not an acceptable hostname. Internationalized domain names must be given in unicode form rather than ASCII (\"punycode\") form.", req.Name, req.CallPos.Filename, req.CallPos.Line),
   316  			))
   317  		} else {
   318  			diags = diags.Append(tfdiags.Sourceless(
   319  				tfdiags.Error,
   320  				"Invalid module registry hostname",
   321  				fmt.Sprintf("The hostname portion of the module %q source address (at %s:%d) is not a valid hostname.", req.Name, req.CallPos.Filename, req.CallPos.Line),
   322  			))
   323  		}
   324  		return nil, nil, diags
   325  	}
   326  
   327  	reg := i.reg
   328  	var resp *response.ModuleVersions
   329  	var exists bool
   330  
   331  	// check if we've already looked up this module from the registry
   332  	if resp, exists = i.moduleVersions[addr.String()]; exists {
   333  		log.Printf("[TRACE] %s using already found available versions of %s at %s", key, addr, hostname)
   334  	} else {
   335  		log.Printf("[DEBUG] %s listing available versions of %s at %s", key, addr, hostname)
   336  		resp, err = reg.ModuleVersions(addr)
   337  		if err != nil {
   338  			if registry.IsModuleNotFound(err) {
   339  				diags = diags.Append(tfdiags.Sourceless(
   340  					tfdiags.Error,
   341  					"Module not found",
   342  					fmt.Sprintf("Module %q (from %s:%d) cannot be found in the module registry at %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname),
   343  				))
   344  			} else {
   345  				diags = diags.Append(tfdiags.Sourceless(
   346  					tfdiags.Error,
   347  					"Error accessing remote module registry",
   348  					fmt.Sprintf("Failed to retrieve available versions for module %q (%s:%d) from %s: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname, err),
   349  				))
   350  			}
   351  			return nil, nil, diags
   352  		}
   353  		i.moduleVersions[addr.String()] = resp
   354  	}
   355  
   356  	// The response might contain information about dependencies to allow us
   357  	// to potentially optimize future requests, but we don't currently do that
   358  	// and so for now we'll just take the first item which is guaranteed to
   359  	// be the address we requested.
   360  	if len(resp.Modules) < 1 {
   361  		// Should never happen, but since this is a remote service that may
   362  		// be implemented by third-parties we will handle it gracefully.
   363  		diags = diags.Append(tfdiags.Sourceless(
   364  			tfdiags.Error,
   365  			"Invalid response from remote module registry",
   366  			fmt.Sprintf("The registry at %s returned an invalid response when Terraform requested available versions for module %q (%s:%d).", hostname, req.Name, req.CallPos.Filename, req.CallPos.Line),
   367  		))
   368  		return nil, nil, diags
   369  	}
   370  
   371  	modMeta := resp.Modules[0]
   372  
   373  	var latestMatch *version.Version
   374  	var latestVersion *version.Version
   375  	for _, mv := range modMeta.Versions {
   376  		v, err := version.NewVersion(mv.Version)
   377  		if err != nil {
   378  			// Should never happen if the registry server is compliant with
   379  			// the protocol, but we'll warn if not to assist someone who
   380  			// might be developing a module registry server.
   381  			diags = diags.Append(tfdiags.Sourceless(
   382  				tfdiags.Warning,
   383  				"Invalid response from remote module registry",
   384  				fmt.Sprintf("The registry at %s returned an invalid version string %q for module %q (%s:%d), which Terraform ignored.", hostname, mv.Version, req.Name, req.CallPos.Filename, req.CallPos.Line),
   385  			))
   386  			continue
   387  		}
   388  
   389  		// If we've found a pre-release version then we'll ignore it unless
   390  		// it was exactly requested.
   391  		if v.Prerelease() != "" && req.VersionConstraints.String() != v.String() {
   392  			log.Printf("[TRACE] ModuleInstaller: %s ignoring %s because it is a pre-release and was not requested exactly", key, v)
   393  			continue
   394  		}
   395  
   396  		if latestVersion == nil || v.GreaterThan(latestVersion) {
   397  			latestVersion = v
   398  		}
   399  
   400  		if req.VersionConstraints.Check(v) {
   401  			if latestMatch == nil || v.GreaterThan(latestMatch) {
   402  				latestMatch = v
   403  			}
   404  		}
   405  	}
   406  
   407  	if latestVersion == nil {
   408  		diags = diags.Append(tfdiags.Sourceless(
   409  			tfdiags.Error,
   410  			"Module has no versions",
   411  			fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallPos.Filename, req.CallPos.Line, hostname),
   412  		))
   413  		return nil, nil, diags
   414  	}
   415  
   416  	if latestMatch == nil {
   417  		diags = diags.Append(tfdiags.Sourceless(
   418  			tfdiags.Error,
   419  			"Unresolvable module version constraint",
   420  			fmt.Sprintf("There is no available version of module %q (%s:%d) which matches the given version constraint. The newest available version is %s.", addr, req.CallPos.Filename, req.CallPos.Line, latestVersion),
   421  		))
   422  		return nil, nil, diags
   423  	}
   424  
   425  	// Report up to the caller that we're about to start downloading.
   426  	packageAddr, _ := splitAddrSubdir(req.SourceAddr)
   427  	hooks.Download(key, packageAddr, latestMatch)
   428  
   429  	// If we manage to get down here then we've found a suitable version to
   430  	// install, so we need to ask the registry where we should download it from.
   431  	// The response to this is a go-getter-style address string.
   432  
   433  	// first check the cache for the download URL
   434  	moduleAddr := moduleVersion{module: addr.String(), version: latestMatch.String()}
   435  	if _, exists := i.moduleVersionsUrl[moduleAddr]; !exists {
   436  		url, err := reg.ModuleLocation(addr, latestMatch.String())
   437  		if err != nil {
   438  			log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err)
   439  			diags = diags.Append(tfdiags.Sourceless(
   440  				tfdiags.Error,
   441  				"Error accessing remote module registry",
   442  				fmt.Sprintf("Failed to retrieve a download URL for %s %s from %s: %s", addr, latestMatch, hostname, err),
   443  			))
   444  			return nil, nil, diags
   445  		}
   446  		i.moduleVersionsUrl[moduleVersion{module: addr.String(), version: latestMatch.String()}] = url
   447  	}
   448  
   449  	dlAddr := i.moduleVersionsUrl[moduleAddr]
   450  
   451  	log.Printf("[TRACE] ModuleInstaller: %s %s %s is available at %q", key, addr, latestMatch, dlAddr)
   452  
   453  	modDir, err := getter.getWithGoGetter(instPath, dlAddr)
   454  	if err != nil {
   455  		// Errors returned by go-getter have very inconsistent quality as
   456  		// end-user error messages, but for now we're accepting that because
   457  		// we have no way to recognize any specific errors to improve them
   458  		// and masking the error entirely would hide valuable diagnostic
   459  		// information from the user.
   460  		diags = diags.Append(tfdiags.Sourceless(
   461  			tfdiags.Error,
   462  			"Failed to download module",
   463  			fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, dlAddr, err),
   464  		))
   465  		return nil, nil, diags
   466  	}
   467  
   468  	log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, dlAddr, modDir)
   469  
   470  	if addr.RawSubmodule != "" {
   471  		// Append the user's requested subdirectory to any subdirectory that
   472  		// was implied by any of the nested layers we expanded within go-getter.
   473  		modDir = filepath.Join(modDir, addr.RawSubmodule)
   474  	}
   475  
   476  	log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir)
   477  
   478  	// Finally we are ready to try actually loading the module.
   479  	mod, mDiags := earlyconfig.LoadModule(modDir)
   480  	if mod == nil {
   481  		// nil indicates missing or unreadable directory, so we'll
   482  		// discard the returned diags and return a more specific
   483  		// error message here. For registry modules this actually
   484  		// indicates a bug in the code above, since it's not the
   485  		// user's responsibility to create the directory in this case.
   486  		diags = diags.Append(tfdiags.Sourceless(
   487  			tfdiags.Error,
   488  			"Unreadable module directory",
   489  			fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir),
   490  		))
   491  	} else {
   492  		diags = append(diags, mDiags...)
   493  	}
   494  
   495  	// Note the local location in our manifest.
   496  	manifest[key] = modsdir.Record{
   497  		Key:        key,
   498  		Version:    latestMatch,
   499  		Dir:        modDir,
   500  		SourceAddr: req.SourceAddr,
   501  	}
   502  	log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
   503  	hooks.Install(key, latestMatch, modDir)
   504  
   505  	return mod, latestMatch, diags
   506  }
   507  
   508  func (i *ModuleInstaller) installGoGetterModule(req *earlyconfig.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks ModuleInstallHooks, getter reusingGetter) (*tfconfig.Module, tfdiags.Diagnostics) {
   509  	var diags tfdiags.Diagnostics
   510  
   511  	// Report up to the caller that we're about to start downloading.
   512  	packageAddr, _ := splitAddrSubdir(req.SourceAddr)
   513  	hooks.Download(key, packageAddr, nil)
   514  
   515  	if len(req.VersionConstraints) != 0 {
   516  		diags = diags.Append(tfdiags.Sourceless(
   517  			tfdiags.Error,
   518  			"Invalid version constraint",
   519  			fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a non Registry URL.", req.Name, req.CallPos.Filename, req.CallPos.Line),
   520  		))
   521  		return nil, diags
   522  	}
   523  
   524  	modDir, err := getter.getWithGoGetter(instPath, req.SourceAddr)
   525  	if err != nil {
   526  		if _, ok := err.(*MaybeRelativePathErr); ok {
   527  			log.Printf(
   528  				"[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../",
   529  				req.SourceAddr,
   530  			)
   531  			diags = diags.Append(tfdiags.Sourceless(
   532  				tfdiags.Error,
   533  				"Module not found",
   534  				fmt.Sprintf(
   535  					"The module address %q could not be resolved.\n\n"+
   536  						"If you intended this as a path relative to the current "+
   537  						"module, use \"./%s\" instead. The \"./\" prefix "+
   538  						"indicates that the address is a relative filesystem path.",
   539  					req.SourceAddr, req.SourceAddr,
   540  				),
   541  			))
   542  		} else {
   543  			// Errors returned by go-getter have very inconsistent quality as
   544  			// end-user error messages, but for now we're accepting that because
   545  			// we have no way to recognize any specific errors to improve them
   546  			// and masking the error entirely would hide valuable diagnostic
   547  			// information from the user.
   548  			diags = diags.Append(tfdiags.Sourceless(
   549  				tfdiags.Error,
   550  				"Failed to download module",
   551  				fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s", req.Name, req.CallPos.Filename, req.CallPos.Line, packageAddr, err),
   552  			))
   553  		}
   554  		return nil, diags
   555  
   556  	}
   557  
   558  	log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, req.SourceAddr, modDir)
   559  
   560  	mod, mDiags := earlyconfig.LoadModule(modDir)
   561  	if mod == nil {
   562  		// nil indicates missing or unreadable directory, so we'll
   563  		// discard the returned diags and return a more specific
   564  		// error message here. For go-getter modules this actually
   565  		// indicates a bug in the code above, since it's not the
   566  		// user's responsibility to create the directory in this case.
   567  		diags = diags.Append(tfdiags.Sourceless(
   568  			tfdiags.Error,
   569  			"Unreadable module directory",
   570  			fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir),
   571  		))
   572  	} else {
   573  		diags = append(diags, mDiags...)
   574  	}
   575  
   576  	// Note the local location in our manifest.
   577  	manifest[key] = modsdir.Record{
   578  		Key:        key,
   579  		Dir:        modDir,
   580  		SourceAddr: req.SourceAddr,
   581  	}
   582  	log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
   583  	hooks.Install(key, nil, modDir)
   584  
   585  	return mod, diags
   586  }
   587  
   588  func (i *ModuleInstaller) packageInstallPath(modulePath addrs.Module) string {
   589  	return filepath.Join(i.modsDir, strings.Join(modulePath, "."))
   590  }