blob: 46e3fb533af9955d5068e2fc4f967b353693b232 [file] [log] [blame]
package tfinstall
import (
"fmt"
"io/ioutil"
"log"
"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
}
}
if terraformPath == "" {
return "", fmt.Errorf("could not find terraform executable")
}
err := runTerraformVersion(terraformPath)
if err != nil {
return "", fmt.Errorf("executable found at path %s is not terraform: %s", terraformPath, err)
}
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
}
httpGetter := &getter.HttpGetter{
Netrc: true,
Client: newHTTPClient(),
}
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
}
// very basic sanity check
if !strings.Contains(string(out), "Terraform v") {
return fmt.Errorf("located executable at %s, but output of `terraform version` was:\n%s", execPath, out)
}
return nil
}