github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/initwd/module_install.go (about) 1 package initwd 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "path/filepath" 8 "strings" 9 10 version "github.com/hashicorp/go-version" 11 "github.com/hashicorp/terraform-config-inspect/tfconfig" 12 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 13 "github.com/hashicorp/terraform-plugin-sdk/internal/earlyconfig" 14 "github.com/hashicorp/terraform-plugin-sdk/internal/modsdir" 15 "github.com/hashicorp/terraform-plugin-sdk/internal/registry" 16 "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" 17 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 18 ) 19 20 type ModuleInstaller struct { 21 modsDir string 22 reg *registry.Client 23 } 24 25 func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller { 26 return &ModuleInstaller{ 27 modsDir: modsDir, 28 reg: reg, 29 } 30 } 31 32 // InstallModules analyses the root module in the given directory and installs 33 // all of its direct and transitive dependencies into the given modules 34 // directory, which must already exist. 35 // 36 // Since InstallModules makes possibly-time-consuming calls to remote services, 37 // a hook interface is supported to allow the caller to be notified when 38 // each module is installed and, for remote modules, when downloading begins. 39 // LoadConfig guarantees that two hook calls will not happen concurrently but 40 // it does not guarantee any particular ordering of hook calls. This mechanism 41 // is for UI feedback only and does not give the caller any control over the 42 // process. 43 // 44 // If modules are already installed in the target directory, they will be 45 // skipped unless their source address or version have changed or unless 46 // the upgrade flag is set. 47 // 48 // InstallModules never deletes any directory, except in the case where it 49 // needs to replace a directory that is already present with a newly-extracted 50 // package. 51 // 52 // If the returned diagnostics contains errors then the module installation 53 // may have wholly or partially completed. Modules must be loaded in order 54 // to find their dependencies, so this function does many of the same checks 55 // as LoadConfig as a side-effect. 56 // 57 // If successful (the returned diagnostics contains no errors) then the 58 // first return value is the early configuration tree that was constructed by 59 // the installation process. 60 func (i *ModuleInstaller) InstallModules(rootDir string, upgrade bool, hooks ModuleInstallHooks) (*earlyconfig.Config, tfdiags.Diagnostics) { 61 log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir) 62 63 rootMod, diags := earlyconfig.LoadModule(rootDir) 64 if rootMod == nil { 65 return nil, diags 66 } 67 68 manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir) 69 if err != nil { 70 diags = diags.Append(tfdiags.Sourceless( 71 tfdiags.Error, 72 "Failed to read modules manifest file", 73 fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err), 74 )) 75 return nil, diags 76 } 77 78 getter := reusingGetter{} 79 cfg, instDiags := i.installDescendentModules(rootMod, rootDir, manifest, upgrade, hooks, getter) 80 diags = append(diags, instDiags...) 81 82 return cfg, diags 83 } 84 85 func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, getter reusingGetter) (*earlyconfig.Config, tfdiags.Diagnostics) { 86 var diags tfdiags.Diagnostics 87 88 if hooks == nil { 89 // Use our no-op implementation as a placeholder 90 hooks = ModuleInstallHooksImpl{} 91 } 92 93 // Create a manifest record for the root module. This will be used if 94 // there are any relative-pathed modules in the root. 95 manifest[""] = modsdir.Record{ 96 Key: "", 97 Dir: rootDir, 98 } 99 100 cfg, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc( 101 func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { 102 103 key := manifest.ModuleKey(req.Path) 104 instPath := i.packageInstallPath(req.Path) 105 106 log.Printf("[DEBUG] Module installer: begin %s", key) 107 108 // First we'll check if we need to upgrade/replace an existing 109 // installed module, and delete it out of the way if so. 110 replace := upgrade 111 if !replace { 112 record, recorded := manifest[key] 113 switch { 114 case !recorded: 115 log.Printf("[TRACE] ModuleInstaller: %s is not yet installed", key) 116 replace = true 117 case record.SourceAddr != req.SourceAddr: 118 log.Printf("[TRACE] ModuleInstaller: %s source address has changed from %q to %q", key, record.SourceAddr, req.SourceAddr) 119 replace = true 120 case record.Version != nil && !req.VersionConstraints.Check(record.Version): 121 log.Printf("[TRACE] ModuleInstaller: %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraints) 122 replace = true 123 } 124 } 125 126 // If we _are_ planning to replace this module, then we'll remove 127 // it now so our installation code below won't conflict with any 128 // existing remnants. 129 if replace { 130 if _, recorded := manifest[key]; recorded { 131 log.Printf("[TRACE] ModuleInstaller: discarding previous record of %s prior to reinstall", key) 132 } 133 delete(manifest, key) 134 // Deleting a module invalidates all of its descendent modules too. 135 keyPrefix := key + "." 136 for subKey := range manifest { 137 if strings.HasPrefix(subKey, keyPrefix) { 138 if _, recorded := manifest[subKey]; recorded { 139 log.Printf("[TRACE] ModuleInstaller: also discarding downstream %s", subKey) 140 } 141 delete(manifest, subKey) 142 } 143 } 144 } 145 146 record, recorded := manifest[key] 147 if !recorded { 148 // Clean up any stale cache directory that might be present. 149 // If this is a local (relative) source then the dir will 150 // not exist, but we'll ignore that. 151 log.Printf("[TRACE] ModuleInstaller: cleaning directory %s prior to install of %s", instPath, key) 152 err := os.RemoveAll(instPath) 153 if err != nil && !os.IsNotExist(err) { 154 log.Printf("[TRACE] ModuleInstaller: failed to remove %s: %s", key, err) 155 diags = diags.Append(tfdiags.Sourceless( 156 tfdiags.Error, 157 "Failed to remove local module cache", 158 fmt.Sprintf( 159 "Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s", 160 instPath, err, 161 ), 162 )) 163 return nil, nil, diags 164 } 165 } else { 166 // If this module is already recorded and its root directory 167 // exists then we will just load what's already there and 168 // keep our existing record. 169 info, err := os.Stat(record.Dir) 170 if err == nil && info.IsDir() { 171 mod, mDiags := earlyconfig.LoadModule(record.Dir) 172 diags = diags.Append(mDiags) 173 174 log.Printf("[TRACE] ModuleInstaller: Module installer: %s %s already installed in %s", key, record.Version, record.Dir) 175 return mod, record.Version, diags 176 } 177 } 178 179 // If we get down here then it's finally time to actually install 180 // the module. There are some variants to this process depending 181 // on what type of module source address we have. 182 switch { 183 184 case isLocalSourceAddr(req.SourceAddr): 185 log.Printf("[TRACE] ModuleInstaller: %s has local path %q", key, req.SourceAddr) 186 mod, mDiags := i.installLocalModule(req, key, manifest, hooks) 187 diags = append(diags, mDiags...) 188 return mod, nil, diags 189 190 case isRegistrySourceAddr(req.SourceAddr): 191 addr, err := regsrc.ParseModuleSource(req.SourceAddr) 192 if err != nil { 193 // Should never happen because isRegistrySourceAddr already validated 194 panic(err) 195 } 196 log.Printf("[TRACE] ModuleInstaller: %s is a registry module at %s", key, addr) 197 198 mod, v, mDiags := i.installRegistryModule(req, key, instPath, addr, manifest, hooks, getter) 199 diags = append(diags, mDiags...) 200 return mod, v, diags 201 202 default: 203 log.Printf("[TRACE] ModuleInstaller: %s address %q will be handled by go-getter", key, req.SourceAddr) 204 205 mod, mDiags := i.installGoGetterModule(req, key, instPath, manifest, hooks, getter) 206 diags = append(diags, mDiags...) 207 return mod, nil, diags 208 } 209 210 }, 211 )) 212 diags = append(diags, cDiags...) 213 214 err := manifest.WriteSnapshotToDir(i.modsDir) 215 if err != nil { 216 diags = diags.Append(tfdiags.Sourceless( 217 tfdiags.Error, 218 "Failed to update module manifest", 219 fmt.Sprintf("Unable to write the module manifest file: %s", err), 220 )) 221 } 222 223 return cfg, diags 224 } 225 226 func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) { 227 var diags tfdiags.Diagnostics 228 229 parentKey := manifest.ModuleKey(req.Parent.Path) 230 parentRecord, recorded := manifest[parentKey] 231 if !recorded { 232 // This is indicative of a bug rather than a user-actionable error 233 panic(fmt.Errorf("missing manifest record for parent module %s", parentKey)) 234 } 235 236 if len(req.VersionConstraints) != 0 { 237 diags = diags.Append(tfdiags.Sourceless( 238 tfdiags.Error, 239 "Invalid version constraint", 240 fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a relative local path.", req.Name, req.CallPos.Filename, req.CallPos.Line), 241 )) 242 } 243 244 // For local sources we don't actually need to modify the 245 // filesystem at all because the parent already wrote 246 // the files we need, and so we just load up what's already here. 247 newDir := filepath.Join(parentRecord.Dir, req.SourceAddr) 248 249 log.Printf("[TRACE] ModuleInstaller: %s uses directory from parent: %s", key, newDir) 250 // it is possible that the local directory is a symlink 251 newDir, err := filepath.EvalSymlinks(newDir) 252 if err != nil { 253 diags = diags.Append(tfdiags.Sourceless( 254 tfdiags.Error, 255 "Unreadable module directory", 256 fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()), 257 )) 258 } 259 260 mod, mDiags := earlyconfig.LoadModule(newDir) 261 if mod == nil { 262 // nil indicates missing or unreadable directory, so we'll 263 // discard the returned diags and return a more specific 264 // error message here. 265 diags = diags.Append(tfdiags.Sourceless( 266 tfdiags.Error, 267 "Unreadable module directory", 268 fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallPos.Filename, req.CallPos.Line), 269 )) 270 } else { 271 diags = diags.Append(mDiags) 272 } 273 274 // Note the local location in our manifest. 275 manifest[key] = modsdir.Record{ 276 Key: key, 277 Dir: newDir, 278 SourceAddr: req.SourceAddr, 279 } 280 log.Printf("[DEBUG] Module installer: %s installed at %s", key, newDir) 281 hooks.Install(key, nil, newDir) 282 283 return mod, diags 284 } 285 286 func (i *ModuleInstaller) installRegistryModule(req *earlyconfig.ModuleRequest, key string, instPath string, addr *regsrc.Module, manifest modsdir.Manifest, hooks ModuleInstallHooks, getter reusingGetter) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { 287 var diags tfdiags.Diagnostics 288 289 hostname, err := addr.SvcHost() 290 if err != nil { 291 // If it looks like the user was trying to use punycode then we'll generate 292 // a specialized error for that case. We require the unicode form of 293 // hostname so that hostnames are always human-readable in configuration 294 // and punycode can't be used to hide a malicious module hostname. 295 if strings.HasPrefix(addr.RawHost.Raw, "xn--") { 296 diags = diags.Append(tfdiags.Sourceless( 297 tfdiags.Error, 298 "Invalid module registry hostname", 299 fmt.Sprintf("The hostname portion of the module %q source address (at %s:%d) is not an acceptable hostname. Internationalized domain names must be given in unicode form rather than ASCII (\"punycode\") form.", req.Name, req.CallPos.Filename, req.CallPos.Line), 300 )) 301 } else { 302 diags = diags.Append(tfdiags.Sourceless( 303 tfdiags.Error, 304 "Invalid module registry hostname", 305 fmt.Sprintf("The hostname portion of the module %q source address (at %s:%d) is not a valid hostname.", req.Name, req.CallPos.Filename, req.CallPos.Line), 306 )) 307 } 308 return nil, nil, diags 309 } 310 311 reg := i.reg 312 313 log.Printf("[DEBUG] %s listing available versions of %s at %s", key, addr, hostname) 314 resp, err := reg.ModuleVersions(addr) 315 if err != nil { 316 if registry.IsModuleNotFound(err) { 317 diags = diags.Append(tfdiags.Sourceless( 318 tfdiags.Error, 319 "Module not found", 320 fmt.Sprintf("Module %q (from %s:%d) cannot be found in the module registry at %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname), 321 )) 322 } else { 323 diags = diags.Append(tfdiags.Sourceless( 324 tfdiags.Error, 325 "Error accessing remote module registry", 326 fmt.Sprintf("Failed to retrieve available versions for module %q (%s:%d) from %s: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname, err), 327 )) 328 } 329 return nil, nil, diags 330 } 331 332 // The response might contain information about dependencies to allow us 333 // to potentially optimize future requests, but we don't currently do that 334 // and so for now we'll just take the first item which is guaranteed to 335 // be the address we requested. 336 if len(resp.Modules) < 1 { 337 // Should never happen, but since this is a remote service that may 338 // be implemented by third-parties we will handle it gracefully. 339 diags = diags.Append(tfdiags.Sourceless( 340 tfdiags.Error, 341 "Invalid response from remote module registry", 342 fmt.Sprintf("The registry at %s returned an invalid response when Terraform requested available versions for module %q (%s:%d).", hostname, req.Name, req.CallPos.Filename, req.CallPos.Line), 343 )) 344 return nil, nil, diags 345 } 346 347 modMeta := resp.Modules[0] 348 349 var latestMatch *version.Version 350 var latestVersion *version.Version 351 for _, mv := range modMeta.Versions { 352 v, err := version.NewVersion(mv.Version) 353 if err != nil { 354 // Should never happen if the registry server is compliant with 355 // the protocol, but we'll warn if not to assist someone who 356 // might be developing a module registry server. 357 diags = diags.Append(tfdiags.Sourceless( 358 tfdiags.Warning, 359 "Invalid response from remote module registry", 360 fmt.Sprintf("The registry at %s returned an invalid version string %q for module %q (%s:%d), which Terraform ignored.", hostname, mv.Version, req.Name, req.CallPos.Filename, req.CallPos.Line), 361 )) 362 continue 363 } 364 365 // If we've found a pre-release version then we'll ignore it unless 366 // it was exactly requested. 367 if v.Prerelease() != "" && req.VersionConstraints.String() != v.String() { 368 log.Printf("[TRACE] ModuleInstaller: %s ignoring %s because it is a pre-release and was not requested exactly", key, v) 369 continue 370 } 371 372 if latestVersion == nil || v.GreaterThan(latestVersion) { 373 latestVersion = v 374 } 375 376 if req.VersionConstraints.Check(v) { 377 if latestMatch == nil || v.GreaterThan(latestMatch) { 378 latestMatch = v 379 } 380 } 381 } 382 383 if latestVersion == nil { 384 diags = diags.Append(tfdiags.Sourceless( 385 tfdiags.Error, 386 "Module has no versions", 387 fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallPos.Filename, req.CallPos.Line, hostname), 388 )) 389 return nil, nil, diags 390 } 391 392 if latestMatch == nil { 393 diags = diags.Append(tfdiags.Sourceless( 394 tfdiags.Error, 395 "Unresolvable module version constraint", 396 fmt.Sprintf("There is no available version of module %q (%s:%d) which matches the given version constraint. The newest available version is %s.", addr, req.CallPos.Filename, req.CallPos.Line, latestVersion), 397 )) 398 return nil, nil, diags 399 } 400 401 // Report up to the caller that we're about to start downloading. 402 packageAddr, _ := splitAddrSubdir(req.SourceAddr) 403 hooks.Download(key, packageAddr, latestMatch) 404 405 // If we manage to get down here then we've found a suitable version to 406 // install, so we need to ask the registry where we should download it from. 407 // The response to this is a go-getter-style address string. 408 dlAddr, err := reg.ModuleLocation(addr, latestMatch.String()) 409 if err != nil { 410 log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err) 411 diags = diags.Append(tfdiags.Sourceless( 412 tfdiags.Error, 413 "Invalid response from remote module registry", 414 fmt.Sprintf("The remote registry at %s failed to return a download URL for %s %s.", hostname, addr, latestMatch), 415 )) 416 return nil, nil, diags 417 } 418 419 log.Printf("[TRACE] ModuleInstaller: %s %s %s is available at %q", key, addr, latestMatch, dlAddr) 420 421 modDir, err := getter.getWithGoGetter(instPath, dlAddr) 422 if err != nil { 423 // Errors returned by go-getter have very inconsistent quality as 424 // end-user error messages, but for now we're accepting that because 425 // we have no way to recognize any specific errors to improve them 426 // and masking the error entirely would hide valuable diagnostic 427 // information from the user. 428 diags = diags.Append(tfdiags.Sourceless( 429 tfdiags.Error, 430 "Failed to download module", 431 fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, dlAddr, err), 432 )) 433 return nil, nil, diags 434 } 435 436 log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, dlAddr, modDir) 437 438 if addr.RawSubmodule != "" { 439 // Append the user's requested subdirectory to any subdirectory that 440 // was implied by any of the nested layers we expanded within go-getter. 441 modDir = filepath.Join(modDir, addr.RawSubmodule) 442 } 443 444 log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir) 445 446 // Finally we are ready to try actually loading the module. 447 mod, mDiags := earlyconfig.LoadModule(modDir) 448 if mod == nil { 449 // nil indicates missing or unreadable directory, so we'll 450 // discard the returned diags and return a more specific 451 // error message here. For registry modules this actually 452 // indicates a bug in the code above, since it's not the 453 // user's responsibility to create the directory in this case. 454 diags = diags.Append(tfdiags.Sourceless( 455 tfdiags.Error, 456 "Unreadable module directory", 457 fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), 458 )) 459 } else { 460 diags = append(diags, mDiags...) 461 } 462 463 // Note the local location in our manifest. 464 manifest[key] = modsdir.Record{ 465 Key: key, 466 Version: latestMatch, 467 Dir: modDir, 468 SourceAddr: req.SourceAddr, 469 } 470 log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir) 471 hooks.Install(key, latestMatch, modDir) 472 473 return mod, latestMatch, diags 474 } 475 476 func (i *ModuleInstaller) installGoGetterModule(req *earlyconfig.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks ModuleInstallHooks, getter reusingGetter) (*tfconfig.Module, tfdiags.Diagnostics) { 477 var diags tfdiags.Diagnostics 478 479 // Report up to the caller that we're about to start downloading. 480 packageAddr, _ := splitAddrSubdir(req.SourceAddr) 481 hooks.Download(key, packageAddr, nil) 482 483 if len(req.VersionConstraints) != 0 { 484 diags = diags.Append(tfdiags.Sourceless( 485 tfdiags.Error, 486 "Invalid version constraint", 487 fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a non Registry URL.", req.Name, req.CallPos.Filename, req.CallPos.Line), 488 )) 489 return nil, diags 490 } 491 492 modDir, err := getter.getWithGoGetter(instPath, req.SourceAddr) 493 if err != nil { 494 if _, ok := err.(*MaybeRelativePathErr); ok { 495 log.Printf( 496 "[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../", 497 req.SourceAddr, 498 ) 499 diags = diags.Append(tfdiags.Sourceless( 500 tfdiags.Error, 501 "Module not found", 502 fmt.Sprintf( 503 "The module address %q could not be resolved.\n\n"+ 504 "If you intended this as a path relative to the current "+ 505 "module, use \"./%s\" instead. The \"./\" prefix "+ 506 "indicates that the address is a relative filesystem path.", 507 req.SourceAddr, req.SourceAddr, 508 ), 509 )) 510 } else { 511 // Errors returned by go-getter have very inconsistent quality as 512 // end-user error messages, but for now we're accepting that because 513 // we have no way to recognize any specific errors to improve them 514 // and masking the error entirely would hide valuable diagnostic 515 // information from the user. 516 diags = diags.Append(tfdiags.Sourceless( 517 tfdiags.Error, 518 "Failed to download module", 519 fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s", req.Name, req.CallPos.Filename, req.CallPos.Line, packageAddr, err), 520 )) 521 } 522 return nil, diags 523 524 } 525 526 log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, req.SourceAddr, modDir) 527 528 mod, mDiags := earlyconfig.LoadModule(modDir) 529 if mod == nil { 530 // nil indicates missing or unreadable directory, so we'll 531 // discard the returned diags and return a more specific 532 // error message here. For go-getter modules this actually 533 // indicates a bug in the code above, since it's not the 534 // user's responsibility to create the directory in this case. 535 diags = diags.Append(tfdiags.Sourceless( 536 tfdiags.Error, 537 "Unreadable module directory", 538 fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), 539 )) 540 } else { 541 diags = append(diags, mDiags...) 542 } 543 544 // Note the local location in our manifest. 545 manifest[key] = modsdir.Record{ 546 Key: key, 547 Dir: modDir, 548 SourceAddr: req.SourceAddr, 549 } 550 log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir) 551 hooks.Install(key, nil, modDir) 552 553 return mod, diags 554 } 555 556 func (i *ModuleInstaller) packageInstallPath(modulePath addrs.Module) string { 557 return filepath.Join(i.modsDir, strings.Join(modulePath, ".")) 558 }