add vendor

This commit is contained in:
Malar Invention
2022-04-03 09:37:16 +05:30
parent f96ba5f172
commit 00ebcd295e
2339 changed files with 705854 additions and 0 deletions

1
vendor/github.com/hashicorp/hc-install/.go-version generated vendored Normal file
View File

@ -0,0 +1 @@
1.17.3

29
vendor/github.com/hashicorp/hc-install/.goreleaser.yml generated vendored Normal file
View File

@ -0,0 +1,29 @@
project_name: tfinstall
builds:
- env:
- CGO_ENABLED=0
main: ./cmd/hcinstall/main.go
mod_timestamp: '{{ .CommitTimestamp }}'
id: "tfinstall"
binary: tfinstall
flags:
- -trimpath
ldflags:
- '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}'
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
archives:
- files: []
format: zip
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
checksum:
name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
algorithm: sha256
changelog:
skip: true

373
vendor/github.com/hashicorp/hc-install/LICENSE generated vendored Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

133
vendor/github.com/hashicorp/hc-install/README.md generated vendored Normal file
View File

@ -0,0 +1,133 @@
# hc-install
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 hc-install before upgrading terraform-exec to v1.0.0.
## hc-install is not a package manager
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, `hc-install`, is a convenient way of using
the library in ad-hoc or CI shell scripting outside of Go.
`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
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
```go
TODO
```
### Find or install single version
```go
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
}
// run any tests
defer i.Remove()
```
### Install multiple versions
```go
TODO
```
### Install and build multiple versions
```go
i := NewInstaller()
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 {
return err
}
// Do some testing here
_ = execPath
// clean up
os.Remove(execPath)
}
```

View File

@ -0,0 +1,154 @@
package checkpoint
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"time"
checkpoint "github.com/hashicorp/go-checkpoint"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/internal/pubkey"
rjson "github.com/hashicorp/hc-install/internal/releasesjson"
isrc "github.com/hashicorp/hc-install/internal/src"
"github.com/hashicorp/hc-install/internal/validators"
"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
// ArmoredPublicKey is a public PGP key in ASCII/armor format to use
// instead of built-in pubkey to verify signature of downloaded checksums
ArmoredPublicKey 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 !validators.IsProductNameValid(lv.Product.Name) {
return fmt.Errorf("invalid product name: %q", lv.Product.Name)
}
if !validators.IsBinaryNameValid(lv.Product.BinaryName()) {
return fmt.Errorf("invalid binary name: %q", lv.Product.BinaryName())
}
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,
ArmoredPublicKey: pubkey.DefaultPublicKey,
BaseURL: rels.BaseURL,
}
if lv.ArmoredPublicKey != "" {
d.ArmoredPublicKey = lv.ArmoredPublicKey
}
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
}

View File

@ -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
}

View File

@ -0,0 +1,95 @@
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/internal/validators"
"github.com/hashicorp/hc-install/product"
)
// AnyVersion finds an executable binary of any version
// either defined by ExactBinPath, or as part of Product.
//
// When ExactBinPath is used, the source is skipped when
// the binary is not found or accessible/executable.
//
// When Product is used, binary name is looked up within system $PATH
// and any declared ExtraPaths (which are *appended* to
// any directories in $PATH). Source is skipped if no binary
// is found or accessible/executable.
type AnyVersion struct {
// Product represents the product (its binary name to look up),
// conflicts with ExactBinPath
Product *product.Product
// ExtraPaths represents additional dir paths to be appended to
// the default system $PATH, conflicts with ExactBinPath
ExtraPaths []string
// ExactBinPath represents exact path to the binary,
// conflicts with Product and ExtraPaths
ExactBinPath string
logger *log.Logger
}
func (*AnyVersion) IsSourceImpl() src.InstallSrcSigil {
return src.InstallSrcSigil{}
}
func (av *AnyVersion) Validate() error {
if av.ExactBinPath == "" && av.Product == nil {
return fmt.Errorf("must use either ExactBinPath or Product + ExtraPaths")
}
if av.ExactBinPath != "" && (av.Product != nil || len(av.ExtraPaths) > 0) {
return fmt.Errorf("use either ExactBinPath or Product + ExtraPaths, not both")
}
if av.ExactBinPath != "" && !filepath.IsAbs(av.ExactBinPath) {
return fmt.Errorf("expected ExactBinPath (%q) to be an absolute path", av.ExactBinPath)
}
if av.Product != nil && !validators.IsBinaryNameValid(av.Product.BinaryName()) {
return fmt.Errorf("invalid binary name: %q", av.Product.BinaryName())
}
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) {
if av.ExactBinPath != "" {
err := checkExecutable(av.ExactBinPath)
if err != nil {
return "", errors.SkippableErr(err)
}
return av.ExactBinPath, nil
}
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
}

View File

@ -0,0 +1,95 @@
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/internal/validators"
"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 !validators.IsBinaryNameValid(ev.Product.BinaryName()) {
return fmt.Errorf("invalid binary name: %q", ev.Product.BinaryName())
}
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
}

14
vendor/github.com/hashicorp/hc-install/fs/fs.go generated vendored Normal file
View File

@ -0,0 +1,14 @@
package fs
import (
"io/ioutil"
"log"
"time"
)
var (
defaultTimeout = 10 * time.Second
discardLogger = log.New(ioutil.Discard, "", 0)
)
type fileCheckFunc func(path string) error

45
vendor/github.com/hashicorp/hc-install/fs/fs_unix.go generated vendored Normal file
View File

@ -0,0 +1,45 @@
//go:build !windows
// +build !windows
package fs
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
func lookupDirs(extraDirs []string) []string {
pathVar := os.Getenv("PATH")
dirs := filepath.SplitList(pathVar)
for _, ep := range extraDirs {
dirs = append(dirs, ep)
}
return dirs
}
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 "", fmt.Errorf("%s: %w", file, 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
}

View File

@ -0,0 +1,81 @@
package fs
import (
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
)
func lookupDirs(extraDirs []string) []string {
pathVar := os.Getenv("path")
dirs := filepath.SplitList(pathVar)
for _, ep := range extraDirs {
dirs = append(dirs, ep)
}
return dirs
}
func findFile(dirs []string, file string, f fileCheckFunc) (string, error) {
for _, dir := range dirs {
path := filepath.Join(dir, file)
if err := f(path); err == nil {
return path, nil
}
}
return "", fmt.Errorf("%s: %w", file, exec.ErrNotFound)
}
func checkExecutable(file string) error {
var exts []string
x := os.Getenv(`PATHEXT`)
if x != "" {
for _, e := range strings.Split(strings.ToLower(x), `;`) {
if e == "" {
continue
}
if e[0] != '.' {
e = "." + e
}
exts = append(exts, e)
}
} else {
exts = []string{".com", ".exe", ".bat", ".cmd"}
}
if len(exts) == 0 {
return chkStat(file)
}
if hasExt(file) {
if chkStat(file) == nil {
return nil
}
}
for _, e := range exts {
if f := file + e; chkStat(f) == nil {
return nil
}
}
return fs.ErrNotExist
}
func chkStat(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if d.IsDir() {
return fs.ErrPermission
}
return nil
}
func hasExt(file string) bool {
i := strings.LastIndex(file, ".")
if i < 0 {
return false
}
return strings.LastIndexAny(file, `:\/`) < i
}

154
vendor/github.com/hashicorp/hc-install/installer.go generated vendored Normal file
View File

@ -0,0 +1,154 @@
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) SetLogger(logger *log.Logger) {
i.logger = logger
}
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 errs.ErrorOrNil() != nil {
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
i.removableSources = make([]src.Removable, 0)
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)
continue
}
}
if s, ok := source.(src.Removable); ok {
i.removableSources = append(i.removableSources, s)
}
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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,37 @@
package httpclient
import (
"fmt"
"net/http"
"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
}
func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", rt.userAgent)
}
return rt.inner.RoundTrip(req)
}

View File

@ -0,0 +1,127 @@
package pubkey
const (
// See https://www.hashicorp.com/security
DefaultPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
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
ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==
=7pIB
-----END PGP PUBLIC KEY BLOCK-----`
)

