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