github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/initwd/from_module.go (about)

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