View File

@ -0,0 +1,203 @@
package releasesjson
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"net/url"
"strings"
"github.com/hashicorp/hc-install/internal/httpclient"
"golang.org/x/crypto/openpgp"
)
type ChecksumDownloader struct {
ProductVersion *ProductVersion
Logger *log.Logger
ArmoredPublicKey string
BaseURL string
}
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 := cd.findSigFilename(cd.ProductVersion)
if err != nil {
return nil, err
}
client := httpclient.NewHTTPClient()
sigURL := fmt.Sprintf("%s/%s/%s/%s", cd.BaseURL,
url.PathEscape(cd.ProductVersion.Name),
url.PathEscape(cd.ProductVersion.RawVersion),
url.PathEscape(sigFilename))
cd.Logger.Printf("downloading signature from %s", sigURL)
sigResp, err := client.Get(sigURL)
if err != nil {
return nil, err
}
if sigResp.StatusCode != 200 {
return nil, fmt.Errorf("failed to download signature from %q: %s", sigURL, sigResp.Status)
}
defer sigResp.Body.Close()
shasumsURL := fmt.Sprintf("%s/%s/%s/%s", cd.BaseURL,
url.PathEscape(cd.ProductVersion.Name),
url.PathEscape(cd.ProductVersion.RawVersion),
url.PathEscape(cd.ProductVersion.SHASUMS))
cd.Logger.Printf("downloading checksums from %s", shasumsURL)
sumsResp, err := client.Get(shasumsURL)
if err != nil {
return nil, err
}
if sumsResp.StatusCode != 200 {
return nil, fmt.Errorf("failed to download checksums from %q: %s", shasumsURL, sumsResp.Status)
}
defer sumsResp.Body.Close()
var shaSums strings.Builder
sumsReader := io.TeeReader(sumsResp.Body, &shaSums)
err = cd.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 (cd *ChecksumDownloader) verifySumsSignature(checksums, signature io.Reader) error {
el, err := cd.keyEntityList()
if err != nil {
return err
}
_, err = openpgp.CheckDetachedSignature(el, checksums, signature)
if err != nil {
return fmt.Errorf("unable to verify checksums signature: %w", err)
}
cd.Logger.Printf("checksum signature is valid")
return nil
}
func (cd *ChecksumDownloader) findSigFilename(pv *ProductVersion) (string, error) {
sigFiles := pv.SHASUMSSigs
if len(sigFiles) == 0 {
sigFiles = []string{pv.SHASUMSSig}
}
keyIds, err := cd.pubKeyIds()
if err != nil {
return "", err
}
for _, filename := range sigFiles {
for _, keyID := range keyIds {
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")
}
func (cd *ChecksumDownloader) pubKeyIds() ([]string, error) {
entityList, err := cd.keyEntityList()
if err != nil {
return nil, err
}
fingerprints := make([]string, 0)
for _, entity := range entityList {
fingerprints = append(fingerprints, entity.PrimaryKey.KeyIdShortString())
}
return fingerprints, nil
}
func (cd *ChecksumDownloader) keyEntityList() (openpgp.EntityList, error) {
if cd.ArmoredPublicKey == "" {
return nil, fmt.Errorf("no public key provided")
}
return openpgp.ReadArmoredKeyRing(strings.NewReader(cd.ArmoredPublicKey))
}

View File

@ -0,0 +1,182 @@
package releasesjson
import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"github.com/hashicorp/hc-install/internal/httpclient"
)
type Downloader struct {
Logger *log.Logger
VerifyChecksum bool
ArmoredPublicKey string
BaseURL string
}
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.FilterBuild(runtime.GOOS, runtime.GOARCH, "zip")
if !ok {
return fmt.Errorf("no ZIP archive found for %s %s %s/%s",
pv.Name, pv.Version, runtime.GOOS, runtime.GOARCH)
}
var verifiedChecksum HashSum
if d.VerifyChecksum {
v := &ChecksumDownloader{
BaseURL: d.BaseURL,
ProductVersion: pv,
Logger: d.Logger,
ArmoredPublicKey: d.ArmoredPublicKey,
}
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()
archiveURL := pb.URL
if d.BaseURL != "" {
// ensure that absolute download links from mocked responses
// are still pointing to the mock server if one is set
baseURL, err := url.Parse(d.BaseURL)
if err != nil {
return err
}
u, err := url.Parse(archiveURL)
if err != nil {
return err
}
u.Scheme = baseURL.Scheme
u.Host = baseURL.Host
archiveURL = u.String()
}
d.Logger.Printf("downloading archive from %s", archiveURL)
resp, err := client.Get(archiveURL)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("failed to download ZIP archive from %q: %s", archiveURL, resp.Status)
}
defer resp.Body.Close()
var pkgReader io.Reader
pkgReader = resp.Body
contentType := resp.Header.Get("content-type")
if !contentTypeIsZip(contentType) {
return fmt.Errorf("unexpected content-type: %s (expected any of %q)",
contentType, zipMimeTypes)
}
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
}
d.Logger.Printf("unpacking %s to %s", f.Name, dstDir)
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
}
// The production release site uses consistent single mime type
// but mime types are platform-dependent
// and we may use different OS under test
var zipMimeTypes = []string{
"application/x-zip-compressed", // Windows
"application/zip", // Unix
}
func contentTypeIsZip(contentType string) bool {
for _, mt := range zipMimeTypes {
if mt == contentType {
return true
}
}
return false
}

