Skip to content

Commit

Permalink
releases: support enterprise versions (#148)
Browse files Browse the repository at this point in the history
* releases: support enterprise versions

* releases: refactor enterprise options into struct

* fix: readme spelling/grammar

* releases: support enterprise options for ExactVersion and Versions

* releases: require enterprise license installation

* releases: e2e test for enterprise installation

* releases: example of installing enterprise versions

* releases: add missing copywrite header

* installer: fix failing windows test

* simplify destination path conditional

* releases: make EnterpriseOptions a pointer instead

* fix error messages

* refactoring: reduce nil checks

* releases: revert to copying EnterpriseOptions

I'm not entirely sure this is actually necessary or beneficial (from memory consumption perspective) but I'm keeping it as-is as it should do no harm to existing users who do not install enterprise products.

---------

Co-authored-by: Radek Simko <[email protected]>
  • Loading branch information
colinodell and radeksimko authored Aug 31, 2023
1 parent 850464c commit 89c9d1a
Show file tree
Hide file tree
Showing 14 changed files with 351 additions and 35 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ Each comes with different trade-offs described below.
- `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
- Allows installation of enterprise versions
- **Cons:**
- Installation may consume some bandwith, disk space and a little time
- Installation may consume some bandwidth, 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)
- Installation may consume some bandwidth, disk space and a little time
- Currently doesn't allow installation of old versions or enterprise 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).
Expand Down
2 changes: 1 addition & 1 deletion checkpoint/latest_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (lv *LatestVersion) Install(ctx context.Context) (string, error) {
if lv.ArmoredPublicKey != "" {
d.ArmoredPublicKey = lv.ArmoredPublicKey
}
zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir)
zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir, "")
if zipFilePath != "" {
lv.pathsToRemove = append(lv.pathsToRemove, zipFilePath)
}
Expand Down
25 changes: 25 additions & 0 deletions installer_examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,28 @@ func ExampleInstaller_installAndBuildMultipleVersions() {
// run any tests
}
}

// Installation of a single exact enterprise version
func ExampleInstaller_enterpriseVersion() {
ctx := context.Background()
i := install.NewInstaller()
defer i.Remove(ctx)
v1_9 := version.Must(version.NewVersion("1.9.8"))
licenseDir := "/some/path"

execPath, err := i.Install(ctx, []src.Installable{
&releases.ExactVersion{
Product: product.Vault,
Version: v1_9,
Enterprise: &releases.EnterpriseOptions{ // specify that we want the enterprise version
LicenseDir: licenseDir, // where license files should be placed (required for enterprise versions)
},
},
})
if err != nil {
log.Fatal(err)
}
log.Printf("Vault %s Enterprise installed to %s; license information installed to %s", v1_9, execPath, licenseDir)

// run any tests
}
51 changes: 51 additions & 0 deletions installer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"context"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/hashicorp/go-version"
"github.com/hashicorp/hc-install"
"github.com/hashicorp/hc-install/fs"
"github.com/hashicorp/hc-install/internal/testutil"
Expand Down Expand Up @@ -98,3 +100,52 @@ func TestInstaller_Install(t *testing.T) {
t.Fatal(err)
}
}

func TestInstaller_Install_enterprise(t *testing.T) {
testutil.EndToEndTest(t)

// most of this logic is already tested within individual packages
// so this is just a simple E2E test to ensure the public API
// also works and continues working

tmpBinaryDir := t.TempDir()
tmpLicenseDir := t.TempDir()

i := install.NewInstaller()
i.SetLogger(testutil.TestLogger())
ctx := context.Background()
_, err := i.Install(ctx, []src.Installable{
&releases.ExactVersion{
Product: product.Vault,
Version: version.Must(version.NewVersion("1.9.8")),
InstallDir: tmpBinaryDir,
Enterprise: &releases.EnterpriseOptions{
LicenseDir: tmpLicenseDir,
},
},
})
if err != nil {
t.Fatal(err)
}

// Ensure the binary was installed
binName := "vault"
if runtime.GOOS == "windows" {
binName = "vault.exe"
}
if _, err = os.Stat(filepath.Join(tmpBinaryDir, binName)); err != nil {
t.Fatal(err)
}
// Ensure the enterprise license files were installed
if _, err = os.Stat(filepath.Join(tmpLicenseDir, "EULA.txt")); err != nil {
t.Fatal(err)
}
if _, err = os.Stat(filepath.Join(tmpLicenseDir, "TermsOfEvaluation.txt")); err != nil {
t.Fatal(err)
}

err = i.Remove(ctx)
if err != nil {
t.Fatal(err)
}
}
24 changes: 23 additions & 1 deletion internal/releasesjson/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Downloader struct {
BaseURL string
}

func (d *Downloader) DownloadAndUnpack(ctx context.Context, pv *ProductVersion, dstDir string) (zipFilePath string, err error) {
func (d *Downloader) DownloadAndUnpack(ctx context.Context, pv *ProductVersion, binDir string, licenseDir string) (zipFilePath string, err error) {
if len(pv.Builds) == 0 {
return "", fmt.Errorf("no builds found for %s %s", pv.Name, pv.Version)
}
Expand Down Expand Up @@ -166,6 +166,12 @@ func (d *Downloader) DownloadAndUnpack(ctx context.Context, pv *ProductVersion,
return pkgFilePath, err
}

// Determine the appropriate destination file path
dstDir := binDir
if isLicenseFile(f.Name) && licenseDir != "" {
dstDir = licenseDir
}

d.Logger.Printf("unpacking %s to %s", f.Name, dstDir)
dstPath := filepath.Join(dstDir, f.Name)
dstFile, err := os.Create(dstPath)
Expand Down Expand Up @@ -200,3 +206,19 @@ func contentTypeIsZip(contentType string) bool {
}
return false
}

// Enterprise products have a few additional license files
// that need to be extracted to a separate directory
var licenseFiles = []string{
"EULA.txt",
"TermsOfEvaluation.txt",
}

func isLicenseFile(filename string) bool {
for _, lf := range licenseFiles {
if lf == filename {
return true
}
}
return false
}
20 changes: 0 additions & 20 deletions internal/releasesjson/releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,24 +115,13 @@ func (r *Releases) ListProductVersions(ctx context.Context, productName string)
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",
Expand Down Expand Up @@ -178,12 +167,3 @@ func (r *Releases) GetProductVersion(ctx context.Context, product string, versio

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
}
18 changes: 11 additions & 7 deletions internal/releasesjson/releases_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/hashicorp/hc-install/internal/testutil"
)

func TestListProductVersions_excludesEnterpriseBuilds(t *testing.T) {
func TestListProductVersions_includesEnterpriseBuilds(t *testing.T) {
testutil.EndToEndTest(t)

r := NewReleases()
Expand All @@ -25,12 +25,12 @@ func TestListProductVersions_excludesEnterpriseBuilds(t *testing.T) {

testEntVersion := "1.9.8+ent"
_, ok := pVersions[testEntVersion]
if ok {
t.Fatalf("Found unexpected Consul Enterprise version %q", testEntVersion)
if !ok {
t.Fatalf("Failed to find expected Consul Enterprise version %q", testEntVersion)
}
}

func TestGetProductVersion_excludesEnterpriseBuild(t *testing.T) {
func TestGetProductVersion_includesEnterpriseBuild(t *testing.T) {
testutil.EndToEndTest(t)

r := NewReleases()
Expand All @@ -40,9 +40,13 @@ func TestGetProductVersion_excludesEnterpriseBuild(t *testing.T) {

testEntVersion := version.Must(version.NewVersion("1.9.8+ent"))

_, err := r.GetProductVersion(ctx, "consul", testEntVersion)
if err == nil {
t.Fatalf("Expected enterprise version %q to error out",
version, err := r.GetProductVersion(ctx, "consul", testEntVersion)
if err != nil {
t.Fatalf("Unexpected error getting enterprise version %q",
testEntVersion.String())
}

if version.RawVersion != testEntVersion.Original() {
t.Fatalf("Expected version %q, got %q", testEntVersion.String(), version.Version.String())
}
}
38 changes: 38 additions & 0 deletions releases/enterprise.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package releases

import "fmt"

type EnterpriseOptions struct {
// LicenseDir represents directory path where to install license files (required)
LicenseDir string

// Meta represents optional version metadata (e.g. hsm, fips1402)
Meta string
}

func enterpriseVersionMetadata(eo *EnterpriseOptions) string {
if eo == nil {
return ""
}

metadata := "ent"
if eo.Meta != "" {
metadata += "." + eo.Meta
}
return metadata
}

func validateEnterpriseOptions(eo *EnterpriseOptions) error {
if eo == nil {
return nil
}

if eo.LicenseDir == "" {
return fmt.Errorf("LicenseDir must be provided when requesting enterprise versions")
}

return nil
}
37 changes: 35 additions & 2 deletions releases/exact_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type ExactVersion struct {
InstallDir string
Timeout time.Duration

// Enterprise indicates installation of enterprise version (leave nil for Community editions)
Enterprise *EnterpriseOptions

SkipChecksumVerification bool

// ArmoredPublicKey is a public PGP key in ASCII/armor format to use
Expand Down Expand Up @@ -67,6 +70,10 @@ func (ev *ExactVersion) Validate() error {
return fmt.Errorf("unknown version")
}

if err := validateEnterpriseOptions(ev.Enterprise); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -100,7 +107,11 @@ func (ev *ExactVersion) Install(ctx context.Context) (string, error) {
rels.BaseURL = ev.apiBaseURL
}
rels.SetLogger(ev.log())
pv, err := rels.GetProductVersion(ctx, ev.Product.Name, ev.Version)
installVersion := ev.Version
if ev.Enterprise != nil {
installVersion = versionWithMetadata(installVersion, enterpriseVersionMetadata(ev.Enterprise))
}
pv, err := rels.GetProductVersion(ctx, ev.Product.Name, installVersion)
if err != nil {
return "", err
}
Expand All @@ -118,7 +129,11 @@ func (ev *ExactVersion) Install(ctx context.Context) (string, error) {
d.BaseURL = ev.apiBaseURL
}

zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir)
licenseDir := ""
if ev.Enterprise != nil {
licenseDir = ev.Enterprise.LicenseDir
}
zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir, licenseDir)
if zipFilePath != "" {
ev.pathsToRemove = append(ev.pathsToRemove, zipFilePath)
}
Expand Down Expand Up @@ -151,3 +166,21 @@ func (ev *ExactVersion) Remove(ctx context.Context) error {

return nil
}

// versionWithMetadata returns a new version by combining the given version with the given metadata
func versionWithMetadata(v *version.Version, metadata string) *version.Version {
if v == nil {
return nil
}

if metadata == "" {
return v
}

v2, err := version.NewVersion(fmt.Sprintf("%s+%s", v.Core(), metadata))
if err != nil {
return nil
}

return v2
}
8 changes: 8 additions & 0 deletions releases/exact_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ func TestExactVersionValidate(t *testing.T) {
},
expectedErr: fmt.Errorf("unknown version"),
},
"Enterprise-missing-license-dir": {
ev: ExactVersion{
Product: product.Vault,
Version: version.Must(version.NewVersion("1.9.8")),
Enterprise: &EnterpriseOptions{},
},
expectedErr: fmt.Errorf("LicenseDir must be provided when requesting enterprise versions"),
},
}

for name, testCase := range testCases {
Expand Down
Loading

0 comments on commit 89c9d1a

Please sign in to comment.