diff --git a/.go-version b/.go-version new file mode 100644 index 0000000..141f2e8 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.15.0 diff --git a/README.md b/README.md index 8a1a1c8..906db77 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,135 @@ -# hcinstall +# hc-install **DO NOT USE: WIP** An **experimental** Go module for downloading or locating HashiCorp binaries, verifying signatures and checksums, and asserting version constraints. -This module is a successor to tfinstall, available in pre-1.0 versions of [terraform-exec](https://github.com/hashicorp/terraform-exec). Current users of tfinstall are advised to move to hcinstall on upgrading terraform-exec to v1.0.0. +This module is a successor to tfinstall, available in pre-1.0 versions of [terraform-exec](https://github.com/hashicorp/terraform-exec). Current users of tfinstall are advised to move to hc-install before upgrading terraform-exec to v1.0.0. -## hcinstall is not a package manager +## hc-install is not a package manager -This library is intended for use within Go programs which have some business downloading or otherwise locating HashiCorp binaries. +This library is intended for use within Go programs or automated environments (such as CIs) +which have some business downloading or otherwise locating HashiCorp binaries. -The included command-line utility, `hcinstall`, is a convenient way of using the library in ad-hoc or CI shell scripting. +The included command-line utility, `hc-install`, is a convenient way of using +the library in ad-hoc or CI shell scripting outside of Go. -hcinstall will not: - - Install binaries to the appropriate place in your operating system. It does not know whether you think `terraform` should go in `/usr/bin` or `/usr/local/bin`, and does not want to get involved in the discussion. - - Upgrade existing binaries on your system by overwriting them in place. - - Add downloaded binaries to your `PATH`. +`hc-install` does **not**: + + - Determine suitable installation path based on target system. e.g. in `/usr/bin` or `/usr/local/bin` on Unix based system. + - Deal with execution of installed binaries (via service files or otherwise). + - Upgrade existing binaries on your system. + - Add nor link downloaded binaries to your `$PATH`. ## API -Loosely inspired by [go-getter](https://github.com/hashicorp/go-getter), the API provides: +The `Installer` offers a few high-level methods: + + - `Ensure(context.Context, []src.Source)` to find, install, or build a product version + - `Install(context.Context, []src.Installable)` to install a product version + +### Sources + +The `Installer` methods accept number of different `Source` types. +Each comes with different trade-offs described below. + + - `fs.{AnyVersion,ExactVersion}` - Finds a binary in `$PATH` (or additional paths) + - **Pros:** + - This is most convenient when you already have the product installed on your system + which you already manage. + - **Cons:** + - Only relies on a single version, expects _you_ to manage the installation + - _Not recommended_ for any environment where product installation is not controlled or managed by you (e.g. default GitHub Actions image managed by GitHub) + - `releases.{LatestVersion,ExactVersion}` - Downloads, verifies & installs any known product from `releases.hashicorp.com` + - **Pros:** + - Fast and reliable way of obtaining any pre-built version of any product + - **Cons:** + - Installation may consume some bandwith, disk space and a little time + - Potentially less stable builds (see `checkpoint` below) + - `checkpoint.{LatestVersion}` - Downloads, verifies & installs any known product available in HashiCorp Checkpoint + - **Pros:** + - Checkpoint typically contains only product versions considered stable + - **Cons:** + - Installation may consume some bandwith, disk space and a little time + - Currently doesn't allow installation of a old versions (see `releases` above) + - `build.{GitRevision}` - Clones raw source code and builds the product from it + - **Pros:** + - Useful for catching bugs and incompatibilities as early as possible (prior to product release). + - **Cons:** + - Building from scratch can consume significant amount of time & resources (CPU, memory, bandwith, disk space) + - There are no guarantees that build instructions will always be up-to-date + - There's increased likelihood of build containing bugs prior to release + - Any CI builds relying on this are likely to be fragile + +## Example Usage + +### Install single version - - Simple one-line `Install()` function for locating a product binary of a given, or latest, version, with sensible defaults. - - Customisable `Client`: - - Version constraint parsing - - Tries each `Getter` in turn to locate a binary matching version constraints - - Verifies downloaded binary signatures and checksums +```go +TODO +``` -### Simple +### Find or install single version ```go -package main +i := NewInstaller() + +v0_14_0 := version.Must(version.NewVersion("0.14.0")) + +execPath, err := i.Ensure(context.Background(), []src.Source{ + &fs.ExactVersion{ + Product: product.Terraform, + Version: v0_14_0, + }, + &releases.ExactVersion{ + Product: product.Terraform, + Version: v0_14_0, + }, +}) +if err != nil { + // process err +} -import ( - "fmt" - - "github.com/hashicorp/hcinstall") -) +// run any tests -func main() { - tfPath, err := hcinstall.Install(context.Background(), "", hcinstall.ProductTerraform, "0.13.5", true) - if err != nil { - panic(err) - } - fmt.Printf("Path to Terraform binary: %s", tfPath) -} +defer i.Remove() ``` -### Advanced +### Install multiple versions ```go -package main +TODO +``` -import ( - "fmt" - - "github.com/hashicorp/hcinstall" -) +### Install and build multiple versions -func main() { - v, err := NewVersionConstraints("0.13.5", true) - if err != nil { - panic(err) - } +```go +i := NewInstaller() - client := &hcinstall.Client{ - Product: hcinstall.ProductTerraform, - InstallDir: "/usr/local/bin", - Getters: []Getter{hcinstall.LookPath(), hcinstall.Releases()}, - VersionConstraints: v, - } - - tfPath, err := client.Install(context.Background()) +vc, _ := version.NewConstraint(">= 0.12") +rv := &releases.Versions{ + Product: product.Terraform, + Constraints: vc, +} + +versions, err := rv.List(context.Background()) +if err != nil { + return err +} +versions = append(versions, &build.GitRevision{Ref: "HEAD"}) + +for _, installableVersion := range versions { + execPath, err := i.Ensure(context.Background(), []src.Source{ + installableVersion, + }) if err != nil { - panic(err) + return err } - - fmt.Printf("Path to Terraform binary: %s", tfPath) + + // Do some testing here + _ = execPath + + // clean up + os.Remove(execPath) } ``` diff --git a/build/git_revision.go b/build/git_revision.go new file mode 100644 index 0000000..1d86ec2 --- /dev/null +++ b/build/git_revision.go @@ -0,0 +1,172 @@ +package build + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "os" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + isrc "github.com/hashicorp/hc-install/internal/src" + "github.com/hashicorp/hc-install/product" +) + +var ( + cloneTimeout = 1 * time.Minute + buildTimeout = 2 * time.Minute + discardLogger = log.New(ioutil.Discard, "", 0) +) + +// GitRevision installs a particular git revision by cloning +// the repository and building it per product BuildInstructions +type GitRevision struct { + Product product.Product + InstallDir string + Ref string + CloneTimeout time.Duration + BuildTimeout time.Duration + + logger *log.Logger + pathsToRemove []string +} + +func (*GitRevision) IsSourceImpl() isrc.InstallSrcSigil { + return isrc.InstallSrcSigil{} +} + +func (gr *GitRevision) SetLogger(logger *log.Logger) { + gr.logger = logger +} + +func (gr *GitRevision) log() *log.Logger { + if gr.logger == nil { + return discardLogger + } + return gr.logger +} + +func (gr *GitRevision) Validate() error { + if gr.Product.Name == "" { + return fmt.Errorf("unknown product name") + } + if gr.Product.BinaryName == "" { + return fmt.Errorf("unknown binary name") + } + + bi := gr.Product.BuildInstructions + if bi == nil { + return fmt.Errorf("no build instructions") + } + if bi.GitRepoURL == "" { + return fmt.Errorf("missing repository URL") + } + if bi.Build == nil { + return fmt.Errorf("missing build instructions") + } + + return nil +} + +func (gr *GitRevision) Build(ctx context.Context) (string, error) { + buildTimeout := buildTimeout + if gr.BuildTimeout > 0 { + buildTimeout = gr.BuildTimeout + } + + bi := gr.Product.BuildInstructions + + if bi.PreCloneCheck != nil { + pccCtx, cancelFunc := context.WithTimeout(ctx, buildTimeout) + defer cancelFunc() + + gr.log().Printf("running pre-clone check (timeout: %s)", buildTimeout) + err := bi.PreCloneCheck.Check(pccCtx) + if err != nil { + return "", err + } + gr.log().Printf("pre-clone check finished") + } + + if gr.pathsToRemove == nil { + gr.pathsToRemove = make([]string, 0) + } + + repoDir, err := ioutil.TempDir("", + fmt.Sprintf("hc-install-build-%s", gr.Product.Name)) + if err != nil { + return "", err + } + gr.pathsToRemove = append(gr.pathsToRemove, repoDir) + + ref := gr.Ref + if ref == "" { + ref = "HEAD" + } + + timeout := cloneTimeout + if gr.BuildTimeout > 0 { + timeout = gr.BuildTimeout + } + cloneCtx, cancelFunc := context.WithTimeout(ctx, timeout) + defer cancelFunc() + + gr.log().Printf("cloning repository from %s to %s (timeout: %s)", + gr.Product.BuildInstructions.GitRepoURL, + repoDir, timeout) + repo, err := git.PlainCloneContext(cloneCtx, repoDir, false, &git.CloneOptions{ + URL: gr.Product.BuildInstructions.GitRepoURL, + ReferenceName: plumbing.ReferenceName(gr.Ref), + Depth: 1, + }) + if err != nil { + return "", fmt.Errorf("unable to clone %q: %w", + gr.Product.BuildInstructions.GitRepoURL, err) + } + gr.log().Printf("cloning finished") + head, err := repo.Head() + if err != nil { + return "", err + } + + gr.log().Printf("repository HEAD is at %s", head.Hash()) + + buildCtx, cancelFunc := context.WithTimeout(ctx, buildTimeout) + defer cancelFunc() + + if loggableBuilder, ok := bi.Build.(withLogger); ok { + loggableBuilder.SetLogger(gr.log()) + } + installDir := gr.InstallDir + if installDir == "" { + tmpDir, err := ioutil.TempDir("", + fmt.Sprintf("hc-install-%s-%s", gr.Product.Name, head.Hash())) + if err != nil { + return "", err + } + installDir = tmpDir + gr.pathsToRemove = append(gr.pathsToRemove, installDir) + } + + gr.log().Printf("building (timeout: %s)", buildTimeout) + return bi.Build.Build(buildCtx, repoDir, installDir, gr.Product.BinaryName) +} + +func (gr *GitRevision) Remove(ctx context.Context) error { + if gr.pathsToRemove != nil { + for _, path := range gr.pathsToRemove { + err := os.RemoveAll(path) + if err != nil { + return err + } + } + } + + return gr.Product.BuildInstructions.Build.Remove(ctx) +} + +type withLogger interface { + SetLogger(*log.Logger) +} diff --git a/build/git_revision_test.go b/build/git_revision_test.go new file mode 100644 index 0000000..fd40186 --- /dev/null +++ b/build/git_revision_test.go @@ -0,0 +1,75 @@ +package build + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/internal/testutil" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/src" +) + +var ( + _ src.Buildable = &GitRevision{} + _ src.Removable = &GitRevision{} + _ src.LoggerSettable = &GitRevision{} +) + +func TestGitRevision_terraform(t *testing.T) { + testutil.EndToEndTest(t) + + gr := &GitRevision{Product: product.Terraform} + gr.SetLogger(testutil.TestLogger()) + + ctx := context.Background() + + execPath, err := gr.Build(ctx) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { gr.Remove(ctx) }) + + v, err := product.Terraform.GetVersion(ctx, execPath) + if err != nil { + t.Fatal(err) + } + + latestConstraint, err := version.NewConstraint(">= 1.0") + if err != nil { + t.Fatal(err) + } + if !latestConstraint.Check(v.Core()) { + t.Fatalf("versions don't match (expected: %s, installed: %s)", + latestConstraint, v) + } +} + +func TestGitRevision_consul(t *testing.T) { + testutil.EndToEndTest(t) + + gr := &GitRevision{Product: product.Consul} + gr.SetLogger(testutil.TestLogger()) + + ctx := context.Background() + + execPath, err := gr.Build(ctx) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { gr.Remove(ctx) }) + + v, err := product.Consul.GetVersion(ctx, execPath) + if err != nil { + t.Fatal(err) + } + + latestConstraint, err := version.NewConstraint(">= 1.0") + if err != nil { + t.Fatal(err) + } + if !latestConstraint.Check(v.Core()) { + t.Fatalf("versions don't match (expected: %s, installed: %s)", + latestConstraint, v) + } +} diff --git a/checkpoint/latest_version.go b/checkpoint/latest_version.go new file mode 100644 index 0000000..ed9fecb --- /dev/null +++ b/checkpoint/latest_version.go @@ -0,0 +1,143 @@ +package checkpoint + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "time" + + checkpoint "github.com/hashicorp/go-checkpoint" + "github.com/hashicorp/go-version" + rjson "github.com/hashicorp/hc-install/internal/releasesjson" + isrc "github.com/hashicorp/hc-install/internal/src" + "github.com/hashicorp/hc-install/product" +) + +var ( + defaultTimeout = 30 * time.Second + discardLogger = log.New(ioutil.Discard, "", 0) +) + +// LatestVersion installs the latest version known to Checkpoint +// to OS temp directory, or to InstallDir (if not empty) +type LatestVersion struct { + Product product.Product + Timeout time.Duration + SkipChecksumVerification bool + InstallDir string + + logger *log.Logger + pathsToRemove []string +} + +func (*LatestVersion) IsSourceImpl() isrc.InstallSrcSigil { + return isrc.InstallSrcSigil{} +} + +func (lv *LatestVersion) SetLogger(logger *log.Logger) { + lv.logger = logger +} + +func (lv *LatestVersion) log() *log.Logger { + if lv.logger == nil { + return discardLogger + } + return lv.logger +} + +func (lv *LatestVersion) Validate() error { + if lv.Product.Name == "" { + return fmt.Errorf("unknown product name") + } + if lv.Product.BinaryName == "" { + return fmt.Errorf("unknown binary name") + } + + return nil +} + +func (lv *LatestVersion) Install(ctx context.Context) (string, error) { + timeout := defaultTimeout + if lv.Timeout > 0 { + timeout = lv.Timeout + } + ctx, cancelFunc := context.WithTimeout(ctx, timeout) + defer cancelFunc() + + // TODO: Introduce CheckWithContext to allow for cancellation + resp, err := checkpoint.Check(&checkpoint.CheckParams{ + Product: lv.Product.Name, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + Force: true, + }) + if err != nil { + return "", err + } + + latestVersion, err := version.NewVersion(resp.CurrentVersion) + if err != nil { + return "", err + } + + if lv.pathsToRemove == nil { + lv.pathsToRemove = make([]string, 0) + } + + dstDir := lv.InstallDir + if dstDir == "" { + var err error + dirName := fmt.Sprintf("%s_*", lv.Product.Name) + dstDir, err = ioutil.TempDir("", dirName) + if err != nil { + return "", err + } + lv.pathsToRemove = append(lv.pathsToRemove, dstDir) + lv.log().Printf("created new temp dir at %s", dstDir) + } + lv.log().Printf("will install into dir at %s", dstDir) + + rels := rjson.NewReleases() + rels.SetLogger(lv.log()) + pv, err := rels.GetProductVersion(ctx, lv.Product.Name, latestVersion) + if err != nil { + return "", err + } + + d := &rjson.Downloader{ + Logger: lv.log(), + VerifyChecksum: !lv.SkipChecksumVerification, + } + err = d.DownloadAndUnpack(ctx, pv, dstDir) + if err != nil { + return "", err + } + + execPath := filepath.Join(dstDir, lv.Product.BinaryName) + + lv.pathsToRemove = append(lv.pathsToRemove, execPath) + + lv.log().Printf("changing perms of %s", execPath) + err = os.Chmod(execPath, 0o700) + if err != nil { + return "", err + } + + return execPath, nil +} + +func (lv *LatestVersion) Remove(ctx context.Context) error { + if lv.pathsToRemove != nil { + for _, path := range lv.pathsToRemove { + err := os.RemoveAll(path) + if err != nil { + return err + } + } + } + return nil +} diff --git a/checkpoint/latest_version_test.go b/checkpoint/latest_version_test.go new file mode 100644 index 0000000..1f3f37a --- /dev/null +++ b/checkpoint/latest_version_test.go @@ -0,0 +1,48 @@ +package checkpoint + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/internal/testutil" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/src" +) + +var ( + _ src.Installable = &LatestVersion{} + _ src.Removable = &LatestVersion{} + _ src.LoggerSettable = &LatestVersion{} +) + +func TestLatestVersion(t *testing.T) { + testutil.EndToEndTest(t) + + lv := &LatestVersion{ + Product: product.Terraform, + } + lv.SetLogger(testutil.TestLogger()) + + ctx := context.Background() + + execPath, err := lv.Install(ctx) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { lv.Remove(ctx) }) + + v, err := product.Terraform.GetVersion(ctx, execPath) + if err != nil { + t.Fatal(err) + } + + latestConstraint, err := version.NewConstraint(">= 1.0") + if err != nil { + t.Fatal(err) + } + if !latestConstraint.Check(v.Core()) { + t.Fatalf("versions don't match (expected: %s, installed: %s)", + latestConstraint, v) + } +} diff --git a/cmd/hcinstall/main.go b/cmd/hc-install/main.go similarity index 100% rename from cmd/hcinstall/main.go rename to cmd/hc-install/main.go diff --git a/doc.go b/doc.go deleted file mode 100644 index c3b18ff..0000000 --- a/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package hcinstall offers multiple strategies for finding and/or installing -// a binary version of Terraform. Some of the strategies can also authenticate -// the source of the binary as an official HashiCorp release. -package hcinstall diff --git a/download.go b/download.go deleted file mode 100644 index 5ef9ac3..0000000 --- a/download.go +++ /dev/null @@ -1,114 +0,0 @@ -package hcinstall - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - - goGetter "github.com/hashicorp/go-getter" - "golang.org/x/crypto/openpgp" -) - -func downloadWithVerification(ctx context.Context, product string, productVersion string, installDir string, appendUserAgent string) (string, error) { - osName := runtime.GOOS - archName := runtime.GOARCH - baseURL := releasesURL + "/" + product - - // setup: ensure we have a place to put our downloaded binary - tfDir, err := ensureInstallDir(installDir) - if err != nil { - return "", err - } - - httpGetter := &goGetter.HttpGetter{ - Netrc: true, - Client: newHTTPClient(appendUserAgent), - } - client := goGetter.Client{ - Ctx: ctx, - Getters: map[string]goGetter.Getter{ - "https": httpGetter, - }, - } - client.Mode = goGetter.ClientModeAny - - // firstly, download and verify the signature of the checksum file - - sumsTmpDir, err := ioutil.TempDir("", "hcinstall") - if err != nil { - return "", err - } - defer os.RemoveAll(sumsTmpDir) - - sumsFilename := product + "_" + productVersion + "_SHA256SUMS" - sumsSigFilename := sumsFilename + ".sig" - - sumsURL := fmt.Sprintf("%s/%s/%s", baseURL, productVersion, sumsFilename) - sumsSigURL := fmt.Sprintf("%s/%s/%s", baseURL, productVersion, 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 the binary itself, verifying the checksum - url := hcURL(product, productVersion, osName, archName) - client.Src = url - client.Dst = tfDir - client.Mode = goGetter.ClientModeDir - err = client.Get() - if err != nil { - return "", err - } - - return filepath.Join(tfDir, product), 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 hcURL(product, productVersion, osName, archName string) string { - sumsFilename := product + "_" + productVersion + "_SHA256SUMS" - sumsURL := fmt.Sprintf("%s/%s/%s/%s", releasesURL, product, productVersion, sumsFilename) - return fmt.Sprintf( - "%s/%s/%s/%s_%s_%s_%s.zip?checksum=file:%s", - releasesURL, product, productVersion, product, productVersion, osName, archName, sumsURL, - ) -} diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..8d4f1d2 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,18 @@ +package errors + +type skippableErr struct { + Err error +} + +func (e skippableErr) Error() string { + return e.Err.Error() +} + +func SkippableErr(err error) skippableErr { + return skippableErr{Err: err} +} + +func IsErrorSkippable(err error) bool { + _, ok := err.(skippableErr) + return ok +} diff --git a/exact_path.go b/exact_path.go deleted file mode 100644 index 8dda1b0..0000000 --- a/exact_path.go +++ /dev/null @@ -1,26 +0,0 @@ -package hcinstall - -import ( - "context" - "os" -) - -func ExactPath(path string) *ExactPathGetter { - return &ExactPathGetter{ - path: path, - } -} - -type ExactPathGetter struct { - getter - path string -} - -func (g *ExactPathGetter) Get(ctx context.Context) (string, error) { - if _, err := os.Stat(g.path); err != nil { - // fall through to the next strategy if the local path does not exist - return "", nil - } - - return g.path, nil -} diff --git a/fs/any_version.go b/fs/any_version.go new file mode 100644 index 0000000..4dd5f0b --- /dev/null +++ b/fs/any_version.go @@ -0,0 +1,60 @@ +package fs + +import ( + "context" + "fmt" + "log" + "path/filepath" + + "github.com/hashicorp/hc-install/errors" + "github.com/hashicorp/hc-install/internal/src" + "github.com/hashicorp/hc-install/product" +) + +// AnyVersion finds the first executable binary of the product name +// within system $PATH and any declared ExtraPaths +// (which are *appended* to any directories in $PATH) +type AnyVersion struct { + Product product.Product + ExtraPaths []string + + logger *log.Logger +} + +func (*AnyVersion) IsSourceImpl() src.InstallSrcSigil { + return src.InstallSrcSigil{} +} + +func (av *AnyVersion) Validate() error { + if av.Product.BinaryName == "" { + return fmt.Errorf("unknown binary name") + } + return nil +} + +func (av *AnyVersion) SetLogger(logger *log.Logger) { + av.logger = logger +} + +func (av *AnyVersion) log() *log.Logger { + if av.logger == nil { + return discardLogger + } + return av.logger +} + +func (av *AnyVersion) Find(ctx context.Context) (string, error) { + execPath, err := findFile(lookupDirs(av.ExtraPaths), av.Product.BinaryName, checkExecutable) + if err != nil { + return "", errors.SkippableErr(err) + } + + if !filepath.IsAbs(execPath) { + var err error + execPath, err = filepath.Abs(execPath) + if err != nil { + return "", errors.SkippableErr(err) + } + } + return execPath, nil +} diff --git a/fs/exact_version.go b/fs/exact_version.go new file mode 100644 index 0000000..ea558ec --- /dev/null +++ b/fs/exact_version.go @@ -0,0 +1,94 @@ +package fs + +import ( + "context" + "fmt" + "log" + "path/filepath" + "time" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/errors" + "github.com/hashicorp/hc-install/internal/src" + "github.com/hashicorp/hc-install/product" +) + +// ExactVersion finds the first executable binary of the product name +// which matches the Version within system $PATH and any declared ExtraPaths +// (which are *appended* to any directories in $PATH) +type ExactVersion struct { + Product product.Product + Version *version.Version + ExtraPaths []string + Timeout time.Duration + + logger *log.Logger +} + +func (*ExactVersion) IsSourceImpl() src.InstallSrcSigil { + return src.InstallSrcSigil{} +} + +func (ev *ExactVersion) SetLogger(logger *log.Logger) { + ev.logger = logger +} + +func (ev *ExactVersion) log() *log.Logger { + if ev.logger == nil { + return discardLogger + } + return ev.logger +} + +func (ev *ExactVersion) Validate() error { + if ev.Product.BinaryName == "" { + return fmt.Errorf("undeclared binary name") + } + if ev.Version == nil { + return fmt.Errorf("undeclared version") + } + if ev.Product.GetVersion == nil { + return fmt.Errorf("undeclared version getter") + } + return nil +} + +func (ev *ExactVersion) Find(ctx context.Context) (string, error) { + timeout := defaultTimeout + if ev.Timeout > 0 { + timeout = ev.Timeout + } + ctx, cancelFunc := context.WithTimeout(ctx, timeout) + defer cancelFunc() + + execPath, err := findFile(lookupDirs(ev.ExtraPaths), ev.Product.BinaryName, func(file string) error { + err := checkExecutable(file) + if err != nil { + return err + } + + v, err := ev.Product.GetVersion(ctx, file) + if err != nil { + return err + } + + if !ev.Version.Equal(v) { + return fmt.Errorf("version (%s) doesn't match %s", v, ev.Version) + } + + return nil + }) + if err != nil { + return "", errors.SkippableErr(err) + } + + if !filepath.IsAbs(execPath) { + var err error + execPath, err = filepath.Abs(execPath) + if err != nil { + return "", errors.SkippableErr(err) + } + } + + return execPath, nil +} diff --git a/fs/fs.go b/fs/fs.go new file mode 100644 index 0000000..754a421 --- /dev/null +++ b/fs/fs.go @@ -0,0 +1,51 @@ +package fs + +import ( + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "time" +) + +var ( + defaultTimeout = 10 * time.Second + discardLogger = log.New(ioutil.Discard, "", 0) +) + +func lookupDirs(extraDirs []string) []string { + pathVar := os.Getenv("PATH") + dirs := filepath.SplitList(pathVar) + for _, ep := range extraDirs { + dirs = append(dirs, ep) + } + return dirs +} + +type fileCheckFunc func(path string) error + +func findFile(dirs []string, file string, f fileCheckFunc) (string, error) { + for _, dir := range dirs { + if dir == "" { + // Unix shell semantics: path element "" means "." + dir = "." + } + path := filepath.Join(dir, file) + if err := f(path); err == nil { + return path, nil + } + } + return "", exec.ErrNotFound +} + +func checkExecutable(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if m := d.Mode(); !m.IsDir() && m&0111 != 0 { + return nil + } + return os.ErrPermission +} diff --git a/fs/fs_test.go b/fs/fs_test.go new file mode 100644 index 0000000..8bdfb43 --- /dev/null +++ b/fs/fs_test.go @@ -0,0 +1,116 @@ +package fs + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/internal/testutil" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/src" +) + +var ( + _ src.Findable = &AnyVersion{} + _ src.LoggerSettable = &AnyVersion{} + + _ src.Findable = &ExactVersion{} + _ src.LoggerSettable = &ExactVersion{} +) + +func TestAnyVersion_notExecutable(t *testing.T) { + testutil.EndToEndTest(t) + + originalPath := os.Getenv("PATH") + os.Setenv("PATH", "") + t.Cleanup(func() { + os.Setenv("PATH", originalPath) + }) + + dirPath, fileName := createTempFile(t, "") + os.Setenv("PATH", dirPath) + + av := &AnyVersion{ + Product: product.Product{ + BinaryName: fileName, + }, + } + av.SetLogger(testutil.TestLogger()) + _, err := av.Find(context.Background()) + if err == nil { + t.Fatalf("expected %s not to be found in %s", fileName, dirPath) + } +} + +func TestAnyVersion_executable(t *testing.T) { + testutil.EndToEndTest(t) + + originalPath := os.Getenv("PATH") + os.Setenv("PATH", "") + t.Cleanup(func() { + os.Setenv("PATH", originalPath) + }) + + dirPath, fileName := createTempFile(t, "") + os.Setenv("PATH", dirPath) + + fullPath := filepath.Join(dirPath, fileName) + err := os.Chmod(fullPath, 0700) + if err != nil { + t.Fatal(err) + } + + av := &AnyVersion{ + Product: product.Product{ + BinaryName: fileName, + }, + } + av.SetLogger(testutil.TestLogger()) + _, err = av.Find(context.Background()) + if err != nil { + t.Fatal(err) + } +} + +func TestExactVersion(t *testing.T) { + t.Skip("TODO") + testutil.EndToEndTest(t) + + // TODO: mock out command execution? + + originalPath := os.Getenv("PATH") + os.Setenv("PATH", "") + t.Cleanup(func() { + os.Setenv("PATH", originalPath) + }) + + ev := &ExactVersion{ + Product: product.Terraform, + Version: version.Must(version.NewVersion("0.14.0")), + } + ev.SetLogger(testutil.TestLogger()) + _, err := ev.Find(context.Background()) + if err != nil { + t.Fatal(err) + } +} + +func createTempFile(t *testing.T, content string) (string, string) { + tmpDir := t.TempDir() + fileName := t.Name() + + filePath := filepath.Join(tmpDir, fileName) + f, err := os.Create(filePath) + if err != nil { + t.Fatal(err) + } + defer f.Close() + _, err = f.WriteString(content) + if err != nil { + t.Fatal(err) + } + + return tmpDir, fileName +} diff --git a/gitref.go b/gitref.go deleted file mode 100644 index 282cd39..0000000 --- a/gitref.go +++ /dev/null @@ -1,100 +0,0 @@ -package hcinstall - -import ( - "context" - "errors" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "runtime" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" -) - -func GitRef(ref string) *GitGetter { - return &GitGetter{ - ref: ref, - } -} - -func GitCommit(hash string) *GitGetter { - return &GitGetter{ - commit: hash, - } -} - -type GitGetter struct { - getter - ref string - commit string -} - -func (g *GitGetter) Get(ctx context.Context) (string, error) { - if g.c.Product.RepoURL == "" { - return "", fmt.Errorf("GitRefGetter is not available for product %s", g.c.Product.Name) - } - - tmpBuildDir, err := ioutil.TempDir("", "hcinstall-build") - if err != nil { - return "", err - } - - if g.ref != "" { - ref := plumbing.ReferenceName(g.ref) - _, err := git.PlainClone(tmpBuildDir, false, &git.CloneOptions{ - URL: g.c.Product.RepoURL, - ReferenceName: ref, - Depth: 1, - Tags: git.NoTags, - }) - if err != nil { - return "", fmt.Errorf("Unable to clone %q: %w", g.c.Product.RepoURL, err) - } - } else if g.commit != "" { - repo, err := git.PlainClone(tmpBuildDir, false, &git.CloneOptions{ - URL: g.c.Product.RepoURL, - Tags: git.NoTags, - }) - worktree, err := repo.Worktree() - if err != nil { - return "", fmt.Errorf("Error obtaining worktree: %w", err) - } - - err = worktree.Checkout(&git.CheckoutOptions{ - Hash: plumbing.NewHash(g.commit), - }) - if err != nil { - return "", fmt.Errorf("Error checking out commit %s: %w", g.commit, err) - } - - } else { - return "", errors.New("Either ref or commit must be specified for GitGetter. Please use GitRef() or GitCommit() functions.") - } - - var productFilename string - if runtime.GOOS == "windows" { - productFilename = g.c.Product.Name + ".exe" - } else { - productFilename = g.c.Product.Name - } - - goArgs := []string{"build", "-o", filepath.Join(g.c.InstallDir, productFilename)} - - // TODO is this needed? - vendorDir := filepath.Join(g.c.InstallDir, "vendor") - if fi, err := os.Stat(vendorDir); err == nil && fi.IsDir() { - goArgs = append(goArgs, "-mod", "vendor") - } - - cmd := exec.CommandContext(ctx, "go", goArgs...) - cmd.Dir = tmpBuildDir - out, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("unable to build Terraform: %w\n%s", err, out) - } - - return filepath.Join(g.c.InstallDir, productFilename), nil -} diff --git a/go.mod b/go.mod index 6309ba2..0fcd14d 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,13 @@ -module github.com/hashicorp/hcinstall +module github.com/hashicorp/hc-install go 1.15 require ( - github.com/go-git/go-git/v5 v5.2.0 + github.com/go-git/go-git/v5 v5.4.2 github.com/hashicorp/go-checkpoint v0.5.0 - github.com/hashicorp/go-cleanhttp v0.5.1 - github.com/hashicorp/go-getter v1.5.1 - github.com/hashicorp/go-version v1.2.1 - github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.11.0 - github.com/mitchellh/cli v1.1.1 - golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 + github.com/hashicorp/go-cleanhttp v0.5.2 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-version v1.3.0 + golang.org/dl v0.0.0-20210610154546-0cc6883720ee // indirect + golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e ) diff --git a/go.sum b/go.sum index 7594b1c..26da6c2 100644 --- a/go.sum +++ b/go.sum @@ -1,247 +1,119 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1 h1:lRi0CHyU+ytlvylOlFKKq0af6JncuyoRh1J+QJBqQx0= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= -github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= +github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= +github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= +github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY= -github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= -github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-git v1.0.0 h1:YcN9iDGDoXuIw0vHls6rINwV416HYa0EB2X+RBsyYp4= -github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA= -github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= -github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M= -github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= -github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= -github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI= -github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= +github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= +github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= +github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= +github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= -github.com/hashicorp/go-getter v1.5.1 h1:lM9sM02nvEApQGFgkXxWbhfqtyN+AyhQmi+MaMdBDOI= -github.com/hashicorp/go-getter v1.5.1/go.mod h1:a7z7NPPfNQpJWcn4rSWFtdrSldqLdLPEF3d8nFMsSLM= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= -github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.11.0 h1:RKXeXVEqJ7MKQN+G50RtXw1aBbzCGqJaszawq8ak1Sc= -github.com/hashicorp/terraform-exec v0.11.0/go.mod h1:eQdBvA0Xr/ZJNilY8TzrtePLSqLyexk9PSwVwzzHTjY= -github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU= -github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= -github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= +github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mitchellh/cli v1.1.1 h1:J64v/xD7Clql+JVKSvkYojLOXu1ibnY9ZjGLwSt/89w= -github.com/mitchellh/cli v1.1.1/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= -github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= +github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +golang.org/dl v0.0.0-20210610154546-0cc6883720ee h1:/dP+h9roJdlwxcn+wiX9EtvrdQhAPxVF9bHHr8lIU74= +golang.org/dl v0.0.0-20210610154546-0cc6883720ee/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc= -golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= +golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hcinstall.go b/hcinstall.go deleted file mode 100644 index a573db3..0000000 --- a/hcinstall.go +++ /dev/null @@ -1,173 +0,0 @@ -package hcinstall - -import ( - "context" - "errors" - "fmt" - "io/ioutil" - "log" - "os" - - "github.com/hashicorp/go-checkpoint" - "github.com/hashicorp/go-version" - "github.com/hashicorp/hcinstall/products" -) - -type Getter interface { - Get(context.Context) (string, error) - SetClient(*Client) -} - -type getter struct { - c *Client -} - -func (g *getter) SetClient(c *Client) { g.c = c } - -// Client is a client for finding and installing binaries. -// -// Convenience functions such as hcinstall.Install use a Client with default -// values. A Client can be instantiated and -type Client struct { - Product products.Product - - InstallDir string - - Getters []Getter - - VersionConstraints *VersionConstraints - - DisableVersionCheck bool -} - -func (c *Client) Install(ctx context.Context) (string, error) { - var execPath string - - for _, getter := range c.Getters { - getter.SetClient(c) - } - - // go through the options in order - // until a valid terraform executable is found - for _, g := range c.Getters { - p, err := g.Get(ctx) - if err != nil { - return "", fmt.Errorf("unexpected error: %s", err) - } - - // assert version - if !c.DisableVersionCheck { - if err := c.assertVersion(p); err != nil { - log.Printf("[WARN] Executable at %s did not satisfy version constraint: %s", p, err) - continue - } - } - - if p == "" { - // strategy did not locate an executable - fall through to next - continue - } else { - execPath = p - break - } - } - - if execPath == "" { - return "", fmt.Errorf("could not find executable") - } - - return execPath, nil -} - -// assertVersion returns an error if the product executable at execPath does not -// satisfy the client VersionConstraints. -func (c *Client) assertVersion(execPath string) error { - if c.VersionConstraints == nil { - return errors.New("Version check is enabled but VersionConstraints is set to nil. Either set DisableVersionCheck to true or specify valid VersionConstraints.") - } - - var v *version.Version - - actualVersion, err := c.Product.GetVersion(execPath) - if err != nil { - return err - } - - versionConstraints := c.VersionConstraints.constraints - if versionConstraints != nil { - if versionConstraints.Check(actualVersion) { - return nil - } else { - return fmt.Errorf("reported version %s did not satisfy version constraints %s", actualVersion, versionConstraints.String()) - } - } - - if c.VersionConstraints.latest { - resp, err := checkpoint.Check(&checkpoint.CheckParams{ - Product: c.Product.Name, - Force: c.VersionConstraints.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") - } - - v, err = version.NewVersion(resp.CurrentVersion) - if err != nil { - return err - } - } else if c.VersionConstraints.exact != nil { - v = c.VersionConstraints.exact - } - - if !actualVersion.Equal(v) { - return fmt.Errorf("reported version %s did not match required version %s", actualVersion, v) - } - - return nil -} - -// Install downloads and verifies the signature of the specified product -// executable, returning its path. -// Note that the DefaultFinders are applied in order, and therefore if a local -// executable is found that satisfies the version constraints and checksum, -// no download need take place. -func Install(ctx context.Context, dstDir string, product products.Product, versionConstraints string, forceCheckpoint bool) (string, error) { - installDir, err := ensureInstallDir(dstDir) - if err != nil { - return "", err - } - - v, err := NewVersionConstraints(versionConstraints, forceCheckpoint) - if err != nil { - return "", err - } - - defaultGetters := []Getter{LookPath(), Releases()} - - c := Client{ - InstallDir: installDir, - Getters: defaultGetters, - VersionConstraints: v, - Product: product, - } - - return c.Install(ctx) -} - -// ensureInstallDir checks whether the supplied installDir is suitable for the -// downloaded binary, creating a temporary directory if installDir is blank. -func ensureInstallDir(installDir string) (string, error) { - if installDir == "" { - return ioutil.TempDir("", "hcinstall") - } - - if _, err := os.Stat(installDir); err != nil { - return "", fmt.Errorf("could not access directory %s for installation: %w", installDir, err) - } - - return installDir, nil -} diff --git a/hcinstall_test.go b/hcinstall_test.go deleted file mode 100644 index b1170b4..0000000 --- a/hcinstall_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package hcinstall - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "os/exec" - "strings" - "testing" - - "github.com/hashicorp/hcinstall/products" -) - -func TestInstall(t *testing.T) { - tfPath, err := Install(context.Background(), "", products.Terraform, "0.12.26", true) - 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) - } -} - -func TestConsul_releases(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "hcinstall-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - v, err := NewVersionConstraints(">1.9.3, <1.9.5", true) - if err != nil { - t.Fatal(err) - } - - client := &Client{ - Product: products.Consul, - Getters: []Getter{Releases()}, - VersionConstraints: v, - InstallDir: tmpDir, - } - - consulPath, err := client.Install(context.Background()) - if err != nil { - t.Fatal(err) - } - - // run "terraform version" to check we've downloaded a terraform 0.12.26 binary - cmd := exec.Command(consulPath, "version") - - out, err := cmd.Output() - if err != nil { - t.Fatal(err) - } - - expected := "Consul v1.9.4" - actual := string(out) - if !strings.HasPrefix(actual, expected) { - t.Fatalf("ran consul version, expected %s, but got %s", expected, actual) - } -} - -func TestTerraform_releases(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "hcinstall-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - v, err := NewVersionConstraints(">0.13.4, <0.13.6", true) - if err != nil { - t.Fatal(err) - } - - client := &Client{ - Product: products.Terraform, - Getters: []Getter{Releases()}, - VersionConstraints: v, - InstallDir: tmpDir, - } - - tfPath, err := client.Install(context.Background()) - 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.5" - actual := string(out) - if !strings.HasPrefix(actual, expected) { - t.Fatalf("ran terraform version, expected %s, but got %s", expected, actual) - } -} - -func TestTerraform_gitref(t *testing.T) { - for i, c := range []struct { - gitRef string - expectedVersion string - }{ - {"refs/heads/main", "Terraform v0.15.0-dev"}, - {"refs/tags/v0.12.29", "Terraform v0.12.29"}, - {"refs/pull/26921/head", "Terraform v0.14.0-dev"}, - } { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "hcinstall-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - // v, err := NewVersionConstraints(">=0.1.0-a", true) - // if err != nil { - // t.Fatal(err) - // } - - client := &Client{ - Product: products.Terraform, - Getters: []Getter{GitRef(c.gitRef)}, - // VersionConstraints: v, - InstallDir: tmpDir, - DisableVersionCheck: true, - } - - tfPath, err := client.Install(context.Background()) - if err != nil { - t.Fatal(err) - } - - cmd := exec.Command(tfPath, "version") - - out, err := cmd.Output() - if err != nil { - t.Fatal(err) - } - - actual := string(out) - if !strings.HasPrefix(actual, c.expectedVersion) { - t.Fatalf("ran terraform version, expected %s, but got %s", c.expectedVersion, actual) - } - }) - } -} - -func TestTerraform_gitcommit(t *testing.T) { - for i, c := range []struct { - gitCommit string - expectedVersion string - }{ - // using CHANGELOG commits, since these are unlikely to be removed - {"45b795b3fd02d5177666218c3703e26252eeb745", "Terraform v0.15.0-dev"}, - } { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "hcinstall-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - // v, err := NewVersionConstraints(">=0.1.0-a", true) - // if err != nil { - // t.Fatal(err) - // } - - client := &Client{ - Product: products.Terraform, - Getters: []Getter{GitCommit(c.gitCommit)}, - // VersionConstraints: v, - InstallDir: tmpDir, - DisableVersionCheck: true, - } - - tfPath, err := client.Install(context.Background()) - if err != nil { - t.Fatal(err) - } - - cmd := exec.Command(tfPath, "version") - - out, err := cmd.Output() - if err != nil { - t.Fatal(err) - } - - // expected := "Terraform v0.15.0-dev" - actual := string(out) - if !strings.HasPrefix(actual, c.expectedVersion) { - t.Fatalf("ran terraform version, expected %s, but got %s", c.expectedVersion, actual) - } - }) - } -} diff --git a/installer.go b/installer.go new file mode 100644 index 0000000..5e57bf5 --- /dev/null +++ b/installer.go @@ -0,0 +1,136 @@ +package install + +import ( + "context" + "fmt" + "io/ioutil" + "log" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/hc-install/errors" + "github.com/hashicorp/hc-install/src" +) + +type Installer struct { + logger *log.Logger + + removableSources []src.Removable +} + +type RemoveFunc func(ctx context.Context) error + +func NewInstaller() *Installer { + discardLogger := log.New(ioutil.Discard, "", 0) + return &Installer{ + logger: discardLogger, + } +} + +func (i *Installer) Ensure(ctx context.Context, sources []src.Source) (string, error) { + var errs *multierror.Error + + for _, source := range sources { + if srcWithLogger, ok := source.(src.LoggerSettable); ok { + srcWithLogger.SetLogger(i.logger) + } + + if srcValidatable, ok := source.(src.Validatable); ok { + err := srcValidatable.Validate() + if err != nil { + errs = multierror.Append(errs, err) + } + } + } + + if len(errs.Errors) > 0 { + return "", errs + } + + i.removableSources = make([]src.Removable, 0) + + for _, source := range sources { + if s, ok := source.(src.Removable); ok { + i.removableSources = append(i.removableSources, s) + } + + switch s := source.(type) { + case src.Findable: + execPath, err := s.Find(ctx) + if err != nil { + if errors.IsErrorSkippable(err) { + errs = multierror.Append(errs, err) + continue + } + return "", err + } + + return execPath, nil + case src.Installable: + execPath, err := s.Install(ctx) + if err != nil { + if errors.IsErrorSkippable(err) { + errs = multierror.Append(errs, err) + continue + } + return "", err + } + + return execPath, nil + case src.Buildable: + execPath, err := s.Build(ctx) + if err != nil { + if errors.IsErrorSkippable(err) { + errs = multierror.Append(errs, err) + continue + } + return "", err + } + + return execPath, nil + default: + return "", fmt.Errorf("unknown source: %T", s) + } + } + + return "", fmt.Errorf("unable to find, install, or build from %d sources: %s", + len(sources), errs.ErrorOrNil()) +} + +func (i *Installer) Install(ctx context.Context, sources []src.Installable) (string, error) { + var errs *multierror.Error + + for _, source := range sources { + if srcWithLogger, ok := source.(src.LoggerSettable); ok { + srcWithLogger.SetLogger(i.logger) + } + + execPath, err := source.Install(ctx) + if err != nil { + if errors.IsErrorSkippable(err) { + errs = multierror.Append(errs, err) + continue + } + return "", err + } + + return execPath, nil + } + + return "", fmt.Errorf("unable install from %d sources: %s", + len(sources), errs.ErrorOrNil()) +} + +func (i *Installer) Remove(ctx context.Context) error { + var errs *multierror.Error + + if i.removableSources != nil { + for _, rs := range i.removableSources { + err := rs.Remove(ctx) + if err != nil { + errs = multierror.Append(errs, err) + } + } + } + + return errs.ErrorOrNil() +} diff --git a/installer_test.go b/installer_test.go new file mode 100644 index 0000000..c0ed29c --- /dev/null +++ b/installer_test.go @@ -0,0 +1 @@ +package install diff --git a/internal/build/get_go_version.go b/internal/build/get_go_version.go new file mode 100644 index 0000000..3a92985 --- /dev/null +++ b/internal/build/get_go_version.go @@ -0,0 +1,37 @@ +package build + +import ( + "context" + "fmt" + "os/exec" + "regexp" + "strings" + + "github.com/hashicorp/go-version" +) + +// GetGoVersion obtains version of locally installed Go via "go version" +func GetGoVersion(ctx context.Context) (*version.Version, error) { + cmd := exec.CommandContext(ctx, "go", "version") + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("unable to build: %w\n%s", err, out) + } + + output := strings.TrimSpace(string(out)) + + // e.g. "go version go1.15" + re := regexp.MustCompile(`^go version go([0-9.]+)\s+`) + matches := re.FindStringSubmatch(output) + if len(matches) != 2 { + return nil, fmt.Errorf("unexpected go version output: %q", output) + } + + rawGoVersion := matches[1] + v, err := version.NewVersion(rawGoVersion) + if err != nil { + return nil, fmt.Errorf("unexpected go version output: %w", err) + } + + return v, nil +} diff --git a/internal/build/go_build.go b/internal/build/go_build.go new file mode 100644 index 0000000..2f3f832 --- /dev/null +++ b/internal/build/go_build.go @@ -0,0 +1,123 @@ +package build + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + + "github.com/hashicorp/go-version" +) + +var discardLogger = log.New(ioutil.Discard, "", 0) + +// GoBuild represents a Go builder (to run "go build") +type GoBuild struct { + Version *version.Version + DetectVendoring bool + + pathToRemove string + logger *log.Logger +} + +func (gb *GoBuild) SetLogger(logger *log.Logger) { + gb.logger = logger +} + +func (gb *GoBuild) log() *log.Logger { + if gb.logger == nil { + return discardLogger + } + return gb.logger +} + +// Build runs "go build" within a given repo to produce binaryName in targetDir +func (gb *GoBuild) Build(ctx context.Context, repoDir, targetDir, binaryName string) (string, error) { + goCmd, cleanupFunc, err := gb.ensureRequiredGoVersion(ctx, repoDir) + if err != nil { + return "", err + } + defer cleanupFunc(ctx) + + goArgs := []string{"build", "-o", filepath.Join(targetDir, binaryName)} + + if gb.DetectVendoring { + vendorDir := filepath.Join(repoDir, "vendor") + if fi, err := os.Stat(vendorDir); err == nil && fi.IsDir() { + goArgs = append(goArgs, "-mod", "vendor") + } + } + + gb.log().Printf("executing %s %q in %q", goCmd, goArgs, repoDir) + cmd := exec.CommandContext(ctx, goCmd, goArgs...) + cmd.Dir = repoDir + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("unable to build: %w\n%s", err, out) + } + + binPath := filepath.Join(targetDir, binaryName) + + gb.pathToRemove = binPath + + return binPath, nil +} + +func (gb *GoBuild) Remove(ctx context.Context) error { + return os.RemoveAll(gb.pathToRemove) +} + +func (gb *GoBuild) ensureRequiredGoVersion(ctx context.Context, repoDir string) (string, CleanupFunc, error) { + cmdName := "go" + noopCleanupFunc := func(context.Context) {} + + if gb.Version != nil { + goVersion, err := GetGoVersion(ctx) + if err != nil { + return cmdName, noopCleanupFunc, err + } + + if !goVersion.GreaterThanOrEqual(gb.Version) { + // found incompatible version, try downloading the desired one + return gb.installGoVersion(ctx, gb.Version) + } + } + + if requiredVersion, ok := guessRequiredGoVersion(repoDir); ok { + goVersion, err := GetGoVersion(ctx) + if err != nil { + return cmdName, noopCleanupFunc, err + } + + if !goVersion.GreaterThanOrEqual(requiredVersion) { + // found incompatible version, try downloading the desired one + return gb.installGoVersion(ctx, requiredVersion) + } + } + + return cmdName, noopCleanupFunc, nil +} + +// CleanupFunc represents a function to be called once Go is no longer needed +// e.g. to remove any version installed temporarily per requirements +type CleanupFunc func(context.Context) + +func guessRequiredGoVersion(repoDir string) (*version.Version, bool) { + goEnvFile := filepath.Join(repoDir, ".go-version") + if fi, err := os.Stat(goEnvFile); err == nil && !fi.IsDir() { + b, err := ioutil.ReadFile(goEnvFile) + if err != nil { + return nil, false + } + requiredVersion, err := version.NewVersion(string(bytes.TrimSpace(b))) + if err != nil { + return nil, false + } + return requiredVersion, true + } + return nil, false +} diff --git a/internal/build/go_is_installed.go b/internal/build/go_is_installed.go new file mode 100644 index 0000000..6a81d19 --- /dev/null +++ b/internal/build/go_is_installed.go @@ -0,0 +1,28 @@ +package build + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// GoIsInstalled represents a checker of whether Go is installed locally +type GoIsInstalled struct { + RequiredVersion version.Constraints +} + +// Check checks whether any Go version is installed locally +func (gii *GoIsInstalled) Check(ctx context.Context) error { + goVersion, err := GetGoVersion(ctx) + if err != nil { + return err + } + + if gii.RequiredVersion != nil && !gii.RequiredVersion.Check(goVersion) { + return fmt.Errorf("go %s required (%s available)", + gii.RequiredVersion, goVersion) + } + + return nil +} diff --git a/internal/build/go_test.go b/internal/build/go_test.go new file mode 100644 index 0000000..f3d2de3 --- /dev/null +++ b/internal/build/go_test.go @@ -0,0 +1,8 @@ +package build + +// import "github.com/hashicorp/hc-install/product" + +// var ( +// _ product.Checker = &GoIsInstalled{} +// _ product.Builder = &GoBuild{} +// ) diff --git a/internal/build/install_go_version.go b/internal/build/install_go_version.go new file mode 100644 index 0000000..f97c859 --- /dev/null +++ b/internal/build/install_go_version.go @@ -0,0 +1,53 @@ +package build + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/hashicorp/go-version" +) + +// installGoVersion installs given version of Go using Go +// according to https://golang.org/doc/manage-install +func (gb *GoBuild) installGoVersion(ctx context.Context, v *version.Version) (string, CleanupFunc, error) { + // trim 0 patch versions as that's how Go does it :shrug: + shortVersion := strings.TrimSuffix(v.String(), ".0") + + pkgURL := fmt.Sprintf("golang.org/dl/go%s", shortVersion) + + gb.log().Printf("go getting %q", pkgURL) + cmd := exec.CommandContext(ctx, "go", "get", pkgURL) + out, err := cmd.CombinedOutput() + if err != nil { + return "", nil, fmt.Errorf("unable to install Go %s: %w\n%s", v, err, out) + } + + cmdName := fmt.Sprintf("go%s", shortVersion) + + gb.log().Printf("downloading go %q", shortVersion) + cmd = exec.CommandContext(ctx, cmdName, "download") + out, err = cmd.CombinedOutput() + if err != nil { + return "", nil, fmt.Errorf("unable to download Go %s: %w\n%s", v, err, out) + } + gb.log().Printf("download of go %q finished", shortVersion) + + cleanupFunc := func(ctx context.Context) { + cmd = exec.CommandContext(ctx, cmdName, "env", "GOROOT") + out, err = cmd.CombinedOutput() + if err != nil { + return + } + rootPath := strings.TrimSpace(string(out)) + + // run some extra checks before deleting, just to be sure + if rootPath != "" && strings.HasSuffix(rootPath, v.String()) { + os.RemoveAll(rootPath) + } + } + + return cmdName, cleanupFunc, nil +} diff --git a/http.go b/internal/httpclient/httpclient.go similarity index 54% rename from http.go rename to internal/httpclient/httpclient.go index e30a004..159f705 100644 --- a/http.go +++ b/internal/httpclient/httpclient.go @@ -1,16 +1,29 @@ -package hcinstall +package httpclient import ( "fmt" "net/http" - "os" - "strings" - cleanhttp "github.com/hashicorp/go-cleanhttp" - - intversion "github.com/hashicorp/hcinstall/internal/version" + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/hc-install/internal/version" ) +// NewHTTPClient provides a pre-configured http.Client +// e.g. with relevant User-Agent header +func NewHTTPClient() *http.Client { + client := cleanhttp.DefaultClient() + + userAgent := fmt.Sprintf("hc-install/%s", version.ModuleVersion()) + + cli := cleanhttp.DefaultPooledClient() + cli.Transport = &userAgentRoundTripper{ + userAgent: userAgent, + inner: cli.Transport, + } + + return client +} + type userAgentRoundTripper struct { inner http.RoundTripper userAgent string @@ -22,16 +35,3 @@ func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, e } return rt.inner.RoundTrip(req) } - -func newHTTPClient(appendUA string) *http.Client { - appendUA = strings.TrimSpace(appendUA + " " + os.Getenv("TF_APPEND_USER_AGENT")) - userAgent := strings.TrimSpace(fmt.Sprintf("HashiCorp-hcinstall/%s %s", intversion.ModuleVersion(), appendUA)) - - cli := cleanhttp.DefaultPooledClient() - cli.Transport = &userAgentRoundTripper{ - userAgent: userAgent, - inner: cli.Transport, - } - - return cli -} diff --git a/internal/releasesjson/checksum_downloader.go b/internal/releasesjson/checksum_downloader.go new file mode 100644 index 0000000..dc98a94 --- /dev/null +++ b/internal/releasesjson/checksum_downloader.go @@ -0,0 +1,154 @@ +package releasesjson + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "log" + "strings" + + "github.com/hashicorp/hc-install/internal/httpclient" + "golang.org/x/crypto/openpgp" +) + +type ChecksumDownloader struct { + ProductVersion *ProductVersion + Logger *log.Logger +} + +type ChecksumFileMap map[string]HashSum + +type HashSum []byte + +func (hs HashSum) Size() int { + return len(hs) +} + +func (hs HashSum) String() string { + return hex.EncodeToString(hs) +} + +func HashSumFromHexDigest(hexDigest string) (HashSum, error) { + sumBytes, err := hex.DecodeString(hexDigest) + if err != nil { + return nil, err + } + return HashSum(sumBytes), nil +} + +func (cd *ChecksumDownloader) DownloadAndVerifyChecksums() (ChecksumFileMap, error) { + sigFilename, err := findSigFilename(cd.ProductVersion) + if err != nil { + return nil, err + } + + client := httpclient.NewHTTPClient() + sigURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, + cd.ProductVersion.Name, + cd.ProductVersion.Version, + sigFilename) + sigResp, err := client.Get(sigURL) + if err != nil { + return nil, err + } + defer sigResp.Body.Close() + + shasumsURL := fmt.Sprintf("%s/%s/%s/%s", baseURL, + cd.ProductVersion.Name, + cd.ProductVersion.Version, + cd.ProductVersion.SHASUMS) + sumsResp, err := client.Get(shasumsURL) + if err != nil { + return nil, err + } + defer sumsResp.Body.Close() + + var shaSums strings.Builder + sumsReader := io.TeeReader(sumsResp.Body, &shaSums) + + err = verifySumsSignature(sumsReader, sigResp.Body) + if err != nil { + return nil, err + } + + return fileMapFromChecksums(shaSums) +} + +func fileMapFromChecksums(checksums strings.Builder) (ChecksumFileMap, error) { + csMap := make(ChecksumFileMap, 0) + + lines := strings.Split(checksums.String(), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.Fields(line) + if len(parts) != 2 { + return nil, fmt.Errorf("unexpected checksum line format: %q", line) + } + + h, err := HashSumFromHexDigest(parts[0]) + if err != nil { + return nil, err + } + + if h.Size() != sha256.Size { + return nil, fmt.Errorf("unexpected sha256 format (len: %d, expected: %d)", + h.Size(), sha256.Size) + } + + csMap[parts[1]] = h + } + return csMap, nil +} + +func compareChecksum(logger *log.Logger, r io.Reader, verifiedHashSum HashSum) error { + h := sha256.New() + _, err := io.Copy(h, r) + if err != nil { + return err + } + + calculatedSum := h.Sum(nil) + if !bytes.Equal(calculatedSum, verifiedHashSum) { + return fmt.Errorf("checksum mismatch (expected %q, calculated %q)", + verifiedHashSum, + hex.EncodeToString(calculatedSum)) + } + + logger.Printf("checksum matches: %q", hex.EncodeToString(calculatedSum)) + + return nil +} + +func verifySumsSignature(checksums, signature io.Reader) error { + el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(publicKey)) + if err != nil { + return err + } + + _, err = openpgp.CheckDetachedSignature(el, checksums, signature) + + return err +} + +func findSigFilename(pv *ProductVersion) (string, error) { + sigFiles := pv.SHASUMSSigs + if len(sigFiles) == 0 { + sigFiles = []string{pv.SHASUMSSig} + } + + for _, filename := range sigFiles { + if strings.HasSuffix(filename, fmt.Sprintf("_SHA256SUMS.%s.sig", keyID)) { + return filename, nil + } + if strings.HasSuffix(filename, "_SHA256SUMS.sig") { + return filename, nil + } + } + + return "", fmt.Errorf("no suitable sig file found") +} diff --git a/internal/releasesjson/downloader.go b/internal/releasesjson/downloader.go new file mode 100644 index 0000000..c058fcb --- /dev/null +++ b/internal/releasesjson/downloader.go @@ -0,0 +1,137 @@ +package releasesjson + +import ( + "archive/zip" + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "strconv" + + "github.com/hashicorp/hc-install/internal/httpclient" +) + +type Downloader struct { + Logger *log.Logger + VerifyChecksum bool +} + +func (d *Downloader) DownloadAndUnpack(ctx context.Context, pv *ProductVersion, dstDir string) error { + if len(pv.Builds) == 0 { + return fmt.Errorf("no builds found for %s %s", pv.Name, pv.Version) + } + + pb, ok := pv.Builds.BuildForOsArch(runtime.GOOS, runtime.GOARCH) + if !ok { + return fmt.Errorf("no build found for %s/%s", runtime.GOOS, runtime.GOARCH) + } + + var verifiedChecksum HashSum + if d.VerifyChecksum { + v := &ChecksumDownloader{ + ProductVersion: pv, + Logger: d.Logger, + } + verifiedChecksums, err := v.DownloadAndVerifyChecksums() + if err != nil { + return err + } + var ok bool + verifiedChecksum, ok = verifiedChecksums[pb.Filename] + if !ok { + return fmt.Errorf("no checksum found for %q", pb.Filename) + } + } + + client := httpclient.NewHTTPClient() + resp, err := client.Get(pb.URL) + if err != nil { + return err + } + defer resp.Body.Close() + + var pkgReader io.Reader + pkgReader = resp.Body + + if resp.StatusCode != 200 { + return fmt.Errorf("unexpected response code (%d)", resp.StatusCode) + } + + contentType := resp.Header.Get("content-type") + if contentType != "application/zip" { + return fmt.Errorf("unexpected content-type: %s (expected application/zip)", + contentType) + } + + if d.VerifyChecksum { + d.Logger.Printf("calculating checksum of %q", pb.Filename) + // provide extra reader to calculate & compare checksum + var buf bytes.Buffer + r := io.TeeReader(resp.Body, &buf) + pkgReader = &buf + + err := compareChecksum(d.Logger, r, verifiedChecksum) + if err != nil { + return err + } + } + + pkgFile, err := ioutil.TempFile("", pb.Filename) + if err != nil { + return err + } + defer pkgFile.Close() + + d.Logger.Printf("copying downloaded file to %s", pkgFile.Name()) + bytesCopied, err := io.Copy(pkgFile, pkgReader) + if err != nil { + return err + } + d.Logger.Printf("copied %d bytes to %s", bytesCopied, pkgFile.Name()) + + expectedSize := 0 + if length := resp.Header.Get("content-length"); length != "" { + var err error + expectedSize, err = strconv.Atoi(length) + if err != nil { + return err + } + } + if expectedSize != 0 && bytesCopied != int64(expectedSize) { + return fmt.Errorf("unexpected size (downloaded: %d, expected: %d)", + bytesCopied, expectedSize) + } + + r, err := zip.OpenReader(pkgFile.Name()) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + srcFile, err := f.Open() + if err != nil { + return err + } + + dstPath := filepath.Join(dstDir, f.Name) + dstFile, err := os.Create(dstPath) + if err != nil { + return err + } + + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return err + } + srcFile.Close() + dstFile.Close() + } + + return nil +} diff --git a/internal/releasesjson/pubkey.go b/internal/releasesjson/pubkey.go new file mode 100644 index 0000000..8a59213 --- /dev/null +++ b/internal/releasesjson/pubkey.go @@ -0,0 +1,128 @@ +package releasesjson + +const ( + // See https://www.hashicorp.com/security + keyID = `72D7468F` + publicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg== +=7pIB +-----END PGP PUBLIC KEY BLOCK-----` +) diff --git a/internal/releasesjson/releases.go b/internal/releasesjson/releases.go new file mode 100644 index 0000000..da67f53 --- /dev/null +++ b/internal/releasesjson/releases.go @@ -0,0 +1,124 @@ +package releasesjson + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/internal/httpclient" +) + +var baseURL = "https://releases.hashicorp.com" + +// Product is a top-level product like "Consul" or "Nomad". A Product may have +// one or more versions. +type Product struct { + Name string `json:"name"` + Versions map[string]*ProductVersion `json:"versions"` +} + +// ProductVersion is a wrapper around a particular product version like +// "consul 0.5.1". A ProductVersion may have one or more builds. +type ProductVersion struct { + Name string `json:"name"` + Version string `json:"version"` + SHASUMS string `json:"shasums,omitempty"` + SHASUMSSig string `json:"shasums_signature,omitempty"` + SHASUMSSigs []string `json:"shasums_signatures,omitempty"` + Builds ProductBuilds `json:"builds"` +} + +type ProductBuilds []*ProductBuild + +func (pbs ProductBuilds) BuildForOsArch(os string, arch string) (*ProductBuild, bool) { + for _, pb := range pbs { + if pb.OS == os && pb.Arch == arch { + return pb, true + } + } + return nil, false +} + +// ProductBuild is an OS/arch-specific representation of a product. This is the +// actual file that a user would download, like "consul_0.5.1_linux_amd64". +type ProductBuild struct { + Name string `json:"name"` + Version string `json:"version"` + OS string `json:"os"` + Arch string `json:"arch"` + Filename string `json:"filename"` + URL string `json:"url"` +} + +type Releases struct { + logger *log.Logger +} + +func NewReleases() *Releases { + return &Releases{ + logger: log.New(ioutil.Discard, "", 0), + } +} + +func (r *Releases) SetLogger(logger *log.Logger) { + r.logger = logger +} + +func (r *Releases) ListProductVersions(ctx context.Context, productName string) (map[string]*ProductVersion, error) { + client := httpclient.NewHTTPClient() + + productIndexURL := fmt.Sprintf("%s/%s/index.json", baseURL, productName) + r.logger.Printf("requesting versions from %s", productIndexURL) + + resp, err := client.Get(productIndexURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + r.logger.Printf("received %s", resp.Status) + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + p := Product{} + err = json.Unmarshal(body, &p) + if err != nil { + return nil, err + } + + return p.Versions, nil +} + +func (r *Releases) GetProductVersion(ctx context.Context, product string, version *version.Version) (*ProductVersion, error) { + client := httpclient.NewHTTPClient() + + indexURL := fmt.Sprintf("%s/%s/%s/index.json", baseURL, product, version) + r.logger.Printf("requesting version from %s", indexURL) + + resp, err := client.Get(indexURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + r.logger.Printf("received %s", resp.Status) + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + pv := &ProductVersion{} + err = json.Unmarshal(body, pv) + if err != nil { + return nil, err + } + + return pv, nil +} diff --git a/internal/releasesjson/releases_test.go b/internal/releasesjson/releases_test.go new file mode 100644 index 0000000..b79a784 --- /dev/null +++ b/internal/releasesjson/releases_test.go @@ -0,0 +1 @@ +package releasesjson diff --git a/internal/src/src.go b/internal/src/src.go new file mode 100644 index 0000000..5b53d92 --- /dev/null +++ b/internal/src/src.go @@ -0,0 +1,3 @@ +package src + +type InstallSrcSigil struct{} diff --git a/internal/testutil/e2e.go b/internal/testutil/e2e.go new file mode 100644 index 0000000..d9e8f3f --- /dev/null +++ b/internal/testutil/e2e.go @@ -0,0 +1,16 @@ +package testutil + +import ( + "os" + "testing" +) + +const e2eTestEnvVar = "E2E_TESTING" + +func EndToEndTest(t *testing.T) { + t.Helper() + if os.Getenv(e2eTestEnvVar) == "" { + t.Logf("%s is not set", e2eTestEnvVar) + t.SkipNow() + } +} diff --git a/internal/testutil/logger.go b/internal/testutil/logger.go new file mode 100644 index 0000000..f4f2ee5 --- /dev/null +++ b/internal/testutil/logger.go @@ -0,0 +1,15 @@ +package testutil + +import ( + "io/ioutil" + "log" + "os" + "testing" +) + +func TestLogger() *log.Logger { + if testing.Verbose() { + return log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile) + } + return log.New(ioutil.Discard, "", 0) +} diff --git a/internal/version/version.go b/internal/version/version.go index 1aac63a..d8bc462 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,7 +2,7 @@ package version const version = "0.1.0" -// ModuleVersion returns the current version of the github.com/hashicorp/hcinstall Go module. +// ModuleVersion returns the current version of the github.com/hashicorp/hc-install Go module. // This is a function to allow for future possible enhancement using debug.BuildInfo. func ModuleVersion() string { return version diff --git a/look_path.go b/look_path.go deleted file mode 100644 index 60ec993..0000000 --- a/look_path.go +++ /dev/null @@ -1,28 +0,0 @@ -package hcinstall - -import ( - "context" - "log" - "os/exec" -) - -func LookPath() *LookPathGetter { - return &LookPathGetter{} -} - -type LookPathGetter struct { - getter -} - -func (g *LookPathGetter) Get(ctx context.Context) (string, error) { - p, err := exec.LookPath(g.c.Product.Name) - if err != nil { - if notFoundErr, ok := err.(*exec.Error); ok && notFoundErr.Err == exec.ErrNotFound { - log.Printf("[WARN] could not locate a %s executable on system path; continuing", g.c.Product.Name) - return "", nil - } - return "", err - } - - return p, nil -} diff --git a/products/consul.go b/product/consul.go similarity index 53% rename from products/consul.go rename to product/consul.go index 2c9a401..a508a04 100644 --- a/products/consul.go +++ b/product/consul.go @@ -1,20 +1,29 @@ -package products +package product import ( + "context" "fmt" "os/exec" "regexp" "strings" "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/internal/build" ) var consulVersionOutputRe = regexp.MustCompile(`Consul ` + simpleVersionRe) +var ( + v1_16 = version.Must(version.NewVersion("1.16")) + // TODO: version.MustConstraint() ? + v1_16c, _ = version.NewConstraint("1.16") +) + var Consul = Product{ - Name: "consul", - GetVersion: func(path string) (*version.Version, error) { - cmd := exec.Command(path, "version") + Name: "consul", + BinaryName: "consul", + GetVersion: func(ctx context.Context, path string) (*version.Version, error) { + cmd := exec.CommandContext(ctx, path, "version") out, err := cmd.Output() if err != nil { @@ -34,5 +43,9 @@ var Consul = Product{ return v, err }, - RepoURL: "https://github.com/hashicorp/consul.git", + BuildInstructions: &BuildInstructions{ + GitRepoURL: "https://github.com/hashicorp/consul.git", + PreCloneCheck: &build.GoIsInstalled{}, + Build: &build.GoBuild{Version: v1_16}, + }, } diff --git a/product/product.go b/product/product.go new file mode 100644 index 0000000..ec29693 --- /dev/null +++ b/product/product.go @@ -0,0 +1,42 @@ +package product + +import ( + "context" + + "github.com/hashicorp/go-version" +) + +type Product struct { + // Name which identifies the product + // on releases.hashicorp.com and in Checkpoint + Name string + + // BinaryName represents name of the unpacked binary to be executed or built + BinaryName string + + // GetVersion represents how to obtain the version of the product + // reflecting any output or CLI flag differences + GetVersion func(ctx context.Context, execPath string) (*version.Version, error) + + // BuildInstructions represents how to build the product "from scratch" + BuildInstructions *BuildInstructions +} + +type BuildInstructions struct { + GitRepoURL string + + PreCloneCheck Checker + + // Build represents how to build the product + // after checking out the source code + Build Builder +} + +type Checker interface { + Check(ctx context.Context) error +} + +type Builder interface { + Build(ctx context.Context, repoDir, targetDir, binaryName string) (string, error) + Remove(ctx context.Context) error +} diff --git a/products/terraform.go b/product/terraform.go similarity index 62% rename from products/terraform.go rename to product/terraform.go index 35a3741..dd67bce 100644 --- a/products/terraform.go +++ b/product/terraform.go @@ -1,12 +1,14 @@ -package products +package product import ( + "context" "fmt" "os/exec" "regexp" "strings" "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/internal/build" ) var ( @@ -16,9 +18,10 @@ var ( ) var Terraform = Product{ - Name: "terraform", - GetVersion: func(path string) (*version.Version, error) { - cmd := exec.Command(path, "version") + Name: "terraform", + BinaryName: "terraform", + GetVersion: func(ctx context.Context, path string) (*version.Version, error) { + cmd := exec.CommandContext(ctx, path, "version") out, err := cmd.Output() if err != nil { @@ -37,7 +40,10 @@ var Terraform = Product{ } return v, err - }, - RepoURL: "https://github.com/hashicorp/terraform.git", + BuildInstructions: &BuildInstructions{ + GitRepoURL: "https://github.com/hashicorp/terraform.git", + PreCloneCheck: &build.GoIsInstalled{}, + Build: &build.GoBuild{DetectVendoring: true}, + }, } diff --git a/products/products.go b/products/products.go deleted file mode 100644 index 3545a1f..0000000 --- a/products/products.go +++ /dev/null @@ -1,19 +0,0 @@ -package products - -import ( - "github.com/hashicorp/go-version" -) - -// Product is a HashiCorp product downloadable via hcinstall. -type Product struct { - // Name is the name of the binary to be installed, also used as a - // friendly name in log messages. - Name string - - // GetVersion tries to determine the version of the executable at the - // supplied path. - GetVersion func(string) (*version.Version, error) - - // RepoURL is the URL for the product's git repo. - RepoURL string -} diff --git a/pubkey.go b/pubkey.go deleted file mode 100644 index 63826ff..0000000 --- a/pubkey.go +++ /dev/null @@ -1,32 +0,0 @@ -package hcinstall - -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/releases.go b/releases.go deleted file mode 100644 index 8e66c0f..0000000 --- a/releases.go +++ /dev/null @@ -1,136 +0,0 @@ -package hcinstall - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/hashicorp/go-checkpoint" - "github.com/hashicorp/go-version" - "github.com/hashicorp/hcinstall/products" -) - -const releasesURL = "https://releases.hashicorp.com" - -func Releases() *ReleasesGetter { - return &ReleasesGetter{} -} - -func (g *ReleasesGetter) AppendUserAgent(ua string) { - g.appendUserAgent = ua -} - -type ReleasesGetter struct { - getter - appendUserAgent string -} - -func (g *ReleasesGetter) Get(ctx context.Context) (string, error) { - var productVersion string - var err error - - if g.c.VersionConstraints.latest { - productVersion, err = getLatestVersion(g.c.Product, g.c.VersionConstraints.forceCheckpoint) - if err != nil { - return "", err - } - } else if g.c.VersionConstraints.exact != nil { - productVersion = g.c.VersionConstraints.exact.String() - } else { - productVersion, err = getLatestVersionMatchingConstraints(g.c.Product, g.c.VersionConstraints.constraints) - if err != nil { - return "", err - } - } - - p, err := downloadWithVerification(ctx, g.c.Product.Name, productVersion, g.c.InstallDir, g.appendUserAgent) - if err != nil { - return "", err - } - - return p, nil -} - -func getLatestVersion(product products.Product, forceCheckpoint bool) (string, error) { - resp, err := checkpoint.Check(&checkpoint.CheckParams{ - Product: product.Name, - Force: forceCheckpoint, - }) - if err != nil { - return "", err - } - - if resp.CurrentVersion == "" { - return "", fmt.Errorf("could not determine latest version using checkpoint: CHECKPOINT_DISABLE may be set") - } - - return resp.CurrentVersion, nil -} - -// Product is a top-level product like "Consul" or "Nomad". A Product may have -// one or more versions. -type releasedProduct struct { - Name string `json:"name"` - Versions map[string]*releasedProductVersion `json:"versions"` -} - -// ProductVersion is a wrapper around a particular product version like -// "consul 0.5.1". A ProductVersion may have one or more builds. -type releasedProductVersion struct { - Name string `json:"name"` - Version string `json:"version"` - // SHASUMS string `json:"shasums,omitempty"` - // SHASUMSSig string `json:"shasums_signature,omitempty"` - // Builds []*ProductBuild `json:"builds"` -} - -func getLatestVersionMatchingConstraints(product products.Product, constraints version.Constraints) (string, error) { - allProductVersions := releasedProduct{} - - httpClient := &http.Client{Timeout: 10 * time.Second} - - r, err := httpClient.Get(releasesURL + "/" + product.Name + "/index.json") - if err != nil { - return "", err - } - defer r.Body.Close() - - err = json.NewDecoder(r.Body).Decode(&allProductVersions) - if err != nil { - return "", err - } - - // allProductVersions is an unsorted list of all available versions: - // we must therefore visit each one to determine the maximum version - // satisfying the constraints - - zeroVersion, err := version.NewVersion("0.0.0") - if err != nil { - return "", fmt.Errorf("Unexpected error parsing initial value of maxVersion: this is a bug in hcinstall") - } - - maxVersion := zeroVersion - - for v := range allProductVersions.Versions { - vers, err := version.NewVersion(v) - if err != nil { - // hc-releases runs all versions through version.NewVersion, - // so something is seriously wrong if we can't parse here - return "", fmt.Errorf("Error parsing releases version %s: %s", v, err) - } - - if constraints.Check(vers) { - if vers.GreaterThan(maxVersion) { - maxVersion = vers - } - } - } - - if maxVersion == zeroVersion { - return "", fmt.Errorf("No version of %s found satisfying version constraints %s", product.Name, constraints) - } - - return maxVersion.String(), nil -} diff --git a/releases/exact_version.go b/releases/exact_version.go new file mode 100644 index 0000000..820e445 --- /dev/null +++ b/releases/exact_version.go @@ -0,0 +1,128 @@ +package releases + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "time" + + "github.com/hashicorp/go-version" + rjson "github.com/hashicorp/hc-install/internal/releasesjson" + isrc "github.com/hashicorp/hc-install/internal/src" + "github.com/hashicorp/hc-install/product" +) + +// ExactVersion installs the given Version of product +// to OS temp directory, or to InstallDir (if not empty) +type ExactVersion struct { + Product product.Product + Version *version.Version + InstallDir string + Timeout time.Duration + + SkipChecksumVerification bool + + logger *log.Logger + pathsToRemove []string +} + +func (*ExactVersion) IsSourceImpl() isrc.InstallSrcSigil { + return isrc.InstallSrcSigil{} +} + +func (ev *ExactVersion) SetLogger(logger *log.Logger) { + ev.logger = logger +} + +func (ev *ExactVersion) log() *log.Logger { + if ev.logger == nil { + return discardLogger + } + return ev.logger +} + +func (ev *ExactVersion) Validate() error { + if ev.Product.Name == "" { + return fmt.Errorf("unknown product name") + } + + if ev.Product.BinaryName == "" { + return fmt.Errorf("unknown binary name") + } + + if ev.Version == nil { + return fmt.Errorf("unknown version") + } + + return nil +} + +func (ev *ExactVersion) Install(ctx context.Context) (string, error) { + timeout := defaultInstallTimeout + if ev.Timeout > 0 { + timeout = ev.Timeout + } + ctx, cancelFunc := context.WithTimeout(ctx, timeout) + defer cancelFunc() + + if ev.pathsToRemove == nil { + ev.pathsToRemove = make([]string, 0) + } + + dstDir := ev.InstallDir + if dstDir == "" { + var err error + dirName := fmt.Sprintf("%s_*", ev.Product.Name) + dstDir, err = ioutil.TempDir("", dirName) + if err != nil { + return "", err + } + ev.pathsToRemove = append(ev.pathsToRemove, dstDir) + ev.log().Printf("created new temp dir at %s", dstDir) + } + ev.log().Printf("will install into dir at %s", dstDir) + + rels := rjson.NewReleases() + rels.SetLogger(ev.log()) + pv, err := rels.GetProductVersion(ctx, ev.Product.Name, ev.Version) + if err != nil { + return "", err + } + + d := &rjson.Downloader{ + Logger: ev.log(), + VerifyChecksum: !ev.SkipChecksumVerification, + } + err = d.DownloadAndUnpack(ctx, pv, dstDir) + if err != nil { + return "", err + } + + execPath := filepath.Join(dstDir, ev.Product.BinaryName) + + ev.pathsToRemove = append(ev.pathsToRemove, execPath) + + ev.log().Printf("changing perms of %s", execPath) + err = os.Chmod(execPath, 0o700) + if err != nil { + return "", err + } + + return execPath, nil +} + +func (ev *ExactVersion) Remove(ctx context.Context) error { + if ev.pathsToRemove != nil { + for _, path := range ev.pathsToRemove { + err := os.RemoveAll(path) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/releases/latest_version.go b/releases/latest_version.go new file mode 100644 index 0000000..511812d --- /dev/null +++ b/releases/latest_version.go @@ -0,0 +1,151 @@ +package releases + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "time" + + "github.com/hashicorp/go-version" + rjson "github.com/hashicorp/hc-install/internal/releasesjson" + isrc "github.com/hashicorp/hc-install/internal/src" + "github.com/hashicorp/hc-install/product" +) + +type LatestVersion struct { + Product product.Product + Constraints version.Constraints + InstallDir string + Timeout time.Duration + + SkipChecksumVerification bool + + logger *log.Logger + pathsToRemove []string +} + +func (*LatestVersion) IsSourceImpl() isrc.InstallSrcSigil { + return isrc.InstallSrcSigil{} +} + +func (lv *LatestVersion) SetLogger(logger *log.Logger) { + lv.logger = logger +} + +func (lv *LatestVersion) log() *log.Logger { + if lv.logger == nil { + return discardLogger + } + return lv.logger +} + +func (lv *LatestVersion) Validate() error { + if lv.Product.Name == "" { + return fmt.Errorf("unknown product name") + } + + if lv.Product.BinaryName == "" { + return fmt.Errorf("unknown binary name") + } + + return nil +} + +func (lv *LatestVersion) Install(ctx context.Context) (string, error) { + timeout := defaultInstallTimeout + if lv.Timeout > 0 { + timeout = lv.Timeout + } + ctx, cancelFunc := context.WithTimeout(ctx, timeout) + defer cancelFunc() + + if lv.pathsToRemove == nil { + lv.pathsToRemove = make([]string, 0) + } + + dstDir := lv.InstallDir + if dstDir == "" { + var err error + dirName := fmt.Sprintf("%s_*", lv.Product.Name) + dstDir, err = ioutil.TempDir("", dirName) + if err != nil { + return "", err + } + lv.pathsToRemove = append(lv.pathsToRemove, dstDir) + lv.log().Printf("created new temp dir at %s", dstDir) + } + lv.log().Printf("will install into dir at %s", dstDir) + + rels := rjson.NewReleases() + rels.SetLogger(lv.log()) + versions, err := rels.ListProductVersions(ctx, lv.Product.Name) + if err != nil { + return "", err + } + + if len(versions) == 0 { + return "", fmt.Errorf("no versions found for %q", lv.Product.Name) + } + + versionToInstall, ok := findLatestMatchingVersion(versions, lv.Constraints) + if !ok { + return "", fmt.Errorf("no matching version found for %q", lv.Constraints) + } + + d := &rjson.Downloader{ + Logger: lv.log(), + VerifyChecksum: !lv.SkipChecksumVerification, + } + err = d.DownloadAndUnpack(ctx, versionToInstall, dstDir) + if err != nil { + return "", err + } + + execPath := filepath.Join(dstDir, lv.Product.BinaryName) + + lv.pathsToRemove = append(lv.pathsToRemove, execPath) + + lv.log().Printf("changing perms of %s", execPath) + err = os.Chmod(execPath, 0o700) + if err != nil { + return "", err + } + + return execPath, nil +} + +func (lv *LatestVersion) Remove(ctx context.Context) error { + if lv.pathsToRemove != nil { + for _, path := range lv.pathsToRemove { + err := os.RemoveAll(path) + if err != nil { + return err + } + } + } + return nil +} + +func findLatestMatchingVersion(pvs map[string]*rjson.ProductVersion, vc version.Constraints) (*rjson.ProductVersion, bool) { + versions := make(version.Collection, 0) + for _, pv := range pvs { + v, err := version.NewVersion(pv.Version) + if err != nil { + continue + } + versions = append(versions, v) + } + + if len(versions) == 0 { + return nil, false + } + + sort.Stable(versions) + latestVersion := versions[len(versions)-1] + + return pvs[latestVersion.Original()], true +} diff --git a/releases/releases.go b/releases/releases.go new file mode 100644 index 0000000..2c3f309 --- /dev/null +++ b/releases/releases.go @@ -0,0 +1,13 @@ +package releases + +import ( + "io/ioutil" + "log" + "time" +) + +var ( + defaultInstallTimeout = 30 * time.Second + defaultListTimeout = 10 * time.Second + discardLogger = log.New(ioutil.Discard, "", 0) +) diff --git a/releases/releases_test.go b/releases/releases_test.go new file mode 100644 index 0000000..5793719 --- /dev/null +++ b/releases/releases_test.go @@ -0,0 +1,82 @@ +package releases + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/internal/testutil" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/src" +) + +var ( + _ src.Installable = &ExactVersion{} + _ src.Removable = &ExactVersion{} + + _ src.Installable = &LatestVersion{} + _ src.Removable = &LatestVersion{} +) + +func TestLatestVersion(t *testing.T) { + testutil.EndToEndTest(t) + + lv := &LatestVersion{ + Product: product.Terraform, + } + lv.SetLogger(testutil.TestLogger()) + + ctx := context.Background() + + execPath, err := lv.Install(ctx) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { lv.Remove(ctx) }) + + v, err := product.Terraform.GetVersion(ctx, execPath) + if err != nil { + t.Fatal(err) + } + + latestConstraint, err := version.NewConstraint(">= 1.0") + if err != nil { + t.Fatal(err) + } + if !latestConstraint.Check(v.Core()) { + t.Fatalf("versions don't match (expected: %s, installed: %s)", + latestConstraint, v) + } +} + +func TestExactVersion(t *testing.T) { + testutil.EndToEndTest(t) + + versionToInstall := version.Must(version.NewVersion("0.14.0")) + ev := &ExactVersion{ + Product: product.Terraform, + Version: versionToInstall, + } + ev.SetLogger(testutil.TestLogger()) + + ctx := context.Background() + + execPath, err := ev.Install(ctx) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { ev.Remove(ctx) }) + + t.Logf("exec path of installed: %s", execPath) + + v, err := product.Terraform.GetVersion(ctx, execPath) + if err != nil { + t.Fatal(err) + } + + if !versionToInstall.Equal(v) { + t.Fatalf("versions don't match (expected: %s, installed: %s)", + versionToInstall, v) + } +} diff --git a/releases/versions.go b/releases/versions.go new file mode 100644 index 0000000..346d3aa --- /dev/null +++ b/releases/versions.go @@ -0,0 +1,69 @@ +package releases + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-version" + rjson "github.com/hashicorp/hc-install/internal/releasesjson" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/src" +) + +// Versions allows listing all versions of a product +// which match Constraints +type Versions struct { + Product product.Product + Constraints version.Constraints + + ListTimeout time.Duration + InstallTimeout time.Duration + InstallDir string + SkipChecksumVerification bool +} + +func (v *Versions) List(ctx context.Context) ([]src.Source, error) { + if v.Product.Name == "" { + return nil, fmt.Errorf("unknown product name") + } + + timeout := defaultListTimeout + if v.ListTimeout > 0 { + timeout = v.ListTimeout + } + ctx, cancelFunc := context.WithTimeout(ctx, timeout) + defer cancelFunc() + + r := rjson.NewReleases() + pvs, err := r.ListProductVersions(ctx, v.Product.Name) + if err != nil { + return nil, err + } + + installables := make([]src.Source, 0) + for _, pv := range pvs { + installableVersion, err := version.NewVersion(pv.Version) + if err != nil { + continue + } + + if !v.Constraints.Check(installableVersion) { + // skip version which doesn't match constraint + continue + } + + ev := &ExactVersion{ + Product: v.Product, + Version: installableVersion, + InstallDir: v.InstallDir, + Timeout: v.InstallTimeout, + + SkipChecksumVerification: v.SkipChecksumVerification, + } + + installables = append(installables, ev) + } + + return installables, nil +} diff --git a/releases_test.go b/releases_test.go deleted file mode 100644 index a67f5db..0000000 --- a/releases_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package hcinstall - -import ( - "fmt" - "testing" - - "github.com/hashicorp/go-version" - "github.com/hashicorp/hcinstall/products" -) - -func TestGetLatestVersionMatchingConstraints(t *testing.T) { - for i, c := range []struct { - constraints string - product products.Product - expectedLatestVersion string - }{ - { - "0.12.26", products.Terraform, "0.12.26", - }, - { - "<0.11.4", products.Terraform, "0.11.3", - }, - { - ">0.13.0, <0.13.2", products.Terraform, "0.13.1", - }, - { - ">0.12.0-alpha4, <0.12.0-rc2", products.Terraform, "0.12.0-rc1", - }, - } { - t.Run(fmt.Sprintf("%d %s", i, c.expectedLatestVersion), func(t *testing.T) { - cs, err := version.NewConstraint(c.constraints) - if err != nil { - t.Fatal(err) - } - - v, err := getLatestVersionMatchingConstraints(c.product, cs) - if err != nil { - t.Fatal(err) - } - - if v != c.expectedLatestVersion { - t.Fatalf("expected %s, got %s", c.expectedLatestVersion, v) - } - }) - } -} - -func TestGetLatestVersionMatchingConstraints_no_available_version(t *testing.T) { - cs, err := version.NewConstraint(">0.999.999, <1.0") - if err != nil { - t.Fatal(err) - } - - v, err := getLatestVersionMatchingConstraints(products.Terraform, cs) - if err == nil { - t.Fatalf("Expected getLatestVersionMatchingConstraints to error, but it did not (returned version: %s)", v) - } -} diff --git a/src/src.go b/src/src.go new file mode 100644 index 0000000..11fef78 --- /dev/null +++ b/src/src.go @@ -0,0 +1,42 @@ +package src + +import ( + "context" + "log" + + isrc "github.com/hashicorp/hc-install/internal/src" +) + +// Source represents an installer, finder, or builder +type Source interface { + IsSourceImpl() isrc.InstallSrcSigil +} + +type Installable interface { + Source + Install(ctx context.Context) (string, error) +} + +type Findable interface { + Source + Find(ctx context.Context) (string, error) +} + +type Buildable interface { + Source + Build(ctx context.Context) (string, error) +} + +type Validatable interface { + Source + Validate() error +} + +type Removable interface { + Source + Remove(ctx context.Context) error +} + +type LoggerSettable interface { + SetLogger(logger *log.Logger) +} diff --git a/version_constraints.go b/version_constraints.go deleted file mode 100644 index 54e5f2a..0000000 --- a/version_constraints.go +++ /dev/null @@ -1,59 +0,0 @@ -package hcinstall - -import ( - "fmt" - "regexp" - - "github.com/hashicorp/go-version" -) - -type VersionConstraints struct { - latest bool - - constraints version.Constraints - - exact *version.Version - - forceCheckpoint bool -} - -// NewVersionConstraints constructs a new version constraints, erroring if -// invalid. Constraints are parsed from strings such as ">= 1.0" using -// hashicorp/go-version. If the special string "latest" is supplied, the version -// is constrained to the latest version Checkpoint reports as available, which -// is determined at runtime during Install. -// Multiple constraints such as ">=1.2, < 1.0" are supported. Please see the -// documentation for hashicorp/go-version for more details. -func NewVersionConstraints(constraints string, forceCheckpoint bool) (*VersionConstraints, error) { - if constraints == "latest" { - return &VersionConstraints{ - latest: true, - forceCheckpoint: forceCheckpoint, - }, nil - } - - // we treat single exact version constraints as a special case - // to save a network request in Get - exactVersionRegexp := regexp.MustCompile(`^=?(` + version.SemverRegexpRaw + `)$`) - matches := exactVersionRegexp.FindStringSubmatch(constraints) - if matches != nil { - v, err := version.NewSemver(matches[2]) - if err != nil { - return nil, fmt.Errorf("Error parsing version %s: %s", constraints, err) - } - return &VersionConstraints{ - exact: v, - forceCheckpoint: forceCheckpoint, - }, nil - } - - c, err := version.NewConstraint(constraints) - if err != nil { - return nil, fmt.Errorf("Error parsing version constraints %s: %s", constraints, err) - } - - return &VersionConstraints{ - constraints: c, - forceCheckpoint: forceCheckpoint, - }, nil -}