github.com/hugorut/terraform@v1.1.3/src/providercache/package_install.go (about) 1 package providercache 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "os" 9 "path/filepath" 10 11 getter "github.com/hashicorp/go-getter" 12 13 "github.com/hugorut/terraform/src/copy" 14 "github.com/hugorut/terraform/src/getproviders" 15 "github.com/hugorut/terraform/src/httpclient" 16 ) 17 18 // We borrow the "unpack a zip file into a target directory" logic from 19 // go-getter, even though we're not otherwise using go-getter here. 20 // (We don't need the same flexibility as we have for modules, because 21 // providers _always_ come from provider registries, which have a very 22 // specific protocol and set of expectations.) 23 var unzip = getter.ZipDecompressor{} 24 25 func installFromHTTPURL(ctx context.Context, meta getproviders.PackageMeta, targetDir string, allowedHashes []getproviders.Hash) (*getproviders.PackageAuthenticationResult, error) { 26 url := meta.Location.String() 27 28 // When we're installing from an HTTP URL we expect the URL to refer to 29 // a zip file. We'll fetch that into a temporary file here and then 30 // delegate to installFromLocalArchive below to actually extract it. 31 // (We're not using go-getter here because its HTTP getter has a bunch 32 // of extraneous functionality we don't need or want, like indirection 33 // through X-Terraform-Get header, attempting partial fetches for 34 // files that already exist, etc.) 35 36 httpClient := httpclient.New() 37 req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 38 if err != nil { 39 return nil, fmt.Errorf("invalid provider download request: %s", err) 40 } 41 resp, err := httpClient.Do(req) 42 if err != nil { 43 if ctx.Err() == context.Canceled { 44 // "context canceled" is not a user-friendly error message, 45 // so we'll return a more appropriate one here. 46 return nil, fmt.Errorf("provider download was interrupted") 47 } 48 return nil, err 49 } 50 defer resp.Body.Close() 51 52 if resp.StatusCode != http.StatusOK { 53 return nil, fmt.Errorf("unsuccessful request to %s: %s", url, resp.Status) 54 } 55 56 f, err := ioutil.TempFile("", "terraform-provider") 57 if err != nil { 58 return nil, fmt.Errorf("failed to open temporary file to download from %s", url) 59 } 60 defer f.Close() 61 defer os.Remove(f.Name()) 62 63 // We'll borrow go-getter's "cancelable copy" implementation here so that 64 // the download can potentially be interrupted partway through. 65 n, err := getter.Copy(ctx, f, resp.Body) 66 if err == nil && n < resp.ContentLength { 67 err = fmt.Errorf("incorrect response size: expected %d bytes, but got %d bytes", resp.ContentLength, n) 68 } 69 if err != nil { 70 return nil, err 71 } 72 73 archiveFilename := f.Name() 74 localLocation := getproviders.PackageLocalArchive(archiveFilename) 75 76 var authResult *getproviders.PackageAuthenticationResult 77 if meta.Authentication != nil { 78 if authResult, err = meta.Authentication.AuthenticatePackage(localLocation); err != nil { 79 return authResult, err 80 } 81 } 82 83 // We can now delegate to installFromLocalArchive for extraction. To do so, 84 // we construct a new package meta description using the local archive 85 // path as the location, and skipping authentication. installFromLocalMeta 86 // is responsible for verifying that the archive matches the allowedHashes, 87 // though. 88 localMeta := getproviders.PackageMeta{ 89 Provider: meta.Provider, 90 Version: meta.Version, 91 ProtocolVersions: meta.ProtocolVersions, 92 TargetPlatform: meta.TargetPlatform, 93 Filename: meta.Filename, 94 Location: localLocation, 95 Authentication: nil, 96 } 97 if _, err := installFromLocalArchive(ctx, localMeta, targetDir, allowedHashes); err != nil { 98 return nil, err 99 } 100 return authResult, nil 101 } 102 103 func installFromLocalArchive(ctx context.Context, meta getproviders.PackageMeta, targetDir string, allowedHashes []getproviders.Hash) (*getproviders.PackageAuthenticationResult, error) { 104 var authResult *getproviders.PackageAuthenticationResult 105 if meta.Authentication != nil { 106 var err error 107 if authResult, err = meta.Authentication.AuthenticatePackage(meta.Location); err != nil { 108 return nil, err 109 } 110 } 111 112 if len(allowedHashes) > 0 { 113 if matches, err := meta.MatchesAnyHash(allowedHashes); err != nil { 114 return authResult, fmt.Errorf( 115 "failed to calculate checksum for %s %s package at %s: %s", 116 meta.Provider, meta.Version, meta.Location, err, 117 ) 118 } else if !matches { 119 return authResult, fmt.Errorf( 120 "the current package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file", 121 meta.Provider, meta.Version, 122 ) 123 } 124 } 125 126 filename := meta.Location.String() 127 128 err := unzip.Decompress(targetDir, filename, true, 0000) 129 if err != nil { 130 return authResult, err 131 } 132 133 return authResult, nil 134 } 135 136 // installFromLocalDir is the implementation of both installing a package from 137 // a local directory source _and_ of linking a package from another cache 138 // in LinkFromOtherCache, because they both do fundamentally the same 139 // operation: symlink if possible, or deep-copy otherwise. 140 func installFromLocalDir(ctx context.Context, meta getproviders.PackageMeta, targetDir string, allowedHashes []getproviders.Hash) (*getproviders.PackageAuthenticationResult, error) { 141 sourceDir := meta.Location.String() 142 143 absNew, err := filepath.Abs(targetDir) 144 if err != nil { 145 return nil, fmt.Errorf("failed to make target path %s absolute: %s", targetDir, err) 146 } 147 absCurrent, err := filepath.Abs(sourceDir) 148 if err != nil { 149 return nil, fmt.Errorf("failed to make source path %s absolute: %s", sourceDir, err) 150 } 151 152 // Before we do anything else, we'll do a quick check to make sure that 153 // these two paths are not pointing at the same physical directory on 154 // disk. This compares the files by their OS-level device and directory 155 // entry identifiers, not by their virtual filesystem paths. 156 if same, err := copy.SameFile(absNew, absCurrent); same { 157 return nil, fmt.Errorf("cannot install existing provider directory %s to itself", targetDir) 158 } else if err != nil { 159 return nil, fmt.Errorf("failed to determine if %s and %s are the same: %s", sourceDir, targetDir, err) 160 } 161 162 var authResult *getproviders.PackageAuthenticationResult 163 if meta.Authentication != nil { 164 // (we have this here for completeness but note that local filesystem 165 // mirrors typically don't include enough information for package 166 // authentication and so we'll rarely get in here in practice.) 167 var err error 168 if authResult, err = meta.Authentication.AuthenticatePackage(meta.Location); err != nil { 169 return nil, err 170 } 171 } 172 173 // If the caller provided at least one hash in allowedHashes then at 174 // least one of those hashes ought to match. However, for local directories 175 // in particular we can't actually verify the legacy "zh:" hash scheme 176 // because it requires access to the original .zip archive, and so as a 177 // measure of pragmatism we'll treat a set of hashes where all are "zh:" 178 // the same as no hashes at all, and let anything pass. This is definitely 179 // non-ideal but accepted for two reasons: 180 // - Packages we find on local disk can be considered a little more trusted 181 // than packages coming from over the network, because we assume that 182 // they were either placed intentionally by an operator or they were 183 // automatically installed by a previous network operation that would've 184 // itself verified the hashes. 185 // - Our installer makes a concerted effort to record at least one new-style 186 // hash for each lock entry, so we should very rarely end up in this 187 // situation anyway. 188 suitableHashCount := 0 189 for _, hash := range allowedHashes { 190 if !hash.HasScheme(getproviders.HashSchemeZip) { 191 suitableHashCount++ 192 } 193 } 194 if suitableHashCount > 0 { 195 if matches, err := meta.MatchesAnyHash(allowedHashes); err != nil { 196 return authResult, fmt.Errorf( 197 "failed to calculate checksum for %s %s package at %s: %s", 198 meta.Provider, meta.Version, meta.Location, err, 199 ) 200 } else if !matches { 201 return authResult, fmt.Errorf( 202 "the local package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms)", 203 meta.Provider, meta.Version, 204 ) 205 } 206 } 207 208 // Delete anything that's already present at this path first. 209 err = os.RemoveAll(targetDir) 210 if err != nil && !os.IsNotExist(err) { 211 return nil, fmt.Errorf("failed to remove existing %s before linking it to %s: %s", sourceDir, targetDir, err) 212 } 213 214 // We'll prefer to create a symlink if possible, but we'll fall back to 215 // a recursive copy if symlink creation fails. It could fail for a number 216 // of reasons, including being on Windows 8 without administrator 217 // privileges or being on a legacy filesystem like FAT that has no way 218 // to represent a symlink. (Generalized symlink support for Windows was 219 // introduced in a Windows 10 minor update.) 220 // 221 // We use an absolute path for the symlink to reduce the risk of it being 222 // broken by moving things around later, since the source directory is 223 // likely to be a shared directory independent on any particular target 224 // and thus we can't assume that they will move around together. 225 linkTarget := absCurrent 226 227 parentDir := filepath.Dir(absNew) 228 err = os.MkdirAll(parentDir, 0755) 229 if err != nil { 230 return nil, fmt.Errorf("failed to create parent directories leading to %s: %s", targetDir, err) 231 } 232 233 err = os.Symlink(linkTarget, absNew) 234 if err == nil { 235 // Success, then! 236 return nil, nil 237 } 238 239 // If we get down here then symlinking failed and we need a deep copy 240 // instead. To make a copy, we first need to create the target directory, 241 // which would otherwise be a symlink. 242 err = os.Mkdir(absNew, 0755) 243 if err != nil && os.IsExist(err) { 244 return nil, fmt.Errorf("failed to create directory %s: %s", absNew, err) 245 } 246 err = copy.CopyDir(absNew, absCurrent) 247 if err != nil { 248 return nil, fmt.Errorf("failed to either symlink or copy %s to %s: %s", absCurrent, absNew, err) 249 } 250 251 // If we got here then apparently our copy succeeded, so we're done. 252 return nil, nil 253 }