move local module detection ahead of the registry

The getter.FileDetector was intended to be the final detector, only
converting a path to a file URL and returning a true in all cases. We
want to check for a local module before checking the registry so no
local modules that happen to match a registry module are broken.

Wrap the getter.FileDetector to check the module source's existence
before delegating the search to the registry.
This commit is contained in:
James Bardin 2017-09-08 18:18:47 -04:00
parent d216d19f21
commit f9fb2b4c9e
3 changed files with 159 additions and 15 deletions

View File

@ -5,6 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
"net/url"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@ -86,8 +87,8 @@ var detectors = []getter.Detector{
new(getter.GitHubDetector), new(getter.GitHubDetector),
new(getter.BitBucketDetector), new(getter.BitBucketDetector),
new(getter.S3Detector), new(getter.S3Detector),
new(localDetector),
new(registryDetector), new(registryDetector),
new(getter.FileDetector),
} }
// these prefixes can't be registry IDs // these prefixes can't be registry IDs
@ -121,8 +122,6 @@ func (d registryDetector) Detect(src, _ string) (string, bool, error) {
} }
// Lookup the module in the registry. // Lookup the module in the registry.
// Since existing module sources may match a registry ID format, we only log
// registry errors and continue discovery.
func (d registryDetector) lookupModule(src string) (string, bool, error) { func (d registryDetector) lookupModule(src string) (string, bool, error) {
if d.api == "" { if d.api == "" {
d.api = registryAPI d.api = registryAPI
@ -137,7 +136,7 @@ func (d registryDetector) lookupModule(src string) (string, bool, error) {
// determine if it's truly valid. // determine if it's truly valid.
resp, err := d.client.Get(fmt.Sprintf("%s/%s/download", d.api, src)) resp, err := d.client.Get(fmt.Sprintf("%s/%s/download", d.api, src))
if err != nil { if err != nil {
log.Println("[WARN] error looking up module %q: %s", src, err) log.Printf("[WARN] error looking up module %q: %s", src, err)
return "", false, nil return "", false, nil
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -145,7 +144,7 @@ func (d registryDetector) lookupModule(src string) (string, bool, error) {
// there should be no body, but save it for logging // there should be no body, but save it for logging
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
fmt.Println("[WARN] error reading response body from registry: %s", err) fmt.Printf("[WARN] error reading response body from registry: %s", err)
return "", false, nil return "", false, nil
} }
@ -169,3 +168,40 @@ func (d registryDetector) lookupModule(src string) (string, bool, error) {
return location, true, nil return location, true, nil
} }
// localDetector wraps the default getter.FileDetector and checks if the module
// exists in the local filesystem. The default FileDetector only converts paths
// into file URLs, and returns found. We want to first check for a local module
// before passing it off to the registryDetector so we don't inadvertently
// replace a local module with a registry module of the same name.
type localDetector struct{}
func (d localDetector) Detect(src, wd string) (string, bool, error) {
localSrc, ok, err := new(getter.FileDetector).Detect(src, wd)
if err != nil {
return src, ok, err
}
if !ok {
return "", false, nil
}
u, err := url.Parse(localSrc)
if err != nil {
return "", false, err
}
_, err = os.Stat(u.Path)
// just continue detection if it doesn't exist
if os.IsNotExist(err) {
return "", false, nil
}
// return any other errors
if err != nil {
return "", false, err
}
return localSrc, true, nil
}

View File

@ -4,11 +4,14 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"path/filepath"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"testing" "testing"
getter "github.com/hashicorp/go-getter"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
) )
@ -79,52 +82,52 @@ func TestDetectRegistry(t *testing.T) {
} }
for _, tc := range []struct { for _, tc := range []struct {
module string source string
location string location string
found bool found bool
err bool err bool
}{ }{
{ {
module: "registry/foo/bar", source: "registry/foo/bar",
location: "download/registry/foo/bar/0.2.3", location: "download/registry/foo/bar/0.2.3",
found: true, found: true,
}, },
{ {
module: "registry/foo/baz", source: "registry/foo/baz",
location: "download/registry/foo/baz/1.10.0", location: "download/registry/foo/baz/1.10.0",
found: true, found: true,
}, },
// this should not be found, but not stop detection // this should not be found, but not stop detection
{ {
module: "registry/foo/notfound", source: "registry/foo/notfound",
found: false, found: false,
}, },
// a full url should not be detected // a full url should not be detected
{ {
module: "http://example.com/registry/foo/notfound", source: "http://example.com/registry/foo/notfound",
found: false, found: false,
}, },
// paths should not be detected // paths should not be detected
{ {
module: "./local/foo/notfound", source: "./local/foo/notfound",
found: false, found: false,
}, },
{ {
module: "/local/foo/notfound", source: "/local/foo/notfound",
found: false, found: false,
}, },
// wrong number of parts can't be regisry IDs // wrong number of parts can't be regisry IDs
{ {
module: "something/registry/foo/notfound", source: "something/registry/foo/notfound",
found: false, found: false,
}, },
} { } {
t.Run(tc.module, func(t *testing.T) { t.Run(tc.source, func(t *testing.T) {
loc, ok, err := detector.Detect(tc.module, "") loc, ok, err := detector.Detect(tc.source, "")
if (err == nil) == tc.err { if (err == nil) == tc.err {
t.Fatalf("expected error? %t; got error :%v", tc.err, err) t.Fatalf("expected error? %t; got error :%v", tc.err, err)
} }
@ -141,3 +144,105 @@ func TestDetectRegistry(t *testing.T) {
} }
} }
// check that the full set of detectors works as expected
func TestDetectors(t *testing.T) {
server := mockRegistry()
defer server.Close()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
regDetector := &registryDetector{
api: server.URL + "/v1/modules/",
client: server.Client(),
}
detectors := []getter.Detector{
new(getter.GitHubDetector),
new(getter.BitBucketDetector),
new(getter.S3Detector),
new(localDetector),
regDetector,
}
for _, tc := range []struct {
source string
location string
fixture string
err bool
}{
{
source: "registry/foo/bar",
location: "download/registry/foo/bar/0.2.3",
},
// this should not be found, but not stop detection
{
source: "registry/foo/notfound",
err: true,
},
// a full url should be unchanged
{
source: "http://example.com/registry/foo/notfound?" +
"checksum=sha256:f19056b80a426d797ff9e470da069c171a6c6befa83e2da7f6c706207742acab",
location: "http://example.com/registry/foo/notfound?" +
"checksum=sha256:f19056b80a426d797ff9e470da069c171a6c6befa83e2da7f6c706207742acab",
},
// forced getters will return untouched
{
source: "git::http://example.com/registry/foo/notfound?param=value",
location: "git::http://example.com/registry/foo/notfound?param=value",
},
// local paths should be detected as such, even if they're match
// registry modules.
{
source: "./registry/foo/bar",
err: true,
},
{
source: "/registry/foo/bar",
err: true,
},
// wrong number of parts can't be regisry IDs
{
source: "something/registry/foo/notfound",
err: true,
},
// make sure a local module that looks like a registry id takes precedence
{
source: "namespace/identifier/provider",
fixture: "discover-subdirs",
// this should be found locally
location: "file://" + filepath.Join(wd, fixtureDir, "discover-subdirs/namespace/identifier/provider"),
},
} {
t.Run(tc.source, func(t *testing.T) {
dir := wd
if tc.fixture != "" {
dir = filepath.Join(wd, fixtureDir, tc.fixture)
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
defer os.Chdir(wd)
}
loc, err := getter.Detect(tc.source, dir, detectors)
if (err == nil) == tc.err {
t.Fatalf("expected error? %t; got error :%v", tc.err, err)
}
loc = strings.TrimPrefix(loc, server.URL+"/")
if strings.TrimPrefix(loc, server.URL) != tc.location {
t.Fatalf("expected location: %q, got %q", tc.location, loc)
}
})
}
}

View File

@ -13,6 +13,7 @@ import (
const fixtureDir = "./test-fixtures" const fixtureDir = "./test-fixtures"
func tempDir(t *testing.T) string { func tempDir(t *testing.T) string {
t.Helper()
dir, err := ioutil.TempDir("", "tf") dir, err := ioutil.TempDir("", "tf")
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -25,6 +26,7 @@ func tempDir(t *testing.T) string {
} }
func testConfig(t *testing.T, n string) *config.Config { func testConfig(t *testing.T, n string) *config.Config {
t.Helper()
c, err := config.LoadDir(filepath.Join(fixtureDir, n)) c, err := config.LoadDir(filepath.Join(fixtureDir, n))
if err != nil { if err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
@ -34,5 +36,6 @@ func testConfig(t *testing.T, n string) *config.Config {
} }
func testStorage(t *testing.T) getter.Storage { func testStorage(t *testing.T) getter.Storage {
t.Helper()
return &getter.FolderStorage{StorageDir: tempDir(t)} return &getter.FolderStorage{StorageDir: tempDir(t)}
} }