terraform/internal/getproviders/registry_client_test.go

208 lines
7.4 KiB
Go
Raw Normal View History

package getproviders
import (
2020-04-08 22:22:07 +02:00
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"strings"
"testing"
svchost "github.com/hashicorp/terraform-svchost"
disco "github.com/hashicorp/terraform-svchost/disco"
)
// testServices starts up a local HTTP server running a fake provider registry
// service and returns a service discovery object pre-configured to consider
// the host "example.com" to be served by the fake registry service.
//
// The returned discovery object also knows the hostname "not.example.com"
// which does not have a provider registry at all and "too-new.example.com"
// which has a "providers.v99" service that is inoperable but could be useful
// to test the error reporting for detecting an unsupported protocol version.
// It also knows fails.example.com but it refers to an endpoint that doesn't
// correctly speak HTTP, to simulate a protocol error.
//
// The second return value is a function to call at the end of a test function
// to shut down the test server. After you call that function, the discovery
// object becomes useless.
func testServices(t *testing.T) (services *disco.Disco, baseURL string, cleanup func()) {
server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler))
services = disco.New()
services.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{
"providers.v1": server.URL + "/providers/v1/",
})
services.ForceHostServices(svchost.Hostname("not.example.com"), map[string]interface{}{})
services.ForceHostServices(svchost.Hostname("too-new.example.com"), map[string]interface{}{
// This service doesn't actually work; it's here only to be
// detected as "too new" by the discovery logic.
"providers.v99": server.URL + "/providers/v99/",
})
services.ForceHostServices(svchost.Hostname("fails.example.com"), map[string]interface{}{
"providers.v1": server.URL + "/fails-immediately/",
})
internal/getproviders: LookupLegacyProvider This is a temporary helper so that we can potentially ship the new provider installer without making a breaking change by relying on the old default namespace lookup API on the default registry to find a proper FQN for a legacy provider provider address during installation. If it's given a non-legacy provider address then it just returns the given address verbatim, so any codepath using it will also correctly handle explicit full provider addresses. This also means it will automatically self-disable once we stop using addrs.NewLegacyProvider in the config loader, because there will therefore no longer be any legacy provider addresses in the config to resolve. (They'll be "default" provider addresses instead, assumed to be under registry.terraform.io/hashicorp/* ) It's not decided yet whether we will actually introduce the new provider in a minor release, but even if we don't this API function will likely be useful for a hypothetical automatic upgrade tool to introduce explicit full provider addresses into existing modules that currently rely on the equivalent to this lookup in the current provider installer. This is dead code for now, but my intent is that it would either be called as part of new provider installation to produce an address suitable to pass to Source.AvailableVersions, or it would be called from the aforementioned hypothetical upgrade tool. Whatever happens, these functions can be removed no later than one whole major release after the new provider installer is introduced, when everyone's had the opportunity to update their legacy unqualified addresses.
2020-01-22 01:01:49 +01:00
// We'll also permit registry.terraform.io here just because it's our
// default and has some unique features that are not allowed on any other
// hostname. It behaves the same as example.com, which should be preferred
// if you're not testing something specific to the default registry in order
// to ensure that most things are hostname-agnostic.
services.ForceHostServices(svchost.Hostname("registry.terraform.io"), map[string]interface{}{
"providers.v1": server.URL + "/providers/v1/",
})
return services, server.URL, func() {
server.Close()
}
}
// testRegistrySource is a wrapper around testServices that uses the created
// discovery object to produce a Source instance that is ready to use with the
// fake registry services.
//
// As with testServices, the second return value is a function to call at the end
// of your test in order to shut down the test server.
func testRegistrySource(t *testing.T) (source *RegistrySource, baseURL string, cleanup func()) {
services, baseURL, close := testServices(t)
source = NewRegistrySource(services)
return source, baseURL, close
}
func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
path := req.URL.EscapedPath()
if strings.HasPrefix(path, "/fails-immediately/") {
// Here we take over the socket and just close it immediately, to
// simulate one possible way a server might not be an HTTP server.
hijacker, ok := resp.(http.Hijacker)
if !ok {
// Not hijackable, so we'll just fail normally.
// If this happens, tests relying on this will fail.
resp.WriteHeader(500)
resp.Write([]byte(`cannot hijack`))
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
resp.WriteHeader(500)
resp.Write([]byte(`hijack failed`))
return
}
conn.Close()
return
}
2020-04-08 22:22:07 +02:00
if strings.HasPrefix(path, "/pkg/") {
switch path {
case "/pkg/awesomesauce/happycloud_1.2.0.zip":
resp.Write([]byte("some zip file"))
case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS":
resp.Write([]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n"))
case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS.sig":
resp.Write([]byte("GPG signature"))
default:
resp.WriteHeader(404)
resp.Write([]byte("unknown package file download"))
}
return
}
if !strings.HasPrefix(path, "/providers/v1/") {
resp.WriteHeader(404)
resp.Write([]byte(`not a provider registry endpoint`))
return
}
pathParts := strings.Split(path, "/")[3:]
internal/getproviders: LookupLegacyProvider This is a temporary helper so that we can potentially ship the new provider installer without making a breaking change by relying on the old default namespace lookup API on the default registry to find a proper FQN for a legacy provider provider address during installation. If it's given a non-legacy provider address then it just returns the given address verbatim, so any codepath using it will also correctly handle explicit full provider addresses. This also means it will automatically self-disable once we stop using addrs.NewLegacyProvider in the config loader, because there will therefore no longer be any legacy provider addresses in the config to resolve. (They'll be "default" provider addresses instead, assumed to be under registry.terraform.io/hashicorp/* ) It's not decided yet whether we will actually introduce the new provider in a minor release, but even if we don't this API function will likely be useful for a hypothetical automatic upgrade tool to introduce explicit full provider addresses into existing modules that currently rely on the equivalent to this lookup in the current provider installer. This is dead code for now, but my intent is that it would either be called as part of new provider installation to produce an address suitable to pass to Source.AvailableVersions, or it would be called from the aforementioned hypothetical upgrade tool. Whatever happens, these functions can be removed no later than one whole major release after the new provider installer is introduced, when everyone's had the opportunity to update their legacy unqualified addresses.
2020-01-22 01:01:49 +01:00
if len(pathParts) < 3 {
resp.WriteHeader(404)
resp.Write([]byte(`unexpected number of path parts`))
return
}
log.Printf("[TRACE] fake provider registry request for %#v", pathParts)
if pathParts[2] == "versions" {
if len(pathParts) != 3 {
resp.WriteHeader(404)
resp.Write([]byte(`extraneous path parts`))
return
}
switch pathParts[0] + "/" + pathParts[1] {
case "awesomesauce/happycloud":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
// Note that these version numbers are intentionally misordered
// so we can test that the client-side code places them in the
// correct order (lowest precedence first).
resp.Write([]byte(`{"versions":[{"version":"1.2.0"}, {"version":"1.0.0"}]}`))
case "weaksauce/no-versions":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
resp.Write([]byte(`{"versions":[]}`))
case "-/legacy":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
// This response is used for testing LookupLegacyProvider
resp.Write([]byte(`{"id":"legacycorp/legacy"}`))
case "-/changetype":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
// This (unrealistic) response is used for error handling code coverage
resp.Write([]byte(`{"id":"legacycorp/newtype"}`))
case "-/invalid":
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
// This (unrealistic) response is used for error handling code coverage
resp.Write([]byte(`{"id":"some/invalid/id/string"}`))
default:
resp.WriteHeader(404)
resp.Write([]byte(`unknown namespace or provider type`))
}
return
}
if len(pathParts) == 6 && pathParts[3] == "download" {
switch pathParts[0] + "/" + pathParts[1] {
case "awesomesauce/happycloud":
if pathParts[4] == "nonexist" {
resp.WriteHeader(404)
resp.Write([]byte(`unsupported OS`))
return
}
2020-04-08 22:22:07 +02:00
body := map[string]interface{}{
"protocols": []string{"5.0"},
"os": pathParts[4],
"arch": pathParts[5],
"filename": "happycloud_" + pathParts[2] + ".zip",
"shasum": "000000000000000000000000000000000000000000000000000000000000f00d",
"download_url": "/pkg/awesomesauce/happycloud_" + pathParts[2] + ".zip",
"shasums_url": "/pkg/awesomesauce/happycloud_" + pathParts[2] + "_SHA256SUMS",
"shasums_signature_url": "/pkg/awesomesauce/happycloud_" + pathParts[2] + "_SHA256SUMS.sig",
"signing_keys": map[string]interface{}{
"gpg_public_keys": []map[string]interface{}{
{
"ascii_armor": HashicorpPublicKey,
},
},
},
}
enc, err := json.Marshal(body)
if err != nil {
resp.WriteHeader(500)
resp.Write([]byte("failed to encode body"))
}
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(200)
2020-04-08 22:22:07 +02:00
resp.Write(enc)
default:
resp.WriteHeader(404)
resp.Write([]byte(`unknown namespace/provider/version/architecture`))
}
return
}
resp.WriteHeader(404)
resp.Write([]byte(`unrecognized path scheme`))
}