2020-03-11 17:38:55 +01:00
package getproviders
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform/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 )
2020-07-28 19:26:01 +02:00
// We don't support symlinks at intermediate points inside the directory
2021-02-05 15:11:38 +01:00
// hierarchy because that could potentially cause our walk to get into
2020-07-28 19:26:01 +02:00
// 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 {
2020-10-21 01:49:38 +02:00
if finalDir != filepath . Clean ( baseDir ) {
log . Printf ( "[TRACE] getproviders.SearchLocalDirectory: using %s instead of %s" , finalDir , baseDir )
}
2020-07-28 19:26:01 +02:00
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 )
}
2020-03-11 17:38:55 +01:00
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.
2020-03-25 20:22:39 +01:00
log . Printf ( "[TRACE] getproviders.SearchLocalDirectory: ignoring malformed path %q during walk: %s" , fullPath , err )
2020-03-11 17:38:55 +01:00
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.
2020-07-28 19:26:01 +02:00
if ( info . Mode ( ) & os . ModeSymlink ) != 0 {
// We don't allow symlinks for intermediate steps in the
2021-02-05 15:11:38 +01:00
// hierarchy because otherwise this walk would risk getting
2020-07-28 19:26:01 +02:00
// 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 )
}
2020-03-11 17:38:55 +01:00
return nil
}
hostnameGiven := parts [ 0 ]
namespace := parts [ 1 ]
typeName := parts [ 2 ]
2020-05-13 19:16:09 +02:00
// 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
}
2020-03-11 17:38:55 +01:00
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 . DefaultRegistryHost {
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 )
}
2020-03-25 20:24:18 +01:00
// 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 {
2021-01-08 11:18:13 +01:00
log . Printf ( "[WARN] failed to read metadata about %s: %s" , fullPath , err )
return nil
2020-03-25 20:24:18 +01:00
}
2020-03-11 17:38:55 +01:00
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
}
2020-03-25 20:22:39 +01:00
log . Printf ( "[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s" , providerAddr , version , platform , fullPath )
2020-03-11 17:38:55 +01:00
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 ) {
2020-03-25 20:22:39 +01:00
log . Printf ( "[WARN] ignoring file %q as possible package for %s: filename lacks expected prefix %q" , fsPath , providerAddr , prefix )
2020-03-11 17:38:55 +01:00
return nil
}
if ! strings . HasSuffix ( normFilename , suffix ) {
2020-03-25 20:22:39 +01:00
log . Printf ( "[WARN] ignoring file %q as possible package for %s: filename lacks expected suffix %q" , fsPath , providerAddr , suffix )
2020-03-11 17:38:55 +01:00
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 {
2020-03-25 20:22:39 +01:00
log . Printf ( "[WARN] ignoring file %q as possible package for %s: filename does not include version number, target OS, and target architecture" , fsPath , providerAddr )
2020-03-11 17:38:55 +01:00
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
}
2020-03-25 20:22:39 +01:00
log . Printf ( "[TRACE] getproviders.SearchLocalDirectory: found %s v%s for %s at %s" , providerAddr , version , platform , fullPath )
2020-03-11 17:38:55 +01:00
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
}
2020-03-12 01:53:19 +01:00
// 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 ( ) ,
) )
}
2020-04-24 03:12:01 +02:00
// 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 ( ) ) ,
) )
}