Skip to content

Commit 89c9d1a

Browse files
releases: support enterprise versions (#148)
* 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]>
1 parent 850464c commit 89c9d1a

14 files changed

+351
-35
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,16 @@ Each comes with different trade-offs described below.
4141
- `releases.{LatestVersion,ExactVersion}` - Downloads, verifies & installs any known product from `releases.hashicorp.com`
4242
- **Pros:**
4343
- Fast and reliable way of obtaining any pre-built version of any product
44+
- Allows installation of enterprise versions
4445
- **Cons:**
45-
- Installation may consume some bandwith, disk space and a little time
46+
- Installation may consume some bandwidth, disk space and a little time
4647
- Potentially less stable builds (see `checkpoint` below)
4748
- `checkpoint.LatestVersion` - Downloads, verifies & installs any known product available in HashiCorp Checkpoint
4849
- **Pros:**
4950
- Checkpoint typically contains only product versions considered stable
5051
- **Cons:**
51-
- Installation may consume some bandwith, disk space and a little time
52-
- Currently doesn't allow installation of a old versions (see `releases` above)
52+
- Installation may consume some bandwidth, disk space and a little time
53+
- Currently doesn't allow installation of old versions or enterprise versions (see `releases` above)
5354
- `build.GitRevision` - Clones raw source code and builds the product from it
5455
- **Pros:**
5556
- Useful for catching bugs and incompatibilities as early as possible (prior to product release).

checkpoint/latest_version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func (lv *LatestVersion) Install(ctx context.Context) (string, error) {
126126
if lv.ArmoredPublicKey != "" {
127127
d.ArmoredPublicKey = lv.ArmoredPublicKey
128128
}
129-
zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir)
129+
zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir, "")
130130
if zipFilePath != "" {
131131
lv.pathsToRemove = append(lv.pathsToRemove, zipFilePath)
132132
}

installer_examples_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,28 @@ func ExampleInstaller_installAndBuildMultipleVersions() {
131131
// run any tests
132132
}
133133
}
134+
135+
// Installation of a single exact enterprise version
136+
func ExampleInstaller_enterpriseVersion() {
137+
ctx := context.Background()
138+
i := install.NewInstaller()
139+
defer i.Remove(ctx)
140+
v1_9 := version.Must(version.NewVersion("1.9.8"))
141+
licenseDir := "/some/path"
142+
143+
execPath, err := i.Install(ctx, []src.Installable{
144+
&releases.ExactVersion{
145+
Product: product.Vault,
146+
Version: v1_9,
147+
Enterprise: &releases.EnterpriseOptions{ // specify that we want the enterprise version
148+
LicenseDir: licenseDir, // where license files should be placed (required for enterprise versions)
149+
},
150+
},
151+
})
152+
if err != nil {
153+
log.Fatal(err)
154+
}
155+
log.Printf("Vault %s Enterprise installed to %s; license information installed to %s", v1_9, execPath, licenseDir)
156+
157+
// run any tests
158+
}

installer_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import (
77
"context"
88
"os"
99
"path/filepath"
10+
"runtime"
1011
"testing"
1112

13+
"github.com/hashicorp/go-version"
1214
"github.com/hashicorp/hc-install"
1315
"github.com/hashicorp/hc-install/fs"
1416
"github.com/hashicorp/hc-install/internal/testutil"
@@ -98,3 +100,52 @@ func TestInstaller_Install(t *testing.T) {
98100
t.Fatal(err)
99101
}
100102
}
103+
104+
func TestInstaller_Install_enterprise(t *testing.T) {
105+
testutil.EndToEndTest(t)
106+
107+
// most of this logic is already tested within individual packages
108+
// so this is just a simple E2E test to ensure the public API
109+
// also works and continues working
110+
111+
tmpBinaryDir := t.TempDir()
112+
tmpLicenseDir := t.TempDir()
113+
114+
i := install.NewInstaller()
115+
i.SetLogger(testutil.TestLogger())
116+
ctx := context.Background()
117+
_, err := i.Install(ctx, []src.Installable{
118+
&releases.ExactVersion{
119+
Product: product.Vault,
120+
Version: version.Must(version.NewVersion("1.9.8")),
121+
InstallDir: tmpBinaryDir,
122+
Enterprise: &releases.EnterpriseOptions{
123+
LicenseDir: tmpLicenseDir,
124+
},
125+
},
126+
})
127+
if err != nil {
128+
t.Fatal(err)
129+
}
130+
131+
// Ensure the binary was installed
132+
binName := "vault"
133+
if runtime.GOOS == "windows" {
134+
binName = "vault.exe"
135+
}
136+
if _, err = os.Stat(filepath.Join(tmpBinaryDir, binName)); err != nil {
137+
t.Fatal(err)
138+
}
139+
// Ensure the enterprise license files were installed
140+
if _, err = os.Stat(filepath.Join(tmpLicenseDir, "EULA.txt")); err != nil {
141+
t.Fatal(err)
142+
}
143+
if _, err = os.Stat(filepath.Join(tmpLicenseDir, "TermsOfEvaluation.txt")); err != nil {
144+
t.Fatal(err)
145+
}
146+
147+
err = i.Remove(ctx)
148+
if err != nil {
149+
t.Fatal(err)
150+
}
151+
}

