-
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
481 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 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-----` |
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,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 | ||
} |
Oops, something went wrong.