github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/initwd/module_install.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package initwd
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"log"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/apparentlymart/go-versions/versions"
    17  	version "github.com/hashicorp/go-version"
    18  	"github.com/hashicorp/hcl/v2"
    19  
    20  	"github.com/terramate-io/tf/addrs"
    21  	"github.com/terramate-io/tf/configs"
    22  	"github.com/terramate-io/tf/configs/configload"
    23  	"github.com/terramate-io/tf/getmodules"
    24  	"github.com/terramate-io/tf/modsdir"
    25  	"github.com/terramate-io/tf/registry"
    26  	"github.com/terramate-io/tf/registry/regsrc"
    27  	"github.com/terramate-io/tf/registry/response"
    28  	"github.com/terramate-io/tf/tfdiags"
    29  )
    30  
    31  type ModuleInstaller struct {
    32  	modsDir string
    33  	loader  *configload.Loader
    34  	reg     *registry.Client
    35  
    36  	// The keys in moduleVersions are resolved and trimmed registry source
    37  	// addresses and the values are the registry response.
    38  	registryPackageVersions map[addrs.ModuleRegistryPackage]*response.ModuleVersions
    39  
    40  	// The keys in moduleVersionsUrl are the moduleVersion struct below and
    41  	// addresses and the values are underlying remote source addresses.
    42  	registryPackageSources map[moduleVersion]addrs.ModuleSourceRemote
    43  }
    44  
    45  type moduleVersion struct {
    46  	module  addrs.ModuleRegistryPackage
    47  	version string
    48  }
    49  
    50  func NewModuleInstaller(modsDir string, loader *configload.Loader, reg *registry.Client) *ModuleInstaller {
    51  	return &ModuleInstaller{
    52  		modsDir:                 modsDir,
    53  		loader:                  loader,
    54  		reg:                     reg,
    55  		registryPackageVersions: make(map[addrs.ModuleRegistryPackage]*response.ModuleVersions),
    56  		registryPackageSources:  make(map[moduleVersion]addrs.ModuleSourceRemote),
    57  	}
    58  }
    59  
    60  // InstallModules analyses the root module in the given directory and installs
    61  // all of its direct and transitive dependencies into the given modules
    62  // directory, which must already exist.
    63  //
    64  // Since InstallModules makes possibly-time-consuming calls to remote services,
    65  // a hook interface is supported to allow the caller to be notified when
    66  // each module is installed and, for remote modules, when downloading begins.
    67  // LoadConfig guarantees that two hook calls will not happen concurrently but
    68  // it does not guarantee any particular ordering of hook calls. This mechanism
    69  // is for UI feedback only and does not give the caller any control over the
    70  // process.
    71  //
    72  // If modules are already installed in the target directory, they will be
    73  // skipped unless their source address or version have changed or unless
    74  // the upgrade flag is set.
    75  //
    76  // InstallModules never deletes any directory, except in the case where it
    77  // needs to replace a directory that is already present with a newly-extracted
    78  // package.
    79  //
    80  // installErrsOnly installs modules but converts validation errors from
    81  // building the configuration after installation to warnings. This is used by
    82  // commands like `get` or `init -from-module` where the established behavior
    83  // was only to install the requested module, and extra validation can break
    84  // compatibility.
    85  //
    86  // If the returned diagnostics contains errors then the module installation
    87  // may have wholly or partially completed. Modules must be loaded in order
    88  // to find their dependencies, so this function does many of the same checks
    89  // as LoadConfig as a side-effect.
    90  //
    91  // If successful (the returned diagnostics contains no errors) then the
    92  // first return value is the early configuration tree that was constructed by
    93  // the installation process.
    94  func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir, testsDir string, upgrade, installErrsOnly bool, hooks ModuleInstallHooks) (*configs.Config, tfdiags.Diagnostics) {
    95  	log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir)
    96  	var diags tfdiags.Diagnostics
    97  
    98  	rootMod, mDiags := i.loader.Parser().LoadConfigDirWithTests(rootDir, testsDir)
    99  	if rootMod == nil {
   100  		// We drop the diagnostics here because we only want to report module
   101  		// loading errors after checking the core version constraints, which we
   102  		// can only do if the module can be at least partially loaded.
   103  		return nil, diags
   104  	} else if vDiags := rootMod.CheckCoreVersionRequirements(nil, nil); vDiags.HasErrors() {
   105  		// If the core version requirements are not met, we drop any other
   106  		// diagnostics, as they may reflect language changes from future
   107  		// Terraform versions.
   108  		diags = diags.Append(vDiags)
   109  	} else {
   110  		diags = diags.Append(mDiags)
   111  	}
   112  
   113  	manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir)
   114  	if err != nil {
   115  		diags = diags.Append(tfdiags.Sourceless(
   116  			tfdiags.Error,
   117  			"Failed to read modules manifest file",
   118  			fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err),
   119  		))
   120  		return nil, diags
   121  	}
   122  
   123  	fetcher := getmodules.NewPackageFetcher()
   124  
   125  	if hooks == nil {
   126  		// Use our no-op implementation as a placeholder
   127  		hooks = ModuleInstallHooksImpl{}
   128  	}
   129  
   130  	// Create a manifest record for the root module. This will be used if
   131  	// there are any relative-pathed modules in the root.
   132  	manifest[""] = modsdir.Record{
   133  		Key: "",
   134  		Dir: rootDir,
   135  	}
   136  	walker := i.moduleInstallWalker(ctx, manifest, upgrade, hooks, fetcher)
   137  
   138  	cfg, instDiags := i.installDescendentModules(rootMod, manifest, walker, installErrsOnly)
   139  	diags = append(diags, instDiags...)
   140  
   141  	return cfg, diags
   142  }
   143  
   144  func (i *ModuleInstaller) moduleInstallWalker(ctx context.Context, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) configs.ModuleWalker {
   145  	return configs.ModuleWalkerFunc(
   146  		func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   147  			var diags hcl.Diagnostics
   148  
   149  			if req.SourceAddr == nil {
   150  				// If the parent module failed to parse the module source
   151  				// address, we can't load it here. Return nothing as the parent
   152  				// module's diagnostics should explain this.
   153  				return nil, nil, diags
   154  			}
   155  
   156  			if req.Name == "" {
   157  				// An empty string for a module instance name breaks our
   158  				// manifest map, which uses that to indicate the root module.
   159  				// Because we descend into modules which have errors, we need
   160  				// to look out for this case, but the config loader's
   161  				// diagnostics will report the error later.
   162  				return nil, nil, diags
   163  			}
   164  
   165  			key := manifest.ModuleKey(req.Path)
   166  			instPath := i.packageInstallPath(req.Path)
   167  
   168  			log.Printf("[DEBUG] Module installer: begin %s", key)
   169  
   170  			// First we'll check if we need to upgrade/replace an existing
   171  			// installed module, and delete it out of the way if so.
   172  			replace := upgrade
   173  			if !replace {
   174  				record, recorded := manifest[key]
   175  				switch {
   176  				case !recorded:
   177  					log.Printf("[TRACE] ModuleInstaller: %s is not yet installed", key)
   178  					replace = true
   179  				case record.SourceAddr != req.SourceAddr.String():
   180  					log.Printf("[TRACE] ModuleInstaller: %s source address has changed from %q to %q", key, record.SourceAddr, req.SourceAddr)
   181  					replace = true
   182  				case record.Version != nil && !req.VersionConstraint.Required.Check(record.Version):
   183  					log.Printf("[TRACE] ModuleInstaller: %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraint.Required)
   184  					replace = true
   185  				}
   186  			}
   187  
   188  			// If we _are_ planning to replace this module, then we'll remove
   189  			// it now so our installation code below won't conflict with any
   190  			// existing remnants.
   191  			if replace {
   192  				if _, recorded := manifest[key]; recorded {
   193  					log.Printf("[TRACE] ModuleInstaller: discarding previous record of %s prior to reinstall", key)
   194  				}
   195  				delete(manifest, key)
   196  				// Deleting a module invalidates all of its descendent modules too.
   197  				keyPrefix := key + "."
   198  				for subKey := range manifest {
   199  					if strings.HasPrefix(subKey, keyPrefix) {
   200  						if _, recorded := manifest[subKey]; recorded {
   201  							log.Printf("[TRACE] ModuleInstaller: also discarding downstream %s", subKey)
   202  						}
   203  						delete(manifest, subKey)
   204  					}
   205  				}
   206  			}
   207  
   208  			record, recorded := manifest[key]
   209  			if !recorded {
   210  				// Clean up any stale cache directory that might be present.
   211  				// If this is a local (relative) source then the dir will
   212  				// not exist, but we'll ignore that.
   213  				log.Printf("[TRACE] ModuleInstaller: cleaning directory %s prior to install of %s", instPath, key)
   214  				err := os.RemoveAll(instPath)
   215  				if err != nil && !os.IsNotExist(err) {
   216  					log.Printf("[TRACE] ModuleInstaller: failed to remove %s: %s", key, err)
   217  					diags = diags.Append(&hcl.Diagnostic{
   218  						Severity: hcl.DiagError,
   219  						Summary:  "Failed to remove local module cache",
   220  						Detail: fmt.Sprintf(
   221  							"Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s",
   222  							instPath, err,
   223  						),
   224  					})
   225  					return nil, nil, diags
   226  				}
   227  			} else {
   228  				// If this module is already recorded and its root directory
   229  				// exists then we will just load what's already there and
   230  				// keep our existing record.
   231  				info, err := os.Stat(record.Dir)
   232  				if err == nil && info.IsDir() {
   233  					mod, mDiags := i.loader.Parser().LoadConfigDir(record.Dir)
   234  					if mod == nil {
   235  						// nil indicates an unreadable module, which should never happen,
   236  						// so we return the full loader diagnostics here.
   237  						diags = diags.Extend(mDiags)
   238  					} else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() {
   239  						// If the core version requirements are not met, we drop any other
   240  						// diagnostics, as they may reflect language changes from future
   241  						// Terraform versions.
   242  						diags = diags.Extend(vDiags)
   243  					} else {
   244  						diags = diags.Extend(mDiags)
   245  					}
   246  
   247  					log.Printf("[TRACE] ModuleInstaller: Module installer: %s %s already installed in %s", key, record.Version, record.Dir)
   248  					return mod, record.Version, diags
   249  				}
   250  			}
   251  
   252  			// If we get down here then it's finally time to actually install
   253  			// the module. There are some variants to this process depending
   254  			// on what type of module source address we have.
   255  
   256  			switch addr := req.SourceAddr.(type) {
   257  
   258  			case addrs.ModuleSourceLocal:
   259  				log.Printf("[TRACE] ModuleInstaller: %s has local path %q", key, addr.String())
   260  				mod, mDiags := i.installLocalModule(req, key, manifest, hooks)
   261  				mDiags = maybeImproveLocalInstallError(req, mDiags)
   262  				diags = append(diags, mDiags...)
   263  				return mod, nil, diags
   264  
   265  			case addrs.ModuleSourceRegistry:
   266  				log.Printf("[TRACE] ModuleInstaller: %s is a registry module at %s", key, addr.String())
   267  				mod, v, mDiags := i.installRegistryModule(ctx, req, key, instPath, addr, manifest, hooks, fetcher)
   268  				diags = append(diags, mDiags...)
   269  				return mod, v, diags
   270  
   271  			case addrs.ModuleSourceRemote:
   272  				log.Printf("[TRACE] ModuleInstaller: %s address %q will be handled by go-getter", key, addr.String())
   273  				mod, mDiags := i.installGoGetterModule(ctx, req, key, instPath, manifest, hooks, fetcher)
   274  				diags = append(diags, mDiags...)
   275  				return mod, nil, diags
   276  
   277  			default:
   278  				// Shouldn't get here, because there are no other implementations
   279  				// of addrs.ModuleSource.
   280  				panic(fmt.Sprintf("unsupported module source address %#v", addr))
   281  			}
   282  		},
   283  	)
   284  }
   285  
   286  func (i *ModuleInstaller) installDescendentModules(rootMod *configs.Module, manifest modsdir.Manifest, installWalker configs.ModuleWalker, installErrsOnly bool) (*configs.Config, tfdiags.Diagnostics) {
   287  	var diags tfdiags.Diagnostics
   288  
   289  	// When attempting to initialize the current directory with a module
   290  	// source, some use cases may want to ignore configuration errors from the
   291  	// building of the entire configuration structure, but we still need to
   292  	// capture installation errors. Because the actual module installation
   293  	// happens in the ModuleWalkFunc callback while building the config, we
   294  	// need to create a closure to capture the installation diagnostics
   295  	// separately.
   296  	var instDiags hcl.Diagnostics
   297  	walker := installWalker
   298  	if installErrsOnly {
   299  		walker = configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
   300  			mod, version, diags := installWalker.LoadModule(req)
   301  			instDiags = instDiags.Extend(diags)
   302  			return mod, version, diags
   303  		})
   304  	}
   305  
   306  	cfg, cDiags := configs.BuildConfig(rootMod, walker)
   307  	diags = diags.Append(cDiags)
   308  	if installErrsOnly {
   309  		// We can't continue if there was an error during installation, but
   310  		// return all diagnostics in case there happens to be anything else
   311  		// useful when debugging the problem. Any instDiags will be included in
   312  		// diags already.
   313  		if instDiags.HasErrors() {
   314  			return cfg, diags
   315  		}
   316  
   317  		// If there are any errors here, they must be only from building the
   318  		// config structures. We don't want to block initialization at this
   319  		// point, so convert these into warnings. Any actual errors in the
   320  		// configuration will be raised as soon as the config is loaded again.
   321  		// We continue below because writing the manifest is required to finish
   322  		// module installation.
   323  		diags = tfdiags.OverrideAll(diags, tfdiags.Warning, nil)
   324  	}
   325  
   326  	err := manifest.WriteSnapshotToDir(i.modsDir)
   327  	if err != nil {
   328  		diags = diags.Append(tfdiags.Sourceless(
   329  			tfdiags.Error,
   330  			"Failed to update module manifest",
   331  			fmt.Sprintf("Unable to write the module manifest file: %s", err),
   332  		))
   333  	}
   334  
   335  	return cfg, diags
   336  }
   337  
   338  func (i *ModuleInstaller) installLocalModule(req *configs.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*configs.Module, hcl.Diagnostics) {
   339  	var diags hcl.Diagnostics
   340  
   341  	parentKey := manifest.ModuleKey(req.Parent.Path)
   342  	parentRecord, recorded := manifest[parentKey]
   343  	if !recorded {
   344  		// This is indicative of a bug rather than a user-actionable error
   345  		panic(fmt.Errorf("missing manifest record for parent module %s", parentKey))
   346  	}
   347  
   348  	if len(req.VersionConstraint.Required) != 0 {
   349  		diags = diags.Append(&hcl.Diagnostic{
   350  			Severity: hcl.DiagError,
   351  			Summary:  "Invalid version constraint",
   352  			Detail:   fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a relative local path.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line),
   353  			Subject:  req.CallRange.Ptr(),
   354  		})
   355  	}
   356  
   357  	// For local sources we don't actually need to modify the
   358  	// filesystem at all because the parent already wrote
   359  	// the files we need, and so we just load up what's already here.
   360  	newDir := filepath.Join(parentRecord.Dir, req.SourceAddr.String())
   361  
   362  	log.Printf("[TRACE] ModuleInstaller: %s uses directory from parent: %s", key, newDir)
   363  	// it is possible that the local directory is a symlink
   364  	newDir, err := filepath.EvalSymlinks(newDir)
   365  	if err != nil {
   366  		diags = diags.Append(&hcl.Diagnostic{
   367  			Severity: hcl.DiagError,
   368  			Summary:  "Unreadable module directory",
   369  			Detail:   fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()),
   370  		})
   371  	}
   372  
   373  	// Finally we are ready to try actually loading the module.
   374  	mod, mDiags := i.loader.Parser().LoadConfigDir(newDir)
   375  	if mod == nil {
   376  		// nil indicates missing or unreadable directory, so we'll
   377  		// discard the returned diags and return a more specific
   378  		// error message here.
   379  		diags = diags.Append(&hcl.Diagnostic{
   380  			Severity: hcl.DiagError,
   381  			Summary:  "Unreadable module directory",
   382  			Detail:   fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallRange.Filename, req.CallRange.Start.Line),
   383  		})
   384  	} else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() {
   385  		// If the core version requirements are not met, we drop any other
   386  		// diagnostics, as they may reflect language changes from future
   387  		// Terraform versions.
   388  		diags = diags.Extend(vDiags)
   389  	} else {
   390  		diags = diags.Extend(mDiags)
   391  	}
   392  
   393  	// Note the local location in our manifest.
   394  	manifest[key] = modsdir.Record{
   395  		Key:        key,
   396  		Dir:        newDir,
   397  		SourceAddr: req.SourceAddr.String(),
   398  	}
   399  	log.Printf("[DEBUG] Module installer: %s installed at %s", key, newDir)
   400  	hooks.Install(key, nil, newDir)
   401  
   402  	return mod, diags
   403  }
   404  
   405  func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *configs.ModuleRequest, key string, instPath string, addr addrs.ModuleSourceRegistry, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*configs.Module, *version.Version, hcl.Diagnostics) {
   406  	var diags hcl.Diagnostics
   407  
   408  	hostname := addr.Package.Host
   409  	reg := i.reg
   410  	var resp *response.ModuleVersions
   411  	var exists bool
   412  
   413  	// A registry entry isn't _really_ a module package, but we'll pretend it's
   414  	// one for the sake of this reporting by just trimming off any source
   415  	// directory.
   416  	packageAddr := addr.Package
   417  
   418  	// Our registry client is still using the legacy model of addresses, so
   419  	// we'll shim it here for now.
   420  	regsrcAddr := regsrc.ModuleFromRegistryPackageAddr(packageAddr)
   421  
   422  	// check if we've already looked up this module from the registry
   423  	if resp, exists = i.registryPackageVersions[packageAddr]; exists {
   424  		log.Printf("[TRACE] %s using already found available versions of %s at %s", key, addr, hostname)
   425  	} else {
   426  		var err error
   427  		log.Printf("[DEBUG] %s listing available versions of %s at %s", key, addr, hostname)
   428  		resp, err = reg.ModuleVersions(ctx, regsrcAddr)
   429  		if err != nil {
   430  			if registry.IsModuleNotFound(err) {
   431  				diags = diags.Append(&hcl.Diagnostic{
   432  					Severity: hcl.DiagError,
   433  					Summary:  "Module not found",
   434  					Detail:   fmt.Sprintf("Module %q (from %s:%d) cannot be found in the module registry at %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, hostname),
   435  					Subject:  req.CallRange.Ptr(),
   436  				})
   437  			} else if errors.Is(err, context.Canceled) {
   438  				diags = diags.Append(&hcl.Diagnostic{
   439  					Severity: hcl.DiagError,
   440  					Summary:  "Module installation was interrupted",
   441  					Detail:   fmt.Sprintf("Received interrupt signal while retrieving available versions for module %q.", req.Name),
   442  				})
   443  			} else {
   444  				diags = diags.Append(&hcl.Diagnostic{
   445  					Severity: hcl.DiagError,
   446  					Summary:  "Error accessing remote module registry",
   447  					Detail:   fmt.Sprintf("Failed to retrieve available versions for module %q (%s:%d) from %s: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, hostname, err),
   448  					Subject:  req.CallRange.Ptr(),
   449  				})
   450  			}
   451  			return nil, nil, diags
   452  		}
   453  		i.registryPackageVersions[packageAddr] = resp
   454  	}
   455  
   456  	// The response might contain information about dependencies to allow us
   457  	// to potentially optimize future requests, but we don't currently do that
   458  	// and so for now we'll just take the first item which is guaranteed to
   459  	// be the address we requested.
   460  	if len(resp.Modules) < 1 {
   461  		// Should never happen, but since this is a remote service that may
   462  		// be implemented by third-parties we will handle it gracefully.
   463  		diags = diags.Append(&hcl.Diagnostic{
   464  			Severity: hcl.DiagError,
   465  			Summary:  "Invalid response from remote module registry",
   466  			Detail:   fmt.Sprintf("The registry at %s returned an invalid response when Terraform requested available versions for module %q (%s:%d).", hostname, req.Name, req.CallRange.Filename, req.CallRange.Start.Line),
   467  			Subject:  req.CallRange.Ptr(),
   468  		})
   469  		return nil, nil, diags
   470  	}
   471  
   472  	modMeta := resp.Modules[0]
   473  
   474  	var latestMatch *version.Version
   475  	var latestVersion *version.Version
   476  	for _, mv := range modMeta.Versions {
   477  		v, err := version.NewVersion(mv.Version)
   478  		if err != nil {
   479  			// Should never happen if the registry server is compliant with
   480  			// the protocol, but we'll warn if not to assist someone who
   481  			// might be developing a module registry server.
   482  			diags = diags.Append(&hcl.Diagnostic{
   483  				Severity: hcl.DiagWarning,
   484  				Summary:  "Invalid response from remote module registry",
   485  				Detail:   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.CallRange.Filename, req.CallRange.Start.Line),
   486  				Subject:  req.CallRange.Ptr(),
   487  			})
   488  			continue
   489  		}
   490  
   491  		// If we've found a pre-release version then we'll ignore it unless
   492  		// it was exactly requested.
   493  		//
   494  		// The prerelease checking will be handled by a different library for
   495  		// 2 reasons. First, this other library automatically includes the
   496  		// "prerelease versions must be exactly requested" behaviour that we are
   497  		// looking for. Second, this other library is used to handle all version
   498  		// constraints for the provider logic and this is the first step to
   499  		// making the module and provider version logic match.
   500  		if v.Prerelease() != "" {
   501  			// At this point all versions published by the module with
   502  			// prerelease metadata will be checked. Users may not have even
   503  			// requested this prerelease so don't print lots of unnecessary #
   504  			// warnings.
   505  			acceptableVersions, err := versions.MeetingConstraintsString(req.VersionConstraint.Required.String())
   506  			if err != nil {
   507  				log.Printf("[WARN] ModuleInstaller: %s ignoring %s because the version constraints (%s) could not be parsed: %s", key, v, req.VersionConstraint.Required.String(), err.Error())
   508  				continue
   509  			}
   510  
   511  			// Validate the version is also readable by the other versions
   512  			// library.
   513  			version, err := versions.ParseVersion(v.String())
   514  			if err != nil {
   515  				log.Printf("[WARN] ModuleInstaller: %s ignoring %s because the version (%s) reported by the module could not be parsed: %s", key, v, v.String(), err.Error())
   516  				continue
   517  			}
   518  
   519  			// Finally, check if the prerelease is acceptable to version. As
   520  			// highlighted previously, we go through all of this because the
   521  			// apparentlymart/go-versions library handles prerelease constraints
   522  			// in the apporach we want to.
   523  			if !acceptableVersions.Has(version) {
   524  				log.Printf("[TRACE] ModuleInstaller: %s ignoring %s because it is a pre-release and was not requested exactly", key, v)
   525  				continue
   526  			}
   527  
   528  			// If we reach here, it means this prerelease version was exactly
   529  			// requested according to the extra constraints of this library.
   530  			// We fall through and allow the other library to also validate it
   531  			// for consistency.
   532  		}
   533  
   534  		if latestVersion == nil || v.GreaterThan(latestVersion) {
   535  			latestVersion = v
   536  		}
   537  
   538  		if req.VersionConstraint.Required.Check(v) {
   539  			if latestMatch == nil || v.GreaterThan(latestMatch) {
   540  				latestMatch = v
   541  			}
   542  		}
   543  	}
   544  
   545  	if latestVersion == nil {
   546  		diags = diags.Append(&hcl.Diagnostic{
   547  			Severity: hcl.DiagError,
   548  			Summary:  "Module has no versions",
   549  			Detail:   fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallRange.Filename, req.CallRange.Start.Line, hostname),
   550  			Subject:  req.CallRange.Ptr(),
   551  		})
   552  		return nil, nil, diags
   553  	}
   554  
   555  	if latestMatch == nil {
   556  		diags = diags.Append(&hcl.Diagnostic{
   557  			Severity: hcl.DiagError,
   558  			Summary:  "Unresolvable module version constraint",
   559  			Detail:   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.CallRange.Filename, req.CallRange.Start.Line, latestVersion),
   560  			Subject:  req.CallRange.Ptr(),
   561  		})
   562  		return nil, nil, diags
   563  	}
   564  
   565  	// Report up to the caller that we're about to start downloading.
   566  	hooks.Download(key, packageAddr.String(), latestMatch)
   567  
   568  	// If we manage to get down here then we've found a suitable version to
   569  	// install, so we need to ask the registry where we should download it from.
   570  	// The response to this is a go-getter-style address string.
   571  
   572  	// first check the cache for the download URL
   573  	moduleAddr := moduleVersion{module: packageAddr, version: latestMatch.String()}
   574  	if _, exists := i.registryPackageSources[moduleAddr]; !exists {
   575  		realAddrRaw, err := reg.ModuleLocation(ctx, regsrcAddr, latestMatch.String())
   576  		if err != nil {
   577  			log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err)
   578  			diags = diags.Append(&hcl.Diagnostic{
   579  				Severity: hcl.DiagError,
   580  				Summary:  "Error accessing remote module registry",
   581  				Detail:   fmt.Sprintf("Failed to retrieve a download URL for %s %s from %s: %s", addr, latestMatch, hostname, err),
   582  			})
   583  			return nil, nil, diags
   584  		}
   585  		realAddr, err := addrs.ParseModuleSource(realAddrRaw)
   586  		if err != nil {
   587  			diags = diags.Append(&hcl.Diagnostic{
   588  				Severity: hcl.DiagError,
   589  				Summary:  "Invalid package location from module registry",
   590  				Detail:   fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: %s.", hostname, realAddrRaw, addr, latestMatch, err),
   591  			})
   592  			return nil, nil, diags
   593  		}
   594  		switch realAddr := realAddr.(type) {
   595  		// Only a remote source address is allowed here: a registry isn't
   596  		// allowed to return a local path (because it doesn't know what
   597  		// its being called from) and we also don't allow recursively pointing
   598  		// at another registry source for simplicity's sake.
   599  		case addrs.ModuleSourceRemote:
   600  			i.registryPackageSources[moduleAddr] = realAddr
   601  		default:
   602  			diags = diags.Append(&hcl.Diagnostic{
   603  				Severity: hcl.DiagError,
   604  				Summary:  "Invalid package location from module registry",
   605  				Detail:   fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: must be a direct remote package address.", hostname, realAddrRaw, addr, latestMatch),
   606  			})
   607  			return nil, nil, diags
   608  		}
   609  	}
   610  
   611  	dlAddr := i.registryPackageSources[moduleAddr]
   612  
   613  	log.Printf("[TRACE] ModuleInstaller: %s %s %s is available at %q", key, packageAddr, latestMatch, dlAddr.Package)
   614  
   615  	err := fetcher.FetchPackage(ctx, instPath, dlAddr.Package.String())
   616  	if errors.Is(err, context.Canceled) {
   617  		diags = diags.Append(&hcl.Diagnostic{
   618  			Severity: hcl.DiagError,
   619  			Summary:  "Module download was interrupted",
   620  			Detail:   fmt.Sprintf("Interrupt signal received when downloading module %s.", addr),
   621  		})
   622  		return nil, nil, diags
   623  	}
   624  	if err != nil {
   625  		// Errors returned by go-getter have very inconsistent quality as
   626  		// end-user error messages, but for now we're accepting that because
   627  		// we have no way to recognize any specific errors to improve them
   628  		// and masking the error entirely would hide valuable diagnostic
   629  		// information from the user.
   630  		diags = diags.Append(&hcl.Diagnostic{
   631  			Severity: hcl.DiagError,
   632  			Summary:  "Failed to download module",
   633  			Detail:   fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, dlAddr, err),
   634  			Subject:  req.CallRange.Ptr(),
   635  		})
   636  		return nil, nil, diags
   637  	}
   638  
   639  	log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, dlAddr.Package, instPath)
   640  
   641  	// Incorporate any subdir information from the original path into the
   642  	// address returned by the registry in order to find the final directory
   643  	// of the target module.
   644  	finalAddr := dlAddr.FromRegistry(addr)
   645  	subDir := filepath.FromSlash(finalAddr.Subdir)
   646  	modDir := filepath.Join(instPath, subDir)
   647  
   648  	log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir)
   649  
   650  	// Finally we are ready to try actually loading the module.
   651  	mod, mDiags := i.loader.Parser().LoadConfigDir(modDir)
   652  	if mod == nil {
   653  		// nil indicates missing or unreadable directory, so we'll
   654  		// discard the returned diags and return a more specific
   655  		// error message here. For registry modules this actually
   656  		// indicates a bug in the code above, since it's not the
   657  		// user's responsibility to create the directory in this case.
   658  		diags = diags.Append(&hcl.Diagnostic{
   659  			Severity: hcl.DiagError,
   660  			Summary:  "Unreadable module directory",
   661  			Detail:   fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir),
   662  		})
   663  	} else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() {
   664  		// If the core version requirements are not met, we drop any other
   665  		// diagnostics, as they may reflect language changes from future
   666  		// Terraform versions.
   667  		diags = diags.Extend(vDiags)
   668  	} else {
   669  		diags = diags.Extend(mDiags)
   670  	}
   671  
   672  	// Note the local location in our manifest.
   673  	manifest[key] = modsdir.Record{
   674  		Key:        key,
   675  		Version:    latestMatch,
   676  		Dir:        modDir,
   677  		SourceAddr: req.SourceAddr.String(),
   678  	}
   679  	log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
   680  	hooks.Install(key, latestMatch, modDir)
   681  
   682  	return mod, latestMatch, diags
   683  }
   684  
   685  func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *configs.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*configs.Module, hcl.Diagnostics) {
   686  	var diags hcl.Diagnostics
   687  
   688  	// Report up to the caller that we're about to start downloading.
   689  	addr := req.SourceAddr.(addrs.ModuleSourceRemote)
   690  	packageAddr := addr.Package
   691  	hooks.Download(key, packageAddr.String(), nil)
   692  
   693  	if len(req.VersionConstraint.Required) != 0 {
   694  		diags = diags.Append(&hcl.Diagnostic{
   695  			Severity: hcl.DiagError,
   696  			Summary:  "Invalid version constraint",
   697  			Detail:   fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it doesn't come from a module registry.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line),
   698  			Subject:  req.CallRange.Ptr(),
   699  		})
   700  		return nil, diags
   701  	}
   702  
   703  	err := fetcher.FetchPackage(ctx, instPath, packageAddr.String())
   704  	if err != nil {
   705  		// go-getter generates a poor error for an invalid relative path, so
   706  		// we'll detect that case and generate a better one.
   707  		if _, ok := err.(*getmodules.MaybeRelativePathErr); ok {
   708  			log.Printf(
   709  				"[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../",
   710  				req.SourceAddr,
   711  			)
   712  			diags = diags.Append(&hcl.Diagnostic{
   713  				Severity: hcl.DiagError,
   714  				Summary:  "Module not found",
   715  				Detail: fmt.Sprintf(
   716  					"The module address %q could not be resolved.\n\n"+
   717  						"If you intended this as a path relative to the current "+
   718  						"module, use \"./%s\" instead. The \"./\" prefix "+
   719  						"indicates that the address is a relative filesystem path.",
   720  					req.SourceAddr, req.SourceAddr,
   721  				),
   722  			})
   723  		} else {
   724  			// Errors returned by go-getter have very inconsistent quality as
   725  			// end-user error messages, but for now we're accepting that because
   726  			// we have no way to recognize any specific errors to improve them
   727  			// and masking the error entirely would hide valuable diagnostic
   728  			// information from the user.
   729  			diags = diags.Append(&hcl.Diagnostic{
   730  				Severity: hcl.DiagError,
   731  				Summary:  "Failed to download module",
   732  				Detail:   fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, packageAddr, err),
   733  				Subject:  req.CallRange.Ptr(),
   734  			})
   735  		}
   736  		return nil, diags
   737  	}
   738  
   739  	modDir, err := getmodules.ExpandSubdirGlobs(instPath, addr.Subdir)
   740  	if err != nil {
   741  		diags = diags.Append(&hcl.Diagnostic{
   742  			Severity: hcl.DiagError,
   743  			Summary:  "Failed to expand subdir globs",
   744  			Detail:   err.Error(),
   745  		})
   746  		return nil, diags
   747  	}
   748  
   749  	log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, addr, modDir)
   750  
   751  	// Finally we are ready to try actually loading the module.
   752  	mod, mDiags := i.loader.Parser().LoadConfigDir(modDir)
   753  	if mod == nil {
   754  		// nil indicates missing or unreadable directory, so we'll
   755  		// discard the returned diags and return a more specific
   756  		// error message here. For go-getter modules this actually
   757  		// indicates a bug in the code above, since it's not the
   758  		// user's responsibility to create the directory in this case.
   759  		diags = diags.Append(&hcl.Diagnostic{
   760  			Severity: hcl.DiagError,
   761  			Summary:  "Unreadable module directory",
   762  			Detail:   fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir),
   763  		})
   764  	} else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() {
   765  		// If the core version requirements are not met, we drop any other
   766  		// diagnostics, as they may reflect language changes from future
   767  		// Terraform versions.
   768  		diags = diags.Extend(vDiags)
   769  	} else {
   770  		diags = diags.Extend(mDiags)
   771  	}
   772  
   773  	// Note the local location in our manifest.
   774  	manifest[key] = modsdir.Record{
   775  		Key:        key,
   776  		Dir:        modDir,
   777  		SourceAddr: req.SourceAddr.String(),
   778  	}
   779  	log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
   780  	hooks.Install(key, nil, modDir)
   781  
   782  	return mod, diags
   783  }
   784  
   785  func (i *ModuleInstaller) packageInstallPath(modulePath addrs.Module) string {
   786  	return filepath.Join(i.modsDir, strings.Join(modulePath, "."))
   787  }
   788  
   789  // maybeImproveLocalInstallError is a helper function which can recognize
   790  // some specific situations where it can return a more helpful error message
   791  // and thus replace the given errors with those if so.
   792  //
   793  // If this function can't do anything about a particular situation then it
   794  // will just return the given diags verbatim.
   795  //
   796  // This function's behavior is only reasonable for errors returned from the
   797  // ModuleInstaller.installLocalModule function.
   798  func maybeImproveLocalInstallError(req *configs.ModuleRequest, diags hcl.Diagnostics) hcl.Diagnostics {
   799  	if !diags.HasErrors() {
   800  		return diags
   801  	}
   802  
   803  	// The main situation we're interested in detecting here is whether the
   804  	// current module or any of its ancestors use relative paths that reach
   805  	// outside of the "package" established by the nearest non-local ancestor.
   806  	// That's never really valid, but unfortunately we historically didn't
   807  	// have any explicit checking for it and so now for compatibility in
   808  	// situations where things just happened to "work" we treat this as an
   809  	// error only in situations where installation would've failed anyway,
   810  	// so we can give a better error about it than just a generic
   811  	// "directory not found" or whatever.
   812  	//
   813  	// Since it's never actually valid to relative out of the containing
   814  	// package, we just assume that any failed local package install which
   815  	// does so was caused by that, because to stop doing it should always
   816  	// improve the situation, even if it leads to another error describing
   817  	// a different problem.
   818  
   819  	// To decide this we need to find the subset of our ancestors that
   820  	// belong to the same "package" as our request, along with the closest
   821  	// ancestor that defined that package, and then we can work forwards
   822  	// to see if any of the local paths "escaped" the package.
   823  	type Step struct {
   824  		Path       addrs.Module
   825  		SourceAddr addrs.ModuleSource
   826  	}
   827  	var packageDefiner Step
   828  	var localRefs []Step
   829  	localRefs = append(localRefs, Step{
   830  		Path:       req.Path,
   831  		SourceAddr: req.SourceAddr,
   832  	})
   833  	current := req.Parent // a configs.Config where Children isn't populated yet
   834  	for {
   835  		if current == nil || current.SourceAddr == nil {
   836  			// We've reached the root module, in which case we aren't
   837  			// in an external "package" at all and so our special case
   838  			// can't apply.
   839  			return diags
   840  		}
   841  		if _, ok := current.SourceAddr.(addrs.ModuleSourceLocal); !ok {
   842  			// We've found the package definer, then!
   843  			packageDefiner = Step{
   844  				Path:       current.Path,
   845  				SourceAddr: current.SourceAddr,
   846  			}
   847  			break
   848  		}
   849  
   850  		localRefs = append(localRefs, Step{
   851  			Path:       current.Path,
   852  			SourceAddr: current.SourceAddr,
   853  		})
   854  		current = current.Parent
   855  	}
   856  	// Our localRefs list is reversed because we were traversing up the tree,
   857  	// so we'll flip it the other way and thus walk "downwards" through it.
   858  	for i, j := 0, len(localRefs)-1; i < j; i, j = i+1, j-1 {
   859  		localRefs[i], localRefs[j] = localRefs[j], localRefs[i]
   860  	}
   861  
   862  	// Our method here is to start with a known base path prefix and
   863  	// then apply each of the local refs to it in sequence until one of
   864  	// them causes us to "lose" the prefix. If that happens, we've found
   865  	// an escape to report. This is not an exact science but good enough
   866  	// heuristic for choosing a better error message.
   867  	const prefix = "*/" // NOTE: this can find a false negative if the user chooses "*" as a directory name, but we consider that unlikely
   868  	packageAddr, startPath := splitAddrSubdir(packageDefiner.SourceAddr)
   869  	currentPath := path.Join(prefix, startPath)
   870  	for _, step := range localRefs {
   871  		rel := step.SourceAddr.String()
   872  
   873  		nextPath := path.Join(currentPath, rel)
   874  		if !strings.HasPrefix(nextPath, prefix) { // ESCAPED!
   875  			escapeeAddr := step.Path.String()
   876  
   877  			var newDiags hcl.Diagnostics
   878  
   879  			// First we'll copy over any non-error diagnostics from the source diags
   880  			for _, diag := range diags {
   881  				if diag.Severity != hcl.DiagError {
   882  					newDiags = newDiags.Append(diag)
   883  				}
   884  			}
   885  
   886  			// ...but we'll replace any errors with this more precise error.
   887  			var suggestion string
   888  			if strings.HasPrefix(packageAddr, "/") || filepath.VolumeName(packageAddr) != "" {
   889  				// It might be somewhat surprising that Terraform treats
   890  				// absolute filesystem paths as "external" even though it
   891  				// treats relative paths as local, so if it seems like that's
   892  				// what the user was doing then we'll add an additional note
   893  				// about it.
   894  				suggestion = "\n\nTerraform treats absolute filesystem paths as external modules which establish a new module package. To treat this directory as part of the same package as its caller, use a local path starting with either \"./\" or \"../\"."
   895  			}
   896  			newDiags = newDiags.Append(&hcl.Diagnostic{
   897  				Severity: hcl.DiagError,
   898  				Summary:  "Local module path escapes module package",
   899  				Detail: fmt.Sprintf(
   900  					"The given source directory for %s would be outside of its containing package %q. Local source addresses starting with \"../\" must stay within the same package that the calling module belongs to.%s",
   901  					escapeeAddr, packageAddr, suggestion,
   902  				),
   903  			})
   904  
   905  			return newDiags
   906  		}
   907  
   908  		currentPath = nextPath
   909  	}
   910  
   911  	// If we get down here then we have nothing useful to do, so we'll just
   912  	// echo back what we were given.
   913  	return diags
   914  }
   915  
   916  func splitAddrSubdir(addr addrs.ModuleSource) (string, string) {
   917  	switch addr := addr.(type) {
   918  	case addrs.ModuleSourceRegistry:
   919  		subDir := addr.Subdir
   920  		addr.Subdir = ""
   921  		return addr.String(), subDir
   922  	case addrs.ModuleSourceRemote:
   923  		return addr.Package.String(), addr.Subdir
   924  	case nil:
   925  		panic("splitAddrSubdir on nil addrs.ModuleSource")
   926  	default:
   927  		return addr.String(), ""
   928  	}
   929  }