View File

@ -0,0 +1,41 @@
package releasesjson
import "github.com/hashicorp/go-version"
// 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"`
RawVersion string `json:"version"`
Version *version.Version `json:"-"`
SHASUMS string `json:"shasums,omitempty"`
SHASUMSSig string `json:"shasums_signature,omitempty"`
SHASUMSSigs []string `json:"shasums_signatures,omitempty"`
Builds ProductBuilds `json:"builds"`
}
type ProductVersionsMap map[string]*ProductVersion
type ProductVersions []*ProductVersion
func (pv ProductVersions) Len() int {
return len(pv)
}
func (pv ProductVersions) Less(i, j int) bool {
return pv[i].Version.LessThan(pv[j].Version)
}
func (pv ProductVersions) Swap(i, j int) {
pv[i], pv[j] = pv[j], pv[i]
}
func (pvm ProductVersionsMap) AsSlice() ProductVersions {
versions := make(ProductVersions, 0)
for _, pVersion := range pvm {
versions = append(versions, pVersion)
}
return versions
}

View File

@ -0,0 +1,177 @@
package releasesjson
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/url"
"strings"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/internal/httpclient"
)
const defaultBaseURL = "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 ProductVersionsMap `json:"versions"`
}
type ProductBuilds []*ProductBuild
func (pbs ProductBuilds) FilterBuild(os string, arch string, suffix string) (*ProductBuild, bool) {
for _, pb := range pbs {
if pb.OS == os && pb.Arch == arch && strings.HasSuffix(pb.Filename, suffix) {
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
BaseURL string
}
func NewReleases() *Releases {
return &Releases{
logger: log.New(ioutil.Discard, "", 0),
BaseURL: defaultBaseURL,
}
}
func (r *Releases) SetLogger(logger *log.Logger) {
r.logger = logger
}
func (r *Releases) ListProductVersions(ctx context.Context, productName string) (ProductVersionsMap, error) {
client := httpclient.NewHTTPClient()
productIndexURL := fmt.Sprintf("%s/%s/index.json",
r.BaseURL,
url.PathEscape(productName))
r.logger.Printf("requesting versions from %s", productIndexURL)
resp, err := client.Get(productIndexURL)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to obtain product versions from %q: %s ",
productIndexURL, resp.Status)
}
contentType := resp.Header.Get("content-type")
if contentType != "application/json" {
return nil, fmt.Errorf("unexpected Content-Type: %q", contentType)
}
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, fmt.Errorf("%w: failed to unmarshal: %q",
err, string(body))
}
for rawVersion := range p.Versions {
v, err := version.NewVersion(rawVersion)
if err != nil {
// remove unparseable version
delete(p.Versions, rawVersion)
continue
}
if ok, _ := versionIsSupported(v); !ok {
// Remove (currently unsupported) enterprise
// version and any other "custom" build
delete(p.Versions, rawVersion)
continue
}
p.Versions[rawVersion].Version = v
}
return p.Versions, nil
}
func (r *Releases) GetProductVersion(ctx context.Context, product string, version *version.Version) (*ProductVersion, error) {
if ok, err := versionIsSupported(version); !ok {
return nil, fmt.Errorf("%s: %w", product, err)
}
client := httpclient.NewHTTPClient()
indexURL := fmt.Sprintf("%s/%s/%s/index.json",
r.BaseURL,
url.PathEscape(product),
url.PathEscape(version.String()))
r.logger.Printf("requesting version from %s", indexURL)
resp, err := client.Get(indexURL)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to obtain product version from %q: %s ",
indexURL, resp.Status)
}
contentType := resp.Header.Get("content-type")
if contentType != "application/json" {
return nil, fmt.Errorf("unexpected Content-Type: %q", contentType)
}
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, fmt.Errorf("%w: failed to unmarshal response: %q",
err, string(body))
}
return pv, nil
}
func versionIsSupported(v *version.Version) (bool, error) {
isSupported := v.Metadata() == ""
if !isSupported {
return false, fmt.Errorf("cannot obtain %s (enterprise versions are not supported)",
v.String())
}
return true, nil
}

