Skip to content

Commit 0ecc78e

Browse files
Add support for a KV V2 Secret Metadata resource (#1687)
1 parent 0766787 commit 0ecc78e

5 files changed

Lines changed: 213 additions & 18 deletions

File tree

internal/consts/consts.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ const (
192192
FieldServersJSON = "servers_json"
193193
FieldUpgradeInfo = "upgrade_info"
194194
FieldUpgradeInfoJSON = "upgrade_info_json"
195+
FieldMaxVersions = "max_versions"
196+
FieldCASRequired = "cas_required"
197+
FieldDeleteVersionAfter = "delete_version_after"
198+
FieldCustomMetadata = "custom_metadata"
199+
FieldCustomMetadataJSON = "custom_metadata_json"
195200

196201
/*
197202
common environment variables

vault/resource_kv_secret_v2.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@ import (
55
"encoding/json"
66
"fmt"
77
"log"
8+
"strings"
9+
"time"
810

911
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1012
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/vault/api"
1114

1215
"github.com/hashicorp/terraform-provider-vault/internal/consts"
1316
"github.com/hashicorp/terraform-provider-vault/internal/provider"
1417
)
1518

19+
var kvMetadataFields = map[string]string{
20+
consts.FieldMaxVersions: consts.FieldMaxVersions,
21+
consts.FieldCASRequired: consts.FieldCASRequired,
22+
consts.FieldDeleteVersionAfter: consts.FieldDeleteVersionAfter,
23+
consts.FieldCustomMetadata: consts.FieldData,
24+
}
25+
1626
func kvSecretV2Resource(name string) *schema.Resource {
1727
return &schema.Resource{
1828
CreateContext: kvSecretV2Write,
@@ -100,6 +110,41 @@ func kvSecretV2Resource(name string) *schema.Resource {
100110
Default: false,
101111
Description: "If set to true, permanently deletes all versions for the specified key.",
102112
},
113+
114+
consts.FieldCustomMetadata: {
115+
Type: schema.TypeList,
116+
Optional: true,
117+
Computed: true,
118+
Description: "Custom metadata to be set for the secret.",
119+
Elem: &schema.Resource{
120+
Schema: map[string]*schema.Schema{
121+
consts.FieldMaxVersions: {
122+
Type: schema.TypeInt,
123+
Optional: true,
124+
Description: "The number of versions to keep per key.",
125+
},
126+
consts.FieldCASRequired: {
127+
Type: schema.TypeBool,
128+
Optional: true,
129+
Description: "If true, all keys will require the cas " +
130+
"parameter to be set on all write requests.",
131+
},
132+
consts.FieldDeleteVersionAfter: {
133+
Type: schema.TypeInt,
134+
Optional: true,
135+
Description: "If set, specifies the length of time before " +
136+
"a version is deleted.",
137+
},
138+
consts.FieldData: {
139+
Type: schema.TypeMap,
140+
Optional: true,
141+
Description: "A map of arbitrary string to string valued " +
142+
"user-provided metadata meant to describe the secret.",
143+
},
144+
},
145+
},
146+
MaxItems: 1,
147+
},
103148
},
104149
}
105150
}
@@ -108,6 +153,20 @@ func getKVV2Path(mount, name, prefix string) string {
108153
return fmt.Sprintf("%s/%s/%s", mount, prefix, name)
109154
}
110155

156+
func getCustomMetadata(d *schema.ResourceData) map[string]interface{} {
157+
data := map[string]interface{}{}
158+
159+
fieldPrefix := fmt.Sprintf("%s.0", consts.FieldCustomMetadata)
160+
for vaultKey, stateKey := range kvMetadataFields {
161+
fieldKey := fmt.Sprintf("%s.%s", fieldPrefix, stateKey)
162+
163+
if val, ok := d.GetOk(fieldKey); ok {
164+
data[vaultKey] = val
165+
}
166+
}
167+
return data
168+
}
169+
111170
func kvSecretV2Write(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
112171
client, e := provider.GetClient(d, meta)
113172
if e != nil {
@@ -140,6 +199,17 @@ func kvSecretV2Write(ctx context.Context, d *schema.ResourceData, meta interface
140199

141200
d.SetId(path)
142201

202+
// Write custom metadata for secret if provided
203+
if _, ok := d.GetOk(consts.FieldCustomMetadata); ok {
204+
cm := getCustomMetadata(d)
205+
206+
metadataPath := getKVV2Path(mount, name, consts.FieldMetadata)
207+
log.Printf("[DEBUG] Writing custom metadata for secret at %s", path)
208+
if _, err := client.Logical().Write(metadataPath, cm); err != nil {
209+
return diag.Errorf("error writing custom metadata to %s, err=%s", metadataPath, err)
210+
}
211+
}
212+
143213
return kvSecretV2Read(ctx, d, meta)
144214
}
145215

@@ -184,13 +254,63 @@ func kvSecretV2Read(_ context.Context, d *schema.ResourceData, meta interface{})
184254
if err := d.Set(consts.FieldMetadata, serializeDataMapToString(v)); err != nil {
185255
return diag.FromErr(err)
186256
}
257+
258+
// Read & Set custom metadata
259+
if _, ok := v[consts.FieldCustomMetadata]; ok {
260+
cm, err := readKVV2Metadata(d, client)
261+
if err != nil {
262+
return diag.FromErr(err)
263+
}
264+
265+
if err := d.Set(consts.FieldCustomMetadata, []interface{}{cm}); err != nil {
266+
return diag.FromErr(err)
267+
}
268+
}
187269
}
188270
}
271+
189272
}
190273

191274
return nil
192275
}
193276

277+
func readKVV2Metadata(d *schema.ResourceData, client *api.Client) (map[string]interface{}, error) {
278+
path := strings.Replace(d.Id(), consts.FieldData, consts.FieldMetadata, 1)
279+
280+
log.Printf("[DEBUG] Reading metadata for KVV2 secret at %s", path)
281+
resp, err := client.Logical().Read(path)
282+
if err != nil {
283+
return nil, err
284+
}
285+
286+
if resp == nil {
287+
log.Printf("[DEBUG] no metadata found for secret")
288+
return nil, nil
289+
}
290+
291+
data := map[string]interface{}{}
292+
293+
for vaultKey, tfKey := range kvMetadataFields {
294+
if val, ok := resp.Data[vaultKey]; ok {
295+
// the delete_version_after field is written to
296+
// Vault as an integer but is returned as a string
297+
// of the format "3h12m10s"
298+
if vaultKey == consts.FieldDeleteVersionAfter {
299+
t, err := time.ParseDuration(val.(string))
300+
if err != nil {
301+
return nil, fmt.Errorf("error parsing duration, err=%s", err)
302+
}
303+
val = t.Seconds()
304+
}
305+
306+
data[tfKey] = val
307+
308+
}
309+
}
310+
311+
return data, nil
312+
}
313+
194314
func kvSecretV2Delete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
195315
client, e := provider.GetClient(d, meta)
196316
if e != nil {

vault/resource_kv_secret_v2_test.go

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,35 @@ func TestAccKVSecretV2(t *testing.T) {
2121
PreCheck: func() { testutil.TestAccPreCheck(t) },
2222
Steps: []resource.TestStep{
2323
{
24-
Config: testKVSecretV2Config(mount, name),
24+
Config: testKVSecretV2Config_initial(mount, name),
2525
Check: resource.ComposeTestCheckFunc(
2626
resource.TestCheckResourceAttr(resourceName, consts.FieldMount, mount),
2727
resource.TestCheckResourceAttr(resourceName, consts.FieldName, name),
28-
resource.TestCheckResourceAttr(resourceName, "cas", "1"),
2928
resource.TestCheckResourceAttr(resourceName, consts.FieldPath, fmt.Sprintf("%s/data/%s", mount, name)),
3029
resource.TestCheckResourceAttr(resourceName, "delete_all_versions", "true"),
3130
resource.TestCheckResourceAttr(resourceName, "data.zip", "zap"),
3231
resource.TestCheckResourceAttr(resourceName, "data.foo", "bar"),
3332
resource.TestCheckResourceAttr(resourceName, "data.flag", "false"),
3433
),
3534
},
35+
{
36+
Config: testKVSecretV2Config_updated(mount, name),
37+
Check: resource.ComposeTestCheckFunc(
38+
resource.TestCheckResourceAttr(resourceName, consts.FieldMount, mount),
39+
resource.TestCheckResourceAttr(resourceName, consts.FieldName, name),
40+
resource.TestCheckResourceAttr(resourceName, consts.FieldPath, fmt.Sprintf("%s/data/%s", mount, name)),
41+
resource.TestCheckResourceAttr(resourceName, "delete_all_versions", "true"),
42+
resource.TestCheckResourceAttr(resourceName, "data.zip", "zoop"),
43+
resource.TestCheckResourceAttr(resourceName, "data.foo", "baz"),
44+
resource.TestCheckResourceAttr(resourceName, "data.flag", "false"),
45+
resource.TestCheckResourceAttr(resourceName, "custom_metadata.0.cas_required", "false"),
46+
resource.TestCheckResourceAttr(resourceName, "custom_metadata.0.data.%", "2"),
47+
resource.TestCheckResourceAttr(resourceName, "custom_metadata.0.data.extra", "cheese"),
48+
resource.TestCheckResourceAttr(resourceName, "custom_metadata.0.data.pizza", "please"),
49+
resource.TestCheckResourceAttr(resourceName, "custom_metadata.0.delete_version_after", "0"),
50+
resource.TestCheckResourceAttr(resourceName, "custom_metadata.0.max_versions", "5"),
51+
),
52+
},
3653
{
3754
ResourceName: resourceName,
3855
ImportState: true,
@@ -47,25 +64,54 @@ func TestAccKVSecretV2(t *testing.T) {
4764
})
4865
}
4966

50-
func testKVSecretV2Config(mount, name string) string {
67+
func testKVSecretV2Config_initial(mount, name string) string {
5168
ret := fmt.Sprintf(`
5269
%s
5370
5471
`, kvV2MountConfig(mount))
5572

5673
ret += fmt.Sprintf(`
5774
resource "vault_kv_secret_v2" "test" {
58-
mount = vault_mount.kvv2.path
59-
name = "%s"
60-
cas = 1
61-
delete_all_versions = true
62-
data_json = jsonencode(
63-
{
64-
zip = "zap",
65-
foo = "bar",
66-
flag = false
67-
}
75+
mount = vault_mount.kvv2.path
76+
name = "%s"
77+
delete_all_versions = true
78+
data_json = jsonencode(
79+
{
80+
zip = "zap",
81+
foo = "bar",
82+
flag = false
83+
}
84+
)
85+
}`, name)
86+
87+
return ret
88+
}
89+
90+
func testKVSecretV2Config_updated(mount, name string) string {
91+
ret := fmt.Sprintf(`
92+
%s
93+
94+
`, kvV2MountConfig(mount))
95+
96+
ret += fmt.Sprintf(`
97+
resource "vault_kv_secret_v2" "test" {
98+
mount = vault_mount.kvv2.path
99+
name = "%s"
100+
delete_all_versions = true
101+
data_json = jsonencode(
102+
{
103+
zip = "zoop",
104+
foo = "baz",
105+
flag = false
106+
}
68107
)
108+
custom_metadata {
109+
max_versions = 5
110+
data = {
111+
extra = "cheese",
112+
pizza = "please"
113+
}
114+
}
69115
}`, name)
70116

71117
return ret

website/docs/r/kv_secret_backend_v2.html.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ resource "vault_mount" "kvv2" {
2525
description = "KV Version 2 secret engine mount"
2626
}
2727
28-
resource "vault_kv_secret_backend_v2" "config" {
28+
resource "vault_kv_secret_backend_v2" "example" {
2929
mount = vault_mount.kvv2.path
3030
max_versions = 5
3131
delete_version_after = 12600
@@ -65,8 +65,9 @@ No additional attributes are exported by this resource.
6565

6666
## Import
6767

68-
The KV-V2 secret backend can be imported using the `path`, e.g.
68+
The KV-V2 secret backend can be imported using its unique ID,
69+
the `${mount}/config`, e.g.
6970

7071
```
71-
$ terraform import vault_kv_secret_backend_v2.config kvv2/config
72+
$ terraform import vault_kv_secret_backend_v2.example kvv2/config
7273
```

website/docs/r/kv_secret_v2.html.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ resource "vault_mount" "kvv2" {
2323
description = "KV Version 2 secret engine mount"
2424
}
2525
26-
resource "vault_kv_secret_v2" "secret" {
26+
resource "vault_kv_secret_v2" "example" {
2727
mount = vault_mount.kvv2.path
2828
name = "secret"
2929
cas = 1
@@ -34,6 +34,13 @@ resource "vault_kv_secret_v2" "secret" {
3434
foo = "bar"
3535
}
3636
)
37+
custom_metadata {
38+
max_versions = 5
39+
data = {
40+
foo = "vault@example.com",
41+
bar = "12345"
42+
}
43+
}
3744
}
3845
```
3946

@@ -69,13 +76,29 @@ The following arguments are supported:
6976
* `data_json` - (Required) JSON-encoded string that will be
7077
written as the secret data at the given path.
7178

79+
* `custom_metadata` - (Optional) A nested block that allows configuring metadata for the
80+
KV secret. Refer to the
81+
[Configuration Options](#custom-metadata-configuration-options) for more info.
82+
7283
## Required Vault Capabilities
7384

7485
Use of this resource requires the `create` or `update` capability
7586
(depending on whether the resource already exists) on the given path,
7687
the `delete` capability if the resource is removed from configuration,
7788
and the `read` capability for drift detection (by default).
7889

90+
### Custom Metadata Configuration Options
91+
92+
* `max_versions` - (Optional) The number of versions to keep per key.
93+
94+
* `cas_required` - (Optional) If true, all keys will require the cas
95+
parameter to be set on all write requests.
96+
97+
* `delete_version_after` - (Optional) If set, specifies the length of time before
98+
a version is deleted. Accepts duration in integer seconds.
99+
100+
* `data` - (Optional) A string to string map describing the secret.
101+
79102
## Attributes Reference
80103

81104
The following attributes are exported in addition to the above:
@@ -94,5 +117,5 @@ serialized as JSON.
94117
KV-V2 secrets can be imported using the `path`, e.g.
95118

96119
```
97-
$ terraform import vault_kv_secret_v2.secret kvv2/data/secret
120+
$ terraform import vault_kv_secret_v2.example kvv2/data/secret
98121
```

0 commit comments

Comments
 (0)