From f83d5866fecab55f097d72715bf846980f4dd414 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 2 Aug 2018 14:35:08 -0700 Subject: [PATCH] plugin/discovery: removing deprecated functions --- plugin/discovery/get.go | 323 ++++++++++------------- plugin/discovery/get_test.go | 327 +++++++++++++++++------- registry/regsrc/regsrc.go | 2 +- registry/response/terraform_provider.go | 28 +- 4 files changed, 375 insertions(+), 305 deletions(-) diff --git a/plugin/discovery/get.go b/plugin/discovery/get.go index 842eef08f..24b179480 100644 --- a/plugin/discovery/get.go +++ b/plugin/discovery/get.go @@ -10,12 +10,9 @@ import ( "os" "path/filepath" "runtime" - "sort" "strconv" "strings" - "golang.org/x/net/html" - getter "github.com/hashicorp/go-getter" multierror "github.com/hashicorp/go-multierror" @@ -27,19 +24,10 @@ import ( "github.com/mitchellh/cli" ) -// Releases are located by parsing the html listing from releases.hashicorp.com. -// -// The URL for releases follows the pattern: -// https://releases.hashicorp.com/terraform-provider-name//terraform-provider-name___. -// -// The plugin protocol version will be saved with the release and returned in -// the header X-TERRAFORM_PROTOCOL_VERSION. +// Releases are located by querying the terraform registry. const protocolVersionHeader = "x-terraform-protocol-version" -//var releaseHost = "https://releases.hashicorp.com" -var releaseHost = "https://tf-registry-staging.herokuapp.com" - var httpClient *http.Client var errVersionNotFound = errors.New("version not found") @@ -117,7 +105,7 @@ type ProviderInstaller struct { // be presented alongside context about what is being installed, and thus the // error messages do not redundantly include such information. func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, error) { - // a little bit of initialization + // a little bit of initialization. if i.OS == "" { i.OS = runtime.GOOS } @@ -125,14 +113,15 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, e i.Arch = runtime.GOARCH } if i.registry == nil { - i.registry = registry.NewClient(i.Services, nil, nil) + i.registry = registry.NewClient(i.Services, nil) } + // get a full listing of versions for the requested provider allVersions, err := i.listProviderVersions(provider) // TODO: return multiple errors if err != nil { - return PluginMeta{}, err + return PluginMeta{}, ErrorNoSuchProvider } if len(allVersions.Versions) == 0 { return PluginMeta{}, ErrorNoSuitableVersion @@ -144,71 +133,39 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, e return PluginMeta{}, ErrorNoSuitableVersion } - // sort them newest to oldest - sort.Sort(response.Collection(versions)) - // the winning version is the newest + // sort them newest to oldest. The newest version wins! + response.Collection(versions).Sort() versionMeta := versions[0] - // get a Version from the version string - // we already know this will not error from the preceding functions - v, _ := VersionStr(versionMeta.Version).Parse() + v := VersionStr(versionMeta.Version).MustParse() - // Ensure that our installation directory exists - err = os.MkdirAll(i.Dir, os.ModePerm) - if err != nil { - return PluginMeta{}, fmt.Errorf("failed to create plugin dir %s: %s", i.Dir, err) + // check platform compatibility + if err := i.checkPlatformCompatibility(versionMeta); err != nil { + // filter the list of versions to those that support the requested OS_ARCH + // reset the "current" versionMeta + // versionMeta = filteredVersions[0] + return PluginMeta{}, ErrorNoVersionCompatible } - // check plugin protocol compatibility - // We only validate the most recent version that meets the version constraints. - // see RFC TF-055: Provider Protocol Versioning for more information - protoString := strconv.Itoa(int(i.PluginProtocolVersion)) - protocolVersion, err := VersionStr(protoString).Parse() - if err != nil { - return PluginMeta{}, fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion) - } - protocolConstraint, err := protocolVersion.MinorUpgradeConstraintStr().Parse() - if err != nil { - // This should not fail if the preceding function succeeded. - return PluginMeta{}, fmt.Errorf("invalid plugin protocol version: %q", protocolVersion.String()) - } - - for _, p := range versionMeta.Protocols { - proPro, err := VersionStr(p).Parse() - if err != nil { - // invalid protocol reported by the registry. Move along. - log.Printf("[WARN] invalid provider protocol version %q found in the registry", provider, versionMeta.Version) - continue - } - if !protocolConstraint.Allows(proPro) { - // TODO: get most recent compatible plugin and return a handy-dandy string for the user - // latest, err := getNewestCompatiblePlugin - // i.Ui.output|info): "the latest version of plugin BLAH which supports protocol BLAH is BLAH" - // Add this to your provider block: - // version = ~BLAH - // and if none is found, return ErrorNoVersionCompatible - return PluginMeta{}, fmt.Errorf("The latest version of plugin %q does not support plugin protocol version %q", provider, protocolVersion) - } - } - - var downloadURLs *response.TerraformProviderPlatformLocation - // check plugin platform compatibility - for _, p := range versionMeta.Platforms { - if p.Arch == i.Arch && p.OS == i.OS { - downloadURLs, err = i.listProviderDownloadURLs(provider, versionMeta.Version) - if err != nil { - return PluginMeta{}, fmt.Errorf("Problem getting ") + // check protocol compatibility + if err := i.checkPluginProtocol(versionMeta); err != nil { + closestMatch, err := i.findProtocolCompatibleVersion(versions) + if err == nil { + if err := i.checkPlatformCompatibility(closestMatch); err != nil { + // This is where we give up instead of leap-frogging every version to check protocol & platform + return PluginMeta{}, ErrorNoSuitableVersion } - break + // This is a placeholder message. + i.Ui.Error(fmt.Sprintf("the most recent version of %s to match your platform is %s", provider, closestMatch)) + return PluginMeta{}, ErrorNoVersionCompatible } - // TODO: return the most recent compatible versions - // return PluginMeta{}, ErrorNoVersionCompatibleWithPlatform - return PluginMeta{}, fmt.Errorf("The latest version of plugin %q does not support the requested platform %s %s", provider, i.OS, i.Arch) + return PluginMeta{}, ErrorNoVersionCompatibleWithPlatform } + downloadURLs, err := i.listProviderDownloadURLs(provider, versionMeta.Version) providerURL := downloadURLs.DownloadURL if !i.SkipVerify { - sha256, err := i.getProviderChecksum(provider, downloadURLs) + sha256, err := i.getProviderChecksum(downloadURLs) if err != nil { return PluginMeta{}, err } @@ -283,7 +240,7 @@ func (i *ProviderInstaller) install(provider string, version Version, url string } // Link or copy the cached binary into our install dir so the - // normal resolution machinery can find it. + // normal resolution machinery can find it. filename := filepath.Base(cached) targetPath := filepath.Join(i.Dir, filename) @@ -351,7 +308,6 @@ func (i *ProviderInstaller) install(provider string, version Version, url string return err } } - return nil } @@ -387,44 +343,7 @@ func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaS return removed, errs } -// Plugins are referred to by the short name, but all URLs and files will use -// the full name prefixed with terraform-- -func (i *ProviderInstaller) providerName(name string) string { - return "terraform-provider-" + name -} - -func (i *ProviderInstaller) providerFileName(name, version string) string { - os := i.OS - arch := i.Arch - if os == "" { - os = runtime.GOOS - } - if arch == "" { - arch = runtime.GOARCH - } - return fmt.Sprintf("%s_%s_%s_%s.zip", i.providerName(name), version, os, arch) -} - -// providerVersionsURL returns the path to the released versions directory for the provider: -// https://releases.hashicorp.com/terraform-provider-name/ -func (i *ProviderInstaller) providerVersionsURL(name string) string { - return releaseHost + "/" + i.providerName(name) + "/" -} - -// providerURL returns the full path to the provider file, using the current OS -// and ARCH: -// .../terraform-provider-name_/terraform-provider-name___. -func (i *ProviderInstaller) providerURL(name, version string) string { - return fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, i.providerFileName(name, version)) -} - -func (i *ProviderInstaller) providerChecksumURL(name, version string) string { - fileName := fmt.Sprintf("%s_%s_SHA256SUMS", i.providerName(name), version) - u := fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, fileName) - return u -} - -func (i *ProviderInstaller) getProviderChecksum(name string, urls *response.TerraformProviderPlatformLocation) (string, error) { +func (i *ProviderInstaller) getProviderChecksum(urls *response.TerraformProviderPlatformLocation) (string, error) { checksums, err := getPluginSHA256SUMs(urls.ShasumsURL, urls.ShasumsSignatureURL) if err != nil { return "", err @@ -440,6 +359,86 @@ func (i *ProviderInstaller) listProviderVersions(name string) (*response.Terrafo return versions, err } +func (i *ProviderInstaller) listProviderDownloadURLs(name, version string) (*response.TerraformProviderPlatformLocation, error) { + urls, err := i.registry.TerraformProviderLocation(regsrc.NewTerraformProvider(name, i.OS, i.Arch), version) + if urls == nil { + return nil, fmt.Errorf("No download urls found for provider %s", name) + } + return urls, err +} + +// REVIEWER QUESTION: this ends up swallowing a bunch of errors from +// checkPluginProtocol. Do they need to be percolated up better, or would +// debug messages would suffice in these situations? +func (i *ProviderInstaller) findProtocolCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) { + for _, version := range versions { + if err := i.checkPluginProtocol(version); err == nil { + return version, nil + } + } + return nil, ErrorNoVersionCompatible +} + +func (i *ProviderInstaller) checkPluginProtocol(versionMeta *response.TerraformProviderVersion) error { + // TODO: should this be a different error? We should probably differentiate between + // no compatible versions and no protocol versions listed at all + // No protocols at all! + if len(versionMeta.Protocols) == 0 { + return fmt.Errorf("no plugin protocol versions listed") + } + + protoString := strconv.Itoa(int(i.PluginProtocolVersion)) + protocolVersion, err := VersionStr(protoString).Parse() + if err != nil { + return fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion) + } + protocolConstraint, err := protocolVersion.MinorUpgradeConstraintStr().Parse() + if err != nil { + // This should not fail if the preceding function succeeded. + return fmt.Errorf("invalid plugin protocol version: %q", protocolVersion.String()) + } + + for _, p := range versionMeta.Protocols { + proPro, err := VersionStr(p).Parse() + if err != nil { + // invalid protocol reported by the registry. Move along. + log.Printf("[WARN] invalid provider protocol version %q found in the registry", versionMeta.Version) + continue + } + // success! + if protocolConstraint.Allows(proPro) { + return nil + } + } + + return ErrorNoVersionCompatible +} + +// REVIEWER QUESTION (again): this ends up swallowing a bunch of errors from +// checkPluginProtocol. Do they need to be percolated up better, or would +// debug messages would suffice in these situations? +func (i *ProviderInstaller) findPlatformCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) { + for _, version := range versions { + if err := i.checkPlatformCompatibility(version); err == nil { + return version, nil + } + } + + return nil, ErrorNoVersionCompatibleWithPlatform +} + +func (i *ProviderInstaller) checkPlatformCompatibility(versionMeta *response.TerraformProviderVersion) error { + if len(versionMeta.Platforms) == 0 { + return fmt.Errorf("no supported provider platforms listed") + } + for _, p := range versionMeta.Platforms { + if p.Arch == i.Arch && p.OS == i.OS { + return nil + } + } + return fmt.Errorf("version %s does not support the requested platform %s_%s", versionMeta.Version, i.OS, i.Arch) +} + // take the list of available versions for a plugin, and filter out those that // don't fit the constraints. func allowedVersions(available *response.TerraformProviderVersions, required Constraints) []*response.TerraformProviderVersion { @@ -455,83 +454,9 @@ func allowedVersions(available *response.TerraformProviderVersions, required Con allowed = append(allowed, v) } } - return allowed } -// return a list of the plugin versions at the given URL -func listPluginVersions(url string) ([]Version, error) { - resp, err := httpClient.Get(url) - if err != nil { - // http library produces a verbose error message that includes the - // URL being accessed, etc. - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(resp.Body) - log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body) - - switch resp.StatusCode { - case http.StatusNotFound, http.StatusForbidden: - // These are treated as indicative of the given name not being - // a valid provider name at all. - return nil, ErrorNoSuchProvider - - default: - // All other errors are assumed to be operational problems. - return nil, fmt.Errorf("error accessing %s: %s", url, resp.Status) - } - - } - - body, err := html.Parse(resp.Body) - if err != nil { - log.Fatal(err) - } - - names := []string{} - - // all we need to do is list links on the directory listing page that look like plugins - var f func(*html.Node) - f = func(n *html.Node) { - if n.Type == html.ElementNode && n.Data == "a" { - c := n.FirstChild - if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") { - names = append(names, c.Data) - return - } - } - for c := n.FirstChild; c != nil; c = c.NextSibling { - f(c) - } - } - f(body) - - return versionsFromNames(names), nil -} - -// parse the list of directory names into a sorted list of available versions -func versionsFromNames(names []string) []Version { - var versions []Version - for _, name := range names { - parts := strings.SplitN(name, "_", 2) - if len(parts) == 2 && parts[1] != "" { - v, err := VersionStr(parts[1]).Parse() - if err != nil { - // filter invalid versions scraped from the page - log.Printf("[WARN] invalid version found for %q: %s", name, err) - continue - } - - versions = append(versions, v) - } - } - - return versions -} - func checksumForFile(sums []byte, name string) string { for _, line := range strings.Split(string(sums), "\n") { parts := strings.Fields(line) @@ -579,11 +504,27 @@ func getFile(url string) ([]byte, error) { return data, nil } -func GetReleaseHost() string { - return releaseHost -} +// ProviderProtocolTooOld is a message sent to the CLI UI if the provider's +// supported protocol versions are too old for the user's version of terraform, +// but an older version of the provider is compatible. +const providerProtocolTooOld = `Provider %q v%s is not compatible with Terraform %s. -func (i *ProviderInstaller) listProviderDownloadURLs(name, version string) (*response.TerraformProviderPlatformLocation, error) { - urls, err := i.registry.TerraformProviderLocation(regsrc.NewTerraformProvider(name, i.OS, i.Arch), version) - return urls, err -} +Provider version %s is the earliest compatible version. +Select it with the following version constraint: + + version = %q +` + +// ProviderProtocolTooNew is a message sent to the CLI UI if the provider's +// supported protocol versions are too new for the user's version of terraform, +// and the user could either upgrade terraform or choose an older version of the +// provider +const providerProtocolTooNew = `Provider %q v%s is not compatible with Terraform %s. + +Provider version v%s is the latest compatible version. Select +it with the following constraint: + + version = %q + +Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases. +` diff --git a/plugin/discovery/get_test.go b/plugin/discovery/get_test.go index 1f123e142..61342d2e4 100644 --- a/plugin/discovery/get_test.go +++ b/plugin/discovery/get_test.go @@ -2,30 +2,80 @@ package discovery import ( "archive/zip" + "encoding/json" "fmt" "io" "io/ioutil" + "log" + "net" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" - "regexp" + "runtime" "strings" "testing" + "github.com/hashicorp/terraform/registry" + "github.com/hashicorp/terraform/registry/response" + "github.com/hashicorp/terraform/svchost" + "github.com/hashicorp/terraform/svchost/disco" "github.com/mitchellh/cli" ) const testProviderFile = "test provider binary" +func TestMain(m *testing.M) { + server := testReleaseServer() + l, err := net.Listen("tcp", "127.0.0.1:8080") + if err != nil { + log.Fatal(err) + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + server.Listener.Close() + server.Listener = l + server.Start() + defer server.Close() + + os.Exit(m.Run()) +} + // return the directory listing for the "test" provider func testListingHandler(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(versionList)) + parts := strings.Split(r.URL.Path, "/") + if len(parts) != 6 { + http.Error(w, "not found", http.StatusNotFound) + return + } + provider := parts[4] + if provider == "test" { + js, err := json.Marshal(versionList) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(js) + } + http.Error(w, ErrorNoSuchProvider.Error(), http.StatusNotFound) + return + +} + +// return the download URLs for the "test" provider +func testDownloadHandler(w http.ResponseWriter, r *http.Request) { + js, err := json.Marshal(downloadURLs) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(js) } func testChecksumHandler(w http.ResponseWriter, r *http.Request) { - // this exact plugin has a signnature and checksum file + // this exact plugin has a signature and checksum file if r.URL.Path == "/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS" { http.ServeFile(w, r, "testdata/terraform-provider-template_0.1.0_SHA256SUMS") return @@ -51,32 +101,25 @@ func testChecksumHandler(w http.ResponseWriter, r *http.Request) { // returns a 200 for a valid provider url, using the patch number for the // plugin protocol version. func testHandler(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/terraform-provider-test/" { + if strings.HasSuffix(r.URL.Path, "/versions") { testListingHandler(w, r) return } + if strings.Contains(r.URL.Path, "/download") { + testDownloadHandler(w, r) + return + } + parts := strings.Split(r.URL.Path, "/") - if len(parts) != 4 { + if len(parts) != 7 { http.Error(w, "not found", http.StatusNotFound) return } - filename := parts[3] - - reg := regexp.MustCompile(`(terraform-provider-test)_(\d).(\d).(\d)_([^_]+)_([^._]+).zip`) - - fileParts := reg.FindStringSubmatch(filename) - if len(fileParts) != 7 { - http.Error(w, "invalid provider: "+filename, http.StatusNotFound) - return - } - - w.Header().Set(protocolVersionHeader, fileParts[4]) - // write a dummy file z := zip.NewWriter(w) - fn := fmt.Sprintf("%s_v%s.%s.%s_x%s", fileParts[1], fileParts[2], fileParts[3], fileParts[4], fileParts[4]) + fn := fmt.Sprintf("%s_v%s", parts[4], parts[5]) f, err := z.Create(fn) if err != nil { panic(err) @@ -87,33 +130,42 @@ func testHandler(w http.ResponseWriter, r *http.Request) { func testReleaseServer() *httptest.Server { handler := http.NewServeMux() - handler.HandleFunc("/terraform-provider-test/", testHandler) + handler.HandleFunc("/v1/providers/terraform-providers/", testHandler) handler.HandleFunc("/terraform-provider-template/", testChecksumHandler) handler.HandleFunc("/terraform-provider-badsig/", testChecksumHandler) + handler.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 httptest.NewServer(handler) -} - -func TestMain(m *testing.M) { - server := testReleaseServer() - releaseHost = server.URL - - os.Exit(m.Run()) + return httptest.NewUnstartedServer(handler) } func TestVersionListing(t *testing.T) { - i := &ProviderInstaller{} - versions, err := i.listProviderVersions("test") + server := testReleaseServer() + server.Start() + defer server.Close() + + i := newProviderInstaller(server) + + allVersions, err := i.listProviderVersions("test") + if err != nil { t.Fatal(err) } - Versions(versions).Sort() + var versions []*response.TerraformProviderVersion - expected := []string{ - "1.2.4", - "1.2.3", - "1.2.1", + for _, v := range allVersions.Versions { + versions = append(versions, v) + } + + response.Collection(versions).Sort() + + expected := []*response.TerraformProviderVersion{ + {Version: "1.2.4"}, + {Version: "1.2.3"}, + {Version: "1.2.1"}, } if len(versions) != len(expected) { @@ -121,24 +173,60 @@ func TestVersionListing(t *testing.T) { } for i, v := range versions { - if v.String() != expected[i] { + if v.Version != expected[i].Version { t.Fatalf("incorrect version: %q, expected %q", v, expected[i]) } } } func TestCheckProtocolVersions(t *testing.T) { - i := &ProviderInstaller{} - if checkPlugin(i.providerURL("test", VersionStr("1.2.3").MustParse().String()), 4) { - t.Fatal("protocol version 4 is not compatible") + tests := []struct { + VersionMeta *response.TerraformProviderVersion + Err bool + }{ + { + &response.TerraformProviderVersion{ + Protocols: []string{"1", "2"}, + }, + true, + }, + { + &response.TerraformProviderVersion{ + Protocols: []string{"4"}, + }, + false, + }, + { + &response.TerraformProviderVersion{ + Protocols: []string{"4.2"}, + }, + false, + }, } - if !checkPlugin(i.providerURL("test", VersionStr("1.2.3").MustParse().String()), 3) { - t.Fatal("protocol version 3 should be compatible") + server := testReleaseServer() + server.Start() + defer server.Close() + i := newProviderInstaller(server) + + for _, test := range tests { + err := i.checkPluginProtocol(test.VersionMeta) + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } } } func TestProviderInstallerGet(t *testing.T) { + server := testReleaseServer() + server.Start() + defer server.Close() + tmpDir, err := ioutil.TempDir("", "tf-plugin") if err != nil { t.Fatal(err) @@ -152,17 +240,20 @@ func TestProviderInstallerGet(t *testing.T) { PluginProtocolVersion: 5, SkipVerify: true, Ui: cli.NewMockUi(), + registry: registry.NewClient(Disco(server), nil), } _, err = i.Get("test", AllVersions) - if err != ErrorNoVersionCompatible { + + if err != ErrorNoVersionCompatibleWithPlatform { t.Fatal("want error for incompatible version") } i = &ProviderInstaller{ Dir: tmpDir, - PluginProtocolVersion: 3, + PluginProtocolVersion: 4, SkipVerify: true, Ui: cli.NewMockUi(), + registry: registry.NewClient(Disco(server), nil), } { @@ -184,12 +275,12 @@ func TestProviderInstallerGet(t *testing.T) { t.Fatal(err) } - // we should have version 1.2.3 - dest := filepath.Join(tmpDir, "terraform-provider-test_v1.2.3_x3") + // we should have version 1.2.4 + dest := filepath.Join(tmpDir, "terraform-provider-test_v1.2.4") wantMeta := PluginMeta{ Name: "test", - Version: VersionStr("1.2.3"), + Version: VersionStr("1.2.4"), Path: dest, } if !reflect.DeepEqual(gotMeta, wantMeta) { @@ -209,6 +300,9 @@ func TestProviderInstallerGet(t *testing.T) { } func TestProviderInstallerPurgeUnused(t *testing.T) { + server := testReleaseServer() + defer server.Close() + tmpDir, err := ioutil.TempDir("", "tf-plugin") if err != nil { t.Fatal(err) @@ -235,6 +329,7 @@ func TestProviderInstallerPurgeUnused(t *testing.T) { PluginProtocolVersion: 3, SkipVerify: true, Ui: cli.NewMockUi(), + registry: registry.NewClient(Disco(server), nil), } purged, err := i.PurgeUnused(map[string]PluginMeta{ "test": PluginMeta{ @@ -272,66 +367,102 @@ func TestProviderInstallerPurgeUnused(t *testing.T) { // Test fetching a provider's checksum file while verifying its signature. func TestProviderChecksum(t *testing.T) { - i := &ProviderInstaller{} - - // we only need the checksum, as getter is doing the actual file comparison. - sha256sum, err := i.getProviderChecksum("template", "0.1.0") - if err != nil { - t.Fatal(err) + tests := []struct { + URLs *response.TerraformProviderPlatformLocation + Err bool + }{ + { + &response.TerraformProviderPlatformLocation{ + ShasumsURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS", + ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig", + Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip", + }, + false, + }, + { + &response.TerraformProviderPlatformLocation{ + ShasumsURL: "http://127.0.0.1:8080/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS", + ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS.sig", + Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip", + }, + true, + }, } - // get the expected checksum for our os/arch - sumData, err := ioutil.ReadFile("testdata/terraform-provider-template_0.1.0_SHA256SUMS") - if err != nil { - t.Fatal(err) - } + i := ProviderInstaller{} - expected := checksumForFile(sumData, i.providerFileName("template", "0.1.0")) + for _, test := range tests { + sha256sum, err := i.getProviderChecksum(test.URLs) + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } - if sha256sum != expected { - t.Fatalf("expected: %s\ngot %s\n", sha256sum, expected) + // get the expected checksum for our os/arch + sumData, err := ioutil.ReadFile("testdata/terraform-provider-template_0.1.0_SHA256SUMS") + if err != nil { + t.Fatal(err) + } + + expected := checksumForFile(sumData, test.URLs.Filename) + + if sha256sum != expected { + t.Fatalf("expected: %s\ngot %s\n", sha256sum, expected) + } } } -// Test fetching a provider's checksum file witha bad signature -func TestProviderChecksumBadSignature(t *testing.T) { - i := &ProviderInstaller{} - - // we only need the checksum, as getter is doing the actual file comparison. - sha256sum, err := i.getProviderChecksum("badsig", "0.1.0") - if err == nil { - t.Fatal("expcted error") - } - - if !strings.Contains(err.Error(), "signature") { - t.Fatal("expected signature error, got:", err) - } - - if sha256sum != "" { - t.Fatal("expected no checksum, got:", sha256sum) +// newProviderInstaller returns a minimally-initialized ProviderInstaller +func newProviderInstaller(s *httptest.Server) ProviderInstaller { + return ProviderInstaller{ + registry: registry.NewClient(Disco(s), nil), + OS: runtime.GOOS, + Arch: runtime.GOARCH, } } -const versionList = ` - - - - - - -` +// 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 + "modules.v1": fmt.Sprintf("%s/v1/modules", s.URL), + "providers.v1": fmt.Sprintf("%s/v1/providers", s.URL), + } + d := disco.New() + + 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 +} + +var versionList = response.TerraformProvider{ + ID: "test", + Versions: []*response.TerraformProviderVersion{ + {Version: "1.2.1"}, + {Version: "1.2.3"}, + { + Version: "1.2.4", + Protocols: []string{"4"}, + Platforms: []*response.TerraformProviderPlatform{ + { + OS: "darwin", + Arch: "amd64", + }, + }, + }, + }, +} + +var downloadURLs = response.TerraformProviderPlatformLocation{ + ShasumsURL: "https://registry.terraform.io/terraform-provider-template/1.2.4/terraform-provider-test_1.2.4_SHA256SUMS", + ShasumsSignatureURL: "https://registry.terraform.io/terraform-provider-template/1.2.4/terraform-provider-test_1.2.4_SHA256SUMS.sig", + Filename: "terraform-provider-template_1.2.4_darwin_amd64.zip", + DownloadURL: "http://127.0.0.1:8080/v1/providers/terraform-providers/terraform-provider-test/1.2.4/terraform-provider-test_1.2.4_darwin_amd64.zip", +} diff --git a/registry/regsrc/regsrc.go b/registry/regsrc/regsrc.go index 187659fdc..c430bf141 100644 --- a/registry/regsrc/regsrc.go +++ b/registry/regsrc/regsrc.go @@ -4,5 +4,5 @@ package regsrc var ( // PublicRegistryHost is a FriendlyHost that represents the public registry. - PublicRegistryHost = NewFriendlyHost("tf-registry-staging.herokuapp.com") + PublicRegistryHost = NewFriendlyHost("registry.terraform.io") ) diff --git a/registry/response/terraform_provider.go b/registry/response/terraform_provider.go index 709961a59..8833198be 100644 --- a/registry/response/terraform_provider.go +++ b/registry/response/terraform_provider.go @@ -1,6 +1,10 @@ package response -import version "github.com/hashicorp/go-version" +import ( + "sort" + + version "github.com/hashicorp/go-version" +) // TerraformProvider is the response structure for all required information for // Terraform to choose a download URL. It must include all versions and all @@ -48,21 +52,15 @@ type TerraformProviderPlatformLocation struct { ShasumsSignatureURL string `json:"shasums_signature_url"` } -// Collection type implements the sort.Sort interface so that -// an array of TerraformProviderVersion can be sorted. +// Collection type for TerraformProviderVersion type Collection []*TerraformProviderVersion -func (v Collection) Len() int { - return len(v) -} +// Sort sorts versions from newest to oldest. +func (v Collection) Sort() { + sort.Slice(v, func(i, j int) bool { + versionA, _ := version.NewVersion(v[i].Version) + versionB, _ := version.NewVersion(v[j].Version) -func (v Collection) Less(i, j int) bool { - versionA, _ := version.NewVersion(v[i].Version) - versionB, _ := version.NewVersion(v[j].Version) - - return versionA.LessThan(versionB) -} - -func (v Collection) Swap(i, j int) { - v[i], v[j] = v[j], v[i] + return versionA.GreaterThan(versionB) + }) }