internal/init: Module installation functionality
This is an adaptation of the installation code from configs/configload, now using the "earlyconfig" package instead of the "configs" package. Module installation is an initialization-only process, with all other commands assuming an already-initialized directory. Having it here can therefore simplify the API of configs/configload, which can now focus only on the problem of loading modules that have already been installed. The old installer code in configs/configload is still in place for now because the caller in "terraform init" isn't yet updated to use this.
This commit is contained in:
parent
53ee858851
commit
e27e0ddc9e
|
@ -0,0 +1,114 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// copyDir copies the src directory contents into dst. Both directories
|
||||
// should already exist.
|
||||
func copyDir(dst, src string) error {
|
||||
src, err := filepath.EvalSymlinks(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
walkFn := func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if path == src {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(filepath.Base(path), ".") {
|
||||
// Skip any dot files
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// The "path" has the src prefixed to it. We need to join our
|
||||
// destination with the path without the src on it.
|
||||
dstPath := filepath.Join(dst, path[len(src):])
|
||||
|
||||
// we don't want to try and copy the same file over itself.
|
||||
if eq, err := sameFile(path, dstPath); eq {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we have a directory, make that subdirectory, then continue
|
||||
// the walk.
|
||||
if info.IsDir() {
|
||||
if path == filepath.Join(src, dst) {
|
||||
// dst is in src; don't walk it.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dstPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we have a file, copy the contents.
|
||||
srcF, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcF.Close()
|
||||
|
||||
dstF, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstF.Close()
|
||||
|
||||
if _, err := io.Copy(dstF, srcF); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Chmod it
|
||||
return os.Chmod(dstPath, info.Mode())
|
||||
}
|
||||
|
||||
return filepath.Walk(src, walkFn)
|
||||
}
|
||||
|
||||
// sameFile tried to determine if to paths are the same file.
|
||||
// If the paths don't match, we lookup the inode on supported systems.
|
||||
func sameFile(a, b string) (bool, error) {
|
||||
if a == b {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
aIno, err := inode(a)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
bIno, err := inode(b)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if aIno > 0 && aIno == bIno {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
getter "github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/terraform/registry/regsrc"
|
||||
)
|
||||
|
||||
// We configure our own go-getter detector and getter sets here, because
|
||||
// the set of sources we support is part of Terraform's documentation and
|
||||
// so we don't want any new sources introduced in go-getter to sneak in here
|
||||
// and work even though they aren't documented. This also insulates us from
|
||||
// any meddling that might be done by other go-getter callers linked into our
|
||||
// executable.
|
||||
|
||||
var goGetterDetectors = []getter.Detector{
|
||||
new(getter.GitHubDetector),
|
||||
new(getter.BitBucketDetector),
|
||||
new(getter.S3Detector),
|
||||
}
|
||||
|
||||
var goGetterNoDetectors = []getter.Detector{}
|
||||
|
||||
var goGetterDecompressors = map[string]getter.Decompressor{
|
||||
"bz2": new(getter.Bzip2Decompressor),
|
||||
"gz": new(getter.GzipDecompressor),
|
||||
"xz": new(getter.XzDecompressor),
|
||||
"zip": new(getter.ZipDecompressor),
|
||||
|
||||
"tar.bz2": new(getter.TarBzip2Decompressor),
|
||||
"tar.tbz2": new(getter.TarBzip2Decompressor),
|
||||
|
||||
"tar.gz": new(getter.TarGzipDecompressor),
|
||||
"tgz": new(getter.TarGzipDecompressor),
|
||||
|
||||
"tar.xz": new(getter.TarXzDecompressor),
|
||||
"txz": new(getter.TarXzDecompressor),
|
||||
}
|
||||
|
||||
var goGetterGetters = map[string]getter.Getter{
|
||||
"git": new(getter.GitGetter),
|
||||
"hg": new(getter.HgGetter),
|
||||
"s3": new(getter.S3Getter),
|
||||
"http": getterHTTPGetter,
|
||||
"https": getterHTTPGetter,
|
||||
}
|
||||
|
||||
var getterHTTPClient = cleanhttp.DefaultClient()
|
||||
|
||||
var getterHTTPGetter = &getter.HttpGetter{
|
||||
Client: getterHTTPClient,
|
||||
Netrc: true,
|
||||
}
|
||||
|
||||
// A reusingGetter is a helper for the module installer that remembers
|
||||
// the final resolved addresses of all of the sources it has already been
|
||||
// asked to install, and will copy from a prior installation directory if
|
||||
// it has the same resolved source address.
|
||||
//
|
||||
// The keys in a reusingGetter are resolved and trimmed source addresses
|
||||
// (with a scheme always present, and without any "subdir" component),
|
||||
// and the values are the paths where each source was previously installed.
|
||||
type reusingGetter map[string]string
|
||||
|
||||
// getWithGoGetter retrieves the package referenced in the given address
|
||||
// into the installation path and then returns the full path to any subdir
|
||||
// indicated in the address.
|
||||
//
|
||||
// The errors returned by this function are those surfaced by the underlying
|
||||
// go-getter library, which have very inconsistent quality as
|
||||
// end-user-actionable error messages. At this time we do not have any
|
||||
// reasonable way to improve these error messages at this layer because
|
||||
// the underlying errors are not separatelyr recognizable.
|
||||
func (g reusingGetter) getWithGoGetter(instPath, addr string) (string, error) {
|
||||
packageAddr, subDir := splitAddrSubdir(addr)
|
||||
|
||||
log.Printf("[DEBUG] will download %q to %s", packageAddr, instPath)
|
||||
|
||||
realAddr, err := getter.Detect(packageAddr, instPath, getter.Detectors)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var realSubDir string
|
||||
realAddr, realSubDir = splitAddrSubdir(realAddr)
|
||||
if realSubDir != "" {
|
||||
subDir = filepath.Join(realSubDir, subDir)
|
||||
}
|
||||
|
||||
if realAddr != packageAddr {
|
||||
log.Printf("[TRACE] go-getter detectors rewrote %q to %q", packageAddr, realAddr)
|
||||
}
|
||||
|
||||
if prevDir, exists := g[realAddr]; exists {
|
||||
log.Printf("[TRACE] copying previous install %s to %s", prevDir, instPath)
|
||||
err := os.Mkdir(instPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create directory %s: %s", instPath, err)
|
||||
}
|
||||
err = copyDir(instPath, prevDir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to copy from %s to %s: %s", prevDir, instPath, err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[TRACE] fetching %q to %q", realAddr, instPath)
|
||||
client := getter.Client{
|
||||
Src: realAddr,
|
||||
Dst: instPath,
|
||||
Pwd: instPath,
|
||||
|
||||
Mode: getter.ClientModeDir,
|
||||
|
||||
Detectors: goGetterNoDetectors, // we already did detection above
|
||||
Decompressors: goGetterDecompressors,
|
||||
Getters: goGetterGetters,
|
||||
}
|
||||
err = client.Get()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Remember where we installed this so we might reuse this directory
|
||||
// on subsequent calls to avoid re-downloading.
|
||||
g[realAddr] = instPath
|
||||
}
|
||||
|
||||
// Our subDir string can contain wildcards until this point, so that
|
||||
// e.g. a subDir of * can expand to one top-level directory in a .tar.gz
|
||||
// archive. Now that we've expanded the archive successfully we must
|
||||
// resolve that into a concrete path.
|
||||
var finalDir string
|
||||
if subDir != "" {
|
||||
finalDir, err = getter.SubdirGlob(instPath, subDir)
|
||||
log.Printf("[TRACE] expanded %q to %q", subDir, finalDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
finalDir = instPath
|
||||
}
|
||||
|
||||
// If we got this far then we have apparently succeeded in downloading
|
||||
// the requested object!
|
||||
return filepath.Clean(finalDir), nil
|
||||
}
|
||||
|
||||
// splitAddrSubdir splits the given address (which is assumed to be a
|
||||
// registry address or go-getter-style address) into a package portion
|
||||
// and a sub-directory portion.
|
||||
//
|
||||
// The package portion defines what should be downloaded and then the
|
||||
// sub-directory portion, if present, specifies a sub-directory within
|
||||
// the downloaded object (an archive, VCS repository, etc) that contains
|
||||
// the module's configuration files.
|
||||
//
|
||||
// The subDir portion will be returned as empty if no subdir separator
|
||||
// ("//") is present in the address.
|
||||
func splitAddrSubdir(addr string) (packageAddr, subDir string) {
|
||||
return getter.SourceDirSubdir(addr)
|
||||
}
|
||||
|
||||
var localSourcePrefixes = []string{
|
||||
"./",
|
||||
"../",
|
||||
".\\",
|
||||
"..\\",
|
||||
}
|
||||
|
||||
func isLocalSourceAddr(addr string) bool {
|
||||
for _, prefix := range localSourcePrefixes {
|
||||
if strings.HasPrefix(addr, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isRegistrySourceAddr(addr string) bool {
|
||||
_, err := regsrc.ParseModuleSource(addr)
|
||||
return err == nil
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// +build linux darwin openbsd netbsd solaris dragonfly
|
||||
|
||||
package init
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// lookup the inode of a file on posix systems
|
||||
func inode(path string) (uint64, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if st, ok := stat.Sys().(*syscall.Stat_t); ok {
|
||||
return st.Ino, nil
|
||||
}
|
||||
return 0, fmt.Errorf("could not determine file inode")
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// +build freebsd
|
||||
|
||||
package init
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// lookup the inode of a file on posix systems
|
||||
func inode(path string) (uint64, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if st, ok := stat.Sys().(*syscall.Stat_t); ok {
|
||||
return uint64(st.Ino), nil
|
||||
}
|
||||
return 0, fmt.Errorf("could not determine file inode")
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// +build windows
|
||||
|
||||
package init
|
||||
|
||||
// no syscall.Stat_t on windows, return 0 for inodes
|
||||
func inode(path string) (uint64, error) {
|
||||
return 0, nil
|
||||
}
|
|
@ -0,0 +1,518 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/terraform/registry"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
version "github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/terraform-config-inspect/tfconfig"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/internal/earlyconfig"
|
||||
"github.com/hashicorp/terraform/internal/modsdir"
|
||||
"github.com/hashicorp/terraform/registry/regsrc"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
type ModuleInstaller struct {
|
||||
modsDir string
|
||||
reg *registry.Client
|
||||
}
|
||||
|
||||
func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller {
|
||||
return &ModuleInstaller{
|
||||
modsDir: modsDir,
|
||||
reg: reg,
|
||||
}
|
||||
}
|
||||
|
||||
// InstallModules analyses the root module in the given directory and installs
|
||||
// all of its direct and transitive dependencies into the given modules
|
||||
// directory, which must already exist.
|
||||
//
|
||||
// Since InstallModules makes possibly-time-consuming calls to remote services,
|
||||
// a hook interface is supported to allow the caller to be notified when
|
||||
// each module is installed and, for remote modules, when downloading begins.
|
||||
// LoadConfig guarantees that two hook calls will not happen concurrently but
|
||||
// it does not guarantee any particular ordering of hook calls. This mechanism
|
||||
// is for UI feedback only and does not give the caller any control over the
|
||||
// process.
|
||||
//
|
||||
// If modules are already installed in the target directory, they will be
|
||||
// skipped unless their source address or version have changed or unless
|
||||
// the upgrade flag is set.
|
||||
//
|
||||
// InstallModules never deletes any directory, except in the case where it
|
||||
// needs to replace a directory that is already present with a newly-extracted
|
||||
// package.
|
||||
//
|
||||
// If the returned diagnostics contains errors then the module installation
|
||||
// may have wholly or partially completed. Modules must be loaded in order
|
||||
// to find their dependencies, so this function does many of the same checks
|
||||
// as LoadConfig as a side-effect.
|
||||
//
|
||||
// This function will panic if called on a loader that cannot install modules.
|
||||
// Use CanInstallModules to determine if a loader can install modules, or
|
||||
// refer to the documentation for that method for situations where module
|
||||
// installation capability is guaranteed.
|
||||
func (i *ModuleInstaller) InstallModules(rootDir string, upgrade bool, hooks InstallHooks) tfdiags.Diagnostics {
|
||||
rootMod, diags := earlyconfig.LoadModule(rootDir)
|
||||
if rootMod == nil {
|
||||
return diags
|
||||
}
|
||||
|
||||
manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to read modules manifest file",
|
||||
fmt.Sprintf("Error reading manifest for %s: %s.", i.modsDir, err),
|
||||
))
|
||||
return diags
|
||||
}
|
||||
|
||||
getter := reusingGetter{}
|
||||
instDiags := i.installDescendentModules(rootMod, rootDir, manifest, upgrade, hooks, getter)
|
||||
diags = append(diags, instDiags...)
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (i *ModuleInstaller) installDescendentModules(rootMod *tfconfig.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks InstallHooks, getter reusingGetter) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if hooks == nil {
|
||||
// Use our no-op implementation as a placeholder
|
||||
hooks = InstallHooksImpl{}
|
||||
}
|
||||
|
||||
// Create a manifest record for the root module. This will be used if
|
||||
// there are any relative-pathed modules in the root.
|
||||
manifest[""] = modsdir.Record{
|
||||
Key: "",
|
||||
Dir: rootDir,
|
||||
}
|
||||
|
||||
_, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc(
|
||||
func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) {
|
||||
|
||||
key := manifest.ModuleKey(req.Path)
|
||||
instPath := i.packageInstallPath(req.Path)
|
||||
|
||||
log.Printf("[DEBUG] Module installer: begin %s", key)
|
||||
|
||||
// First we'll check if we need to upgrade/replace an existing
|
||||
// installed module, and delete it out of the way if so.
|
||||
replace := upgrade
|
||||
if !replace {
|
||||
record, recorded := manifest[key]
|
||||
switch {
|
||||
case !recorded:
|
||||
log.Printf("[TRACE] %s is not yet installed", key)
|
||||
replace = true
|
||||
case record.SourceAddr != req.SourceAddr:
|
||||
log.Printf("[TRACE] %s source address has changed from %q to %q", key, record.SourceAddr, req.SourceAddr)
|
||||
replace = true
|
||||
case record.Version != nil && !req.VersionConstraints.Check(record.Version):
|
||||
log.Printf("[TRACE] %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraints)
|
||||
replace = true
|
||||
}
|
||||
}
|
||||
|
||||
// If we _are_ planning to replace this module, then we'll remove
|
||||
// it now so our installation code below won't conflict with any
|
||||
// existing remnants.
|
||||
if replace {
|
||||
if _, recorded := manifest[key]; recorded {
|
||||
log.Printf("[TRACE] discarding previous record of %s prior to reinstall", key)
|
||||
}
|
||||
delete(manifest, key)
|
||||
// Deleting a module invalidates all of its descendent modules too.
|
||||
keyPrefix := key + "."
|
||||
for subKey := range manifest {
|
||||
if strings.HasPrefix(subKey, keyPrefix) {
|
||||
if _, recorded := manifest[subKey]; recorded {
|
||||
log.Printf("[TRACE] also discarding downstream %s", subKey)
|
||||
}
|
||||
delete(manifest, subKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
record, recorded := manifest[key]
|
||||
if !recorded {
|
||||
// Clean up any stale cache directory that might be present.
|
||||
// If this is a local (relative) source then the dir will
|
||||
// not exist, but we'll ignore that.
|
||||
log.Printf("[TRACE] cleaning directory %s prior to install of %s", instPath, key)
|
||||
err := os.RemoveAll(instPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("[TRACE] failed to remove %s: %s", key, err)
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to remove local module cache",
|
||||
fmt.Sprintf(
|
||||
"Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s",
|
||||
instPath, err,
|
||||
),
|
||||
))
|
||||
return nil, nil, diags
|
||||
}
|
||||
} else {
|
||||
// If this module is already recorded and its root directory
|
||||
// exists then we will just load what's already there and
|
||||
// keep our existing record.
|
||||
info, err := os.Stat(record.Dir)
|
||||
if err == nil && info.IsDir() {
|
||||
mod, mDiags := earlyconfig.LoadModule(record.Dir)
|
||||
diags = diags.Append(mDiags)
|
||||
|
||||
log.Printf("[TRACE] Module installer: %s %s already installed in %s", key, record.Version, record.Dir)
|
||||
return mod, record.Version, diags
|
||||
}
|
||||
}
|
||||
|
||||
// If we get down here then it's finally time to actually install
|
||||
// the module. There are some variants to this process depending
|
||||
// on what type of module source address we have.
|
||||
switch {
|
||||
|
||||
case isLocalSourceAddr(req.SourceAddr):
|
||||
log.Printf("[TRACE] %s has local path %q", key, req.SourceAddr)
|
||||
mod, mDiags := i.installLocalModule(req, key, manifest, hooks)
|
||||
diags = append(diags, mDiags...)
|
||||
return mod, nil, diags
|
||||
|
||||
case isRegistrySourceAddr(req.SourceAddr):
|
||||
addr, err := regsrc.ParseModuleSource(req.SourceAddr)
|
||||
if err != nil {
|
||||
// Should never happen because isRegistrySourceAddr already validated
|
||||
panic(err)
|
||||
}
|
||||
log.Printf("[TRACE] %s is a registry module at %s", key, addr)
|
||||
|
||||
mod, v, mDiags := i.installRegistryModule(req, key, instPath, addr, manifest, hooks, getter)
|
||||
diags = append(diags, mDiags...)
|
||||
return mod, v, diags
|
||||
|
||||
default:
|
||||
log.Printf("[TRACE] %s address %q will be handled by go-getter", key, req.SourceAddr)
|
||||
|
||||
mod, mDiags := i.installGoGetterModule(req, key, instPath, manifest, hooks, getter)
|
||||
diags = append(diags, mDiags...)
|
||||
return mod, nil, diags
|
||||
}
|
||||
|
||||
},
|
||||
))
|
||||
diags = append(diags, cDiags...)
|
||||
|
||||
err := manifest.WriteSnapshotToDir(i.modsDir)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to update module manifest",
|
||||
fmt.Sprintf("Unable to write the module manifest file: %s", err),
|
||||
))
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks InstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
parentKey := manifest.ModuleKey(req.Parent.Path)
|
||||
parentRecord, recorded := manifest[parentKey]
|
||||
if !recorded {
|
||||
// This is indicative of a bug rather than a user-actionable error
|
||||
panic(fmt.Errorf("missing manifest record for parent module %s", parentKey))
|
||||
}
|
||||
|
||||
if len(req.VersionConstraints) != 0 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid version constraint",
|
||||
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),
|
||||
))
|
||||
}
|
||||
|
||||
// For local sources we don't actually need to modify the
|
||||
// filesystem at all because the parent already wrote
|
||||
// the files we need, and so we just load up what's already here.
|
||||
newDir := filepath.Join(parentRecord.Dir, req.SourceAddr)
|
||||
log.Printf("[TRACE] %s uses directory from parent: %s", key, newDir)
|
||||
mod, mDiags := earlyconfig.LoadModule(newDir)
|
||||
if mod == nil {
|
||||
// nil indicates missing or unreadable directory, so we'll
|
||||
// discard the returned diags and return a more specific
|
||||
// error message here.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unreadable module directory",
|
||||
fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallPos.Filename, req.CallPos.Line),
|
||||
))
|
||||
} else {
|
||||
diags = diags.Append(mDiags)
|
||||
}
|
||||
|
||||
// Note the local location in our manifest.
|
||||
manifest[key] = modsdir.Record{
|
||||
Key: key,
|
||||
Dir: newDir,
|
||||
SourceAddr: req.SourceAddr,
|
||||
}
|
||||
log.Printf("[DEBUG] Module installer: %s installed at %s", key, newDir)
|
||||
hooks.Install(key, nil, newDir)
|
||||
|
||||
return mod, diags
|
||||
}
|
||||
|
||||
func (i *ModuleInstaller) installRegistryModule(req *earlyconfig.ModuleRequest, key string, instPath string, addr *regsrc.Module, manifest modsdir.Manifest, hooks InstallHooks, getter reusingGetter) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
hostname, err := addr.SvcHost()
|
||||
if err != nil {
|
||||
// If it looks like the user was trying to use punycode then we'll generate
|
||||
// a specialized error for that case. We require the unicode form of
|
||||
// hostname so that hostnames are always human-readable in configuration
|
||||
// and punycode can't be used to hide a malicious module hostname.
|
||||
if strings.HasPrefix(addr.RawHost.Raw, "xn--") {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid module registry hostname",
|
||||
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),
|
||||
))
|
||||
} else {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid module registry hostname",
|
||||
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),
|
||||
))
|
||||
}
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
reg := i.reg
|
||||
|
||||
log.Printf("[DEBUG] %s listing available versions of %s at %s", key, addr, hostname)
|
||||
resp, err := reg.ModuleVersions(addr)
|
||||
if err != nil {
|
||||
if registry.IsModuleNotFound(err) {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Module not found",
|
||||
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),
|
||||
))
|
||||
} else {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error accessing remote module registry",
|
||||
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),
|
||||
))
|
||||
}
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
// The response might contain information about dependencies to allow us
|
||||
// to potentially optimize future requests, but we don't currently do that
|
||||
// and so for now we'll just take the first item which is guaranteed to
|
||||
// be the address we requested.
|
||||
if len(resp.Modules) < 1 {
|
||||
// Should never happen, but since this is a remote service that may
|
||||
// be implemented by third-parties we will handle it gracefully.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid response from remote module registry",
|
||||
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),
|
||||
))
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
modMeta := resp.Modules[0]
|
||||
|
||||
var latestMatch *version.Version
|
||||
var latestVersion *version.Version
|
||||
for _, mv := range modMeta.Versions {
|
||||
v, err := version.NewVersion(mv.Version)
|
||||
if err != nil {
|
||||
// Should never happen if the registry server is compliant with
|
||||
// the protocol, but we'll warn if not to assist someone who
|
||||
// might be developing a module registry server.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Invalid response from remote module registry",
|
||||
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),
|
||||
))
|
||||
continue
|
||||
}
|
||||
|
||||
// If we've found a pre-release version then we'll ignore it unless
|
||||
// it was exactly requested.
|
||||
if v.Prerelease() != "" && req.VersionConstraints.String() != v.String() {
|
||||
log.Printf("[TRACE] %s ignoring %s because it is a pre-release and was not requested exactly", key, v)
|
||||
continue
|
||||
}
|
||||
|
||||
if latestVersion == nil || v.GreaterThan(latestVersion) {
|
||||
latestVersion = v
|
||||
}
|
||||
|
||||
if req.VersionConstraints.Check(v) {
|
||||
if latestMatch == nil || v.GreaterThan(latestMatch) {
|
||||
latestMatch = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if latestVersion == nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Module has no versions",
|
||||
fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallPos.Filename, req.CallPos.Line, hostname),
|
||||
))
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
if latestMatch == nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unresolvable module version constraint",
|
||||
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),
|
||||
))
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
// Report up to the caller that we're about to start downloading.
|
||||
packageAddr, _ := splitAddrSubdir(req.SourceAddr)
|
||||
hooks.Download(key, packageAddr, latestMatch)
|
||||
|
||||
// If we manage to get down here then we've found a suitable version to
|
||||
// install, so we need to ask the registry where we should download it from.
|
||||
// The response to this is a go-getter-style address string.
|
||||
dlAddr, err := reg.ModuleLocation(addr, latestMatch.String())
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err)
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Invalid response from remote module registry",
|
||||
fmt.Sprintf("The remote registry at %s failed to return a download URL for %s %s.", hostname, addr, latestMatch),
|
||||
))
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] %s %s %s is available at %q", key, addr, latestMatch, dlAddr)
|
||||
|
||||
modDir, err := getter.getWithGoGetter(instPath, dlAddr)
|
||||
if err != nil {
|
||||
// Errors returned by go-getter have very inconsistent quality as
|
||||
// end-user error messages, but for now we're accepting that because
|
||||
// we have no way to recognize any specific errors to improve them
|
||||
// and masking the error entirely would hide valuable diagnostic
|
||||
// information from the user.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to download module",
|
||||
fmt.Sprintf("Error attempting to download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, dlAddr, err),
|
||||
))
|
||||
return nil, nil, diags
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] %s %q was downloaded to %s", key, dlAddr, modDir)
|
||||
|
||||
if addr.RawSubmodule != "" {
|
||||
// Append the user's requested subdirectory to any subdirectory that
|
||||
// was implied by any of the nested layers we expanded within go-getter.
|
||||
modDir = filepath.Join(modDir, addr.RawSubmodule)
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] %s should now be at %s", key, modDir)
|
||||
|
||||
// Finally we are ready to try actually loading the module.
|
||||
mod, mDiags := earlyconfig.LoadModule(modDir)
|
||||
if mod == nil {
|
||||
// nil indicates missing or unreadable directory, so we'll
|
||||
// discard the returned diags and return a more specific
|
||||
// error message here. For registry modules this actually
|
||||
// indicates a bug in the code above, since it's not the
|
||||
// user's responsibility to create the directory in this case.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unreadable module directory",
|
||||
fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir),
|
||||
))
|
||||
} else {
|
||||
diags = append(diags, mDiags...)
|
||||
}
|
||||
|
||||
// Note the local location in our manifest.
|
||||
manifest[key] = modsdir.Record{
|
||||
Key: key,
|
||||
Version: latestMatch,
|
||||
Dir: modDir,
|
||||
SourceAddr: req.SourceAddr,
|
||||
}
|
||||
log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
|
||||
hooks.Install(key, latestMatch, modDir)
|
||||
|
||||
return mod, latestMatch, diags
|
||||
}
|
||||
|
||||
func (i *ModuleInstaller) installGoGetterModule(req *earlyconfig.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks InstallHooks, getter reusingGetter) (*tfconfig.Module, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
// Report up to the caller that we're about to start downloading.
|
||||
packageAddr, _ := splitAddrSubdir(req.SourceAddr)
|
||||
hooks.Download(key, packageAddr, nil)
|
||||
|
||||
modDir, err := getter.getWithGoGetter(instPath, req.SourceAddr)
|
||||
if err != nil {
|
||||
// Errors returned by go-getter have very inconsistent quality as
|
||||
// end-user error messages, but for now we're accepting that because
|
||||
// we have no way to recognize any specific errors to improve them
|
||||
// and masking the error entirely would hide valuable diagnostic
|
||||
// information from the user.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to download module",
|
||||
fmt.Sprintf("Error attempting to download module %q (%s:%d) source code from %q: %s", req.Name, req.CallPos.Filename, req.CallPos.Line, packageAddr, err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] %s %q was downloaded to %s", key, req.SourceAddr, modDir)
|
||||
|
||||
mod, mDiags := earlyconfig.LoadModule(modDir)
|
||||
if mod == nil {
|
||||
// nil indicates missing or unreadable directory, so we'll
|
||||
// discard the returned diags and return a more specific
|
||||
// error message here. For go-getter modules this actually
|
||||
// indicates a bug in the code above, since it's not the
|
||||
// user's responsibility to create the directory in this case.
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unreadable module directory",
|
||||
fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir),
|
||||
))
|
||||
} else {
|
||||
diags = append(diags, mDiags...)
|
||||
}
|
||||
|
||||
// Note the local location in our manifest.
|
||||
manifest[key] = modsdir.Record{
|
||||
Key: key,
|
||||
Dir: modDir,
|
||||
SourceAddr: req.SourceAddr,
|
||||
}
|
||||
log.Printf("[DEBUG] Module installer: %s installed at %s", key, modDir)
|
||||
hooks.Install(key, nil, modDir)
|
||||
|
||||
return mod, diags
|
||||
}
|
||||
|
||||
func (i *ModuleInstaller) packageInstallPath(modulePath addrs.Module) string {
|
||||
return filepath.Join(i.modsDir, strings.Join(modulePath, "."))
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
version "github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
// InstallHooks is an interface used to provide notifications about the
|
||||
// installation process being orchestrated by InstallModules.
|
||||
//
|
||||
// This interface may have new methods added in future, so implementers should
|
||||
// embed InstallHooksImpl to get no-op implementations of any unimplemented
|
||||
// methods.
|
||||
type InstallHooks interface {
|
||||
// Download is called for modules that are retrieved from a remote source
|
||||
// before that download begins, to allow a caller to give feedback
|
||||
// on progress through a possibly-long sequence of downloads.
|
||||
Download(moduleAddr, packageAddr string, version *version.Version)
|
||||
|
||||
// Install is called for each module that is installed, even if it did
|
||||
// not need to be downloaded from a remote source.
|
||||
Install(moduleAddr string, version *version.Version, localPath string)
|
||||
}
|
||||
|
||||
// InstallHooksImpl is a do-nothing implementation of InstallHooks that
|
||||
// can be embedded in another implementation struct to allow only partial
|
||||
// implementation of the interface.
|
||||
type InstallHooksImpl struct {
|
||||
}
|
||||
|
||||
func (h InstallHooksImpl) Download(moduleAddr, packageAddr string, version *version.Version) {
|
||||
}
|
||||
|
||||
func (h InstallHooksImpl) Install(moduleAddr string, version *version.Version, localPath string) {
|
||||
}
|
||||
|
||||
var _ InstallHooks = InstallHooksImpl{}
|
Loading…
Reference in New Issue