a series of test commits

wire up HTTP so we can test the mock discovery service

test lookupModuleVersions

Add a versions endpoint to the mock registry, and use that to verify the
lookupModuleVersions behavior.

lookupModuleVersions takes a Disco as the argument so a custom Transport
can be injected, and uses that transport for its own client if it set.

test looking up modules with default registry

Add registry.terrform.io to the hostname that the mock registry resolves
to localhost.

ACC test looking up module versions

Lookup a basic module for the available version in the default registry.
This commit is contained in:
James Bardin 2017-10-25 11:49:41 -04:00
parent ae2903b810
commit ee36cf28e0
3 changed files with 286 additions and 42 deletions

View File

@ -1,7 +1,9 @@
package module package module
import ( import (
"encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
@ -14,6 +16,7 @@ import (
getter "github.com/hashicorp/go-getter" getter "github.com/hashicorp/go-getter"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/registry/response"
) )
// Map of module names and location of test modules. // Map of module names and location of test modules.
@ -26,22 +29,28 @@ type testMod struct {
// All the locationes from the mockRegistry start with a file:// scheme. If // All the locationes from the mockRegistry start with a file:// scheme. If
// the the location string here doesn't have a scheme, the mockRegistry will // the the location string here doesn't have a scheme, the mockRegistry will
// find the absolute path and return a complete URL. // find the absolute path and return a complete URL.
var testMods = map[string]testMod{ var testMods = map[string][]testMod{
"registry/foo/bar": { "registry/foo/bar": {{
location: "file:///download/registry/foo/bar/0.2.3//*?archive=tar.gz", location: "file:///download/registry/foo/bar/0.2.3//*?archive=tar.gz",
version: "0.2.3", version: "0.2.3",
}, }},
"registry/foo/baz": { "registry/foo/baz": {{
location: "file:///download/registry/foo/baz/1.10.0//*?archive=tar.gz", location: "file:///download/registry/foo/baz/1.10.0//*?archive=tar.gz",
version: "1.10.0", version: "1.10.0",
}, }},
"registry/local/sub": { "registry/local/sub": {{
location: "test-fixtures/registry-tar-subdir/foo.tgz//*?archive=tar.gz", location: "test-fixtures/registry-tar-subdir/foo.tgz//*?archive=tar.gz",
version: "0.1.2", version: "0.1.2",
}, }},
"exists-in-registry/identifier/provider": { "exists-in-registry/identifier/provider": {{
location: "file:///registry/exists", location: "file:///registry/exists",
version: "0.2.0", version: "0.2.0",
}},
"test-versions/name/provider": {
{version: "2.2.0"},
{version: "2.1.1"},
{version: "1.2.2"},
{version: "1.2.1"},
}, },
} }
@ -59,30 +68,26 @@ func latestVersion(versions []string) string {
return col[len(col)-1].String() return col[len(col)-1].String()
} }
// Just enough like a registry to exercise our code. func mockRegHandler() http.Handler {
// Returns the location of the latest version
func mockRegistry() *httptest.Server {
mux := http.NewServeMux() mux := http.NewServeMux()
server := httptest.NewServer(mux)
mux.Handle("/v1/modules/", download := func(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/v1/modules/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimLeft(r.URL.Path, "/") p := strings.TrimLeft(r.URL.Path, "/")
// handle download request // handle download request
download := regexp.MustCompile(`^([-a-z]+/\w+/\w+)/download$`) re := regexp.MustCompile(`^([-a-z]+/\w+/\w+)/download$`)
// download lookup // download lookup
matches := download.FindStringSubmatch(p) matches := re.FindStringSubmatch(p)
if len(matches) != 2 { if len(matches) != 2 {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
mod, ok := testMods[matches[1]] versions, ok := testMods[matches[1]]
if !ok { if !ok {
w.WriteHeader(http.StatusNotFound) http.NotFound(w, r)
return return
} }
mod := versions[0]
location := mod.location location := mod.location
if !strings.HasPrefix(location, "file:///") { if !strings.HasPrefix(location, "file:///") {
@ -95,9 +100,82 @@ func mockRegistry() *httptest.Server {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
// no body // no body
return return
}
versions := func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimLeft(r.URL.Path, "/")
re := regexp.MustCompile(`^([-a-z]+/\w+/\w+)/versions$`)
matches := re.FindStringSubmatch(p)
if len(matches) != 2 {
w.WriteHeader(http.StatusBadRequest)
return
}
name := matches[1]
versions, ok := testMods[name]
if !ok {
http.NotFound(w, r)
return
}
// only adding the single requested module for now
// this is the minimal that any regisry is epected to support
mpvs := &response.ModuleProviderVersions{
Source: name,
}
for _, v := range versions {
mv := &response.ModuleVersion{
Version: v.version,
}
mpvs.Versions = append(mpvs.Versions, mv)
}
resp := response.ModuleVersions{
Modules: []*response.ModuleProviderVersions{mpvs},
}
js, err := json.Marshal(resp)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
mux.Handle("/v1/modules/",
http.StripPrefix("/v1/modules/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/download") {
download(w, r)
return
}
if strings.HasSuffix(r.URL.Path, "/versions") {
versions(w, r)
return
}
http.NotFound(w, r)
})), })),
) )
mux.HandleFunc("/.well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"modules.v1":"/v1/modules/"}`)
})
return mux
}
// Just enough like a registry to exercise our code.
// Returns the location of the latest version
func mockRegistry() *httptest.Server {
server := httptest.NewServer(mockRegHandler())
return server
}
func mockTLSRegistry() *httptest.Server {
server := httptest.NewTLSServer(mockRegHandler())
return server return server
} }
@ -138,12 +216,12 @@ func TestDetectRegistry(t *testing.T) {
}{ }{
{ {
source: "registry/foo/bar", source: "registry/foo/bar",
location: testMods["registry/foo/bar"].location, location: testMods["registry/foo/bar"][0].location,
found: true, found: true,
}, },
{ {
source: "registry/foo/baz", source: "registry/foo/baz",
location: testMods["registry/foo/baz"].location, location: testMods["registry/foo/baz"][0].location,
found: true, found: true,
}, },
// this should not be found, and is no longer valid as a local source // this should not be found, and is no longer valid as a local source

View File

@ -30,7 +30,6 @@ const (
var ( var (
client *http.Client client *http.Client
tfVersion = version.String() tfVersion = version.String()
regDisco = disco.NewDisco()
) )
func init() { func init() {
@ -45,21 +44,27 @@ func (e errModuleNotFound) Error() string {
} }
// Lookup module versions in the registry. // Lookup module versions in the registry.
func lookupModuleVersions(module *regsrc.Module) (*response.ModuleVersions, error) { func lookupModuleVersions(regDisco *disco.Disco, module *regsrc.Module) (*response.ModuleVersions, error) {
if module.RawHost == nil { if module.RawHost == nil {
module.RawHost = regsrc.NewFriendlyHost(defaultRegistry) module.RawHost = regsrc.NewFriendlyHost(defaultRegistry)
} }
regUrl := regDisco.DiscoverServiceURL(svchost.Hostname(module.RawHost.Normalized()), serviceID) regURL := regDisco.DiscoverServiceURL(svchost.Hostname(module.RawHost.Normalized()), serviceID)
if regUrl == nil { if regURL == nil {
regUrl = &url.URL{ regURL = &url.URL{
Scheme: "https", Scheme: "https",
Host: module.RawHost.String(), Host: module.RawHost.String(),
Path: defaultApiPath, Path: defaultApiPath,
} }
} }
location := fmt.Sprintf("%s/%s/%s/%s/versions", regUrl, module.RawNamespace, module.RawName, module.RawProvider) service := regURL.String()
if service[len(service)-1] != '/' {
service += "/"
}
location := fmt.Sprintf("%s%s/%s/%s/versions", service, module.RawNamespace, module.RawName, module.RawProvider)
log.Printf("[DEBUG] fetching module versions from %q", location) log.Printf("[DEBUG] fetching module versions from %q", location)
req, err := http.NewRequest("GET", location, nil) req, err := http.NewRequest("GET", location, nil)
@ -69,6 +74,15 @@ func lookupModuleVersions(module *regsrc.Module) (*response.ModuleVersions, erro
req.Header.Set(xTerraformVersion, tfVersion) req.Header.Set(xTerraformVersion, tfVersion)
// if discovery required a custom transport, then we should use that too
client := client
if regDisco.Transport != nil {
client = &http.Client{
Transport: regDisco.Transport,
Timeout: requestTimeout,
}
}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -0,0 +1,152 @@
package module
import (
"context"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
cleanhttp "github.com/hashicorp/go-cleanhttp"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/registry/regsrc"
"github.com/hashicorp/terraform/svchost/disco"
)
// Return a transport to use for this test server.
// This not only loads the tls.Config from the test server for proper cert
// validation, but also inserts a Dialer that resolves localhost and
// example.com to 127.0.0.1 with the correct port, since 127.0.0.1 on its own
// isn't a valid registry hostname.
// TODO: cert validation not working here, so we use don't verify for now.
func mockTransport(server *httptest.Server) *http.Transport {
u, _ := url.Parse(server.URL)
_, port, _ := net.SplitHostPort(u.Host)
transport := cleanhttp.DefaultTransport()
transport.TLSClientConfig = server.TLS
transport.TLSClientConfig.InsecureSkipVerify = true
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
switch host {
case "example.com", "localhost", "localhost.localdomain", "registry.terraform.io":
addr = "127.0.0.1"
if port != "" {
addr += ":" + port
}
}
return (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext(ctx, network, addr)
}
return transport
}
func TestMockDiscovery(t *testing.T) {
server := mockTLSRegistry()
defer server.Close()
regDisco := disco.NewDisco()
regDisco.Transport = mockTransport(server)
regURL := regDisco.DiscoverServiceURL("example.com", serviceID)
if regURL == nil {
t.Fatal("no registry service discovered")
}
if regURL.Host != "example.com" {
t.Fatal("expected registry host example.com, got:", regURL.Host)
}
}
func TestLookupModuleVersions(t *testing.T) {
server := mockTLSRegistry()
defer server.Close()
regDisco := disco.NewDisco()
regDisco.Transport = mockTransport(server)
// test with and without a hostname
for _, src := range []string{
"example.com/test-versions/name/provider",
"test-versions/name/provider",
} {
modsrc, err := regsrc.ParseModuleSource(src)
if err != nil {
t.Fatal(err)
}
resp, err := lookupModuleVersions(regDisco, modsrc)
if err != nil {
t.Fatal(err)
}
if len(resp.Modules) != 1 {
t.Fatal("expected 1 module, got", len(resp.Modules))
}
mod := resp.Modules[0]
name := "test-versions/name/provider"
if mod.Source != name {
t.Fatalf("expected module name %q, got %q", name, mod.Source)
}
if len(mod.Versions) != 4 {
t.Fatal("expected 4 versions, got", len(mod.Versions))
}
for _, v := range mod.Versions {
_, err := version.NewVersion(v.Version)
if err != nil {
t.Fatalf("invalid version %q: %s", v.Version, err)
}
}
}
}
func TestACCLookupModuleVersions(t *testing.T) {
server := mockTLSRegistry()
defer server.Close()
regDisco := disco.NewDisco()
// test with and without a hostname
for _, src := range []string{
"terraform-aws-modules/vpc/aws",
defaultRegistry + "/terraform-aws-modules/vpc/aws",
} {
modsrc, err := regsrc.ParseModuleSource(src)
if err != nil {
t.Fatal(err)
}
resp, err := lookupModuleVersions(regDisco, modsrc)
if err != nil {
t.Fatal(err)
}
if len(resp.Modules) != 1 {
t.Fatal("expected 1 module, got", len(resp.Modules))
}
mod := resp.Modules[0]
name := "terraform-aws-modules/vpc/aws"
if mod.Source != name {
t.Fatalf("expected module name %q, got %q", name, mod.Source)
}
if len(mod.Versions) == 0 {
t.Fatal("expected multiple versions, got 0")
}
for _, v := range mod.Versions {
_, err := version.NewVersion(v.Version)
if err != nil {
t.Fatalf("invalid version %q: %s", v.Version, err)
}
}
}
}