diff --git a/internal/getproviders/filesystem_search.go b/internal/getproviders/filesystem_search.go index ed5a9e194..15cc4662e 100644 --- a/internal/getproviders/filesystem_search.go +++ b/internal/getproviders/filesystem_search.go @@ -21,6 +21,25 @@ import ( // 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 + // heirarchy 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 { + log.Printf("[TRACE] getproviders.SearchLocalDirectory: %s is a symlink to %s", baseDir, finalDir) + 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) @@ -45,6 +64,15 @@ func SearchLocalDirectory(baseDir string) (map[addrs.Provider]PackageMetaList, e 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 + // heirarchy 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 } diff --git a/internal/getproviders/filesystem_search_test.go b/internal/getproviders/filesystem_search_test.go new file mode 100644 index 000000000..857706556 --- /dev/null +++ b/internal/getproviders/filesystem_search_test.go @@ -0,0 +1,52 @@ +package getproviders + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/addrs" +) + +func TestSearchLocalDirectory(t *testing.T) { + tests := []struct { + Fixture string + Subdir string + Want map[addrs.Provider]PackageMetaList + }{ + { + "symlinks", + "symlink", + map[addrs.Provider]PackageMetaList{ + addrs.MustParseProviderSourceString("example.com/foo/bar"): { + { + Provider: addrs.MustParseProviderSourceString("example.com/foo/bar"), + Version: MustParseVersion("1.0.0"), + TargetPlatform: Platform{OS: "linux", Arch: "amd64"}, + Filename: "terraform-provider-bar_1.0.0_linux_amd64.zip", + Location: PackageLocalDir("testdata/search-local-directory/symlinks/real/example.com/foo/bar/1.0.0/linux_amd64"), + }, + }, + // This search doesn't find example.net/foo/bar because only + // the top-level search directory is supported as being a + // symlink, and so we ignore the example.net symlink to + // example.com that is one level deeper. + }, + }, + } + + for _, test := range tests { + t.Run(test.Fixture, func(t *testing.T) { + fullDir := filepath.Join("testdata/search-local-directory", test.Fixture, test.Subdir) + got, err := SearchLocalDirectory(fullDir) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + want := test.Want + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} diff --git a/internal/getproviders/testdata/search-local-directory/symlinks/real/example.com/foo/bar/1.0.0/linux_amd64/terraform-provider-bar b/internal/getproviders/testdata/search-local-directory/symlinks/real/example.com/foo/bar/1.0.0/linux_amd64/terraform-provider-bar new file mode 100644 index 000000000..e69de29bb diff --git a/internal/getproviders/testdata/search-local-directory/symlinks/real/example.net b/internal/getproviders/testdata/search-local-directory/symlinks/real/example.net new file mode 120000 index 000000000..caa12a8fb --- /dev/null +++ b/internal/getproviders/testdata/search-local-directory/symlinks/real/example.net @@ -0,0 +1 @@ +example.com \ No newline at end of file diff --git a/internal/getproviders/testdata/search-local-directory/symlinks/symlink b/internal/getproviders/testdata/search-local-directory/symlinks/symlink new file mode 120000 index 000000000..ac558a3e1 --- /dev/null +++ b/internal/getproviders/testdata/search-local-directory/symlinks/symlink @@ -0,0 +1 @@ +real \ No newline at end of file