internal/releasesjson/downloader.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type Downloader struct {
2929
BaseURL string
3030
}
3131

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

169+
// Determine the appropriate destination file path
170+
dstDir := binDir
171+
if isLicenseFile(f.Name) && licenseDir != "" {
172+
dstDir = licenseDir
173+
}
174+
169175
d.Logger.Printf("unpacking %s to %s", f.Name, dstDir)
170176
dstPath := filepath.Join(dstDir, f.Name)
171177
dstFile, err := os.Create(dstPath)
@@ -200,3 +206,19 @@ func contentTypeIsZip(contentType string) bool {
200206
}
201207
return false
202208
}
209+
210+
// Enterprise products have a few additional license files
211+
// that need to be extracted to a separate directory
212+
var licenseFiles = []string{
213+
"EULA.txt",
214+
"TermsOfEvaluation.txt",
215+
}
216+
217+
func isLicenseFile(filename string) bool {
218+
for _, lf := range licenseFiles {
219+
if lf == filename {
220+
return true
221+
}
222+
}
223+
return false
224+
}

internal/releasesjson/releases.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -115,24 +115,13 @@ func (r *Releases) ListProductVersions(ctx context.Context, productName string)
115115
continue
116116
}
117117

118-
if ok, _ := versionIsSupported(v); !ok {
119-
// Remove (currently unsupported) enterprise
120-
// version and any other "custom" build
121-
delete(p.Versions, rawVersion)
122-
continue
123-
}
124-
125118
p.Versions[rawVersion].Version = v
126119
}
127120

128121
return p.Versions, nil
129122
}
130123