View File

@ -0,0 +1,3 @@
package src
type InstallSrcSigil struct{}

View File

@ -0,0 +1,18 @@
package validators
import "regexp"
var (
productNameRe = regexp.MustCompile(`^[a-z0-9-]+$`)
binaryNameRe = regexp.MustCompile(`^[a-zA-Z0-9-_.]+$`)
)
// IsProductNameValid provides early user-facing validation of a product name
func IsProductNameValid(productName string) bool {
return productNameRe.MatchString(productName)
}
// IsBinaryNameValid provides early user-facing validation of binary name
func IsBinaryNameValid(binaryName string) bool {
return binaryNameRe.MatchString(binaryName)
}

View File

@ -0,0 +1,9 @@
package version
const version = "0.1.0"
// 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
}

View File

@ -0,0 +1,57 @@
package product
import (
"context"
"fmt"
"os/exec"
"regexp"
"runtime"
"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",
BinaryName: func() string {
if runtime.GOOS == "windows" {
return "consul.exe"
}
return "consul"
},
GetVersion: func(ctx context.Context, path string) (*version.Version, error) {
cmd := exec.CommandContext(ctx, path, "version")
out, err := cmd.Output()
if err != nil {
return nil, err
}
stdout := strings.TrimSpace(string(out))
submatches := consulVersionOutputRe.FindStringSubmatch(stdout)
if len(submatches) != 2 {
return nil, fmt.Errorf("unexpected number of version matches %d for %s", len(submatches), stdout)
}
v, err := version.NewVersion(submatches[1])
if err != nil {
return nil, fmt.Errorf("unable to parse version %q: %w", submatches[1], err)
}
return v, err
},
BuildInstructions: &BuildInstructions{
GitRepoURL: "https://github.com/hashicorp/consul.git",
PreCloneCheck: &build.GoIsInstalled{},
Build: &build.GoBuild{Version: v1_16},
},
}

