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 }