131124
func (r *Releases) GetProductVersion(ctx context.Context, product string, version *version.Version) (*ProductVersion, error) {
132-
if ok, err := versionIsSupported(version); !ok {
133-
return nil, fmt.Errorf("%s: %w", product, err)
134-
}
135-
136125
client := httpclient.NewHTTPClient()
137126

138127
indexURL := fmt.Sprintf("%s/%s/%s/index.json",
@@ -178,12 +167,3 @@ func (r *Releases) GetProductVersion(ctx context.Context, product string, versio
178167

179168
return pv, nil
180169
}
181-
182-
func versionIsSupported(v *version.Version) (bool, error) {
183-
isSupported := v.Metadata() == ""
184-
if !isSupported {
185-
return false, fmt.Errorf("cannot obtain %s (enterprise versions are not supported)",
186-
v.String())
187-
}
188-
return true, nil
189-
}

internal/releasesjson/releases_test.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/hashicorp/hc-install/internal/testutil"
1212
)
1313

14-
func TestListProductVersions_excludesEnterpriseBuilds(t *testing.T) {
14+
func TestListProductVersions_includesEnterpriseBuilds(t *testing.T) {
1515
testutil.EndToEndTest(t)
1616

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

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

33-
func TestGetProductVersion_excludesEnterpriseBuild(t *testing.T) {
33+
func TestGetProductVersion_includesEnterpriseBuild(t *testing.T) {
3434
testutil.EndToEndTest(t)
3535

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

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

43-
_, err := r.GetProductVersion(ctx, "consul", testEntVersion)
44-
if err == nil {
45-
t.Fatalf("Expected enterprise version %q to error out",
43+
version, err := r.GetProductVersion(ctx, "consul", testEntVersion)
44+
if err != nil {
45+
t.Fatalf("Unexpected error getting enterprise version %q",
4646
testEntVersion.String())
4747
}
48+
49+
if version.RawVersion != testEntVersion.Original() {
50+
t.Fatalf("Expected version %q, got %q", testEntVersion.String(), version.Version.String())
51+
}
4852
}

releases/enterprise.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package releases
5+
6+
import "fmt"
7+
8+
type EnterpriseOptions struct {
9+
// LicenseDir represents directory path where to install license files (required)
10+
LicenseDir string
11+
12+
// Meta represents optional version metadata (e.g. hsm, fips1402)
13+
Meta string
14+
}
15+
16+
func enterpriseVersionMetadata(eo *EnterpriseOptions) string {
17+
if eo == nil {
18+
return ""
19+
}
20+
21+
metadata := "ent"
22+
if eo.Meta != "" {
23+
metadata += "." + eo.Meta
24+
}
25+
return metadata
26+
}
27+
28+
func validateEnterpriseOptions(eo *EnterpriseOptions) error {
29+
if eo == nil {
30+
return nil
31+
}
32+
33+
if eo.LicenseDir == "" {
34+
return fmt.Errorf("LicenseDir must be provided when requesting enterprise versions")
35+
}
36+
37+
return nil
38+
}

releases/exact_version.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type ExactVersion struct {
2828
InstallDir string
2929
Timeout time.Duration
3030

31+
// Enterprise indicates installation of enterprise version (leave nil for Community editions)
32+
Enterprise *EnterpriseOptions
33+
3134
SkipChecksumVerification bool
3235

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

73+
if err := validateEnterpriseOptions(ev.Enterprise); err != nil {
74+
return err
75+
}
76+
7077
return nil
7178
}
7279

@@ -100,7 +107,11 @@ func (ev *ExactVersion) Install(ctx context.Context) (string, error) {
100107
rels.BaseURL = ev.apiBaseURL
101108
}
102109
rels.SetLogger(ev.log())
103-
pv, err := rels.GetProductVersion(ctx, ev.Product.Name, ev.Version)
110+
installVersion := ev.Version
111+
if ev.Enterprise != nil {
112+
installVersion = versionWithMetadata(installVersion, enterpriseVersionMetadata(ev.Enterprise))
113+
}
114+
pv, err := rels.GetProductVersion(ctx, ev.Product.Name, installVersion)
104115
if err != nil {
105116
return "", err
106117
}
@@ -118,7 +129,11 @@ func (ev *ExactVersion) Install(ctx context.Context) (string, error) {
118129
d.BaseURL = ev.apiBaseURL
119130
}
120131

121-
zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir)
132+
licenseDir := ""
133+
if ev.Enterprise != nil {
134+
licenseDir = ev.Enterprise.LicenseDir
135+
}
136+
zipFilePath, err := d.DownloadAndUnpack(ctx, pv, dstDir, licenseDir)
122137
if zipFilePath != "" {
123138
ev.pathsToRemove = append(ev.pathsToRemove, zipFilePath)
124139
}
@@ -151,3 +166,21 @@ func (ev *ExactVersion) Remove(ctx context.Context) error {
151166

152167
return nil
153168
}
169+
170+
// versionWithMetadata returns a new version by combining the given version with the given metadata
171+
func versionWithMetadata(v *version.Version, metadata string) *version.Version {
172+
if v == nil {
173+
return nil
174+
}
175+
176+
if metadata == "" {
177+
return v
178+
}
179+
180+
v2, err := version.NewVersion(fmt.Sprintf("%s+%s", v.Core(), metadata))
181+
if err != nil {
182+
return nil
183+
}
184+
185+
return v2
186+
}

releases/exact_version_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ func TestExactVersionValidate(t *testing.T) {
4848
},
4949
expectedErr: fmt.Errorf("unknown version"),
5050
},
51+
"Enterprise-missing-license-dir": {
52+
ev: ExactVersion{
53+
Product: product.Vault,
54+
Version: version.Must(version.NewVersion("1.9.8")),
55+
Enterprise: &EnterpriseOptions{},
56+
},
57+
expectedErr: fmt.Errorf("LicenseDir must be provided when requesting enterprise versions"),
58+
},
5159
}
5260

5361
for name, testCase := range testCases {

0 commit comments

Comments
 (0)