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