10 version "github.com/hashicorp/go-version"
11 "github.com/hashicorp/terraform-config-inspect/tfconfig"
12 "github.com/hashicorp/terraform/addrs"
13 "github.com/hashicorp/terraform/internal/earlyconfig"
14 "github.com/hashicorp/terraform/internal/modsdir"
15 "github.com/hashicorp/terraform/registry"
16 "github.com/hashicorp/terraform/registry/regsrc"
17 "github.com/hashicorp/terraform/tfdiags"
20 type ModuleInstaller struct {
25 func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller {
26 return &ModuleInstaller{
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.
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
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.
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
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.
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)
63 rootMod, diags := earlyconfig.LoadModule(rootDir)
68 manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir)
70 diags = diags.Append(tfdiags.Sourceless(
72 "Failed to read modules manifest file",
73 fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err),
78 getter := reusingGetter{}
79 cfg, instDiags := i.installDescendentModules(rootMod, rootDir, manifest, upgrade, hooks, getter)
80 diags = append(diags, instDiags...)
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
89 // Use our no-op implementation as a placeholder
90 hooks = ModuleInstallHooksImpl{}
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{
100 cfg, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc(
101 func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) {
103 key := manifest.ModuleKey(req.Path)
104 instPath := i.packageInstallPath(req.Path)
106 log.Printf("[DEBUG] Module installer: begin %s", key)
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.
112 record, recorded := manifest[key]
115 log.Printf("[TRACE] ModuleInstaller: %s is not yet installed", key)
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)
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)
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.
130 if _, recorded := manifest[key]; recorded {
131 log.Printf("[TRACE] ModuleInstaller: discarding previous record of %s prior to reinstall", key)
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)
141 delete(manifest, subKey)
146 record, recorded := manifest[key]
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(
157 "Failed to remove local module cache",
159 "Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s",
163 return nil, nil, diags
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)
174 log.Printf("[TRACE] ModuleInstaller: Module installer: %s %s already installed in %s", key, record.Version, record.Dir)
175 return mod, record.Version, diags
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.
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
190 case isRegistrySourceAddr(req.SourceAddr):
191 addr, err := regsrc.ParseModuleSource(req.SourceAddr)
193 // Should never happen because isRegistrySourceAddr already validated
196 log.Printf("[TRACE] ModuleInstaller: %s is a registry module at %s", key, addr)
198 mod, v, mDiags := i.installRegistryModule(req, key, instPath, addr, manifest, hooks, getter)
199 diags = append(diags, mDiags...)
203 log.Printf("[TRACE] ModuleInstaller: %s address %q will be handled by go-getter", key, req.SourceAddr)
205 mod, mDiags := i.installGoGetterModule(req, key, instPath, manifest, hooks, getter)
206 diags = append(diags, mDiags...)
207 return mod, nil, diags
212 diags = append(diags, cDiags...)
214 err := manifest.WriteSnapshotToDir(i.modsDir)
216 diags = diags.Append(tfdiags.Sourceless(
218 "Failed to update module manifest",
219 fmt.Sprintf("Unable to write the module manifest file: %s", err),
226 func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) {
227 var diags tfdiags.Diagnostics
229 parentKey := manifest.ModuleKey(req.Parent.Path)
230 parentRecord, recorded := manifest[parentKey]
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))
236 if len(req.VersionConstraints) != 0 {
237 diags = diags.Append(tfdiags.Sourceless(
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),
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)
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)
253 diags = diags.Append(tfdiags.Sourceless(
255 "Unreadable module directory",
256 fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()),
260 mod, mDiags := earlyconfig.LoadModule(newDir)
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(
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),
271 diags = diags.Append(mDiags)
274 // Note the local location in our manifest.
275 manifest[key] = modsdir.Record{
278 SourceAddr: req.SourceAddr,
280 log.Printf("[DEBUG] Module installer: %s installed at %s", key, newDir)
281 hooks.Install(key, nil, newDir)
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
289 hostname, err := addr.SvcHost()
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(
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),
302 diags = diags.Append(tfdiags.Sourceless(
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),
308 return nil, nil, diags
313 log.Printf("[DEBUG] %s listing available versions of %s at %s", key, addr, hostname)
314 resp, err := reg.ModuleVersions(addr)
316 if registry.IsModuleNotFound(err) {
317 diags = diags.Append(tfdiags.Sourceless(
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),
323 diags = diags.Append(tfdiags.Sourceless(
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),
329 return nil, nil, diags
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(
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),
344 return nil, nil, diags
347 modMeta := resp.Modules[0]
349 var latestMatch *version.Version
350 var latestVersion *version.Version
351 for _, mv := range modMeta.Versions {
352 v, err := version.NewVersion(mv.Version)
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(
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),
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)
372 if latestVersion == nil || v.GreaterThan(latestVersion) {
376 if req.VersionConstraints.Check(v) {
377 if latestMatch == nil || v.GreaterThan(latestMatch) {
383 if latestVersion == nil {
384 diags = diags.Append(tfdiags.Sourceless(
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),
389 return nil, nil, diags
392 if latestMatch == nil {
393 diags = diags.Append(tfdiags.Sourceless(
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),
398 return nil, nil, diags
401 // Report up to the caller that we're about to start downloading.
402 packageAddr, _ := splitAddrSubdir(req.SourceAddr)
403 hooks.Download(key, packageAddr, latestMatch)
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())
410 log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err)
411 diags = diags.Append(tfdiags.Sourceless(
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),
416 return nil, nil, diags
419 log.Printf("[TRACE] ModuleInstaller: %s %s %s is available at %q", key, addr, latestMatch, dlAddr)
421 modDir, err := getter.getWithGoGetter(instPath, dlAddr)
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(
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),
433 return nil, nil, diags
436 log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, dlAddr, modDir)
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)
444 log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir)
446 // Finally we are ready to try actually loading the module.
447 mod, mDiags := earlyconfig.LoadModule(modDir)
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(
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),
460 diags = append(diags, mDiags...)
463 // Note the local location in our manifest.
464 manifest[key] = modsdir.Record{
466 Version: latestMatch,
468 SourceAddr: req.SourceAddr,
470 log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
471 hooks.Install(key, latestMatch, modDir)
473 return mod, latestMatch, diags
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
479 // Report up to the caller that we're about to start downloading.
480 packageAddr, _ := splitAddrSubdir(req.SourceAddr)
481 hooks.Download(key, packageAddr, nil)
483 if len(req.VersionConstraints) != 0 {
484 diags = diags.Append(tfdiags.Sourceless(
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),
492 modDir, err := getter.getWithGoGetter(instPath, req.SourceAddr)
494 if _, ok := err.(*MaybeRelativePathErr); ok {
496 "[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../",
499 diags = diags.Append(tfdiags.Sourceless(
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,
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(
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),
526 log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, req.SourceAddr, modDir)
528 mod, mDiags := earlyconfig.LoadModule(modDir)
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(
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),
541 diags = append(diags, mDiags...)
544 // Note the local location in our manifest.
545 manifest[key] = modsdir.Record{
548 SourceAddr: req.SourceAddr,
550 log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
551 hooks.Install(key, nil, modDir)
556 func (i *ModuleInstaller) packageInstallPath(modulePath addrs.Module) string {
557 return filepath.Join(i.modsDir, strings.Join(modulePath, "."))