github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/initwd/from_module.go (about)

     1  package initwd
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"log"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/terraform/internal/copy"
    13  	"github.com/hashicorp/terraform/internal/earlyconfig"
    14  	"github.com/hashicorp/terraform/internal/getmodules"
    15  
    16  	version "github.com/hashicorp/go-version"
    17  	"github.com/hashicorp/terraform-config-inspect/tfconfig"
    18  	"github.com/hashicorp/terraform/internal/modsdir"
    19  	"github.com/hashicorp/terraform/internal/registry"
    20  	"github.com/hashicorp/terraform/internal/tfdiags"
    21  )
    22  
    23  const initFromModuleRootCallName = "root"
    24  const initFromModuleRootKeyPrefix = initFromModuleRootCallName + "."
    25  
    26  // DirFromModule populates the given directory (which must exist and be
    27  // empty) with the contents of the module at the given source address.
    28  //
    29  // It does this by installing the given module and all of its descendent
    30  // modules in a temporary root directory and then copying the installed
    31  // files into suitable locations. As a consequence, any diagnostics it
    32  // generates will reveal the location of this temporary directory to the
    33  // user.
    34  //
    35  // This rather roundabout installation approach is taken to ensure that
    36  // installation proceeds in a manner identical to normal module installation.
    37  //
    38  // If the given source address specifies a sub-directory of the given
    39  // package then only the sub-directory and its descendents will be copied
    40  // into the given root directory, which will cause any relative module
    41  // references using ../ from that module to be unresolvable. Error diagnostics
    42  // are produced in that case, to prompt the user to rewrite the source strings
    43  // to be absolute references to the original remote module.
    44  func DirFromModule(rootDir, modulesDir, sourceAddr string, reg *registry.Client, hooks ModuleInstallHooks) tfdiags.Diagnostics {
    45  	var diags tfdiags.Diagnostics
    46  
    47  	// The way this function works is pretty ugly, but we accept it because
    48  	// -from-module is a less important case than normal module installation
    49  	// and so it's better to keep this ugly complexity out here rather than
    50  	// adding even more complexity to the normal module installer.
    51  
    52  	// The target directory must exist but be empty.
    53  	{
    54  		entries, err := ioutil.ReadDir(rootDir)
    55  		if err != nil {
    56  			if os.IsNotExist(err) {
    57  				diags = diags.Append(tfdiags.Sourceless(
    58  					tfdiags.Error,
    59  					"Target directory does not exist",
    60  					fmt.Sprintf("Cannot initialize non-existent directory %s.", rootDir),
    61  				))
    62  			} else {
    63  				diags = diags.Append(tfdiags.Sourceless(
    64  					tfdiags.Error,
    65  					"Failed to read target directory",
    66  					fmt.Sprintf("Error reading %s to ensure it is empty: %s.", rootDir, err),
    67  				))
    68  			}
    69  			return diags
    70  		}
    71  		haveEntries := false
    72  		for _, entry := range entries {
    73  			if entry.Name() == "." || entry.Name() == ".." || entry.Name() == ".terraform" {
    74  				continue
    75  			}
    76  			haveEntries = true
    77  		}
    78  		if haveEntries {
    79  			diags = diags.Append(tfdiags.Sourceless(
    80  				tfdiags.Error,
    81  				"Can't populate non-empty directory",
    82  				fmt.Sprintf("The target directory %s is not empty, so it cannot be initialized with the -from-module=... option.", rootDir),
    83  			))
    84  			return diags
    85  		}
    86  	}
    87  
    88  	instDir := filepath.Join(rootDir, ".terraform/init-from-module")
    89  	inst := NewModuleInstaller(instDir, reg)
    90  	log.Printf("[DEBUG] installing modules in %s to initialize working directory from %q", instDir, sourceAddr)
    91  	os.RemoveAll(instDir) // if this fails then we'll fail on MkdirAll below too
    92  	err := os.MkdirAll(instDir, os.ModePerm)
    93  	if err != nil {
    94  		diags = diags.Append(tfdiags.Sourceless(
    95  			tfdiags.Error,
    96  			"Failed to create temporary directory",
    97  			fmt.Sprintf("Failed to create temporary directory %s: %s.", instDir, err),
    98  		))
    99  		return diags
   100  	}
   101  
   102  	instManifest := make(modsdir.Manifest)
   103  	retManifest := make(modsdir.Manifest)
   104  
   105  	fakeFilename := fmt.Sprintf("-from-module=%q", sourceAddr)
   106  	fakePos := tfconfig.SourcePos{
   107  		Filename: fakeFilename,
   108  		Line:     1,
   109  	}
   110  
   111  	// -from-module allows relative paths but it's different than a normal
   112  	// module address where it'd be resolved relative to the module call
   113  	// (which is synthetic, here.) To address this, we'll just patch up any
   114  	// relative paths to be absolute paths before we run, ensuring we'll
   115  	// get the right result. This also, as an important side-effect, ensures
   116  	// that the result will be "downloaded" with go-getter (copied from the
   117  	// source location), rather than just recorded as a relative path.
   118  	{
   119  		maybePath := filepath.ToSlash(sourceAddr)
   120  		if maybePath == "." || strings.HasPrefix(maybePath, "./") || strings.HasPrefix(maybePath, "../") {
   121  			if wd, err := os.Getwd(); err == nil {
   122  				sourceAddr = filepath.Join(wd, sourceAddr)
   123  				log.Printf("[TRACE] -from-module relative path rewritten to absolute path %s", sourceAddr)
   124  			}
   125  		}
   126  	}
   127  
   128  	// Now we need to create an artificial root module that will seed our
   129  	// installation process.
   130  	fakeRootModule := &tfconfig.Module{
   131  		ModuleCalls: map[string]*tfconfig.ModuleCall{
   132  			initFromModuleRootCallName: {
   133  				Name:   initFromModuleRootCallName,
   134  				Source: sourceAddr,
   135  				Pos:    fakePos,
   136  			},
   137  		},
   138  	}
   139  
   140  	// wrapHooks filters hook notifications to only include Download calls
   141  	// and to trim off the initFromModuleRootCallName prefix. We'll produce
   142  	// our own Install notifications directly below.
   143  	wrapHooks := installHooksInitDir{
   144  		Wrapped: hooks,
   145  	}
   146  	fetcher := getmodules.NewPackageFetcher()
   147  	_, instDiags := inst.installDescendentModules(fakeRootModule, rootDir, instManifest, true, wrapHooks, fetcher)
   148  	diags = append(diags, instDiags...)
   149  	if instDiags.HasErrors() {
   150  		return diags
   151  	}
   152  
   153  	// If all of that succeeded then we'll now migrate what was installed
   154  	// into the final directory structure.
   155  	err = os.MkdirAll(modulesDir, os.ModePerm)
   156  	if err != nil {
   157  		diags = diags.Append(tfdiags.Sourceless(
   158  			tfdiags.Error,
   159  			"Failed to create local modules directory",
   160  			fmt.Sprintf("Failed to create modules directory %s: %s.", modulesDir, err),
   161  		))
   162  		return diags
   163  	}
   164  
   165  	recordKeys := make([]string, 0, len(instManifest))
   166  	for k := range instManifest {
   167  		recordKeys = append(recordKeys, k)
   168  	}
   169  	sort.Strings(recordKeys)
   170  
   171  	for _, recordKey := range recordKeys {
   172  		record := instManifest[recordKey]
   173  
   174  		if record.Key == initFromModuleRootCallName {
   175  			// We've found the module the user requested, which we must
   176  			// now copy into rootDir so it can be used directly.
   177  			log.Printf("[TRACE] copying new root module from %s to %s", record.Dir, rootDir)
   178  			err := copy.CopyDir(rootDir, record.Dir)
   179  			if err != nil {
   180  				diags = diags.Append(tfdiags.Sourceless(
   181  					tfdiags.Error,
   182  					"Failed to copy root module",
   183  					fmt.Sprintf("Error copying root module %q from %s to %s: %s.", sourceAddr, record.Dir, rootDir, err),
   184  				))
   185  				continue
   186  			}
   187  
   188  			// We'll try to load the newly-copied module here just so we can
   189  			// sniff for any module calls that ../ out of the root directory
   190  			// and must thus be rewritten to be absolute addresses again.
   191  			// For now we can't do this rewriting automatically, but we'll
   192  			// generate an error to help the user do it manually.
   193  			mod, _ := earlyconfig.LoadModule(rootDir) // ignore diagnostics since we're just doing value-add here anyway
   194  			if mod != nil {
   195  				for _, mc := range mod.ModuleCalls {
   196  					if pathTraversesUp(mc.Source) {
   197  						packageAddr, givenSubdir := getmodules.SplitPackageSubdir(sourceAddr)
   198  						newSubdir := filepath.Join(givenSubdir, mc.Source)
   199  						if pathTraversesUp(newSubdir) {
   200  							// This should never happen in any reasonable
   201  							// configuration since this suggests a path that
   202  							// traverses up out of the package root. We'll just
   203  							// ignore this, since we'll fail soon enough anyway
   204  							// trying to resolve this path when this module is
   205  							// loaded.
   206  							continue
   207  						}
   208  
   209  						var newAddr = packageAddr
   210  						if newSubdir != "" {
   211  							newAddr = fmt.Sprintf("%s//%s", newAddr, filepath.ToSlash(newSubdir))
   212  						}
   213  						diags = diags.Append(tfdiags.Sourceless(
   214  							tfdiags.Error,
   215  							"Root module references parent directory",
   216  							fmt.Sprintf("The requested module %q refers to a module via its parent directory. To use this as a new root module this source string must be rewritten as a remote source address, such as %q.", sourceAddr, newAddr),
   217  						))
   218  						continue
   219  					}
   220  				}
   221  			}
   222  
   223  			retManifest[""] = modsdir.Record{
   224  				Key: "",
   225  				Dir: rootDir,
   226  			}
   227  			continue
   228  		}
   229  
   230  		if !strings.HasPrefix(record.Key, initFromModuleRootKeyPrefix) {
   231  			// Ignore the *real* root module, whose key is empty, since
   232  			// we're only interested in the module named "root" and its
   233  			// descendents.
   234  			continue
   235  		}
   236  
   237  		newKey := record.Key[len(initFromModuleRootKeyPrefix):]
   238  		instPath := filepath.Join(modulesDir, newKey)
   239  		tempPath := filepath.Join(instDir, record.Key)
   240  
   241  		// tempPath won't be present for a module that was installed from
   242  		// a relative path, so in that case we just record the installation
   243  		// directory and assume it was already copied into place as part
   244  		// of its parent.
   245  		if _, err := os.Stat(tempPath); err != nil {
   246  			if !os.IsNotExist(err) {
   247  				diags = diags.Append(tfdiags.Sourceless(
   248  					tfdiags.Error,
   249  					"Failed to stat temporary module install directory",
   250  					fmt.Sprintf("Error from stat %s for module %s: %s.", instPath, newKey, err),
   251  				))
   252  				continue
   253  			}
   254  
   255  			var parentKey string
   256  			if lastDot := strings.LastIndexByte(newKey, '.'); lastDot != -1 {
   257  				parentKey = newKey[:lastDot]
   258  			}
   259  
   260  			var parentOld modsdir.Record
   261  			// "" is the root module; all other modules get `root.` added as a prefix
   262  			if parentKey == "" {
   263  				parentOld = instManifest[parentKey]
   264  			} else {
   265  				parentOld = instManifest[initFromModuleRootKeyPrefix+parentKey]
   266  			}
   267  			parentNew := retManifest[parentKey]
   268  
   269  			// We need to figure out which portion of our directory is the
   270  			// parent package path and which portion is the subdirectory
   271  			// under that.
   272  			var baseDirRel string
   273  			baseDirRel, err = filepath.Rel(parentOld.Dir, record.Dir)
   274  			if err != nil {
   275  				// This error may occur when installing a local module with a
   276  				// relative path, for e.g. if the source is in a directory above
   277  				// the destination ("../")
   278  				if parentOld.Dir == "." {
   279  					absDir, err := filepath.Abs(parentOld.Dir)
   280  					if err != nil {
   281  						diags = diags.Append(tfdiags.Sourceless(
   282  							tfdiags.Error,
   283  							"Failed to determine module install directory",
   284  							fmt.Sprintf("Error determine relative source directory for module %s: %s.", newKey, err),
   285  						))
   286  						continue
   287  					}
   288  					baseDirRel, err = filepath.Rel(absDir, record.Dir)
   289  					if err != nil {
   290  						diags = diags.Append(tfdiags.Sourceless(
   291  							tfdiags.Error,
   292  							"Failed to determine relative module source location",
   293  							fmt.Sprintf("Error determining relative source for module %s: %s.", newKey, err),
   294  						))
   295  						continue
   296  					}
   297  				} else {
   298  					diags = diags.Append(tfdiags.Sourceless(
   299  						tfdiags.Error,
   300  						"Failed to determine relative module source location",
   301  						fmt.Sprintf("Error determining relative source for module %s: %s.", newKey, err),
   302  					))
   303  				}
   304  			}
   305  
   306  			newDir := filepath.Join(parentNew.Dir, baseDirRel)
   307  			log.Printf("[TRACE] relative reference for %s rewritten from %s to %s", newKey, record.Dir, newDir)
   308  			newRecord := record // shallow copy
   309  			newRecord.Dir = newDir
   310  			newRecord.Key = newKey
   311  			retManifest[newKey] = newRecord
   312  			hooks.Install(newRecord.Key, newRecord.Version, newRecord.Dir)
   313  			continue
   314  		}
   315  
   316  		err = os.MkdirAll(instPath, os.ModePerm)
   317  		if err != nil {
   318  			diags = diags.Append(tfdiags.Sourceless(
   319  				tfdiags.Error,
   320  				"Failed to create module install directory",
   321  				fmt.Sprintf("Error creating directory %s for module %s: %s.", instPath, newKey, err),
   322  			))
   323  			continue
   324  		}
   325  
   326  		// We copy rather than "rename" here because renaming between directories
   327  		// can be tricky in edge-cases like network filesystems, etc.
   328  		log.Printf("[TRACE] copying new module %s from %s to %s", newKey, record.Dir, instPath)
   329  		err := copy.CopyDir(instPath, tempPath)
   330  		if err != nil {
   331  			diags = diags.Append(tfdiags.Sourceless(
   332  				tfdiags.Error,
   333  				"Failed to copy descendent module",
   334  				fmt.Sprintf("Error copying module %q from %s to %s: %s.", newKey, tempPath, rootDir, err),
   335  			))
   336  			continue
   337  		}
   338  
   339  		subDir, err := filepath.Rel(tempPath, record.Dir)
   340  		if err != nil {
   341  			// Should never happen, because we constructed both directories
   342  			// from the same base and so they must have a common prefix.
   343  			panic(err)
   344  		}
   345  
   346  		newRecord := record // shallow copy
   347  		newRecord.Dir = filepath.Join(instPath, subDir)
   348  		newRecord.Key = newKey
   349  		retManifest[newKey] = newRecord
   350  		hooks.Install(newRecord.Key, newRecord.Version, newRecord.Dir)
   351  	}
   352  
   353  	retManifest.WriteSnapshotToDir(modulesDir)
   354  	if err != nil {
   355  		diags = diags.Append(tfdiags.Sourceless(
   356  			tfdiags.Error,
   357  			"Failed to write module manifest",
   358  			fmt.Sprintf("Error writing module manifest: %s.", err),
   359  		))
   360  	}
   361  
   362  	if !diags.HasErrors() {
   363  		// Try to clean up our temporary directory, but don't worry if we don't
   364  		// succeed since it shouldn't hurt anything.
   365  		os.RemoveAll(instDir)
   366  	}
   367  
   368  	return diags
   369  }
   370  
   371  func pathTraversesUp(path string) bool {
   372  	return strings.HasPrefix(filepath.ToSlash(path), "../")
   373  }
   374  
   375  // installHooksInitDir is an adapter wrapper for an InstallHooks that
   376  // does some fakery to make downloads look like they are happening in their
   377  // final locations, rather than in the temporary loader we use.
   378  //
   379  // It also suppresses "Install" calls entirely, since InitDirFromModule
   380  // does its own installation steps after the initial installation pass
   381  // has completed.
   382  type installHooksInitDir struct {
   383  	Wrapped ModuleInstallHooks
   384  	ModuleInstallHooksImpl
   385  }
   386  
   387  func (h installHooksInitDir) Download(moduleAddr, packageAddr string, version *version.Version) {
   388  	if !strings.HasPrefix(moduleAddr, initFromModuleRootKeyPrefix) {
   389  		// We won't announce the root module, since hook implementations
   390  		// don't expect to see that and the caller will usually have produced
   391  		// its own user-facing notification about what it's doing anyway.
   392  		return
   393  	}
   394  
   395  	trimAddr := moduleAddr[len(initFromModuleRootKeyPrefix):]
   396  	h.Wrapped.Download(trimAddr, packageAddr, version)
   397  }