mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 23:50:12 -06:00
9b7bec31b4
Signed-off-by: Nathan Baulch <nathan.baulch@gmail.com>
157 lines
6.7 KiB
Go
157 lines
6.7 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package providercache
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/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 := os.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: %w", 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 these 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)
|
|
}
|