github.com/partitio/terraform@v0.11.12-beta1/configs/configload/loader_init_from_module.go (about)

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