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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package providercache
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/terramate-io/tf/addrs"
    13  	"github.com/terramate-io/tf/getproviders"
    14  )
    15  
    16  // CachedProvider represents a provider package in a cache directory.
    17  type CachedProvider struct {
    18  	// Provider and Version together identify the specific provider version
    19  	// this cache entry represents.
    20  	Provider addrs.Provider
    21  	Version  getproviders.Version
    22  
    23  	// PackageDir is the local filesystem path to the root directory where
    24  	// the provider's distribution archive was unpacked.
    25  	//
    26  	// The path always uses slashes as path separators, even on Windows, so
    27  	// that the results are consistent between platforms. Windows accepts
    28  	// both slashes and backslashes as long as the separators are consistent
    29  	// within a particular path string.
    30  	PackageDir string
    31  }
    32  
    33  // PackageLocation returns the package directory given in the PackageDir field
    34  // as a getproviders.PackageLocation implementation.
    35  //
    36  // Because cached providers are always in the unpacked structure, the result is
    37  // always of the concrete type getproviders.PackageLocalDir.
    38  func (cp *CachedProvider) PackageLocation() getproviders.PackageLocalDir {
    39  	return getproviders.PackageLocalDir(cp.PackageDir)
    40  }
    41  
    42  // Hash computes a hash of the contents of the package directory associated
    43  // with the receiving cached provider, using whichever hash algorithm is
    44  // the current default.
    45  //
    46  // If you need a specific version of hash rather than just whichever one is
    47  // current default, call that version's corresponding method (e.g. HashV1)
    48  // directly instead.
    49  func (cp *CachedProvider) Hash() (getproviders.Hash, error) {
    50  	return getproviders.PackageHash(cp.PackageLocation())
    51  }
    52  
    53  // MatchesHash returns true if the package on disk matches the given hash,
    54  // or false otherwise. If it cannot traverse the package directory and read
    55  // all of the files in it, or if the hash is in an unsupported format,
    56  // MatchesHash returns an error.
    57  //
    58  // MatchesHash may accept hashes in a number of different formats. Over time
    59  // the set of supported formats may grow and shrink.
    60  func (cp *CachedProvider) MatchesHash(want getproviders.Hash) (bool, error) {
    61  	return getproviders.PackageMatchesHash(cp.PackageLocation(), want)
    62  }
    63  
    64  // MatchesAnyHash returns true if the package on disk matches the given hash,
    65  // or false otherwise. If it cannot traverse the package directory and read
    66  // all of the files in it, MatchesAnyHash returns an error.
    67  //
    68  // Unlike the singular MatchesHash, MatchesAnyHash considers unsupported hash
    69  // formats as successfully non-matching, rather than returning an error.
    70  func (cp *CachedProvider) MatchesAnyHash(allowed []getproviders.Hash) (bool, error) {
    71  	return getproviders.PackageMatchesAnyHash(cp.PackageLocation(), allowed)
    72  }
    73  
    74  // HashV1 computes a hash of the contents of the package directory associated
    75  // with the receiving cached provider using hash algorithm 1.
    76  //
    77  // The hash covers the paths to files in the directory and the contents of
    78  // those files. It does not cover other metadata about the files, such as
    79  // permissions.
    80  //
    81  // This function is named "HashV1" in anticipation of other hashing algorithms
    82  // being added (in a backward-compatible way) in future. The result from
    83  // HashV1 always begins with the prefix "h1:" so that callers can distinguish
    84  // the results of potentially multiple different hash algorithms in future.
    85  func (cp *CachedProvider) HashV1() (getproviders.Hash, error) {
    86  	return getproviders.PackageHashV1(cp.PackageLocation())
    87  }
    88  
    89  // ExecutableFile inspects the cached provider's unpacked package directory for
    90  // something that looks like it's intended to be the executable file for the
    91  // plugin.
    92  //
    93  // This is a bit messy and heuristic-y because historically Terraform used the
    94  // filename itself for local filesystem discovery, allowing some variance in
    95  // the filenames to capture extra metadata, whereas now we're using the
    96  // directory structure leading to the executable instead but need to remain
    97  // compatible with the executable names bundled into existing provider packages.
    98  //
    99  // It will return an error if it can't find a file following the expected
   100  // convention in the given directory.
   101  //
   102  // If found, the path always uses slashes as path separators, even on Windows,
   103  // so that the results are consistent between platforms. Windows accepts both
   104  // slashes and backslashes as long as the separators are consistent within a
   105  // particular path string.
   106  func (cp *CachedProvider) ExecutableFile() (string, error) {
   107  	infos, err := ioutil.ReadDir(cp.PackageDir)
   108  	if err != nil {
   109  		// If the directory itself doesn't exist or isn't readable then we
   110  		// can't access an executable in it.
   111  		return "", fmt.Errorf("could not read package directory: %s", err)
   112  	}
   113  
   114  	// For a provider named e.g. tf.example.com/awesomecorp/happycloud, we
   115  	// expect an executable file whose name starts with
   116  	// "terraform-provider-happycloud", followed by zero or more additional
   117  	// characters. If there _are_ additional characters then the first one
   118  	// must be an underscore or a period, like in thse examples:
   119  	// - terraform-provider-happycloud_v1.0.0
   120  	// - terraform-provider-happycloud.exe
   121  	//
   122  	// We don't require the version in the filename to match because the
   123  	// executable's name is no longer authoritative, but packages of "official"
   124  	// providers may continue to use versioned executable names for backward
   125  	// compatibility with Terraform 0.12.
   126  	//
   127  	// We also presume that providers packaged for Windows will include the
   128  	// necessary .exe extension on their filenames but do not explicitly check
   129  	// for that. If there's a provider package for Windows that has a file
   130  	// without that suffix then it will be detected as an executable but then
   131  	// we'll presumably fail later trying to run it.
   132  	wantPrefix := "terraform-provider-" + cp.Provider.Type
   133  
   134  	// We'll visit all of the directory entries and take the first (in
   135  	// name-lexical order) that looks like a plausible provider executable
   136  	// name. A package with multiple files meeting these criteria is degenerate
   137  	// but we will tolerate it by ignoring the subsequent entries.
   138  	for _, info := range infos {
   139  		if info.IsDir() {
   140  			continue // A directory can never be an executable
   141  		}
   142  		name := info.Name()
   143  		if !strings.HasPrefix(name, wantPrefix) {
   144  			continue
   145  		}
   146  		remainder := name[len(wantPrefix):]
   147  		if len(remainder) > 0 && (remainder[0] != '_' && remainder[0] != '.') {
   148  			continue // subsequent characters must be delimited by _ or .
   149  		}
   150  		return filepath.ToSlash(filepath.Join(cp.PackageDir, name)), nil
   151  	}
   152  
   153  	return "", fmt.Errorf("could not find executable file starting with %s", wantPrefix)
   154  }