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 }