Skip to content

Commit

Permalink
Implement new interface
Browse files Browse the repository at this point in the history
  • Loading branch information
radeksimko committed Jul 5, 2021
1 parent 56a7ae6 commit 58dc4c6
Show file tree
Hide file tree
Showing 38 changed files with 2,485 additions and 269 deletions.
1 change: 1 addition & 0 deletions .go-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.15.0
151 changes: 102 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,134 @@
# 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.

`hc-install` will **not**:

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`.

## 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)
}
```
172 changes: 172 additions & 0 deletions build/git_revision.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 58dc4c6

Please sign in to comment.