Skip to content

Commit 1c93988

Browse files
vinay-gopalanmarcboudreau
authored andcommitted
Add support to perform semantic version comparisons against Vault's server version (hashicorp#1426)
1 parent fae3154 commit 1c93988

9 files changed

Lines changed: 300 additions & 4 deletions

internal/consts/consts.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ const (
8282
MountTypeNomad = "nomad"
8383
MountTypeKubernetes = "kubernetes"
8484

85+
/*
86+
Vault version constants
87+
*/
88+
VaultVersion11 = "1.11.0"
89+
VaultVersion10 = "1.10.0"
90+
VaultVersion9 = "1.9.0"
91+
8592
/*
8693
misc. path related constants
8794
*/

internal/semver/semver.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package semver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/go-version"
8+
"github.com/hashicorp/vault/api"
9+
10+
"github.com/hashicorp/terraform-provider-vault/internal/consts"
11+
)
12+
13+
func getTargetVaultVersion(ctx context.Context, client *api.Client) (string, error) {
14+
resp, err := client.Sys().SealStatusWithContext(ctx)
15+
if err != nil {
16+
return "", err
17+
}
18+
19+
if resp == nil {
20+
return "", fmt.Errorf("expected response data, got nil response")
21+
}
22+
23+
if resp.Version == "" {
24+
return "", fmt.Errorf("key %q not found in response", consts.FieldVersion)
25+
}
26+
27+
return resp.Version, nil
28+
}
29+
30+
// GreaterThanOrEqual receives a context, a Vault API client
31+
// and a minimum version that the Vault server version
32+
// should be above.
33+
//
34+
// It uses the go-version package
35+
// to perform a semantic version comparison, and
36+
// returns:
37+
// - a boolean describing whether the Vault
38+
// server version was above the minimum version
39+
// - the current Vault server version as a string
40+
// - errors captured during operation, if any
41+
//
42+
// This function can be used to perform semantic version comparisons
43+
// to conditionally enable features, or to resolve any diffs in the TF
44+
// state based on the Vault version.
45+
func GreaterThanOrEqual(ctx context.Context, client *api.Client, minVersionString string) (bool, string, error) {
46+
currentVersionString, err := getTargetVaultVersion(ctx, client)
47+
if err != nil {
48+
return false, "", err
49+
}
50+
51+
minVersion, err := version.NewVersion(minVersionString)
52+
if err != nil {
53+
return false, "", err
54+
}
55+
56+
currentVersion, err := version.NewVersion(currentVersionString)
57+
if err != nil {
58+
return false, "", err
59+
}
60+
61+
comparison := currentVersion.GreaterThanOrEqual(minVersion)
62+
63+
return comparison, currentVersionString, nil
64+
}

internal/semver/semver_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package semver
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"strings"
9+
"testing"
10+
11+
"github.com/hashicorp/vault/api"
12+
13+
"github.com/hashicorp/terraform-provider-vault/internal/consts"
14+
"github.com/hashicorp/terraform-provider-vault/testutil"
15+
)
16+
17+
type testSemanticVersionHandler struct {
18+
version string
19+
}
20+
21+
func (t *testSemanticVersionHandler) handler() http.HandlerFunc {
22+
return func(w http.ResponseWriter, req *http.Request) {
23+
if req.Method != http.MethodGet {
24+
w.WriteHeader(http.StatusBadRequest)
25+
return
26+
}
27+
28+
data := map[string]interface{}{
29+
consts.FieldVersion: t.version,
30+
}
31+
32+
m, err := json.Marshal(data)
33+
if err != nil {
34+
w.WriteHeader(http.StatusInternalServerError)
35+
return
36+
}
37+
38+
w.WriteHeader(http.StatusOK)
39+
w.Write(m)
40+
}
41+
}
42+
43+
func TestGreaterThanOrEqual(t *testing.T) {
44+
testCases := []struct {
45+
name string
46+
minVersion string
47+
expected bool
48+
versionHandler *testSemanticVersionHandler
49+
wantErr bool
50+
expectedErr string
51+
}{
52+
{
53+
name: "server-greater-than",
54+
minVersion: "1.8.0",
55+
expected: true,
56+
versionHandler: &testSemanticVersionHandler{
57+
version: "1.11.0",
58+
},
59+
wantErr: false,
60+
},
61+
{
62+
name: "server-less-than",
63+
minVersion: "1.12.0",
64+
expected: false,
65+
versionHandler: &testSemanticVersionHandler{
66+
version: "1.11.0+ent",
67+
},
68+
wantErr: false,
69+
},
70+
{
71+
name: "server-equal",
72+
minVersion: "1.10.0",
73+
expected: true,
74+
versionHandler: &testSemanticVersionHandler{
75+
version: "1.10.0",
76+
},
77+
wantErr: false,
78+
},
79+
{
80+
name: "invalid-min-version",
81+
minVersion: "invalid",
82+
expected: false,
83+
versionHandler: &testSemanticVersionHandler{
84+
version: "1.11.0",
85+
},
86+
wantErr: true,
87+
expectedErr: "Malformed version",
88+
},
89+
}
90+
91+
for _, tt := range testCases {
92+
t.Run(tt.name, func(t *testing.T) {
93+
r := tt.versionHandler
94+
95+
config, ln := testutil.TestHTTPServer(t, r.handler())
96+
defer ln.Close()
97+
98+
config.Address = fmt.Sprintf("http://%s", ln.Addr())
99+
c, err := api.NewClient(config)
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
104+
ctx := context.Background()
105+
106+
isTFVersionGreater, _, err := GreaterThanOrEqual(ctx, c, tt.minVersion)
107+
if (err != nil) != tt.wantErr {
108+
t.Errorf("GreaterThanOrEqual() got an error=%s, wantErr %v", err.Error(), tt.wantErr)
109+
return
110+
}
111+
if err != nil && !strings.Contains(err.Error(), tt.expectedErr) {
112+
t.Errorf("GreaterThanOrEqual() error = %s, expectedErr %s", err.Error(), tt.expectedErr)
113+
}
114+
if isTFVersionGreater != tt.expected {
115+
t.Errorf("GreaterThanOrEqual() got = %v, want %v", isTFVersionGreater, tt.expected)
116+
}
117+
})
118+
}
119+
}

