terraform/internal/getproviders/filesystem_search.go

290 lines
11 KiB
Go

package getproviders
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform/internal/addrs"
)
// SearchLocalDirectory performs an immediate, one-off scan of the given base
// directory for provider plugins using the directory structure defined for
// FilesystemMirrorSource.
//
// This is separated to allow other callers, such as the provider plugin cache
// management in the "internal/providercache" package, to use the same
// directory structure conventions.
func SearchLocalDirectory(baseDir string) (map[addrs.Provider]PackageMetaList, error) {
ret := make(map[addrs.Provider]PackageMetaList)
// We don't support symlinks at intermediate points inside the directory
// hierarchy because that could potentially cause our walk to get into
// an infinite loop, but as a measure of pragmatism we'll allow the
// top-level location itself to be a symlink, so that a user can
// potentially keep their plugins in a non-standard location but use a
// symlink to help Terraform find them anyway.
originalBaseDir := baseDir
if finalDir, err := filepath.EvalSymlinks(baseDir); err == nil {
if finalDir != filepath.Clean(baseDir) {
log.Printf("[TRACE] getproviders.SearchLocalDirectory: using %s instead of %s", finalDir, baseDir)
}
baseDir = finalDir
} else {
// We'll eat this particular error because if we're somehow able to
// find plugins via baseDir below anyway then we'd rather do that than
// hard fail, but we'll log it in case it's useful for diagnosing why
// discovery didn't produce the expected outcome.
log.Printf("[TRACE] getproviders.SearchLocalDirectory: failed to resolve symlinks for %s: %s", baseDir, err)
}
err := filepath.Walk(baseDir, func(fullPath string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("cannot search %s: %s", fullPath, err)
}
// There are two valid directory structures that we support here...
// Unpacked: registry.terraform.io/hashicorp/aws/2.0.0/linux_amd64 (a directory)
// Packed: registry.terraform.io/hashicorp/aws/terraform-provider-aws_2.0.0_linux_amd64.zip (a file)
//
// Both of these give us enough information to identify the package
// metadata.
fsPath, err := filepath.Rel(baseDir, fullPath)
if err != nil {
// This should never happen because the filepath.Walk contract is
// for the paths to include the base path.
log.Printf("[TRACE] getproviders.SearchLocalDirectory: ignoring malformed path %q during walk: %s", fullPath, err)
return nil
}
relPath := filepath.ToSlash(fsPath)
parts := strings.Split(relPath, "/")
if len(parts) < 3 {
// Likely a prefix of a valid path, so we'll ignore it and visit
// the full valid path on a later call.
if (info.Mode() & os.ModeSymlink) != 0 {
// We don't allow symlinks for intermediate steps in the
// hierarchy because otherwise this walk would risk getting
// itself into an infinite loop, but if we do find one then
// we'll warn about it to help with debugging.
log.Printf("[WARN] Provider plugin search ignored symlink %s: only the base directory %s may be a symlink", fullPath, originalBaseDir)
}
return nil
}
hostnameGiven := parts[0]
namespace := parts[1]
typeName := parts[2]
// validate each part
// The legacy provider namespace is a special case.
if namespace != addrs.LegacyProviderNamespace {
_, err = addrs.ParseProviderPart(namespace)
if err != nil {
log.Printf("[WARN] local provider path %q contains invalid namespace %q; ignoring", fullPath, namespace)
return nil
}
}
_, err = addrs.ParseProviderPart(typeName)
if err != nil {
log.Printf("[WARN] local provider path %q contains invalid type %q; ignoring", fullPath, typeName)
return nil
}
hostname, err := svchost.ForComparison(hostnameGiven)
if err != nil {
log.Printf("[WARN] local provider path %q contains invalid hostname %q; ignoring", fullPath, hostnameGiven)
return nil
}
var providerAddr addrs.Provider
if namespace == addrs.LegacyProviderNamespace {
if hostname != addrs.DefaultProviderRegistryHost {
log.Printf("[WARN] local provider path %q indicates a legacy provider not on the default registry host; ignoring", fullPath)
return nil
}
providerAddr = addrs.NewLegacyProvider(typeName)
} else {
providerAddr = addrs.NewProvider(hostname, namespace, typeName)
}
// The "info" passed to our function is an Lstat result, so it might
// be referring to a symbolic link. We'll do a full "Stat" on it
// now to make sure we're making tests against the real underlying
// filesystem object below.
info, err = os.Stat(fullPath)
if err != nil {
log.Printf("[WARN] failed to read metadata about %s: %s", fullPath, err)
return nil
}
switch len(parts) {
case 5: // Might be unpacked layout
if !info.IsDir() {
return nil // packed layout requires a directory
}
versionStr := parts[3]
version, err := ParseVersion(versionStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid version %q: %s", fullPath, versionStr, err)
return nil
}
platformStr := parts[4]
platform, err := ParsePlatform(platformStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid platform %q: %s", fullPath, platformStr, err)
return nil
}
log.Printf("[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s", providerAddr, version, platform, fullPath)
meta := PackageMeta{
Provider: providerAddr,
Version: version,
// FIXME: How do we populate this?
ProtocolVersions: nil,
TargetPlatform: platform,
// Because this is already unpacked, the filename is synthetic
// based on the standard naming scheme.
Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", providerAddr.Type, version, platform),
Location: PackageLocalDir(fullPath),
// FIXME: What about the SHA256Sum field? As currently specified
// it's a hash of the zip file, but this thing is already
// unpacked and so we don't have the zip file to hash.
}
ret[providerAddr] = append(ret[providerAddr], meta)
case 4: // Might be packed layout
if info.IsDir() {
return nil // packed layout requires a file
}
filename := filepath.Base(fsPath)
// the filename components are matched case-insensitively, and
// the normalized form of them is in lowercase so we'll convert
// to lowercase for comparison here. (This normalizes only for case,
// because that is the primary constraint affecting compatibility
// between filesystem implementations on different platforms;
// filenames are expected to be pre-normalized and valid in other
// regards.)
normFilename := strings.ToLower(filename)
// In the packed layout, the version number and target platform
// are derived from the package filename, but only if the
// filename has the expected prefix identifying it as a package
// for the provider in question, and the suffix identifying it
// as a zip file.
prefix := "terraform-provider-" + providerAddr.Type + "_"
const suffix = ".zip"
if !strings.HasPrefix(normFilename, prefix) {
log.Printf("[WARN] ignoring file %q as possible package for %s: filename lacks expected prefix %q", fsPath, providerAddr, prefix)
return nil
}
if !strings.HasSuffix(normFilename, suffix) {
log.Printf("[WARN] ignoring file %q as possible package for %s: filename lacks expected suffix %q", fsPath, providerAddr, suffix)
return nil
}
// Extract the version and target part of the filename, which
// will look like "2.1.0_linux_amd64"
infoSlice := normFilename[len(prefix) : len(normFilename)-len(suffix)]
infoParts := strings.Split(infoSlice, "_")
if len(infoParts) < 3 {
log.Printf("[WARN] ignoring file %q as possible package for %s: filename does not include version number, target OS, and target architecture", fsPath, providerAddr)
return nil
}
versionStr := infoParts[0]
version, err := ParseVersion(versionStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid version %q: %s", fullPath, versionStr, err)
return nil
}
// We'll reassemble this back into a single string just so we can
// easily re-use our existing parser and its normalization rules.
platformStr := infoParts[1] + "_" + infoParts[2]
platform, err := ParsePlatform(platformStr)
if err != nil {
log.Printf("[WARN] ignoring local provider path %q with invalid platform %q: %s", fullPath, platformStr, err)
return nil
}
log.Printf("[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s", providerAddr, version, platform, fullPath)
meta := PackageMeta{
Provider: providerAddr,
Version: version,
// FIXME: How do we populate this?
ProtocolVersions: nil,
TargetPlatform: platform,
// Because this is already unpacked, the filename is synthetic
// based on the standard naming scheme.
Filename: normFilename, // normalized filename, because this field says what it _should_ be called, not what it _is_ called
Location: PackageLocalArchive(fullPath), // non-normalized here, because this is the actual physical location
// TODO: Also populate the SHA256Sum field. Skipping that
// for now because our initial uses of this result --
// scanning already-installed providers in local directories,
// rather than explicit filesystem mirrors -- doesn't do
// any hash verification anyway, and this is consistent with
// the FIXME in the unpacked case above even though technically
// we _could_ populate SHA256Sum here right now.
}
ret[providerAddr] = append(ret[providerAddr], meta)
}
return nil
})
if err != nil {
return nil, err
}
// Sort the results to be deterministic (aside from semver build metadata)
// and consistent with ordering from other functions.
for _, l := range ret {
l.Sort()
}
return ret, nil
}
// UnpackedDirectoryPathForPackage is similar to
// PackageMeta.UnpackedDirectoryPath but makes its decision based on
// individually-passed provider address, version, and target platform so that
// it can be used by callers outside this package that may have other
// types that represent package identifiers.
func UnpackedDirectoryPathForPackage(baseDir string, provider addrs.Provider, version Version, platform Platform) string {
return filepath.ToSlash(filepath.Join(
baseDir,
provider.Hostname.ForDisplay(), provider.Namespace, provider.Type,
version.String(),
platform.String(),
))
}
// PackedFilePathForPackage is similar to
// PackageMeta.PackedFilePath but makes its decision based on
// individually-passed provider address, version, and target platform so that
// it can be used by callers outside this package that may have other
// types that represent package identifiers.
func PackedFilePathForPackage(baseDir string, provider addrs.Provider, version Version, platform Platform) string {
return filepath.ToSlash(filepath.Join(
baseDir,
provider.Hostname.ForDisplay(), provider.Namespace, provider.Type,
fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), platform.String()),
))
}