View File

@ -0,0 +1,60 @@
package product
import (
"context"
"time"
"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 BinaryNameFunc
// 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 BinaryNameFunc func() string
type BuildInstructions struct {
GitRepoURL string
// CloneTimeout overrides default timeout
// for cloning the repository
CloneTimeout time.Duration
// PreCloneCheck represents any checks to run
// prior to building, such as verifying build
// dependencies (e.g. whether Go is installed)
PreCloneCheck Checker
// PreCloneCheckTimeout overrides default timeout
// for the PreCloneCheck
PreCloneCheckTimeout time.Duration
// Build represents how to build the product
// after checking out the source code
Build Builder
// BuildTimeout overrides default timeout
// for the Builder
BuildTimeout time.Duration
}
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
}

View File

@ -0,0 +1,55 @@
package product
import (
"context"
"fmt"
"os/exec"
"regexp"
"runtime"
"strings"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/internal/build"
)
var (
simpleVersionRe = `v?(?P<version>[0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9\.]+)?)`
terraformVersionOutputRe = regexp.MustCompile(`Terraform ` + simpleVersionRe)
)
var Terraform = Product{
Name: "terraform",
BinaryName: func() string {
if runtime.GOOS == "windows" {
return "terraform.exe"
}
return "terraform"
},
GetVersion: func(ctx context.Context, path string) (*version.Version, error) {
cmd := exec.CommandContext(ctx, path, "version")
out, err := cmd.Output()
if err != nil {
return nil, err
}
stdout := strings.TrimSpace(string(out))
submatches := terraformVersionOutputRe.FindStringSubmatch(stdout)
if len(submatches) != 2 {
return nil, fmt.Errorf("unexpected number of version matches %d for %s", len(submatches), stdout)
}
v, err := version.NewVersion(submatches[1])
if err != nil {
return nil, fmt.Errorf("unable to parse version %q: %w", submatches[1], err)
}
return v, err
},
BuildInstructions: &BuildInstructions{
GitRepoURL: "https://github.com/hashicorp/terraform.git",
PreCloneCheck: &build.GoIsInstalled{},
Build: &build.GoBuild{DetectVendoring: true},
},
}

View File

