Skip to content

Commit

Permalink
tfinstall package
Browse files Browse the repository at this point in the history
  • Loading branch information
kmoe committed Jul 3, 2020
1 parent 10c3415 commit cace9f2
Show file tree
Hide file tree
Showing 4 changed files with 481 additions and 0 deletions.
32 changes: 32 additions & 0 deletions tfinstall/pubkey.go
Original file line number Diff line number Diff line change
@@ -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-----`
287 changes: 287 additions & 0 deletions tfinstall/tfinstall.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit cace9f2

Please sign in to comment.