diff --git a/plugin/discovery/get.go b/plugin/discovery/get.go index 4928db234..e5ffeb49b 100644 --- a/plugin/discovery/get.go +++ b/plugin/discovery/get.go @@ -32,35 +32,6 @@ var releaseHost = "https://releases.hashicorp.com" var httpClient = cleanhttp.DefaultClient() -// Plugins are referred to by the short name, but all URLs and files will use -// the full name prefixed with terraform-- -func providerName(name string) string { - return "terraform-provider-" + name -} - -func providerFileName(name, version string) string { - return fmt.Sprintf("%s_%s_%s_%s.zip", providerName(name), version, runtime.GOOS, runtime.GOARCH) -} - -// providerVersionsURL returns the path to the released versions directory for the provider: -// https://releases.hashicorp.com/terraform-provider-name/ -func providerVersionsURL(name string) string { - return releaseHost + "/" + providerName(name) + "/" -} - -// providerURL returns the full path to the provider file, using the current OS -// and ARCH: -// .../terraform-provider-name_/terraform-provider-name___. -func providerURL(name, version string) string { - return fmt.Sprintf("%s%s/%s", providerVersionsURL(name), version, providerFileName(name, version)) -} - -func providerChecksumURL(name, version string) string { - fileName := fmt.Sprintf("%s_%s_SHA256SUMS", providerName(name), version) - u := fmt.Sprintf("%s%s/%s", providerVersionsURL(name), version, fileName) - return u -} - // An Installer maintains a local cache of plugins by downloading plugins // from an online repository. type Installer interface { @@ -78,6 +49,13 @@ type ProviderInstaller struct { PluginProtocolVersion uint + // OS and Arch specify the OS and architecture that should be used when + // installing plugins. These use the same labels as the runtime.GOOS and + // runtime.GOARCH variables respectively, and indeed the values of these + // are used as defaults if either of these is the empty string. + OS string + Arch string + // Skip checksum and signature verification SkipVerify bool } @@ -102,7 +80,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) { - versions, err := listProviderVersions(provider) + versions, err := i.listProviderVersions(provider) // TODO: return multiple errors if err != nil { return PluginMeta{}, err @@ -122,10 +100,10 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, e // take the first matching plugin we find for _, v := range versions { - url := providerURL(provider, v.String()) + url := i.providerURL(provider, v.String()) if !i.SkipVerify { - sha256, err := getProviderChecksum(provider, v.String()) + sha256, err := i.getProviderChecksum(provider, v.String()) if err != nil { return PluginMeta{}, err } @@ -218,6 +196,52 @@ 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, version string) (string, error) { + checksums, err := getPluginSHA256SUMs(i.providerChecksumURL(name, version)) + if err != nil { + return "", err + } + + return checksumForFile(checksums, i.providerFileName(name, version)), nil +} + // Return the plugin version by making a HEAD request to the provided url func checkPlugin(url string, pluginProtocolVersion uint) bool { resp, err := httpClient.Head(url) @@ -246,6 +270,17 @@ func checkPlugin(url string, pluginProtocolVersion uint) bool { return protoVersion == int(pluginProtocolVersion) } +// list the version available for the named plugin +func (i *ProviderInstaller) listProviderVersions(name string) ([]Version, error) { + versions, err := listPluginVersions(i.providerVersionsURL(name)) + if err != nil { + // listPluginVersions returns a verbose error message indicating + // what was being accessed and what failed + return nil, err + } + return versions, nil +} + var errVersionNotFound = errors.New("version not found") // take the list of available versions for a plugin, and filter out those that @@ -262,17 +297,6 @@ func allowedVersions(available []Version, required Constraints) []Version { return allowed } -// list the version available for the named plugin -func listProviderVersions(name string) ([]Version, error) { - versions, err := listPluginVersions(providerVersionsURL(name)) - if err != nil { - // listPluginVersions returns a verbose error message indicating - // what was being accessed and what failed - return nil, err - } - return versions, nil -} - // return a list of the plugin versions at the given URL func listPluginVersions(url string) ([]Version, error) { resp, err := httpClient.Get(url) @@ -346,15 +370,6 @@ func versionsFromNames(names []string) []Version { return versions } -func getProviderChecksum(name, version string) (string, error) { - checksums, err := getPluginSHA256SUMs(providerChecksumURL(name, version)) - if err != nil { - return "", err - } - - return checksumForFile(checksums, providerFileName(name, version)), nil -} - func checksumForFile(sums []byte, name string) string { for _, line := range strings.Split(string(sums), "\n") { parts := strings.Fields(line) diff --git a/plugin/discovery/get_test.go b/plugin/discovery/get_test.go index 213507acd..16ba697cd 100644 --- a/plugin/discovery/get_test.go +++ b/plugin/discovery/get_test.go @@ -100,7 +100,8 @@ func TestMain(m *testing.M) { } func TestVersionListing(t *testing.T) { - versions, err := listProviderVersions("test") + i := &ProviderInstaller{} + versions, err := i.listProviderVersions("test") if err != nil { t.Fatal(err) } @@ -125,11 +126,12 @@ func TestVersionListing(t *testing.T) { } func TestCheckProtocolVersions(t *testing.T) { - if checkPlugin(providerURL("test", VersionStr("1.2.3").MustParse().String()), 4) { + i := &ProviderInstaller{} + if checkPlugin(i.providerURL("test", VersionStr("1.2.3").MustParse().String()), 4) { t.Fatal("protocol version 4 is not compatible") } - if !checkPlugin(providerURL("test", VersionStr("1.2.3").MustParse().String()), 3) { + if !checkPlugin(i.providerURL("test", VersionStr("1.2.3").MustParse().String()), 3) { t.Fatal("protocol version 3 should be compatible") } } @@ -265,8 +267,10 @@ 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 := getProviderChecksum("template", "0.1.0") + sha256sum, err := i.getProviderChecksum("template", "0.1.0") if err != nil { t.Fatal(err) } @@ -277,7 +281,7 @@ func TestProviderChecksum(t *testing.T) { t.Fatal(err) } - expected := checksumForFile(sumData, providerFileName("template", "0.1.0")) + expected := checksumForFile(sumData, i.providerFileName("template", "0.1.0")) if sha256sum != expected { t.Fatalf("expected: %s\ngot %s\n", sha256sum, expected) @@ -286,8 +290,10 @@ func TestProviderChecksum(t *testing.T) { // 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 := getProviderChecksum("badsig", "0.1.0") + sha256sum, err := i.getProviderChecksum("badsig", "0.1.0") if err == nil { t.Fatal("expcted error") }