vault/provider.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/hashicorp/terraform-provider-vault/helper"
1515
"github.com/hashicorp/terraform-provider-vault/internal/consts"
1616
"github.com/hashicorp/terraform-provider-vault/internal/provider"
17+
"github.com/hashicorp/terraform-provider-vault/internal/semver"
1718
)
1819

1920
const (
@@ -864,6 +865,29 @@ func ReadContextWrapper(f schema.ReadContextFunc) schema.ReadContextFunc {
864865
}
865866
}
866867

868+
// MinVersionCheckWrapper performs a minimum version requirement check prior to the
869+
// wrapped schema.CreateContextFunc.
870+
func MinVersionCheckWrapper(f schema.CreateContextFunc, minVersion string) schema.CreateContextFunc {
871+
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
872+
client, e := provider.GetClient(d, meta)
873+
if e != nil {
874+
return diag.FromErr(e)
875+
}
876+
877+
featureEnabled, currentVersion, err := semver.GreaterThanOrEqual(ctx, client, minVersion)
878+
if err != nil {
879+
return diag.FromErr(err)
880+
}
881+
882+
if !featureEnabled {
883+
return diag.Errorf("feature not enabled on current Vault version. min version required=%s; "+
884+
"current vault version=%s", minVersion, currentVersion)
885+
}
886+
887+
return f(ctx, d, meta)
888+
}
889+
}
890+
867891
func importNamespace(d *schema.ResourceData) error {
868892
if ns := os.Getenv(consts.EnvVarVaultNamespaceImport); ns != "" {
869893
s := d.State()

vault/resource_aws_secret_backend.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package vault
22

33
import (
4+
"context"
45
"fmt"
56
"log"
67
"strings"
@@ -10,6 +11,7 @@ import (
1011

1112
"github.com/hashicorp/terraform-provider-vault/internal/consts"
1213
"github.com/hashicorp/terraform-provider-vault/internal/provider"
14+
"github.com/hashicorp/terraform-provider-vault/internal/semver"
1315
)
1416

1517
func awsSecretBackendResource() *schema.Resource {
@@ -22,12 +24,12 @@ func awsSecretBackendResource() *schema.Resource {
2224
Importer: &schema.ResourceImporter{
2325
State: schema.ImportStatePassthrough,
2426
},
27+
CustomizeDiff: mountMigrationCustomizeDiff,
2528

2629
Schema: map[string]*schema.Schema{
2730
consts.FieldPath: {
2831
Type: schema.TypeString,
2932
Optional: true,
30-
ForceNew: true,
3133
Default: "aws",
3234
Description: "Path to mount the backend at.",
3335
ValidateFunc: func(v interface{}, k string) (ws []string, errs []error) {
@@ -97,6 +99,38 @@ func awsSecretBackendResource() *schema.Resource {
9799
}
98100
}
99101

102+
func mountMigrationCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error {
103+
if !diff.HasChange("path") {
104+
return nil
105+
}
106+
107+
o, _ := diff.GetChange("path")
108+
if o == "" {
109+
return nil
110+
}
111+
112+
// Mount Migration is only available for versions >= 1.10
113+
client, e := provider.GetClient(diff, meta)
114+
if e != nil {
115+
return e
116+
}
117+
118+
remountEnabled, _, err := semver.GreaterThanOrEqual(ctx, client, consts.VaultVersion10)
119+
if err != nil {
120+
return err
121+
}
122+
123+
if !remountEnabled {
124+
// Mount migration not available
125+
// Destroy and recreate resource
126+
if err := diff.ForceNew("path"); err != nil {
127+
return err
128+
}
129+
}
130+
131+
return nil
132+
}
133+
100134
func awsSecretBackendCreate(d *schema.ResourceData, meta interface{}) error {
101135
client, e := provider.GetClient(d, meta)
102136
if e != nil {
@@ -241,6 +275,20 @@ func awsSecretBackendUpdate(d *schema.ResourceData, meta interface{}) error {
241275

242276
path := d.Id()
243277
d.Partial(true)
278+
279+
if d.HasChange("path") {
280+
// semantic version check completed in CustomizeDiff
281+
newPath := d.Get("path").(string)
282+
283+
err := client.Sys().Remount(path, newPath)
284+
if err != nil {
285+
return fmt.Errorf("error remounting to %q: %w", newPath, err)
286+
}
287+
288+
path = newPath
289+
d.SetId(path)
290+
}
291+
244292
if d.HasChange("default_lease_ttl_seconds") || d.HasChange("max_lease_ttl_seconds") {
245293
config := api.MountConfigInput{
246294
DefaultLeaseTTL: fmt.Sprintf("%ds", d.Get("default_lease_ttl_seconds")),

vault/resource_aws_secret_backend_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,38 @@ func TestAccAWSSecretBackend_usernameTempl(t *testing.T) {
103103
})
104104
}
105105

106+
func TestAccAWSSecretBackend_remount(t *testing.T) {
107+
path := acctest.RandomWithPrefix("tf-test-aws")
108+
updatedPath := acctest.RandomWithPrefix("tf-test-aws-updated")
109+
110+
resourceName := "vault_aws_secret_backend.test"
111+
accessKey, secretKey := testutil.GetTestAWSCreds(t)
112+
resource.Test(t, resource.TestCase{
113+
Providers: testProviders,
114+
PreCheck: func() { testutil.TestAccPreCheck(t) },
115+
Steps: []resource.TestStep{
116+
{
117+
Config: testAccAWSSecretBackendConfig_basic(path, accessKey, secretKey),
118+
Check: resource.ComposeTestCheckFunc(
119+
resource.TestCheckResourceAttr(resourceName, "path", path),
120+
resource.TestCheckResourceAttr(resourceName, "description", "test description"),
121+
resource.TestCheckResourceAttr(resourceName, "default_lease_ttl_seconds", "3600"),
122+
resource.TestCheckResourceAttr(resourceName, "max_lease_ttl_seconds", "86400"),
123+
),
124+
},
125+
{
126+
Config: testAccAWSSecretBackendConfig_basic(updatedPath, accessKey, secretKey),
127+
Check: resource.ComposeTestCheckFunc(
128+
resource.TestCheckResourceAttr(resourceName, "path", updatedPath),
129+
resource.TestCheckResourceAttr(resourceName, "description", "test description"),
130+
resource.TestCheckResourceAttr(resourceName, "default_lease_ttl_seconds", "3600"),
131+
resource.TestCheckResourceAttr(resourceName, "max_lease_ttl_seconds", "86400"),
132+
),
133+
},
134+
},
135+
})
136+
}
137+
106138
func testAccAWSSecretBackendConfig_basic(path, accessKey, secretKey string) string {
107139
return fmt.Sprintf(`
108140
resource "vault_aws_secret_backend" "test" {

vault/resource_kubernetes_secret_backend.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
99
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
1011
"github.com/hashicorp/terraform-provider-vault/internal/consts"
1112
"github.com/hashicorp/terraform-provider-vault/internal/provider"
1213
"github.com/hashicorp/terraform-provider-vault/util"
@@ -21,7 +22,7 @@ const (
2122

2223
func kubernetesSecretBackendResource() *schema.Resource {
2324
resource := &schema.Resource{
24-
CreateContext: kubernetesSecretBackendCreateUpdate,
25+
CreateContext: MinVersionCheckWrapper(kubernetesSecretBackendCreateUpdate, consts.VaultVersion11),
2526
ReadContext: ReadContextWrapper(kubernetesSecretBackendRead),
2627
UpdateContext: kubernetesSecretBackendCreateUpdate,
2728
DeleteContext: kubernetesSecretBackendDelete,

vault/resource_kubernetes_secret_backend_role.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1111
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
1213
"github.com/hashicorp/terraform-provider-vault/internal/consts"
1314
"github.com/hashicorp/terraform-provider-vault/internal/provider"
1415
)
@@ -30,7 +31,7 @@ const (
3031

3132
func kubernetesSecretBackendRoleResource() *schema.Resource {
3233
return &schema.Resource{
33-
CreateContext: kubernetesSecretBackendRoleCreateUpdate,
34+
CreateContext: MinVersionCheckWrapper(kubernetesSecretBackendRoleCreateUpdate, consts.VaultVersion11),
3435
ReadContext: ReadContextWrapper(kubernetesSecretBackendRoleRead),
3536
UpdateContext: kubernetesSecretBackendRoleCreateUpdate,
3637
DeleteContext: kubernetesSecretBackendRoleDelete,

0 commit comments

Comments
 (0)