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