opentofu/internal/providercache/cached_provider.go
Martin Atkins b3f5c7f1e6 command/init: Read, respect, and update provider dependency locks
This changes the approach used by the provider installer to remember
between runs which selections it has previously made, using the lock file
format implemented in internal/depsfile.

This means that version constraints in the configuration are considered
only for providers we've not seen before or when -upgrade mode is active.
2020-10-09 09:26:23 -07:00

152 lines
6.5 KiB
Go

package providercache
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/internal/getproviders"
)
// CachedProvider represents a provider package in a cache directory.
type CachedProvider struct {
// Provider and Version together identify the specific provider version
// this cache entry represents.
Provider addrs.Provider
Version getproviders.Version
// PackageDir is the local filesystem path to the root directory where
// the provider's distribution archive was unpacked.
//
// The path always uses slashes as path separators, even on Windows, so
// that the results are consistent between platforms. Windows accepts
// both slashes and backslashes as long as the separators are consistent
// within a particular path string.
PackageDir string
}
// PackageLocation returns the package directory given in the PackageDir field
// as a getproviders.PackageLocation implementation.
//
// Because cached providers are always in the unpacked structure, the result is
// always of the concrete type getproviders.PackageLocalDir.
func (cp *CachedProvider) PackageLocation() getproviders.PackageLocalDir {
return getproviders.PackageLocalDir(cp.PackageDir)
}
// Hash computes a hash of the contents of the package directory associated
// with the receiving cached provider, using whichever hash algorithm is
// the current default.
//
// If you need a specific version of hash rather than just whichever one is
// current default, call that version's corresponding method (e.g. HashV1)
// directly instead.
func (cp *CachedProvider) Hash() (getproviders.Hash, error) {
return getproviders.PackageHash(cp.PackageLocation())
}
// MatchesHash returns true if the package on disk matches the given hash,
// or false otherwise. If it cannot traverse the package directory and read
// all of the files in it, or if the hash is in an unsupported format,
// MatchesHash returns an error.
//
// MatchesHash may accept hashes in a number of different formats. Over time
// the set of supported formats may grow and shrink.
func (cp *CachedProvider) MatchesHash(want getproviders.Hash) (bool, error) {
return getproviders.PackageMatchesHash(cp.PackageLocation(), want)
}
// MatchesAnyHash returns true if the package on disk matches the given hash,
// or false otherwise. If it cannot traverse the package directory and read
// all of the files in it, MatchesAnyHash returns an error.
//
// Unlike the singular MatchesHash, MatchesAnyHash considers unsupported hash
// formats as successfully non-matching, rather than returning an error.
func (cp *CachedProvider) MatchesAnyHash(allowed []getproviders.Hash) (bool, error) {
return getproviders.PackageMatchesAnyHash(cp.PackageLocation(), allowed)
}
// HashV1 computes a hash of the contents of the package directory associated
// with the receiving cached provider using hash algorithm 1.
//
// The hash covers the paths to files in the directory and the contents of
// those files. It does not cover other metadata about the files, such as
// permissions.
//
// This function is named "HashV1" in anticipation of other hashing algorithms
// being added (in a backward-compatible way) in future. The result from
// HashV1 always begins with the prefix "h1:" so that callers can distinguish
// the results of potentially multiple different hash algorithms in future.
func (cp *CachedProvider) HashV1() (getproviders.Hash, error) {
return getproviders.PackageHashV1(cp.PackageLocation())
}
// ExecutableFile inspects the cached provider's unpacked package directory for
// something that looks like it's intended to be the executable file for the
// plugin.
//
// This is a bit messy and heuristic-y because historically Terraform used the
// filename itself for local filesystem discovery, allowing some variance in
// the filenames to capture extra metadata, whereas now we're using the
// directory structure leading to the executable instead but need to remain
// compatible with the executable names bundled into existing provider packages.
//
// It will return an error if it can't find a file following the expected
// convention in the given directory.
//
// If found, the path always uses slashes as path separators, even on Windows,
// so that the results are consistent between platforms. Windows accepts both
// slashes and backslashes as long as the separators are consistent within a
// particular path string.
func (cp *CachedProvider) ExecutableFile() (string, error) {
infos, err := ioutil.ReadDir(cp.PackageDir)
if err != nil {
// If the directory itself doesn't exist or isn't readable then we
// can't access an executable in it.
return "", fmt.Errorf("could not read package directory: %s", err)
}
// For a provider named e.g. tf.example.com/awesomecorp/happycloud, we
// expect an executable file whose name starts with
// "terraform-provider-happycloud", followed by zero or more additional
// characters. If there _are_ additional characters then the first one
// must be an underscore or a period, like in thse examples:
// - terraform-provider-happycloud_v1.0.0
// - terraform-provider-happycloud.exe
//
// We don't require the version in the filename to match because the
// executable's name is no longer authoritative, but packages of "official"
// providers may continue to use versioned executable names for backward
// compatibility with Terraform 0.12.
//
// We also presume that providers packaged for Windows will include the
// necessary .exe extension on their filenames but do not explicitly check
// for that. If there's a provider package for Windows that has a file
// without that suffix then it will be detected as an executable but then
// we'll presumably fail later trying to run it.
wantPrefix := "terraform-provider-" + cp.Provider.Type
// We'll visit all of the directory entries and take the first (in
// name-lexical order) that looks like a plausible provider executable
// name. A package with multiple files meeting these criteria is degenerate
// but we will tolerate it by ignoring the subsequent entries.
for _, info := range infos {
if info.IsDir() {
continue // A directory can never be an executable
}
name := info.Name()
if !strings.HasPrefix(name, wantPrefix) {
continue
}
remainder := name[len(wantPrefix):]
if len(remainder) > 0 && (remainder[0] != '_' && remainder[0] != '.') {
continue // subsequent characters must be delimited by _ or .
}
return filepath.ToSlash(filepath.Join(cp.PackageDir, name)), nil
}
return "", fmt.Errorf("could not find executable file starting with %s", wantPrefix)
}