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  }