-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
433 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package tfdisco | ||
|
||
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-----` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
package tfdisco | ||
|
||
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 findOption struct { | ||
find func() (string, error) | ||
name string | ||
} | ||
|
||
func ExactPath(execPath string) *findOption { | ||
opt := &findOption{ | ||
name: "ExactPath", | ||
} | ||
opt.find = func() (string, error) { | ||
if _, err := os.Stat(execPath); err != nil { | ||
// fall through to the next strategy if the local path does not exist | ||
return "", nil | ||
} | ||
return execPath, nil | ||
} | ||
|
||
return opt | ||
} | ||
|
||
func LookPath() *findOption { | ||
opt := &findOption{ | ||
name: "LookPath", | ||
} | ||
opt.find = func() (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 | ||
} | ||
|
||
return opt | ||
} | ||
|
||
func LatestVersion(installDir string) *findOption { | ||
opt := &findOption{ | ||
name: "LatestVersion", | ||
} | ||
opt.find = func() (string, error) { | ||
v, err := latestVersion() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return downloadWithVerification(v, installDir) | ||
} | ||
|
||
return opt | ||
} | ||
|
||
func ExactVersion(tfVersion string, installDir string) *findOption { | ||
opt := &findOption{ | ||
name: "ExactVersion", | ||
} | ||
opt.find = func() (string, error) { | ||
// validate version | ||
_, err := version.NewVersion(tfVersion) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
return downloadWithVerification(tfVersion, installDir) | ||
} | ||
|
||
return opt | ||
} | ||
|
||
func Find(opts ...*findOption) (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.find() | ||
if err != nil { | ||
return "", fmt.Errorf("unexpected error in strategy %s: %s", opt.name, 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-tfdisco/"+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("", "tfdisco") | ||
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() (string, error) { | ||
resp, err := checkpoint.Check(&checkpoint.CheckParams{ | ||
Product: "terraform", | ||
}) | ||
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 | ||
} |
Oops, something went wrong.