@ -0,0 +1,147 @@
package releases
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/internal/pubkey"
rjson "github.com/hashicorp/hc-install/internal/releasesjson"
isrc "github.com/hashicorp/hc-install/internal/src"
"github.com/hashicorp/hc-install/internal/validators"
"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
// ArmoredPublicKey is a public PGP key in ASCII/armor format to use
// instead of built-in pubkey to verify signature of downloaded checksums
ArmoredPublicKey string
apiBaseURL string
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 !validators.IsProductNameValid(ev.Product.Name) {
return fmt.Errorf("invalid product name: %q", ev.Product.Name)
}
if !validators.IsBinaryNameValid(ev.Product.BinaryName()) {
return fmt.Errorf("invalid binary name: %q", ev.Product.BinaryName())
}
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()
if ev.apiBaseURL != "" {
rels.BaseURL = ev.apiBaseURL
}
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,
ArmoredPublicKey: pubkey.DefaultPublicKey,
BaseURL: rels.BaseURL,
}
if ev.ArmoredPublicKey != "" {
d.ArmoredPublicKey = ev.ArmoredPublicKey
}
if ev.apiBaseURL != "" {
d.BaseURL = ev.apiBaseURL
}
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
}

View File

@ -0,0 +1,171 @@
package releases
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"time"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/internal/pubkey"
rjson "github.com/hashicorp/hc-install/internal/releasesjson"
isrc "github.com/hashicorp/hc-install/internal/src"
"github.com/hashicorp/hc-install/internal/validators"
"github.com/hashicorp/hc-install/product"
)
type LatestVersion struct {
Product product.Product
Constraints version.Constraints
InstallDir string
Timeout time.Duration
IncludePrereleases bool
SkipChecksumVerification bool
// ArmoredPublicKey is a public PGP key in ASCII/armor format to use
// instead of built-in pubkey to verify signature of downloaded checksums
ArmoredPublicKey string
apiBaseURL 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 !validators.IsProductNameValid(lv.Product.Name) {
return fmt.Errorf("invalid product name: %q", lv.Product.Name)
}
if !validators.IsBinaryNameValid(lv.Product.BinaryName()) {
return fmt.Errorf("invalid binary name: %q", lv.Product.BinaryName())
}
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()
if lv.apiBaseURL != "" {
rels.BaseURL = lv.apiBaseURL
}
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 := lv.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,
ArmoredPublicKey: pubkey.DefaultPublicKey,
BaseURL: rels.BaseURL,
}
if lv.ArmoredPublicKey != "" {
d.ArmoredPublicKey = lv.ArmoredPublicKey
}
if lv.apiBaseURL != "" {
d.BaseURL = lv.apiBaseURL
}
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 (lv *LatestVersion) findLatestMatchingVersion(pvs rjson.ProductVersionsMap, vc version.Constraints) (*rjson.ProductVersion, bool) {
versions := make(version.Collection, 0)
for _, pv := range pvs.AsSlice() {
if !lv.IncludePrereleases && pv.Version.Prerelease() != "" {
// skip prereleases if desired
continue
}
versions = append(versions, pv.Version)
}
if len(versions) == 0 {
return nil, false
}
sort.Stable(versions)
latestVersion := versions[len(versions)-1]
return pvs[latestVersion.Original()], true
}

View File

@ -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)
)

View File

@ -0,0 +1,82 @@
package releases
import (
"context"
"fmt"
"sort"
"time"
"github.com/hashicorp/go-version"
rjson "github.com/hashicorp/hc-install/internal/releasesjson"
"github.com/hashicorp/hc-install/internal/validators"
"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
// Install represents configuration for installation of any listed version
Install InstallationOptions
}
type InstallationOptions struct {
Timeout time.Duration
Dir string
SkipChecksumVerification bool
// ArmoredPublicKey is a public PGP key in ASCII/armor format to use
// instead of built-in pubkey to verify signature of downloaded checksums
// during installation
ArmoredPublicKey string
}
func (v *Versions) List(ctx context.Context) ([]src.Source, error) {
if !validators.IsProductNameValid(v.Product.Name) {
return nil, fmt.Errorf("invalid product name: %q", v.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
}
versions := pvs.AsSlice()
sort.Stable(versions)
installables := make([]src.Source, 0)
for _, pv := range versions {
if !v.Constraints.Check(pv.Version) {
// skip version which doesn't match constraint
continue
}
ev := &ExactVersion{
Product: v.Product,
Version: pv.Version,
InstallDir: v.Install.Dir,
Timeout: v.Install.Timeout,
ArmoredPublicKey: v.Install.ArmoredPublicKey,
SkipChecksumVerification: v.Install.SkipChecksumVerification,
}
installables = append(installables, ev)
}
return installables, nil
}

42
vendor/github.com/hashicorp/hc-install/src/src.go generated vendored Normal file
View File

@ -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)
}