plugin/discovery: allow customizing the OS/arch for auto-install

Previously we forced only installing for the current GOOS and GOARCH. Now
we allow this to be optionally overridden, which allows building tools
that can, for example, populate a directory with plugins to run on a Linux
server while working on a Mac.
This commit is contained in:
Martin Atkins 2017-07-03 14:56:11 -07:00
parent 9ee2fbaa2b
commit 610fcb605e
2 changed files with 79 additions and 58 deletions

View File

@ -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-<plugin_type>-
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_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
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-<plugin_type>-
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_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
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)

View File

@ -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")
}