-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
56a7ae6
commit 58dc4c6
Showing
38 changed files
with
2,485 additions
and
269 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
1.15.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,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) | ||
} |
Oops, something went wrong.