diff --git a/CHANGELOG.md b/CHANGELOG.md index 231e1e3bc..fcf5216b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## Unreleased +FEATURES: +* Add support for `custom_metadata` on `vault_namespace`: ([#2033](https://github.com/hashicorp/terraform-provider-vault/pull/2033)) + BUGS: * Fix panic when readnig client_secret from a public oidc client ([#2048](https://github.com/hashicorp/terraform-provider-vault/pull/2048)) * Fix API request missing `roles` field for `mongodbatlas_secret_role` resource ([#2047](https://github.com/hashicorp/terraform-provider-vault/pull/2047)) + IMPROVEMENTS: * Updated dependencies: ([#2038](https://github.com/hashicorp/terraform-provider-vault/pull/2038)) * `github.com/aws/aws-sdk-go` v1.44.106 -> v1.45.24 diff --git a/vault/resource_namespace.go b/vault/resource_namespace.go index 4b4cb19bc..f46fb1498 100644 --- a/vault/resource_namespace.go +++ b/vault/resource_namespace.go @@ -4,6 +4,7 @@ package vault import ( + "context" "fmt" "log" "net/http" @@ -11,6 +12,7 @@ import ( "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" @@ -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, }, @@ -48,31 +50,79 @@ func namespaceResource() *schema.Resource { Optional: true, Description: "The fully qualified namespace path.", }, + consts.FieldCustomMetadata: { + Type: schema.TypeMap, + Computed: true, + Optional: true, + Description: "Custom metadata describing this namespace. Value type " + + "is map[string]string.", + }, }, } } -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 e + 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, nil) + _, err := client.Logical().Write(consts.SysNamespaceRoot+path, data) 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 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) + if _, err := client.Logical().JSONMergePatch(ctx, consts.SysNamespaceRoot+path, data); err != nil { + return diag.Errorf("error writing to Vault: %s", err) + } + + return namespaceRead(ctx, d, meta) +} + +func namespaceDelete(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) @@ -99,11 +149,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) } @@ -115,13 +165,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) @@ -130,7 +180,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 { @@ -142,8 +192,16 @@ 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), + // 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 + consts.FieldCustomMetadata: nil, + } + + if provider.IsAPISupported(meta, provider.VaultVersion112) { + toSet[consts.FieldCustomMetadata] = resp.Data[consts.FieldCustomMetadata] } pathFQ := path @@ -153,7 +211,7 @@ func namespaceRead(d *schema.ResourceData, meta interface{}) error { toSet[consts.FieldPathFQ] = pathFQ if err := util.SetResourceData(d, toSet); err != nil { - return err + return diag.FromErr(err) } return nil diff --git a/vault/resource_namespace_test.go b/vault/resource_namespace_test.go index 7f14e52af..298eff5d1 100644 --- a/vault/resource_namespace_test.go +++ b/vault/resource_namespace_test.go @@ -93,6 +93,18 @@ func TestAccNamespace(t *testing.T) { resource.TestCheckResourceAttr(resourceNameParent, consts.FieldPath, namespacePath+"-foo"), testNamespaceDestroy(namespacePath)), }, + { + SkipFunc: func() (bool, error) { + return !testProvider.Meta().(*provider.ProviderMeta).IsAPISupported(provider.VaultVersion112), nil + }, + 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)), + }, }, }) } @@ -160,3 +172,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) +} diff --git a/website/docs/r/namespace.html.md b/website/docs/r/namespace.html.md index 2e1b03c71..d84c0805e 100644 --- a/website/docs/r/namespace.html.md +++ b/website/docs/r/namespace.html.md @@ -79,6 +79,9 @@ The following arguments are supported: * `path` - (Required) The path of the namespace. Must not have a trailing `/`. +* `custom_metadata` - (Optional) Custom metadata describing this namespace. Value type + is `map[string]string`. Requires Vault version 1.12+. + ## Attributes Reference In addition to the above arguments, the following attributes are exported: