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

Add custom metadata support to namespace resource #2033

Merged
merged 9 commits into from
Oct 13, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
FEATURES:
* Add support for setting `not_before_duration` argument on `vault_ssh_secret_backend_role`: ([#2019](https://github.com/hashicorp/terraform-provider-vault/pull/2019))
* Add support for `hmac` key type and key_size to `vault_transit_secret_backend_key`: ([#2034](https://github.com/hashicorp/terraform-provider-vault/pull/2034/))
* Add support for `custom_metadata` on `vault_namespace`: ([#2033](https://github.com/hashicorp/terraform-provider-vault/pull/2033))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


BUGS:
* Fix duplicate timestamp and incorrect level messages: ([#2031](https://github.com/hashicorp/terraform-provider-vault/pull/2031))
Expand Down
98 changes: 79 additions & 19 deletions vault/resource_namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
package vault

import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"

"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/vault/api"

Expand All @@ -21,10 +23,10 @@ import (

func namespaceResource() *schema.Resource {
return &schema.Resource{
Create: namespaceCreate,
Update: namespaceCreate,
Delete: namespaceDelete,
Read: provider.ReadWrapper(namespaceRead),
CreateContext: namespaceCreate,
UpdateContext: namespaceUpdate,
DeleteContext: namespaceDelete,
ReadContext: provider.ReadContextWrapper(namespaceRead),
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Expand All @@ -48,31 +50,80 @@ func namespaceResource() *schema.Resource {
Optional: true,
Description: "The fully qualified namespace path.",
},
consts.FieldCustomMetadata: {
Type: schema.TypeMap,
Computed: true,
Optional: true,
Description: "A map of arbitrary string to string valued user-provided " +
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
"metadata meant to describe the namespace.",
},
},
}
}

func namespaceCreate(d *schema.ResourceData, meta interface{}) error {
func namespaceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return diag.FromErr(e)
}

path := d.Get(consts.FieldPath).(string)

var data map[string]interface{}

// data is non-nil only if Vault version >= 1.12
// and custom_metadata is provided
if provider.IsAPISupported(meta, provider.VaultVersion112) {
if v, ok := d.GetOk(consts.FieldCustomMetadata); ok {
data = map[string]interface{}{
consts.FieldCustomMetadata: v,
}
}
}

log.Printf("[DEBUG] Creating namespace %s in Vault", path)
_, err := client.Logical().Write(consts.SysNamespaceRoot+path, data)
if err != nil {
return diag.Errorf("error writing to Vault: %s", err)
}

return namespaceRead(ctx, d, meta)
}

func namespaceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
// Updating a namespace is only supported in
// Vault versions >= 1.12
if !provider.IsAPISupported(meta, provider.VaultVersion112) {
return nil
}

client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}

path := d.Get(consts.FieldPath).(string)

var data map[string]interface{}
if v, ok := d.GetOk(consts.FieldCustomMetadata); ok {
data = map[string]interface{}{
consts.FieldCustomMetadata: v,
}
}

log.Printf("[DEBUG] Creating namespace %s in Vault", path)
_, err := client.Logical().Write(consts.SysNamespaceRoot+path, nil)
_, err := client.Logical().JSONMergePatch(ctx, consts.SysNamespaceRoot+path, data)
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("error writing to Vault: %s", err)
return diag.Errorf("error writing to Vault: %s", err)
}

return namespaceRead(d, meta)
return namespaceRead(ctx, d, meta)
}

func namespaceDelete(d *schema.ResourceData, meta interface{}) error {
func namespaceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}

path := d.Get(consts.FieldPath).(string)
Expand All @@ -99,11 +150,11 @@ func namespaceDelete(d *schema.ResourceData, meta interface{}) error {
if err := backoff.RetryNotify(deleteNS, bo, func(err error, duration time.Duration) {
log.Printf("[WARN] Deleting namespace %q failed, retrying in %s", path, duration)
}); err != nil {
return fmt.Errorf("error deleting from Vault: %s", err)
return diag.Errorf("error deleting from Vault: %s", err)
}

// wait for the namespace to be gone...
return backoff.RetryNotify(func() error {
return diag.FromErr(backoff.RetryNotify(func() error {
if resp, _ := client.Logical().Read(consts.SysNamespaceRoot + path); resp != nil {
return fmt.Errorf("namespace %q still exists", path)
}
Expand All @@ -115,13 +166,13 @@ func namespaceDelete(d *schema.ResourceData, meta interface{}) error {
"[WARN] Waiting for Vault to garbage collect the %q namespace, retrying in %s",
path, duration)
},
)
))
}

func namespaceRead(d *schema.ResourceData, meta interface{}) error {
func namespaceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}

upgradeNonPathdNamespaceID(d)
Expand All @@ -130,7 +181,7 @@ func namespaceRead(d *schema.ResourceData, meta interface{}) error {

resp, err := client.Logical().Read(consts.SysNamespaceRoot + path)
if err != nil {
return fmt.Errorf("error reading from Vault: %s", err)
return diag.Errorf("error reading from Vault: %s", err)
}

if resp == nil {
Expand All @@ -142,18 +193,27 @@ func namespaceRead(d *schema.ResourceData, meta interface{}) error {
d.SetId(resp.Data[consts.FieldPath].(string))

toSet := map[string]interface{}{
consts.FieldNamespaceID: resp.Data["id"],
consts.FieldNamespaceID: resp.Data[consts.FieldID],
consts.FieldPath: util.TrimSlashes(path),
}

if provider.IsAPISupported(meta, provider.VaultVersion112) {
toSet[consts.FieldCustomMetadata] = resp.Data[consts.FieldCustomMetadata]
} else {
// set computed parameter to nil for vault versions <= 1.11
// prevents 'known after apply' drift in TF state since field
// would never be set otherwise
toSet[consts.FieldCustomMetadata] = nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
}

pathFQ := path
if parent, ok := d.GetOk(consts.FieldNamespace); ok {
pathFQ = strings.Join([]string{parent.(string), path}, "/")
}
toSet[consts.FieldPathFQ] = pathFQ

if err := util.SetResourceData(d, toSet); err != nil {
return err
return diag.FromErr(err)
}

return nil
Expand Down
25 changes: 25 additions & 0 deletions vault/resource_namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ func TestAccNamespace(t *testing.T) {
resource.TestCheckResourceAttr(resourceNameParent, consts.FieldPath, namespacePath+"-foo"),
testNamespaceDestroy(namespacePath)),
},
{
SkipFunc: func() (bool, error) {
meta := testProvider.Meta().(*provider.ProviderMeta)
return !meta.IsAPISupported(provider.VaultVersion112), nil
},
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
Config: testNamespaceCustomMetadata(namespacePath + "-cm"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceNameParent, consts.FieldPath, namespacePath+"-cm"),
resource.TestCheckResourceAttr(resourceNameParent, "custom_metadata.%", "2"),
resource.TestCheckResourceAttr(resourceNameParent, "custom_metadata.foo", "abc"),
resource.TestCheckResourceAttr(resourceNameParent, "custom_metadata.bar", "123"),
testNamespaceDestroy(namespacePath)),
},
},
})
}
Expand Down Expand Up @@ -160,3 +173,15 @@ resource "vault_namespace" "child" {

return config
}

func testNamespaceCustomMetadata(path string) string {
return fmt.Sprintf(`
resource "vault_namespace" "parent" {
path = %q
custom_metadata = {
foo = "abc",
bar = "123"
}
}
`, path)
}
3 changes: 3 additions & 0 deletions website/docs/r/namespace.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ The following arguments are supported:

* `path` - (Required) The path of the namespace. Must not have a trailing `/`.

* `custom_metadata` - (Optional) A map of arbitrary string to string valued user-provided metadata meant
vinay-gopalan marked this conversation as resolved.
Show resolved Hide resolved
to describe the namespace. Requires Vault version 1.12+.

## Attributes Reference

In addition to the above arguments, the following attributes are exported:
Expand Down