github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/initwd/module_install.go (about)

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