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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package getmodules
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"log"
    10  	"os"
    11  
    12  	cleanhttp "github.com/hashicorp/go-cleanhttp"
    13  	getter "github.com/hashicorp/go-getter"
    14  	"github.com/terramate-io/tf/copy"
    15  )
    16  
    17  // We configure our own go-getter detector and getter sets here, because
    18  // the set of sources we support is part of Terraform's documentation and
    19  // so we don't want any new sources introduced in go-getter to sneak in here
    20  // and work even though they aren't documented. This also insulates us from
    21  // any meddling that might be done by other go-getter callers linked into our
    22  // executable.
    23  //
    24  // Note that over time we've found go-getter's design to be not wholly fit
    25  // for Terraform's purposes in various ways, and so we're continuing to use
    26  // it here because our backward compatibility with earlier versions depends
    27  // on it, but we use go-getter very carefully and always only indirectly via
    28  // the public API of this package so that we can get the subset of the
    29  // go-getter functionality we need while working around some of the less
    30  // helpful parts of its design. See the comments in various other functions
    31  // in this package which call into go-getter for more information on what
    32  // tradeoffs we're making here.
    33  
    34  var goGetterDetectors = []getter.Detector{
    35  	new(getter.GitHubDetector),
    36  	new(getter.GitDetector),
    37  
    38  	// Because historically BitBucket supported both Git and Mercurial
    39  	// repositories but used the same repository URL syntax for both,
    40  	// this detector takes the unusual step of actually reaching out
    41  	// to the BitBucket API to recognize the repository type. That
    42  	// means there's the possibility of an outgoing network request
    43  	// inside what is otherwise normally just a local string manipulation
    44  	// operation, but we continue to accept this for now.
    45  	//
    46  	// Perhaps a future version of go-getter will remove the check now
    47  	// that BitBucket only supports Git anyway. Aside from this historical
    48  	// exception, we should avoid adding any new detectors that make network
    49  	// requests in here, and limit ourselves only to ones that can operate
    50  	// entirely through local string manipulation.
    51  	new(getter.BitBucketDetector),
    52  
    53  	new(getter.GCSDetector),
    54  	new(getter.S3Detector),
    55  	new(fileDetector),
    56  }
    57  
    58  var goGetterNoDetectors = []getter.Detector{}
    59  
    60  var goGetterDecompressors = map[string]getter.Decompressor{
    61  	"bz2": new(getter.Bzip2Decompressor),
    62  	"gz":  new(getter.GzipDecompressor),
    63  	"xz":  new(getter.XzDecompressor),
    64  	"zip": new(getter.ZipDecompressor),
    65  
    66  	"tar.bz2":  new(getter.TarBzip2Decompressor),
    67  	"tar.tbz2": new(getter.TarBzip2Decompressor),
    68  
    69  	"tar.gz": new(getter.TarGzipDecompressor),
    70  	"tgz":    new(getter.TarGzipDecompressor),
    71  
    72  	"tar.xz": new(getter.TarXzDecompressor),
    73  	"txz":    new(getter.TarXzDecompressor),
    74  }
    75  
    76  var goGetterGetters = map[string]getter.Getter{
    77  	"file":  new(getter.FileGetter),
    78  	"gcs":   new(getter.GCSGetter),
    79  	"git":   new(getter.GitGetter),
    80  	"hg":    new(getter.HgGetter),
    81  	"s3":    new(getter.S3Getter),
    82  	"http":  getterHTTPGetter,
    83  	"https": getterHTTPGetter,
    84  }
    85  
    86  var getterHTTPClient = cleanhttp.DefaultClient()
    87  
    88  var getterHTTPGetter = &getter.HttpGetter{
    89  	Client:             getterHTTPClient,
    90  	Netrc:              true,
    91  	XTerraformGetLimit: 10,
    92  }
    93  
    94  // A reusingGetter is a helper for the module installer that remembers
    95  // the final resolved addresses of all of the sources it has already been
    96  // asked to install, and will copy from a prior installation directory if
    97  // it has the same resolved source address.
    98  //
    99  // The keys in a reusingGetter are the normalized (post-detection) package
   100  // addresses, and the values are the paths where each source was previously
   101  // installed. (Users of this map should treat the keys as addrs.ModulePackage
   102  // values, but we can't type them that way because the addrs package
   103  // imports getmodules in order to indirectly access our go-getter
   104  // configuration.)
   105  type reusingGetter map[string]string
   106  
   107  // getWithGoGetter fetches the package at the given address into the given
   108  // target directory. The given address must already be in normalized form
   109  // (using NormalizePackageAddress) or else the behavior is undefined.
   110  //
   111  // This function deals only in entire packages, so it's always the caller's
   112  // responsibility to handle any subdirectory specification and select a
   113  // suitable subdirectory of the given installation directory after installation
   114  // has succeeded.
   115  //
   116  // This function would ideally accept packageAddr as a value of type
   117  // addrs.ModulePackage, but we can't do that because the addrs package
   118  // depends on this package for package address parsing. Therefore we just
   119  // use a string here but assume that the caller got that value by calling
   120  // the String method on a valid addrs.ModulePackage value.
   121  //
   122  // The errors returned by this function are those surfaced by the underlying
   123  // go-getter library, which have very inconsistent quality as
   124  // end-user-actionable error messages. At this time we do not have any
   125  // reasonable way to improve these error messages at this layer because
   126  // the underlying errors are not separately recognizable.
   127  func (g reusingGetter) getWithGoGetter(ctx context.Context, instPath, packageAddr string) error {
   128  	var err error
   129  
   130  	if prevDir, exists := g[packageAddr]; exists {
   131  		log.Printf("[TRACE] getmodules: copying previous install of %q from %s to %s", packageAddr, prevDir, instPath)
   132  		err := os.Mkdir(instPath, os.ModePerm)
   133  		if err != nil {
   134  			return fmt.Errorf("failed to create directory %s: %s", instPath, err)
   135  		}
   136  		err = copy.CopyDir(instPath, prevDir)
   137  		if err != nil {
   138  			return fmt.Errorf("failed to copy from %s to %s: %s", prevDir, instPath, err)
   139  		}
   140  	} else {
   141  		log.Printf("[TRACE] getmodules: fetching %q to %q", packageAddr, instPath)
   142  		client := getter.Client{
   143  			Src: packageAddr,
   144  			Dst: instPath,
   145  			Pwd: instPath,
   146  
   147  			Mode: getter.ClientModeDir,
   148  
   149  			Detectors:     goGetterNoDetectors, // our caller should've already done detection
   150  			Decompressors: goGetterDecompressors,
   151  			Getters:       goGetterGetters,
   152  			Ctx:           ctx,
   153  		}
   154  		err = client.Get()
   155  		if err != nil {
   156  			return err
   157  		}
   158  		// Remember where we installed this so we might reuse this directory
   159  		// on subsequent calls to avoid re-downloading.
   160  		g[packageAddr] = instPath
   161  	}
   162  
   163  	// If we get down here then we've either downloaded the package or
   164  	// copied a previous tree we downloaded, and so either way we should
   165  	// have got the full module package structure written into instPath.
   166  	return nil
   167  }