package test import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "regexp" "strings" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/auth" "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/registry/regsrc" "github.com/hashicorp/terraform/internal/registry/response" tfversion "github.com/hashicorp/terraform/version" ) // Disco return a *disco.Disco mapping registry.terraform.io, localhost, // localhost.localdomain, and example.com to the test server. func Disco(s *httptest.Server) *disco.Disco { services := map[string]interface{}{ // Note that both with and without trailing slashes are supported behaviours // TODO: add specific tests to enumerate both possibilities. "modules.v1": fmt.Sprintf("%s/v1/modules", s.URL), "providers.v1": fmt.Sprintf("%s/v1/providers", s.URL), } d := disco.NewWithCredentialsSource(credsSrc) d.SetUserAgent(httpclient.TerraformUserAgent(tfversion.String())) d.ForceHostServices(svchost.Hostname("registry.terraform.io"), services) d.ForceHostServices(svchost.Hostname("localhost"), services) d.ForceHostServices(svchost.Hostname("localhost.localdomain"), services) d.ForceHostServices(svchost.Hostname("example.com"), services) return d } // Map of module names and location of test modules. // Only one version for now, as we only lookup latest from the registry. type testMod struct { location string version string } // Map of provider names and location of test providers. // Only one version for now, as we only lookup latest from the registry. type testProvider struct { version string url string } const ( testCred = "test-auth-token" ) var ( regHost = svchost.Hostname(regsrc.PublicRegistryHost.Normalized()) credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ regHost: {"token": testCred}, }) ) // 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 // find the absolute path and return a complete URL. var testMods = map[string][]testMod{ "registry/foo/bar": {{ location: "file:///download/registry/foo/bar/0.2.3//*?archive=tar.gz", version: "0.2.3", }}, "registry/foo/baz": {{ location: "file:///download/registry/foo/baz/1.10.0//*?archive=tar.gz", version: "1.10.0", }}, "registry/local/sub": {{ location: "testdata/registry-tar-subdir/foo.tgz//*?archive=tar.gz", version: "0.1.2", }}, "exists-in-registry/identifier/provider": {{ location: "file:///registry/exists", version: "0.2.0", }}, "relative/foo/bar": {{ // There is an exception for the "relative/" prefix in the test registry server location: "/relative-path", version: "0.2.0", }}, "test-versions/name/provider": { {version: "2.2.0"}, {version: "2.1.1"}, {version: "1.2.2"}, {version: "1.2.1"}, }, "private/name/provider": { {version: "1.0.0"}, }, } var testProviders = map[string][]testProvider{ "-/foo": { { version: "0.2.3", url: "https://releases.hashicorp.com/terraform-provider-foo/0.2.3/terraform-provider-foo.zip", }, {version: "0.3.0"}, }, "-/bar": { { version: "0.1.1", url: "https://releases.hashicorp.com/terraform-provider-bar/0.1.1/terraform-provider-bar.zip", }, {version: "0.1.2"}, }, } func providerAlias(provider string) string { re := regexp.MustCompile("^-/") if re.MatchString(provider) { return re.ReplaceAllString(provider, "terraform-providers/") } return provider } func init() { // Add provider aliases for provider, info := range testProviders { alias := providerAlias(provider) testProviders[alias] = info } } func mockRegHandler() http.Handler { mux := http.NewServeMux() moduleDownload := func(w http.ResponseWriter, r *http.Request) { p := strings.TrimLeft(r.URL.Path, "/") // handle download request re := regexp.MustCompile(`^([-a-z]+/\w+/\w+).*/download$`) // download lookup matches := re.FindStringSubmatch(p) if len(matches) != 2 { w.WriteHeader(http.StatusBadRequest) return } // check for auth if strings.Contains(matches[0], "private/") { if !strings.Contains(r.Header.Get("Authorization"), testCred) { http.Error(w, "", http.StatusForbidden) return } } versions, ok := testMods[matches[1]] if !ok { http.NotFound(w, r) return } mod := versions[0] location := mod.location if !strings.HasPrefix(matches[0], "relative/") && !strings.HasPrefix(location, "file:///") { // we can't use filepath.Abs because it will clean `//` wd, _ := os.Getwd() location = fmt.Sprintf("file://%s/%s", wd, location) } w.Header().Set("X-Terraform-Get", location) w.WriteHeader(http.StatusNoContent) // no body } moduleVersions := 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 } // check for auth if strings.Contains(matches[1], "private/") { if !strings.Contains(r.Header.Get("Authorization"), testCred) { http.Error(w, "", http.StatusForbidden) } } 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") { moduleDownload(w, r) return } if strings.HasSuffix(r.URL.Path, "/versions") { moduleVersions(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":"http://localhost/v1/modules/", "providers.v1":"http://localhost/v1/providers/"}`) }) return mux } // Registry returns an httptest server that mocks out some registry functionality. func Registry() *httptest.Server { return httptest.NewServer(mockRegHandler()) } // RegistryRetryableErrorsServer returns an httptest server that mocks out the // registry API to return 502 errors. func RegistryRetryableErrorsServer() *httptest.Server { mux := http.NewServeMux() mux.HandleFunc("/v1/modules/", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "mocked server error", http.StatusBadGateway) }) mux.HandleFunc("/v1/providers/", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "mocked server error", http.StatusBadGateway) }) return httptest.NewServer(mux) }