tfinstall package
diff --git a/tfinstall/pubkey.go b/tfinstall/pubkey.go
new file mode 100644
index 0000000..a14f88c
--- /dev/null
+++ b/tfinstall/pubkey.go
@@ -0,0 +1,32 @@
+package tfinstall
+
+const hashicorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFMORM0BCADBRyKO1MhCirazOSVwcfTr1xUxjPvfxD3hjUwHtjsOy/bT6p9f
+W2mRPfwnq2JB5As+paL3UGDsSRDnK9KAxQb0NNF4+eVhr/EJ18s3wwXXDMjpIifq
+fIm2WyH3G+aRLTLPIpscUNKDyxFOUbsmgXAmJ46Re1fn8uKxKRHbfa39aeuEYWFA
+3drdL1WoUngvED7f+RnKBK2G6ZEpO+LDovQk19xGjiMTtPJrjMjZJ3QXqPvx5wca
+KSZLr4lMTuoTI/ZXyZy5bD4tShiZz6KcyX27cD70q2iRcEZ0poLKHyEIDAi3TM5k
+SwbbWBFd5RNPOR0qzrb/0p9ksKK48IIfH2FvABEBAAG0K0hhc2hpQ29ycCBTZWN1
+cml0eSA8c2VjdXJpdHlAaGFzaGljb3JwLmNvbT6JAU4EEwEKADgWIQSRpuf4XQXG
+VjC+8YlRhS2HNI/8TAUCXn0BIQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK
+CRBRhS2HNI/8TJITCACT2Zu2l8Jo/YLQMs+iYsC3gn5qJE/qf60VWpOnP0LG24rj
+k3j4ET5P2ow/o9lQNCM/fJrEB2CwhnlvbrLbNBbt2e35QVWvvxwFZwVcoBQXTXdT
++G2cKS2Snc0bhNF7jcPX1zau8gxLurxQBaRdoL38XQ41aKfdOjEico4ZxQYSrOoC
+RbF6FODXj+ZL8CzJFa2Sd0rHAROHoF7WhKOvTrg1u8JvHrSgvLYGBHQZUV23cmXH
+yvzITl5jFzORf9TUdSv8tnuAnNsOV4vOA6lj61Z3/0Vgor+ZByfiznonPHQtKYtY
+kac1M/Dq2xZYiSf0tDFywgUDIF/IyS348wKmnDGjuQENBFMORM0BCADWj1GNOP4O
+wJmJDjI2gmeok6fYQeUbI/+Hnv5Z/cAK80Tvft3noy1oedxaDdazvrLu7YlyQOWA
+M1curbqJa6ozPAwc7T8XSwWxIuFfo9rStHQE3QUARxIdziQKTtlAbXI2mQU99c6x
+vSueQ/gq3ICFRBwCmPAm+JCwZG+cDLJJ/g6wEilNATSFdakbMX4lHUB2X0qradNO
+J66pdZWxTCxRLomPBWa5JEPanbosaJk0+n9+P6ImPiWpt8wiu0Qzfzo7loXiDxo/
+0G8fSbjYsIF+skY+zhNbY1MenfIPctB9X5iyW291mWW7rhhZyuqqxN2xnmPPgFmi
+QGd+8KVodadHABEBAAGJATwEGAECACYCGwwWIQSRpuf4XQXGVjC+8YlRhS2HNI/8
+TAUCXn0BRAUJEvOKdwAKCRBRhS2HNI/8TEzUB/9pEHVwtTxL8+VRq559Q0tPOIOb
+h3b+GroZRQGq/tcQDVbYOO6cyRMR9IohVJk0b9wnnUHoZpoA4H79UUfIB4sZngma
+enL/9magP1uAHxPxEa5i/yYqR0MYfz4+PGdvqyj91NrkZm3WIpwzqW/KZp8YnD77
+VzGVodT8xqAoHW+bHiza9Jmm9Rkf5/0i0JY7GXoJgk4QBG/Fcp0OR5NUWxN3PEM0
+dpeiU4GI5wOz5RAIOvSv7u1h0ZxMnJG4B4MKniIAr4yD7WYYZh/VxEPeiS/E1CVx
+qHV5VVCoEIoYVHIuFIyFu1lIcei53VD6V690rmn0bp4A5hs+kErhThvkok3c
+=+mCN
+-----END PGP PUBLIC KEY BLOCK-----`
diff --git a/tfinstall/tfinstall.go b/tfinstall/tfinstall.go
new file mode 100644
index 0000000..0330f03
--- /dev/null
+++ b/tfinstall/tfinstall.go
@@ -0,0 +1,287 @@
+package tfinstall
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/hashicorp/go-checkpoint"
+	"github.com/hashicorp/go-getter"
+	"github.com/hashicorp/go-version"
+	"golang.org/x/crypto/openpgp"
+)
+
+const baseUrl = "https://releases.hashicorp.com/terraform"
+
+type ExecPathFinder interface {
+	ExecPath() (string, error)
+}
+
+type ExactPathOption struct {
+	execPath string
+}
+
+func ExactPath(execPath string) *ExactPathOption {
+	opt := &ExactPathOption{
+		execPath: execPath,
+	}
+	return opt
+}
+
+func (opt *ExactPathOption) ExecPath() (string, error) {
+	if _, err := os.Stat(opt.execPath); err != nil {
+		// fall through to the next strategy if the local path does not exist
+		return "", nil
+	}
+	return opt.execPath, nil
+}
+
+type LookPathOption struct {
+}
+
+func LookPath() *LookPathOption {
+	opt := &LookPathOption{}
+
+	return opt
+}
+
+func (opt *LookPathOption) ExecPath() (string, error) {
+	p, err := exec.LookPath("terraform")
+	if err != nil {
+		if notFoundErr, ok := err.(*exec.Error); ok && notFoundErr.Err == exec.ErrNotFound {
+			log.Printf("[WARN] could not locate a terraform executable on system path; continuing")
+			return "", nil
+		}
+		return "", err
+	}
+	return p, nil
+}
+
+type LatestVersionOption struct {
+	forceCheckpoint bool
+	installDir      string
+}
+
+func LatestVersion(installDir string, forceCheckpoint bool) *LatestVersionOption {
+	opt := &LatestVersionOption{
+		forceCheckpoint: forceCheckpoint,
+		installDir:      installDir,
+	}
+
+	return opt
+}
+
+func (opt *LatestVersionOption) ExecPath() (string, error) {
+	v, err := latestVersion(opt.forceCheckpoint)
+	if err != nil {
+		return "", err
+	}
+
+	return downloadWithVerification(v, opt.installDir)
+}
+
+type ExactVersionOption struct {
+	tfVersion  string
+	installDir string
+}
+
+func ExactVersion(tfVersion string, installDir string) *ExactVersionOption {
+	opt := &ExactVersionOption{
+		tfVersion:  tfVersion,
+		installDir: installDir,
+	}
+
+	return opt
+}
+
+func (opt *ExactVersionOption) ExecPath() (string, error) {
+	// validate version
+	_, err := version.NewVersion(opt.tfVersion)
+	if err != nil {
+		return "", err
+	}
+
+	return downloadWithVerification(opt.tfVersion, opt.installDir)
+}
+
+func Find(opts ...ExecPathFinder) (string, error) {
+	var terraformPath string
+
+	// go through the options in order
+	// until a valid terraform executable is found
+	for _, opt := range opts {
+		p, err := opt.ExecPath()
+		if err != nil {
+			return "", fmt.Errorf("unexpected error: %s", err)
+		}
+
+		if p == "" {
+			// strategy did not locate an executable - fall through to next
+			continue
+		} else {
+			terraformPath = p
+			break
+		}
+	}
+
+	err := runTerraformVersion(terraformPath)
+	if err != nil {
+		return "", fmt.Errorf("executable found at path %s is not terraform: %s", terraformPath, err)
+	}
+
+	if terraformPath == "" {
+		return "", fmt.Errorf("could not find terraform executable")
+	}
+
+	return terraformPath, nil
+}
+
+func downloadWithVerification(tfVersion string, installDir string) (string, error) {
+	osName := runtime.GOOS
+	archName := runtime.GOARCH
+
+	// setup: ensure we have a place to put our downloaded terraform binary
+	var tfDir string
+	var err error
+	if installDir == "" {
+		tfDir, err = ioutil.TempDir("", "tfexec")
+		if err != nil {
+			return "", fmt.Errorf("failed to create temp dir: %s", err)
+		}
+	} else {
+		if _, err := os.Stat(installDir); err != nil {
+			return "", fmt.Errorf("could not access directory %s for installing Terraform: %s", installDir, err)
+		}
+		tfDir = installDir
+
+	}
+
+	// setup: getter client
+	httpHeader := make(http.Header)
+	httpHeader.Set("User-Agent", "HashiCorp-tfinstall/"+Version)
+	httpGetter := &getter.HttpGetter{
+		Netrc: true,
+	}
+	client := getter.Client{
+		Getters: map[string]getter.Getter{
+			"https": httpGetter,
+		},
+	}
+	client.Mode = getter.ClientModeAny
+
+	// firstly, download and verify the signature of the checksum file
+
+	sumsTmpDir, err := ioutil.TempDir("", "tfinstall")
+	if err != nil {
+		return "", err
+	}
+	defer os.RemoveAll(sumsTmpDir)
+
+	sumsFilename := "terraform_" + tfVersion + "_SHA256SUMS"
+	sumsSigFilename := sumsFilename + ".sig"
+
+	sumsUrl := fmt.Sprintf("%s/%s/%s",
+		baseUrl, tfVersion, sumsFilename)
+	sumsSigUrl := fmt.Sprintf("%s/%s/%s",
+		baseUrl, tfVersion, sumsSigFilename)
+
+	client.Src = sumsUrl
+	client.Dst = sumsTmpDir
+	err = client.Get()
+	if err != nil {
+		return "", fmt.Errorf("error fetching checksums: %s", err)
+	}
+
+	client.Src = sumsSigUrl
+	err = client.Get()
+	if err != nil {
+		return "", fmt.Errorf("error fetching checksums signature: %s", err)
+	}
+
+	sumsPath := filepath.Join(sumsTmpDir, sumsFilename)
+	sumsSigPath := filepath.Join(sumsTmpDir, sumsSigFilename)
+
+	err = verifySumsSignature(sumsPath, sumsSigPath)
+	if err != nil {
+		return "", err
+	}
+
+	// secondly, download Terraform itself, verifying the checksum
+	url := tfUrl(tfVersion, osName, archName)
+	client.Src = url
+	client.Dst = tfDir
+	client.Mode = getter.ClientModeDir
+	err = client.Get()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Join(tfDir, "terraform"), nil
+}
+
+func tfUrl(tfVersion, osName, archName string) string {
+	sumsFilename := "terraform_" + tfVersion + "_SHA256SUMS"
+	sumsUrl := fmt.Sprintf("%s/%s/%s",
+		baseUrl, tfVersion, sumsFilename)
+	return fmt.Sprintf(
+		"%s/%s/terraform_%s_%s_%s.zip?checksum=file:%s",
+		baseUrl, tfVersion, tfVersion, osName, archName, sumsUrl,
+	)
+}
+
+func latestVersion(forceCheckpoint bool) (string, error) {
+	resp, err := checkpoint.Check(&checkpoint.CheckParams{
+		Product: "terraform",
+		Force:   forceCheckpoint,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	if resp.CurrentVersion == "" {
+		return "", fmt.Errorf("could not determine latest version of terraform using checkpoint: CHECKPOINT_DISABLE may be set")
+	}
+
+	return resp.CurrentVersion, nil
+}
+
+// verifySumsSignature downloads SHA256SUMS and SHA256SUMS.sig and verifies
+// the signature using the HashiCorp public key.
+func verifySumsSignature(sumsPath, sumsSigPath string) error {
+	el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(hashicorpPublicKey))
+	if err != nil {
+		return err
+	}
+	data, err := os.Open(sumsPath)
+	if err != nil {
+		return err
+	}
+	sig, err := os.Open(sumsSigPath)
+	if err != nil {
+		return err
+	}
+	_, err = openpgp.CheckDetachedSignature(el, data, sig)
+
+	return err
+}
+
+func runTerraformVersion(execPath string) error {
+	cmd := exec.Command(execPath, "version")
+
+	out, err := cmd.Output()
+	if err != nil {
+		return err
+	}
+
+	if !strings.HasPrefix(string(out), "Terraform v") {
+		return fmt.Errorf("located executable at %s, but output of `terraform version` was:\n%s", execPath, out)
+	}
+
+	return nil
+}
diff --git a/tfinstall/tfinstall_test.go b/tfinstall/tfinstall_test.go
new file mode 100644
index 0000000..9f29762
--- /dev/null
+++ b/tfinstall/tfinstall_test.go
@@ -0,0 +1,158 @@
+package tfinstall
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"strings"
+	"testing"
+
+	"github.com/hashicorp/go-version"
+)
+
+// downloads terraform 0.12.26 from the live releases site
+func TestFindExactVersion(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "tfinstall-test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	tfpath, err := Find(ExactVersion("0.12.26", tmpDir))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// run "terraform version" to check we've downloaded a terraform 0.12.26 binary
+	cmd := exec.Command(tfpath, "version")
+
+	out, err := cmd.Output()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := "Terraform v0.12.26"
+	actual := string(out)
+	if !strings.HasPrefix(actual, expected) {
+		t.Fatalf("ran terraform version, expected %s, but got %s", expected, actual)
+	}
+}
+
+// downloads terraform 0.13.0-beta1 from the live releases site
+func TestFindExactVersionPrerelease(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "tfinstall-test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	tfpath, err := Find(ExactVersion("0.13.0-beta1", tmpDir))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// run "terraform version" to check we've downloaded a terraform 0.12.26 binary
+	cmd := exec.Command(tfpath, "version")
+
+	out, err := cmd.Output()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := "Terraform v0.13.0-beta1"
+	actual := string(out)
+	if !strings.HasPrefix(actual, expected) {
+		t.Fatalf("ran terraform version, expected %s, but got %s", expected, actual)
+	}
+}
+
+// test that Find returns an appropriate error when given an exact path
+// which exists, but is not a terraform executable
+func TestExactPath(t *testing.T) {
+	// we just want the path to a local executable that definitely exists
+	execPath, err := exec.LookPath("go")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = Find(ExactPath(execPath))
+	if err == nil {
+		t.Fatalf("expected Find() to fail when given ExactPath(%s), but it did not", execPath)
+	}
+
+	expected := fmt.Sprintf("executable found at path %s is not terraform", execPath)
+	if !strings.HasPrefix(err.Error(), expected) {
+		t.Fatalf("expected Find() to return %s, but got %s", expected, err)
+	}
+}
+
+// test that Find falls back to the next working strategy when the file at
+// ExactPath does not exist
+func TestExactVersionFallback(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "tfinstall-test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	tfpath, err := Find(ExactPath("/hopefully/completely/nonexistent/path"), ExactVersion("0.12.26", tmpDir))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// run "terraform version" to check we've downloaded a terraform 0.12.26 binary
+	cmd := exec.Command(tfpath, "version")
+
+	out, err := cmd.Output()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := "Terraform v0.12.26"
+	actual := string(out)
+	if !strings.HasPrefix(actual, expected) {
+		t.Fatalf("ran terraform version, expected %s, but got %s", expected, actual)
+	}
+}
+
+// latest version calculation itself is handled by checkpoint, so the test can be straightforward -
+// just test that we've managed to download a version of terraform later than 0.12.27
+func TestLatestVersion(t *testing.T) {
+	lowerBound := "0.12.27"
+
+	tmpDir, err := ioutil.TempDir("", "tfinstall-test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	tfpath, err := Find(LatestVersion(tmpDir, false))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := exec.Command(tfpath, "version")
+
+	out, err := cmd.Output()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	lowerBoundVersion, err := version.NewVersion("0.12.27")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	outVersion := strings.Trim(string(out), "\n")
+	outVersion = strings.TrimLeft(outVersion, "Terraform v")
+
+	actualVersion, err := version.NewVersion(outVersion)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if actualVersion.LessThan(lowerBoundVersion) {
+		t.Fatalf("ran terraform version, expected version to be greater than %s, but got %s", lowerBound, out)
+	}
+}
diff --git a/tfinstall/version.go b/tfinstall/version.go
new file mode 100644
index 0000000..190fdf2
--- /dev/null
+++ b/tfinstall/version.go
@@ -0,0 +1,4 @@
+package tfinstall
+
+// Version is the tfinstall package version, used in user agent headers
+const Version = "0.1.0"