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 }