github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/initwd/from_module.go (about)

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