2018-02-13 23:40:53 +01:00
package configload
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/terraform/configs"
2018-02-14 01:36:36 +01:00
"github.com/hashicorp/terraform/registry"
2018-02-13 23:40:53 +01:00
"github.com/hashicorp/terraform/registry/regsrc"
)
// InstallModules analyses the root module in the given directory and installs
// all of its direct and transitive dependencies into the loader's 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.
2018-02-14 23:35:03 +01:00
//
// 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.
2018-02-13 23:40:53 +01:00
func ( l * Loader ) InstallModules ( rootDir string , upgrade bool , hooks InstallHooks ) hcl . Diagnostics {
2018-02-14 23:35:03 +01:00
if ! l . CanInstallModules ( ) {
panic ( fmt . Errorf ( "InstallModules called on loader that cannot install modules" ) )
}
2018-02-13 23:40:53 +01:00
rootMod , diags := l . parser . LoadConfigDir ( rootDir )
if rootMod == nil {
return diags
}
2018-02-15 05:13:35 +01:00
instDiags := l . installDescendentModules ( rootMod , rootDir , upgrade , hooks )
diags = append ( diags , instDiags ... )
return diags
}
func ( l * Loader ) installDescendentModules ( rootMod * configs . Module , rootDir string , upgrade bool , hooks InstallHooks ) hcl . Diagnostics {
var diags hcl . Diagnostics
2018-02-13 23:40:53 +01:00
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.
l . modules . manifest [ "" ] = moduleRecord {
Key : "" ,
Dir : rootDir ,
}
_ , cDiags := configs . BuildConfig ( rootMod , configs . ModuleWalkerFunc (
func ( req * configs . ModuleRequest ) ( * configs . Module , * version . Version , hcl . Diagnostics ) {
key := manifestKey ( req . Path )
instPath := l . 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 := l . modules . 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 . VersionConstraint . Required . Check ( record . Version ) :
log . Printf ( "[TRACE] %s version %s no longer compatible with constraints %s" , key , record . Version , req . VersionConstraint . Required )
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 := l . modules . manifest [ key ] ; recorded {
log . Printf ( "[TRACE] discarding previous record of %s prior to reinstall" , key )
}
delete ( l . modules . manifest , key )
// Deleting a module invalidates all of its descendent modules too.
keyPrefix := key + "."
for subKey := range l . modules . manifest {
if strings . HasPrefix ( subKey , keyPrefix ) {
if _ , recorded := l . modules . manifest [ subKey ] ; recorded {
log . Printf ( "[TRACE] also discarding downstream %s" , subKey )
}
delete ( l . modules . manifest , subKey )
}
}
}
record , recorded := l . modules . 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 := l . modules . FS . RemoveAll ( instPath )
if err != nil && ! os . IsNotExist ( err ) {
log . Printf ( "[TRACE] failed to remove %s: %s" , key , err )
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Failed to remove local module cache" ,
Detail : fmt . Sprintf (
"Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s" ,
instPath , err ,
) ,
Subject : & req . CallRange ,
} )
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 := l . modules . FS . Stat ( record . Dir )
if err == nil && info . IsDir ( ) {
mod , mDiags := l . parser . LoadConfigDir ( record . Dir )
diags = append ( diags , 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 := l . installLocalModule ( req , key , 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 )
2018-02-14 01:36:36 +01:00
mod , v , mDiags := l . installRegistryModule ( req , key , instPath , addr , hooks )
diags = append ( diags , mDiags ... )
return mod , v , diags
2018-02-13 23:40:53 +01:00
default :
2018-02-14 01:36:36 +01:00
log . Printf ( "[TRACE] %s address %q will be handled by go-getter" , key , req . SourceAddr )
2018-02-13 23:40:53 +01:00
2018-02-14 01:36:36 +01:00
mod , mDiags := l . installGoGetterModule ( req , key , instPath , hooks )
diags = append ( diags , mDiags ... )
return mod , nil , diags
2018-02-13 23:40:53 +01:00
}
} ,
) )
diags = append ( diags , cDiags ... )
err := l . modules . writeModuleManifestSnapshot ( )
if err != nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Failed to update module manifest" ,
Detail : fmt . Sprintf ( "Unable to write the module manifest file: %s" , err ) ,
} )
}
return diags
}
2018-02-14 23:35:03 +01:00
// CanInstallModules returns true if InstallModules can be used with this
// loader.
//
// Loaders created with NewLoader can always install modules. Loaders created
// from plan files (where the configuration is embedded in the plan file itself)
// cannot install modules, because the plan file is read-only.
func ( l * Loader ) CanInstallModules ( ) bool {
return l . modules . CanInstall
}
2018-02-13 23:40:53 +01:00
func ( l * Loader ) installLocalModule ( req * configs . ModuleRequest , key string , hooks InstallHooks ) ( * configs . Module , hcl . Diagnostics ) {
var diags hcl . Diagnostics
parentKey := manifestKey ( req . Parent . Path )
parentRecord , recorded := l . modules . 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 . VersionConstraint . Required ) != 0 {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid version constraint" ,
Detail : "A version constraint cannot be applied to a module at a relative local path." ,
Subject : & req . VersionConstraint . DeclRange ,
} )
}
// 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 := l . parser . LoadConfigDir ( 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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unreadable module directory" ,
Detail : fmt . Sprintf ( "The directory %s could not be read." , newDir ) ,
Subject : & req . SourceAddrRange ,
} )
} else {
diags = append ( diags , mDiags ... )
}
// Note the local location in our manifest.
l . modules . manifest [ key ] = moduleRecord {
Key : key ,
Dir : newDir ,
SourceAddr : req . SourceAddr ,
}
2018-02-14 01:36:36 +01:00
log . Printf ( "[DEBUG] Module installer: %s installed at %s" , key , newDir )
2018-02-13 23:40:53 +01:00
hooks . Install ( key , nil , newDir )
return mod , diags
}
2018-02-14 01:36:36 +01:00
func ( l * Loader ) installRegistryModule ( req * configs . ModuleRequest , key string , instPath string , addr * regsrc . Module , hooks InstallHooks ) ( * configs . Module , * version . Version , hcl . Diagnostics ) {
var diags hcl . 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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid module registry hostname" ,
Detail : "The hostname portion of this source address is not an acceptable hostname. Internationalized domain names must be given in unicode form rather than ASCII (\"punycode\") form." ,
Subject : & req . SourceAddrRange ,
} )
} else {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid module registry hostname" ,
Detail : "The hostname portion of this source address is not a valid hostname." ,
Subject : & req . SourceAddrRange ,
} )
}
return nil , nil , diags
}
reg := l . modules . Registry
log . Printf ( "[DEBUG] %s listing available versions of %s at %s" , key , addr , hostname )
resp , err := reg . Versions ( addr )
if err != nil {
if registry . IsModuleNotFound ( err ) {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Module not found" ,
Detail : fmt . Sprintf ( "The specified module could not be found in the module registry at %s." , hostname ) ,
Subject : & req . SourceAddrRange ,
} )
} else {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Error accessing remote module registry" ,
Detail : fmt . Sprintf ( "Failed to retrieve available versions for this module from %s: %s." , hostname , err ) ,
Subject : & req . SourceAddrRange ,
} )
}
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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid response from remote module registry" ,
Detail : fmt . Sprintf ( "The registry at %s returned an invalid response when Terraform requested available versions for this module." , hostname ) ,
Subject : & req . SourceAddrRange ,
} )
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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagWarning ,
Summary : "Invalid response from remote module registry" ,
Detail : fmt . Sprintf ( "The registry at %s returned an invalid version string %q for this module, which Terraform ignored." , hostname , mv . Version ) ,
Subject : & req . SourceAddrRange ,
} )
continue
}
// If we've found a pre-release version then we'll ignore it unless
// it was exactly requested.
if v . Prerelease ( ) != "" && req . VersionConstraint . Required . 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 . VersionConstraint . Required . Check ( v ) {
if latestMatch == nil || v . GreaterThan ( latestMatch ) {
latestMatch = v
}
}
}
if latestVersion == nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Module has no versions" ,
Detail : fmt . Sprintf ( "The specified module does not have any available versions." ) ,
Subject : & req . SourceAddrRange ,
} )
return nil , nil , diags
}
if latestMatch == nil {
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unresolvable module version constraint" ,
Detail : fmt . Sprintf ( "There is no available version of %q that matches the given version constraint. The newest available version is %s." , addr , latestVersion ) ,
Subject : & req . VersionConstraint . DeclRange ,
} )
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 . Location ( addr , latestMatch . String ( ) )
if err != nil {
log . Printf ( "[ERROR] %s from %s %s: %s" , key , addr , latestMatch , err )
diags = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid response from remote module registry" ,
Detail : fmt . Sprintf ( "The remote registry at %s failed to return a download URL for %s %s." , hostname , addr , latestMatch ) ,
Subject : & req . VersionConstraint . DeclRange ,
} )
return nil , nil , diags
}
log . Printf ( "[TRACE] %s %s %s is available at %q" , key , addr , latestMatch , dlAddr )
modDir , err := 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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Failed to download module" ,
Detail : fmt . Sprintf ( "Error attempting to download module source code from %q: %s" , dlAddr , err ) ,
Subject : & req . CallRange ,
} )
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 := l . parser . LoadConfigDir ( 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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unreadable module directory" ,
Detail : fmt . Sprintf ( "The directory %s could not be read. This is a bug in Terraform and should be reported." , modDir ) ,
Subject : & req . CallRange ,
} )
} else {
diags = append ( diags , mDiags ... )
}
// Note the local location in our manifest.
l . modules . manifest [ key ] = moduleRecord {
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 ( l * Loader ) installGoGetterModule ( req * configs . ModuleRequest , key string , instPath string , hooks InstallHooks ) ( * configs . Module , hcl . Diagnostics ) {
var diags hcl . Diagnostics
// Report up to the caller that we're about to start downloading.
packageAddr , _ := splitAddrSubdir ( req . SourceAddr )
hooks . Download ( key , packageAddr , nil )
modDir , err := 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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Failed to download module" ,
Detail : fmt . Sprintf ( "Error attempting to download module source code from %q: %s" , packageAddr , err ) ,
Subject : & req . SourceAddrRange ,
} )
return nil , diags
}
log . Printf ( "[TRACE] %s %q was downloaded to %s" , key , req . SourceAddr , modDir )
mod , mDiags := l . parser . LoadConfigDir ( 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 = append ( diags , & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unreadable module directory" ,
Detail : fmt . Sprintf ( "The directory %s could not be read. This is a bug in Terraform and should be reported." , modDir ) ,
Subject : & req . CallRange ,
} )
} else {
diags = append ( diags , mDiags ... )
}
// Note the local location in our manifest.
l . modules . manifest [ key ] = moduleRecord {
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
}
2018-02-13 23:40:53 +01:00
func ( l * Loader ) packageInstallPath ( modulePath [ ] string ) string {
return filepath . Join ( l . modules . Dir , strings . Join ( modulePath , "." ) )
}