Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs.Version to support version constrained binary discovery #52

Merged
merged 9 commits into from May 10, 2022
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -31,7 +31,7 @@ The `Installer` offers a few high-level methods:
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)
- `fs.{AnyVersion,ExactVersion,Version}` - 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.
Expand Down
22 changes: 22 additions & 0 deletions fs/fs_test.go
Expand Up @@ -16,6 +16,9 @@ var (

_ src.Findable = &ExactVersion{}
_ src.LoggerSettable = &ExactVersion{}

_ src.Findable = &Version{}
_ src.LoggerSettable = &Version{}
)

func TestExactVersion(t *testing.T) {
Expand All @@ -36,3 +39,22 @@ func TestExactVersion(t *testing.T) {
t.Fatal(err)
}
}

func TestVersion(t *testing.T) {
t.Skip("TODO")
testutil.EndToEndTest(t)

// TODO: mock out command execution?
radeksimko marked this conversation as resolved.
Show resolved Hide resolved
magodo marked this conversation as resolved.
Show resolved Hide resolved

t.Setenv("PATH", "")

v := &Version{
Product: product.Terraform,
Constraints: version.MustConstraints(version.NewConstraint(">= 1.0")),
}
v.SetLogger(testutil.TestLogger())
_, err := v.Find(context.Background())
if err != nil {
t.Fatal(err)
}
}
97 changes: 97 additions & 0 deletions fs/version.go
@@ -0,0 +1,97 @@
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"
)

// Version finds the first executable binary of the product name
// which matches the version constraint within system $PATH and any declared ExtraPaths
// (which are *appended* to any directories in $PATH)
type Version struct {
Product product.Product
Constraints version.Constraints
ExtraPaths []string
Timeout time.Duration

logger *log.Logger
}

func (*Version) IsSourceImpl() src.InstallSrcSigil {
return src.InstallSrcSigil{}
}

func (v *Version) SetLogger(logger *log.Logger) {
v.logger = logger
}

func (v *Version) log() *log.Logger {
if v.logger == nil {
return discardLogger
}
return v.logger
}

func (v *Version) Validate() error {
if !validators.IsBinaryNameValid(v.Product.BinaryName()) {
return fmt.Errorf("invalid binary name: %q", v.Product.BinaryName())
}
if len(v.Constraints) == 0 {
return fmt.Errorf("undeclared version constraints")
}
if v.Product.GetVersion == nil {
return fmt.Errorf("undeclared version getter")
}
return nil
}

func (v *Version) Find(ctx context.Context) (string, error) {
timeout := defaultTimeout
if v.Timeout > 0 {
timeout = v.Timeout
}
ctx, cancelFunc := context.WithTimeout(ctx, timeout)
defer cancelFunc()

execPath, err := findFile(lookupDirs(v.ExtraPaths), v.Product.BinaryName(), func(file string) error {
err := checkExecutable(file)
if err != nil {
return err
}

ver, err := v.Product.GetVersion(ctx, file)
if err != nil {
return err
}

for _, vc := range v.Constraints {
if !vc.Check(ver) {
return fmt.Errorf("version (%s) doesn't meet constraints %s", ver, vc.String())
}
}

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
}
70 changes: 70 additions & 0 deletions fs/version_test.go
@@ -0,0 +1,70 @@
package fs

import (
"fmt"
"testing"

"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install/product"
)

func TestVersionValidate(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
v Version
expectedErr error
}{
"Product-incorrect-binary-name": {
v: Version{
Product: product.Product{
BinaryName: func() string { return "invalid!" },
},
},
expectedErr: fmt.Errorf("invalid binary name: \"invalid!\""),
},
"Product-missing-get-version": {
v: Version{
Product: product.Product{
BinaryName: product.Terraform.BinaryName,
},
Constraints: version.MustConstraints(version.NewConstraint(">= 1.0")),
},
expectedErr: fmt.Errorf("undeclared version getter"),
},
"Product-missing-version-constraint": {
v: Version{
Product: product.Terraform,
},
expectedErr: fmt.Errorf("undeclared version constraints"),
},
"Product-and-version-constraint": {
v: Version{
Product: product.Terraform,
Constraints: version.MustConstraints(version.NewConstraint(">= 1.0")),
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

err := testCase.v.Validate()

if err == nil && testCase.expectedErr != nil {
t.Fatalf("expected error: %s, got no error", testCase.expectedErr)
}

if err != nil && testCase.expectedErr == nil {
t.Fatalf("expected no error, got error: %s", err)
}

if err != nil && testCase.expectedErr != nil && err.Error() != testCase.expectedErr.Error() {
t.Fatalf("expected error: %s, got error: %s", testCase.expectedErr, err)
}
})
}
}