From b37f333e3278c601a0ab19fa7936307cc66a9e7a Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 15 Nov 2023 12:07:04 -0800 Subject: [PATCH 01/24] add aws destination resource --- vault/provider.go | 4 + .../resource_aws_secrets_sync_destination.go | 141 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 vault/resource_aws_secrets_sync_destination.go diff --git a/vault/provider.go b/vault/provider.go index 31843dda9..c53bf90c0 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -729,6 +729,10 @@ var ( Resource: UpdateSchemaResource(samlAuthBackendRoleResource()), PathInventory: []string{"/auth/saml/role/{name}"}, }, + "vault_aws_secrets_sync_destination": { + Resource: UpdateSchemaResource(awsSecretsSyncDestinationResource()), + PathInventory: []string{"/sys/sync/destinations/aws-sm/{name}"}, + }, } ) diff --git a/vault/resource_aws_secrets_sync_destination.go b/vault/resource_aws_secrets_sync_destination.go new file mode 100644 index 000000000..ac22917e3 --- /dev/null +++ b/vault/resource_aws_secrets_sync_destination.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +const ( + fieldAccessKeyID = "access_key_id" + fieldSecretAccessKey = "secret_access_key" +) + +var awsSyncDestinationFields = []string{ + fieldAccessKeyID, + fieldSecretAccessKey, + consts.FieldAWSRegion, +} + +func awsSecretsSyncDestinationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(awsSecretsSyncDestinationWrite, provider.VaultVersion115), + ReadContext: provider.ReadContextWrapper(awsSecretsSyncDestinationRead), + DeleteContext: awsSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the AWS destination.", + ForceNew: true, + }, + fieldAccessKeyID: { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Access key id to authenticate against the AWS secrets manager.", + ForceNew: true, + }, + fieldSecretAccessKey: { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Secret access key to authenticate against the AWS secrets " + + "manager.", + ForceNew: true, + }, + consts.FieldRegion: { + Type: schema.TypeString, + Optional: true, + Description: "Region where to manage the secrets manager entries.", + ForceNew: true, + }, + }, + } +} + +func awsSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + path := awsSecretsSyncDestinationPath(name) + + data := map[string]interface{}{} + + for _, k := range awsSyncDestinationFields { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Writing AWS sync destination to %q", path) + _, err := client.Logical().Write(path, data) + if err != nil { + return diag.Errorf("error enabling AWS sync destination %q: %s", path, err) + } + log.Printf("[DEBUG] Enabled AWS sync destination %q", path) + + d.SetId(name) + + return awsSecretsSyncDestinationRead(ctx, d, meta) +} + +func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + name := d.Id() + path := awsSecretsSyncDestinationPath(name) + + log.Printf("[DEBUG] Reading AWS sync destination") + resp, err := client.Logical().Read(path) + if err != nil { + return diag.Errorf("error reading AWS sync destination from %q: %s", path, err) + } + log.Printf("[DEBUG] Read AWS sync destination") + + if resp == nil { + log.Printf("[WARN] No info found at %q; removing from state.", path) + d.SetId("") + return nil + } + + if err := d.Set(consts.FieldName, name); err != nil { + return diag.FromErr(err) + } + + for _, k := range awsSyncDestinationFields { + if v, ok := resp.Data[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + + // set sensitive fields that will not be returned from Vault + + return nil +} + +func awsSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // sync destinations can not be deleted + return nil +} + +func awsSecretsSyncDestinationPath(name string) string { + return "sys/sync/destinations/aws-sm/" + name +} From 59dfbde2b5a0b6bff5b364cb2696c30c6ab28072 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Fri, 17 Nov 2023 10:48:12 -0800 Subject: [PATCH 02/24] complete basic secrets sync loop with AWS --- vault/provider.go | 4 + vault/resource_secrets_sync_association.go | 177 +++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 vault/resource_secrets_sync_association.go diff --git a/vault/provider.go b/vault/provider.go index c53bf90c0..fc48c127d 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -733,6 +733,10 @@ var ( Resource: UpdateSchemaResource(awsSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/aws-sm/{name}"}, }, + "vault_secrets_sync_association": { + Resource: UpdateSchemaResource(secretsSyncAssociationResource()), + PathInventory: []string{"/sys/sync/destinations/{type}/{name}/associations/set"}, + }, } ) diff --git a/vault/resource_secrets_sync_association.go b/vault/resource_secrets_sync_association.go new file mode 100644 index 000000000..f3739bbb8 --- /dev/null +++ b/vault/resource_secrets_sync_association.go @@ -0,0 +1,177 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +const ( + fieldSecretName = "secret_name" + fieldSyncStatus = "sync_status" + fieldAccessor = "accessor" + fieldUpdatedAt = "updated_at" +) + +var secretsSyncAssociationFields = []string{ + fieldSecretName, + fieldSyncStatus, + fieldAccessor, + fieldUpdatedAt, +} + +func secretsSyncAssociationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(secretsSyncAssociationWrite, provider.VaultVersion115), + ReadContext: provider.ReadContextWrapper(secretsSyncAssociationRead), + DeleteContext: secretsSyncAssociationDelete, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Name of the store/destination.", + ForceNew: true, + }, + consts.FieldType: { + Type: schema.TypeString, + Required: true, + Description: "Type of secrets store.", + ForceNew: true, + }, + consts.FieldMount: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Specifies the mount where the secret is located.", + }, + fieldAccessor: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Specifies the unique accessor of the mount.", + ValidateFunc: provider.ValidateNoLeadingTrailingSlashes, + }, + fieldSecretName: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Specifies the name of the secret to synchronize.", + }, + fieldSyncStatus: { + Type: schema.TypeString, + Computed: true, + Description: "Specifies the status of the association.", + }, + fieldUpdatedAt: { + Type: schema.TypeString, + Computed: true, + Description: "Duration string stating when the secret was last updated.", + }, + }, + } +} + +func secretsSyncAssociationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + destType := d.Get(consts.FieldType).(string) + path := secretsSyncAssociationSetPath(name, destType) + + data := map[string]interface{}{} + + for _, k := range []string{ + fieldSecretName, + consts.FieldMount, + } { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Writing association to %q", path) + resp, err := client.Logical().Write(path, data) + if err != nil { + return diag.Errorf("error setting secrets sync association %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote association to %q", path) + + // expect accessor to be provided from mount + accessor := d.Get(fieldAccessor).(string) + + // associations can't be read from Vault + // set data that is received from Vault upon writes + vaultRespKey := fmt.Sprintf("%s/%s", accessor, name) + associatedSecrets := "associated_secrets" + for _, k := range secretsSyncAssociationFields { + if secrets, ok := resp.Data[associatedSecrets]; ok { + if secretsMap, ok := secrets.(map[string]interface{}); ok { + if val, ok := secretsMap[vaultRespKey]; ok { + if valMap, ok := val.(map[string]interface{}); ok { + if v, ok := valMap[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.FromErr(err) + } + } + } + } + } + } + } + + d.SetId(path) + + return nil +} + +func secretsSyncAssociationRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil +} + +func secretsSyncAssociationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + destType := d.Get(consts.FieldType).(string) + path := secretsSyncAssociationDeletePath(name, destType) + + data := map[string]interface{}{} + + for _, k := range []string{ + fieldSecretName, + consts.FieldMount, + } { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Removing association from %q", path) + _, err := client.Logical().Write(path, data) + if err != nil { + return diag.Errorf("error removing secrets sync association %q: %s", path, err) + } + log.Printf("[DEBUG] Removed association from %q", path) + + return nil +} + +func secretsSyncAssociationSetPath(name, destType string) string { + return fmt.Sprintf("sys/sync/destinations/%s/%s/associations/set", destType, name) +} + +func secretsSyncAssociationDeletePath(name, destType string) string { + return fmt.Sprintf("sys/sync/destinations/%s/%s/associations/set", destType, name) +} From ed3be1629f46b3e84c1761cd0e21c5cc84207506 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Mon, 20 Nov 2023 21:51:01 -0800 Subject: [PATCH 03/24] add aws destination test --- .../resource_aws_secrets_sync_destination.go | 19 ++++++- ...ource_aws_secrets_sync_destination_test.go | 57 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 vault/resource_aws_secrets_sync_destination_test.go diff --git a/vault/resource_aws_secrets_sync_destination.go b/vault/resource_aws_secrets_sync_destination.go index ac22917e3..838ae7a8c 100644 --- a/vault/resource_aws_secrets_sync_destination.go +++ b/vault/resource_aws_secrets_sync_destination.go @@ -82,7 +82,7 @@ func awsSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, } log.Printf("[DEBUG] Writing AWS sync destination to %q", path) - _, err := client.Logical().Write(path, data) + _, err := client.Logical().WriteWithContext(ctx, path, data) if err != nil { return diag.Errorf("error enabling AWS sync destination %q: %s", path, err) } @@ -102,7 +102,7 @@ func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, path := awsSecretsSyncDestinationPath(name) log.Printf("[DEBUG] Reading AWS sync destination") - resp, err := client.Logical().Read(path) + resp, err := client.Logical().ReadWithContext(ctx, path) if err != nil { return diag.Errorf("error reading AWS sync destination from %q: %s", path, err) } @@ -132,7 +132,20 @@ func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, } func awsSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // sync destinations can not be deleted + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := awsSecretsSyncDestinationPath(d.Id()) + + log.Printf("[DEBUG] Deleting AWS sync destination at %q", path) + _, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.Errorf("error deleting AWS sync destination at %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted AWS sync destination at %q", path) + return nil } diff --git a/vault/resource_aws_secrets_sync_destination_test.go b/vault/resource_aws_secrets_sync_destination_test.go new file mode 100644 index 000000000..a86ee979c --- /dev/null +++ b/vault/resource_aws_secrets_sync_destination_test.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestAWSSecretsSyncDestination(t *testing.T) { + destName := acctest.RandomWithPrefix("tf-sync-dest") + + resourceName := "vault_aws_secrets_sync_destination.test" + + accessKey, secretKey := testutil.GetTestAWSCreds(t) + region := testutil.GetTestAWSRegion(t) + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + }, PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, fieldAccessKeyID, accessKey), + resource.TestCheckResourceAttr(resourceName, fieldSecretAccessKey, secretKey), + resource.TestCheckResourceAttr(resourceName, consts.FieldRegion, region), + ), + }, + }, + }) +} + +func testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName string) string { + ret := fmt.Sprintf(` +resource "vault_aws_secrets_sync_destination" "test" { + name = "%s" + access_key_id = "%s" + secret_access_key = "%s" + region = "%s" +} +`, destName, accessKey, secretKey, region) + + return ret +} From 22883853aa90aee7cdfaa6daf71b3b10852afc7b Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Tue, 21 Nov 2023 15:37:26 -0800 Subject: [PATCH 04/24] add gh sync destination and test --- vault/provider.go | 4 + vault/resource_gh_secrets_sync_destination.go | 155 ++++++++++++++++++ ...source_gh_secrets_sync_destination_test.go | 65 ++++++++ vault/resource_secrets_sync_association.go | 2 +- 4 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 vault/resource_gh_secrets_sync_destination.go create mode 100644 vault/resource_gh_secrets_sync_destination_test.go diff --git a/vault/provider.go b/vault/provider.go index fc48c127d..ca6b5e4e4 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -733,6 +733,10 @@ var ( Resource: UpdateSchemaResource(awsSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/aws-sm/{name}"}, }, + "vault_gh_secrets_sync_destination": { + Resource: UpdateSchemaResource(githubSecretsSyncDestinationResource()), + PathInventory: []string{"/sys/sync/destinations/gh/{name}"}, + }, "vault_secrets_sync_association": { Resource: UpdateSchemaResource(secretsSyncAssociationResource()), PathInventory: []string{"/sys/sync/destinations/{type}/{name}/associations/set"}, diff --git a/vault/resource_gh_secrets_sync_destination.go b/vault/resource_gh_secrets_sync_destination.go new file mode 100644 index 000000000..09645fcdb --- /dev/null +++ b/vault/resource_gh_secrets_sync_destination.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +const ( + fieldAccessToken = "access_token" + fieldRepositoryOwner = "repository_owner" + fieldRepositoryName = "repository_name" +) + +var githubSyncDestinationFields = []string{ + fieldAccessToken, + fieldRepositoryOwner, + fieldRepositoryName, +} + +func githubSecretsSyncDestinationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(githubSecretsSyncDestinationWrite, provider.VaultVersion115), + ReadContext: provider.ReadContextWrapper(githubSecretsSyncDestinationRead), + DeleteContext: githubSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the github destination.", + ForceNew: true, + }, + fieldAccessToken: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Fine-grained or personal access token.", + ForceNew: true, + }, + fieldRepositoryOwner: { + Type: schema.TypeString, + Required: true, + // @TODO confirm if this is sensitive + // Sensitive: true, + Description: "GitHub organization or username that owns the repository.", + ForceNew: true, + }, + fieldRepositoryName: { + Type: schema.TypeString, + Required: true, + Description: "Name of the repository.", + ForceNew: true, + }, + }, + } +} + +func githubSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + path := githubSecretsSyncDestinationPath(name) + + data := map[string]interface{}{} + + for _, k := range githubSyncDestinationFields { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Writing Github sync destination to %q", path) + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error enabling Github sync destination %q: %s", path, err) + } + log.Printf("[DEBUG] Enabled Github sync destination %q", path) + + d.SetId(name) + + return githubSecretsSyncDestinationRead(ctx, d, meta) +} + +func githubSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + name := d.Id() + path := githubSecretsSyncDestinationPath(name) + + log.Printf("[DEBUG] Reading Github sync destination") + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading Github sync destination from %q: %s", path, err) + } + log.Printf("[DEBUG] Read Github sync destination") + + if resp == nil { + log.Printf("[WARN] No info found at %q; removing from state.", path) + d.SetId("") + return nil + } + + if err := d.Set(consts.FieldName, name); err != nil { + return diag.FromErr(err) + } + + for _, k := range githubSyncDestinationFields { + if v, ok := resp.Data[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + + // set sensitive fields that will not be returned from Vault + + return nil +} + +func githubSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := githubSecretsSyncDestinationPath(d.Id()) + + log.Printf("[DEBUG] Deleting Github sync destination at %q", path) + _, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.Errorf("error deleting Github sync destination at %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted Github sync destination at %q", path) + + return nil +} + +func githubSecretsSyncDestinationPath(name string) string { + return "sys/sync/destinations/gh/" + name +} diff --git a/vault/resource_gh_secrets_sync_destination_test.go b/vault/resource_gh_secrets_sync_destination_test.go new file mode 100644 index 000000000..e0a11cbe8 --- /dev/null +++ b/vault/resource_gh_secrets_sync_destination_test.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestGithubSecretsSyncDestination(t *testing.T) { + destName := acctest.RandomWithPrefix("tf-sync-dest") + + resourceName := "vault_aws_secrets_sync_destination.test" + + values := testutil.SkipTestEnvUnset(t, + "GITHUB_ACCESS_TOKEN", + "GITHUB_REPO_OWNER", + "GITHUB_REPO_NAME", + ) + + accessToken := values[0] + repoOwner := values[1] + repoName := values[2] + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + }, PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testGithubSecretsSyncDestinationConfig(accessToken, repoOwner, repoName, destName), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, fieldAccessToken, accessToken), + resource.TestCheckResourceAttr(resourceName, fieldRepositoryOwner, repoOwner), + resource.TestCheckResourceAttr(resourceName, fieldRepositoryName, repoName), + ), + }, + }, + }) +} + +func testGithubSecretsSyncDestinationConfig(accessToken, repoOwner, repoName, destName string) string { + ret := fmt.Sprintf(` +resource "vault_github_secrets_sync_destination" "test" { + name = "%s" + access_token = "%s" + repository_owner = "%s" + repository_name = "%s" +} +`, destName, accessToken, repoOwner, repoName) + + return ret +} diff --git a/vault/resource_secrets_sync_association.go b/vault/resource_secrets_sync_association.go index f3739bbb8..0a540cf25 100644 --- a/vault/resource_secrets_sync_association.go +++ b/vault/resource_secrets_sync_association.go @@ -173,5 +173,5 @@ func secretsSyncAssociationSetPath(name, destType string) string { } func secretsSyncAssociationDeletePath(name, destType string) string { - return fmt.Sprintf("sys/sync/destinations/%s/%s/associations/set", destType, name) + return fmt.Sprintf("sys/sync/destinations/%s/%s/associations/remove", destType, name) } From 609df9e01d6b7d1bafc8210c22fb3f9d5e2435e3 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 22 Nov 2023 18:23:56 -0800 Subject: [PATCH 05/24] add gcp secrets sync destination --- vault/provider.go | 4 + .../resource_gcp_secrets_sync_destination.go | 127 ++++++++++++++++++ ...ource_gcp_secrets_sync_destination_test.go | 56 ++++++++ 3 files changed, 187 insertions(+) create mode 100644 vault/resource_gcp_secrets_sync_destination.go create mode 100644 vault/resource_gcp_secrets_sync_destination_test.go diff --git a/vault/provider.go b/vault/provider.go index ca6b5e4e4..8e777a716 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -737,6 +737,10 @@ var ( Resource: UpdateSchemaResource(githubSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/gh/{name}"}, }, + "vault_gcp_secrets_sync_destination": { + Resource: UpdateSchemaResource(gcpSecretsSyncDestinationResource()), + PathInventory: []string{"/sys/sync/destinations/gcp-sm/{name}"}, + }, "vault_secrets_sync_association": { Resource: UpdateSchemaResource(secretsSyncAssociationResource()), PathInventory: []string{"/sys/sync/destinations/{type}/{name}/associations/set"}, diff --git a/vault/resource_gcp_secrets_sync_destination.go b/vault/resource_gcp_secrets_sync_destination.go new file mode 100644 index 000000000..6289b6f14 --- /dev/null +++ b/vault/resource_gcp_secrets_sync_destination.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +var gcpSyncDestinationFields = []string{ + consts.FieldCredentials, +} + +func gcpSecretsSyncDestinationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(gcpSecretsSyncDestinationWrite, provider.VaultVersion115), + ReadContext: provider.ReadContextWrapper(gcpSecretsSyncDestinationRead), + DeleteContext: gcpSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the GCP destination.", + ForceNew: true, + }, + consts.FieldCredentials: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "JSON credentials (either file contents or '@path/to/file').", + ForceNew: true, + }, + }, + } +} + +func gcpSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + path := gcpSecretsSyncDestinationPath(name) + + data := map[string]interface{}{} + + for _, k := range gcpSyncDestinationFields { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Writing GCP sync destination to %q", path) + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error enabling GCP sync destination %q: %s", path, err) + } + log.Printf("[DEBUG] Enabled GCP sync destination %q", path) + + d.SetId(name) + + return gcpSecretsSyncDestinationRead(ctx, d, meta) +} + +func gcpSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + name := d.Id() + path := gcpSecretsSyncDestinationPath(name) + + log.Printf("[DEBUG] Reading GCP sync destination") + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading GCP sync destination from %q: %s", path, err) + } + log.Printf("[DEBUG] Read GCP sync destination") + + if resp == nil { + log.Printf("[WARN] No info found at %q; removing from state.", path) + d.SetId("") + return nil + } + + if err := d.Set(consts.FieldName, name); err != nil { + return diag.FromErr(err) + } + + // set sensitive fields that will not be returned from Vault + if err := d.Set(consts.FieldCredentials, d.Get(consts.FieldCredentials).(string)); err != nil { + return diag.FromErr(err) + } + return nil +} + +func gcpSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := gcpSecretsSyncDestinationPath(d.Id()) + + log.Printf("[DEBUG] Deleting GCP sync destination at %q", path) + _, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.Errorf("error deleting GCP sync destination at %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted GCP sync destination at %q", path) + + return nil +} + +func gcpSecretsSyncDestinationPath(name string) string { + return "sys/sync/destinations/gcp-sm/" + name +} diff --git a/vault/resource_gcp_secrets_sync_destination_test.go b/vault/resource_gcp_secrets_sync_destination_test.go new file mode 100644 index 000000000..f6adcfbae --- /dev/null +++ b/vault/resource_gcp_secrets_sync_destination_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestGCPSecretsSyncDestination(t *testing.T) { + destName := acctest.RandomWithPrefix("tf-sync-dest") + + resourceName := "vault_gcp_secrets_sync_destination.test" + + values := testutil.SkipTestEnvUnset(t, + "GOOGLE_APPLICATION_CREDENTIALS", + ) + credentials := values[0] + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + }, PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testGCPSecretsSyncDestinationConfig(credentials, destName), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, consts.FieldCredentials, credentials), + ), + }, + }, + }) +} + +func testGCPSecretsSyncDestinationConfig(credentials, destName string) string { + ret := fmt.Sprintf(` +resource "vault_gcp_secrets_sync_destination" "test" { + name = "%s" + credentials = "%s" +} +`, destName, credentials) + + return ret +} From 116d4a7fac7d98a78c62454bb9ab55a6f0e7b857 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 22 Nov 2023 18:38:13 -0800 Subject: [PATCH 06/24] add azure kv destination --- vault/provider.go | 4 + ...resource_azure_secrets_sync_destination.go | 174 ++++++++++++++++++ ...rce_azure_secrets_sync_destination_test.go | 69 +++++++ 3 files changed, 247 insertions(+) create mode 100644 vault/resource_azure_secrets_sync_destination.go create mode 100644 vault/resource_azure_secrets_sync_destination_test.go diff --git a/vault/provider.go b/vault/provider.go index 8e777a716..c8d3872b2 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -741,6 +741,10 @@ var ( Resource: UpdateSchemaResource(gcpSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/gcp-sm/{name}"}, }, + "vault_azure_secrets_sync_destination": { + Resource: UpdateSchemaResource(azureSecretsSyncDestinationResource()), + PathInventory: []string{"/sys/sync/destinations/azure-kv/{name}"}, + }, "vault_secrets_sync_association": { Resource: UpdateSchemaResource(secretsSyncAssociationResource()), PathInventory: []string{"/sys/sync/destinations/{type}/{name}/associations/set"}, diff --git a/vault/resource_azure_secrets_sync_destination.go b/vault/resource_azure_secrets_sync_destination.go new file mode 100644 index 000000000..d14ea4033 --- /dev/null +++ b/vault/resource_azure_secrets_sync_destination.go @@ -0,0 +1,174 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +const ( + fieldKeyVaultURI = "key_vault_uri" + fieldCloud = "cloud" +) + +var azureSyncDestinationFields = []string{ + fieldKeyVaultURI, + fieldCloud, + consts.FieldClientID, + consts.FieldClientSecret, + consts.FieldTenantID, +} + +func azureSecretsSyncDestinationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(azureSecretsSyncDestinationWrite, provider.VaultVersion115), + ReadContext: provider.ReadContextWrapper(azureSecretsSyncDestinationRead), + DeleteContext: azureSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the Azure destination.", + ForceNew: true, + }, + fieldKeyVaultURI: { + Type: schema.TypeString, + Required: true, + Description: "URI of an existing Azure Key Vault instance.", + ForceNew: true, + }, + consts.FieldClientID: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Client ID of an Azure app registration.", + ForceNew: true, + }, + consts.FieldClientSecret: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Client Secret of an Azure app registration.", + ForceNew: true, + }, + consts.FieldTenantID: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "ID of the target Azure tenant.", + ForceNew: true, + }, + fieldCloud: { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a cloud for the client.", + ForceNew: true, + }, + }, + } +} + +func azureSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + path := azureSecretsSyncDestinationPath(name) + + data := map[string]interface{}{} + + for _, k := range azureSyncDestinationFields { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Writing Azure sync destination to %q", path) + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error enabling Azure sync destination %q: %s", path, err) + } + log.Printf("[DEBUG] Enabled Azure sync destination %q", path) + + d.SetId(name) + + return azureSecretsSyncDestinationRead(ctx, d, meta) +} + +func azureSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + name := d.Id() + path := azureSecretsSyncDestinationPath(name) + + log.Printf("[DEBUG] Reading Azure sync destination") + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading Azure sync destination from %q: %s", path, err) + } + log.Printf("[DEBUG] Read Azure sync destination") + + if resp == nil { + log.Printf("[WARN] No info found at %q; removing from state.", path) + d.SetId("") + return nil + } + + if err := d.Set(consts.FieldName, name); err != nil { + return diag.FromErr(err) + } + + for _, k := range azureSyncDestinationFields { + if v, ok := resp.Data[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + + // set sensitive fields that will not be returned from Vault + if err := d.Set(consts.FieldClientID, d.Get(consts.FieldClientID).(string)); err != nil { + return diag.FromErr(err) + } + + if err := d.Set(consts.FieldClientSecret, d.Get(consts.FieldClientSecret).(string)); err != nil { + return diag.FromErr(err) + } + return nil +} + +func azureSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := azureSecretsSyncDestinationPath(d.Id()) + + log.Printf("[DEBUG] Deleting Azure sync destination at %q", path) + _, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.Errorf("error deleting Azure sync destination at %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted Azure sync destination at %q", path) + + return nil +} + +func azureSecretsSyncDestinationPath(name string) string { + return "sys/sync/destinations/azure-kv/" + name +} diff --git a/vault/resource_azure_secrets_sync_destination_test.go b/vault/resource_azure_secrets_sync_destination_test.go new file mode 100644 index 000000000..d213c0101 --- /dev/null +++ b/vault/resource_azure_secrets_sync_destination_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestAzureSecretsSyncDestination(t *testing.T) { + destName := acctest.RandomWithPrefix("tf-sync-dest") + + resourceName := "vault_azure_secrets_sync_destination.test" + + values := testutil.SkipTestEnvUnset(t, + "AZURE_KEY_VAULT_URI", + "AZURE_CLIENT_ID", + "AZURE_CLIENT_SECRET", + "AZURE_TENANT_ID", + ) + keyVaultURI := values[0] + clientID := values[1] + clientSecret := values[2] + tenantID := values[3] + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + }, PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, consts.FieldClientSecret, clientSecret), + resource.TestCheckResourceAttr(resourceName, consts.FieldClientID, clientID), + resource.TestCheckResourceAttr(resourceName, consts.FieldTenantID, tenantID), + resource.TestCheckResourceAttr(resourceName, fieldKeyVaultURI, keyVaultURI), + resource.TestCheckResourceAttr(resourceName, fieldCloud, "cloud"), + ), + }, + }, + }) +} + +func testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName string) string { + ret := fmt.Sprintf(` +resource "vault_azure_secrets_sync_destination" "test" { + name = "%s" + key_vault_uri = "%s" + client_id = "%s" + client_secret = "%s" + tenant_id = "%s" +} +`, destName, keyVaultURI, clientID, clientSecret, tenantID) + + return ret +} From 8ac814953156fd751b0d951aa2e4961dd761dd73 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 20 Dec 2023 16:03:22 -0800 Subject: [PATCH 07/24] add vercel destination --- vault/provider.go | 4 + ...esource_vercel_secrets_sync_destination.go | 173 ++++++++++++++++++ ...ce_vercel_secrets_sync_destination_test.go | 67 +++++++ 3 files changed, 244 insertions(+) create mode 100644 vault/resource_vercel_secrets_sync_destination.go create mode 100644 vault/resource_vercel_secrets_sync_destination_test.go diff --git a/vault/provider.go b/vault/provider.go index c8d3872b2..f33caf8e5 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -745,6 +745,10 @@ var ( Resource: UpdateSchemaResource(azureSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/azure-kv/{name}"}, }, + "vault_vercel_secrets_sync_destination": { + Resource: UpdateSchemaResource(vercelSecretsSyncDestinationResource()), + PathInventory: []string{"/sys/sync/destinations/vercel-project/{name}"}, + }, "vault_secrets_sync_association": { Resource: UpdateSchemaResource(secretsSyncAssociationResource()), PathInventory: []string{"/sys/sync/destinations/{type}/{name}/associations/set"}, diff --git a/vault/resource_vercel_secrets_sync_destination.go b/vault/resource_vercel_secrets_sync_destination.go new file mode 100644 index 000000000..f3bf7cf65 --- /dev/null +++ b/vault/resource_vercel_secrets_sync_destination.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +const ( + fieldProjectID = "project_id" + fieldTeamID = "team_id" + fieldDeploymentEnvironments = "deployment_environments" +) + +var vercelSyncDestinationFields = []string{ + fieldAccessToken, + fieldProjectID, + fieldTeamID, + fieldDeploymentEnvironments, +} + +var vercelNonSensitiveFields = []string{ + fieldProjectID, + fieldTeamID, + fieldDeploymentEnvironments, +} + +func vercelSecretsSyncDestinationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(vercelSecretsSyncDestinationWrite, provider.VaultVersion115), + ReadContext: provider.ReadContextWrapper(vercelSecretsSyncDestinationRead), + DeleteContext: vercelSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the Vercel destination.", + ForceNew: true, + }, + fieldAccessToken: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Vercel API access token with the permissions to manage " + + "environment variables.", + ForceNew: true, + }, + fieldProjectID: { + Type: schema.TypeString, + Required: true, + Description: "Project ID where to manage environment variables.", + ForceNew: true, + }, + fieldTeamID: { + Type: schema.TypeString, + Optional: true, + Description: "Vercel API access token with the permissions to manage " + + "environment variables.", + ForceNew: true, + }, + fieldDeploymentEnvironments: { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "Deployment environments where the environment " + + "variables are available. Accepts 'development', " + + "'preview' & 'production'.", + ForceNew: true, + }, + }, + } +} + +func vercelSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + path := vercelSecretsSyncDestinationPath(name) + + data := map[string]interface{}{} + + for _, k := range vercelSyncDestinationFields { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Writing Vercel sync destination to %q", path) + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error enabling Vercel sync destination %q: %s", path, err) + } + log.Printf("[DEBUG] Enabled Vercel sync destination %q", path) + + d.SetId(name) + + return vercelSecretsSyncDestinationRead(ctx, d, meta) +} + +func vercelSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + name := d.Id() + path := vercelSecretsSyncDestinationPath(name) + + log.Printf("[DEBUG] Reading Vercel sync destination") + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading Vercel sync destination from %q: %s", path, err) + } + log.Printf("[DEBUG] Read Vercel sync destination") + + if resp == nil { + log.Printf("[WARN] No info found at %q; removing from state.", path) + d.SetId("") + return nil + } + + if err := d.Set(consts.FieldName, name); err != nil { + return diag.FromErr(err) + } + + for _, k := range vercelNonSensitiveFields { + if data, ok := resp.Data["connection_details"]; ok { + if m, ok := data.(map[string]interface{}); ok { + if v, ok := m[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + } + } + + return nil +} + +func vercelSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := vercelSecretsSyncDestinationPath(d.Id()) + + log.Printf("[DEBUG] Deleting Vercel sync destination at %q", path) + _, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.Errorf("error deleting Vercel sync destination at %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted Vercel sync destination at %q", path) + + return nil +} + +func vercelSecretsSyncDestinationPath(name string) string { + return "sys/sync/destinations/vercel-project/" + name +} diff --git a/vault/resource_vercel_secrets_sync_destination_test.go b/vault/resource_vercel_secrets_sync_destination_test.go new file mode 100644 index 000000000..c1b0f7457 --- /dev/null +++ b/vault/resource_vercel_secrets_sync_destination_test.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestVercelSecretsSyncDestination(t *testing.T) { + destName := acctest.RandomWithPrefix("tf-sync-dest-vercel") + + resourceName := "vault_vercel_secrets_sync_destination.test" + + values := testutil.SkipTestEnvUnset(t, + "VERCEL_ACCESS_TOKEN", + "VERCEL_PROJECT_ID", + ) + accessToken := values[0] + projectID := values[1] + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + }, PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testVercelSecretsSyncDestinationConfig(accessToken, projectID, destName), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, fieldAccessToken, accessToken), + resource.TestCheckResourceAttr(resourceName, fieldProjectID, projectID), + resource.TestCheckResourceAttr(resourceName, "deployment_environments.#", "3"), + resource.TestCheckResourceAttr(resourceName, "deployment_environments.0", "development"), + resource.TestCheckResourceAttr(resourceName, "deployment_environments.1", "preview"), + resource.TestCheckResourceAttr(resourceName, "deployment_environments.2", "production"), + ), + }, + testutil.GetImportTestStep(resourceName, false, nil, + fieldAccessToken, + ), + }, + }) +} + +func testVercelSecretsSyncDestinationConfig(accessToken, projectID, destName string) string { + ret := fmt.Sprintf(` +resource "vault_vercel_secrets_sync_destination" "test" { + name = "%s" + access_token = "%s" + project_id = "%s" + deployment_environments = ["development", "preview", "production"] +} +`, destName, accessToken, projectID) + + return ret +} From 1016729f4136cc8dfa07a0e43b7550c62ad9f3b8 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 20 Dec 2023 16:16:33 -0800 Subject: [PATCH 08/24] make updates to read methods --- .../resource_aws_secrets_sync_destination.go | 21 +++++++++++++--- ...ource_aws_secrets_sync_destination_test.go | 8 +++++- ...resource_azure_secrets_sync_destination.go | 8 ------ ...rce_azure_secrets_sync_destination_test.go | 2 +- .../resource_gcp_secrets_sync_destination.go | 4 --- ...ource_gcp_secrets_sync_destination_test.go | 4 ++- vault/resource_gh_secrets_sync_destination.go | 25 +++++++++++-------- ...source_gh_secrets_sync_destination_test.go | 9 ++++--- ...esource_vercel_secrets_sync_destination.go | 2 +- 9 files changed, 51 insertions(+), 32 deletions(-) diff --git a/vault/resource_aws_secrets_sync_destination.go b/vault/resource_aws_secrets_sync_destination.go index 838ae7a8c..4019d9f29 100644 --- a/vault/resource_aws_secrets_sync_destination.go +++ b/vault/resource_aws_secrets_sync_destination.go @@ -17,6 +17,8 @@ import ( const ( fieldAccessKeyID = "access_key_id" fieldSecretAccessKey = "secret_access_key" + + vaultFieldConnectionDetails = "connection_details" ) var awsSyncDestinationFields = []string{ @@ -25,6 +27,10 @@ var awsSyncDestinationFields = []string{ consts.FieldAWSRegion, } +var awsSyncNonSensitiveFields = []string{ + consts.FieldAWSRegion, +} + func awsSecretsSyncDestinationResource() *schema.Resource { return &schema.Resource{ CreateContext: provider.MountCreateContextWrapper(awsSecretsSyncDestinationWrite, provider.VaultVersion115), @@ -44,7 +50,6 @@ func awsSecretsSyncDestinationResource() *schema.Resource { fieldAccessKeyID: { Type: schema.TypeString, Optional: true, - Sensitive: true, Description: "Access key id to authenticate against the AWS secrets manager.", ForceNew: true, }, @@ -118,6 +123,18 @@ func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, return diag.FromErr(err) } + for _, k := range awsSyncNonSensitiveFields { + if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { + if m, ok := data.(map[string]interface{}); ok { + if v, ok := m[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + } + } + for _, k := range awsSyncDestinationFields { if v, ok := resp.Data[k]; ok { if err := d.Set(k, v); err != nil { @@ -126,8 +143,6 @@ func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, } } - // set sensitive fields that will not be returned from Vault - return nil } diff --git a/vault/resource_aws_secrets_sync_destination_test.go b/vault/resource_aws_secrets_sync_destination_test.go index a86ee979c..168b41f2c 100644 --- a/vault/resource_aws_secrets_sync_destination_test.go +++ b/vault/resource_aws_secrets_sync_destination_test.go @@ -16,7 +16,7 @@ import ( ) func TestAWSSecretsSyncDestination(t *testing.T) { - destName := acctest.RandomWithPrefix("tf-sync-dest") + destName := acctest.RandomWithPrefix("tf-sync-dest-aws") resourceName := "vault_aws_secrets_sync_destination.test" @@ -39,6 +39,12 @@ func TestAWSSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, consts.FieldRegion, region), ), }, + testutil.GetImportTestStep(resourceName, false, nil, + fieldAccessKeyID, + fieldSecretAccessKey, + // TODO confirm if region will not be returned in response + consts.FieldRegion, + ), }, }) } diff --git a/vault/resource_azure_secrets_sync_destination.go b/vault/resource_azure_secrets_sync_destination.go index d14ea4033..be5627742 100644 --- a/vault/resource_azure_secrets_sync_destination.go +++ b/vault/resource_azure_secrets_sync_destination.go @@ -140,14 +140,6 @@ func azureSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData } } - // set sensitive fields that will not be returned from Vault - if err := d.Set(consts.FieldClientID, d.Get(consts.FieldClientID).(string)); err != nil { - return diag.FromErr(err) - } - - if err := d.Set(consts.FieldClientSecret, d.Get(consts.FieldClientSecret).(string)); err != nil { - return diag.FromErr(err) - } return nil } diff --git a/vault/resource_azure_secrets_sync_destination_test.go b/vault/resource_azure_secrets_sync_destination_test.go index d213c0101..767c88ed4 100644 --- a/vault/resource_azure_secrets_sync_destination_test.go +++ b/vault/resource_azure_secrets_sync_destination_test.go @@ -16,7 +16,7 @@ import ( ) func TestAzureSecretsSyncDestination(t *testing.T) { - destName := acctest.RandomWithPrefix("tf-sync-dest") + destName := acctest.RandomWithPrefix("tf-sync-dest-azure") resourceName := "vault_azure_secrets_sync_destination.test" diff --git a/vault/resource_gcp_secrets_sync_destination.go b/vault/resource_gcp_secrets_sync_destination.go index 6289b6f14..316bfa660 100644 --- a/vault/resource_gcp_secrets_sync_destination.go +++ b/vault/resource_gcp_secrets_sync_destination.go @@ -97,10 +97,6 @@ func gcpSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, return diag.FromErr(err) } - // set sensitive fields that will not be returned from Vault - if err := d.Set(consts.FieldCredentials, d.Get(consts.FieldCredentials).(string)); err != nil { - return diag.FromErr(err) - } return nil } diff --git a/vault/resource_gcp_secrets_sync_destination_test.go b/vault/resource_gcp_secrets_sync_destination_test.go index f6adcfbae..78d6f207d 100644 --- a/vault/resource_gcp_secrets_sync_destination_test.go +++ b/vault/resource_gcp_secrets_sync_destination_test.go @@ -16,7 +16,7 @@ import ( ) func TestGCPSecretsSyncDestination(t *testing.T) { - destName := acctest.RandomWithPrefix("tf-sync-dest") + destName := acctest.RandomWithPrefix("tf-sync-dest-gcp") resourceName := "vault_gcp_secrets_sync_destination.test" @@ -40,6 +40,8 @@ func TestGCPSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, consts.FieldCredentials, credentials), ), }, + testutil.GetImportTestStep(resourceName, false, nil, + consts.FieldCredentials), }, }) } diff --git a/vault/resource_gh_secrets_sync_destination.go b/vault/resource_gh_secrets_sync_destination.go index 09645fcdb..d1ff64383 100644 --- a/vault/resource_gh_secrets_sync_destination.go +++ b/vault/resource_gh_secrets_sync_destination.go @@ -26,6 +26,11 @@ var githubSyncDestinationFields = []string{ fieldRepositoryName, } +var githubNonSensitiveFields = []string{ + fieldRepositoryOwner, + fieldRepositoryName, +} + func githubSecretsSyncDestinationResource() *schema.Resource { return &schema.Resource{ CreateContext: provider.MountCreateContextWrapper(githubSecretsSyncDestinationWrite, provider.VaultVersion115), @@ -50,10 +55,8 @@ func githubSecretsSyncDestinationResource() *schema.Resource { ForceNew: true, }, fieldRepositoryOwner: { - Type: schema.TypeString, - Required: true, - // @TODO confirm if this is sensitive - // Sensitive: true, + Type: schema.TypeString, + Required: true, Description: "GitHub organization or username that owns the repository.", ForceNew: true, }, @@ -119,16 +122,18 @@ func githubSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceDat return diag.FromErr(err) } - for _, k := range githubSyncDestinationFields { - if v, ok := resp.Data[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.Errorf("error setting state key %q: err=%s", k, err) + for _, k := range githubNonSensitiveFields { + if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { + if m, ok := data.(map[string]interface{}); ok { + if v, ok := m[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } } } } - // set sensitive fields that will not be returned from Vault - return nil } diff --git a/vault/resource_gh_secrets_sync_destination_test.go b/vault/resource_gh_secrets_sync_destination_test.go index e0a11cbe8..eda601b70 100644 --- a/vault/resource_gh_secrets_sync_destination_test.go +++ b/vault/resource_gh_secrets_sync_destination_test.go @@ -16,9 +16,9 @@ import ( ) func TestGithubSecretsSyncDestination(t *testing.T) { - destName := acctest.RandomWithPrefix("tf-sync-dest") + destName := acctest.RandomWithPrefix("tf-sync-dest-gh") - resourceName := "vault_aws_secrets_sync_destination.test" + resourceName := "vault_gh_secrets_sync_destination.test" values := testutil.SkipTestEnvUnset(t, "GITHUB_ACCESS_TOKEN", @@ -47,13 +47,16 @@ func TestGithubSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, fieldRepositoryName, repoName), ), }, + testutil.GetImportTestStep(resourceName, false, nil, + fieldAccessToken, + ), }, }) } func testGithubSecretsSyncDestinationConfig(accessToken, repoOwner, repoName, destName string) string { ret := fmt.Sprintf(` -resource "vault_github_secrets_sync_destination" "test" { +resource "vault_gh_secrets_sync_destination" "test" { name = "%s" access_token = "%s" repository_owner = "%s" diff --git a/vault/resource_vercel_secrets_sync_destination.go b/vault/resource_vercel_secrets_sync_destination.go index f3bf7cf65..9761b059e 100644 --- a/vault/resource_vercel_secrets_sync_destination.go +++ b/vault/resource_vercel_secrets_sync_destination.go @@ -136,7 +136,7 @@ func vercelSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceDat } for _, k := range vercelNonSensitiveFields { - if data, ok := resp.Data["connection_details"]; ok { + if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { if m, ok := data.(map[string]interface{}); ok { if v, ok := m[k]; ok { if err := d.Set(k, v); err != nil { From 049303d1a7ba80c9020418f85ce91f1dd4db8188 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 20 Dec 2023 16:39:59 -0800 Subject: [PATCH 09/24] break early in secrets sync association --- vault/resource_secrets_sync_association.go | 23 +++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/vault/resource_secrets_sync_association.go b/vault/resource_secrets_sync_association.go index 0a540cf25..aa5913671 100644 --- a/vault/resource_secrets_sync_association.go +++ b/vault/resource_secrets_sync_association.go @@ -101,7 +101,7 @@ func secretsSyncAssociationWrite(ctx context.Context, d *schema.ResourceData, me } log.Printf("[DEBUG] Writing association to %q", path) - resp, err := client.Logical().Write(path, data) + resp, err := client.Logical().WriteWithContext(ctx, path, data) if err != nil { return diag.Errorf("error setting secrets sync association %q: %s", path, err) } @@ -114,15 +114,20 @@ func secretsSyncAssociationWrite(ctx context.Context, d *schema.ResourceData, me // set data that is received from Vault upon writes vaultRespKey := fmt.Sprintf("%s/%s", accessor, name) associatedSecrets := "associated_secrets" + secretData, ok := resp.Data[associatedSecrets] + if !ok { + // did not find any associated secrets for this mount + // we expect to see data here since we just wrote an association + return diag.Errorf("expected associated secrets; received no secret associations in mount") + } + for _, k := range secretsSyncAssociationFields { - if secrets, ok := resp.Data[associatedSecrets]; ok { - if secretsMap, ok := secrets.(map[string]interface{}); ok { - if val, ok := secretsMap[vaultRespKey]; ok { - if valMap, ok := val.(map[string]interface{}); ok { - if v, ok := valMap[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.FromErr(err) - } + if secretsMap, ok := secretData.(map[string]interface{}); ok { + if val, ok := secretsMap[vaultRespKey]; ok { + if valMap, ok := val.(map[string]interface{}); ok { + if v, ok := valMap[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.FromErr(err) } } } From 5a7f502e9f20f098f834d84fcca5613963af9edf Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 20 Dec 2023 16:49:48 -0800 Subject: [PATCH 10/24] add import step for Azure destination --- .../resource_azure_secrets_sync_destination.go | 17 +++++++++++++---- ...ource_azure_secrets_sync_destination_test.go | 5 +++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/vault/resource_azure_secrets_sync_destination.go b/vault/resource_azure_secrets_sync_destination.go index be5627742..543719bf9 100644 --- a/vault/resource_azure_secrets_sync_destination.go +++ b/vault/resource_azure_secrets_sync_destination.go @@ -27,6 +27,11 @@ var azureSyncDestinationFields = []string{ consts.FieldTenantID, } +var azureNonSensitiveFields = []string{ + fieldKeyVaultURI, + fieldCloud, +} + func azureSecretsSyncDestinationResource() *schema.Resource { return &schema.Resource{ CreateContext: provider.MountCreateContextWrapper(azureSecretsSyncDestinationWrite, provider.VaultVersion115), @@ -132,10 +137,14 @@ func azureSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData return diag.FromErr(err) } - for _, k := range azureSyncDestinationFields { - if v, ok := resp.Data[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.Errorf("error setting state key %q: err=%s", k, err) + for _, k := range azureNonSensitiveFields { + if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { + if m, ok := data.(map[string]interface{}); ok { + if v, ok := m[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } } } } diff --git a/vault/resource_azure_secrets_sync_destination_test.go b/vault/resource_azure_secrets_sync_destination_test.go index 767c88ed4..aecb627c1 100644 --- a/vault/resource_azure_secrets_sync_destination_test.go +++ b/vault/resource_azure_secrets_sync_destination_test.go @@ -50,6 +50,11 @@ func TestAzureSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, fieldCloud, "cloud"), ), }, + testutil.GetImportTestStep(resourceName, false, nil, + consts.FieldClientID, + consts.FieldClientSecret, + consts.FieldTenantID, + ), }, }) } From 07a39c7abbc732d16d056bc4a351a7264f1f1491 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 10 Jan 2024 11:49:14 -0800 Subject: [PATCH 11/24] generalize secrets sync destination code into package --- internal/consts/consts.go | 3 + internal/provider/meta.go | 1 + internal/provider/schema_util.go | 34 ++++ vault/provider.go | 18 +- .../resource_aws_secrets_sync_destination.go | 169 ----------------- vault/resource_gh_secrets_sync_destination.go | 160 ---------------- .../resource_secrets_sync_aws_destination.go | 102 +++++++++++ ...urce_secrets_sync_aws_destination_test.go} | 35 ++-- ...esource_secrets_sync_azure_destination.go} | 0 ...ce_secrets_sync_azure_destination_test.go} | 0 ... resource_secrets_sync_gcp_destination.go} | 0 ...urce_secrets_sync_gcp_destination_test.go} | 0 vault/resource_secrets_sync_gh_destination.go | 94 ++++++++++ ...ource_secrets_sync_gh_destination_test.go} | 22 ++- ...esource_secrets_sync_vercel_destination.go | 106 +++++++++++ ...e_secrets_sync_vercel_destination_test.go} | 14 +- ...esource_vercel_secrets_sync_destination.go | 173 ------------------ 17 files changed, 391 insertions(+), 540 deletions(-) delete mode 100644 vault/resource_aws_secrets_sync_destination.go delete mode 100644 vault/resource_gh_secrets_sync_destination.go create mode 100644 vault/resource_secrets_sync_aws_destination.go rename vault/{resource_aws_secrets_sync_destination_test.go => resource_secrets_sync_aws_destination_test.go} (65%) rename vault/{resource_azure_secrets_sync_destination.go => resource_secrets_sync_azure_destination.go} (100%) rename vault/{resource_azure_secrets_sync_destination_test.go => resource_secrets_sync_azure_destination_test.go} (100%) rename vault/{resource_gcp_secrets_sync_destination.go => resource_secrets_sync_gcp_destination.go} (100%) rename vault/{resource_gcp_secrets_sync_destination_test.go => resource_secrets_sync_gcp_destination_test.go} (100%) create mode 100644 vault/resource_secrets_sync_gh_destination.go rename vault/{resource_gh_secrets_sync_destination_test.go => resource_secrets_sync_gh_destination_test.go} (75%) create mode 100644 vault/resource_secrets_sync_vercel_destination.go rename vault/{resource_vercel_secrets_sync_destination_test.go => resource_secrets_sync_vercel_destination_test.go} (83%) delete mode 100644 vault/resource_vercel_secrets_sync_destination.go diff --git a/internal/consts/consts.go b/internal/consts/consts.go index 4d00b09b9..a780e7904 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -364,6 +364,8 @@ const ( FieldDisableISSValidation = "disable_iss_validation" FieldPEMKeys = "pem_keys" FieldSetNamespaceFromToken = "set_namespace_from_token" + FieldCustomTags = "custom_tags" + FieldSecretNameTemplate = "secret_name_template" /* common environment variables */ @@ -435,6 +437,7 @@ const ( VaultVersion113 = "1.13.0" VaultVersion114 = "1.14.0" VaultVersion115 = "1.15.0" + VaultVersion116 = "1.16.0-beta1+ent" /* Vault auth methods diff --git a/internal/provider/meta.go b/internal/provider/meta.go index b7cb02edd..4f741f8c6 100644 --- a/internal/provider/meta.go +++ b/internal/provider/meta.go @@ -41,6 +41,7 @@ var ( VaultVersion113 = version.Must(version.NewSemver(consts.VaultVersion113)) VaultVersion114 = version.Must(version.NewSemver(consts.VaultVersion114)) VaultVersion115 = version.Must(version.NewSemver(consts.VaultVersion115)) + VaultVersion116 = version.Must(version.NewSemver(consts.VaultVersion116)) TokenTTLMinRecommended = time.Minute * 15 ) diff --git a/internal/provider/schema_util.go b/internal/provider/schema_util.go index 50aa6fb03..93cb40110 100644 --- a/internal/provider/schema_util.go +++ b/internal/provider/schema_util.go @@ -66,6 +66,40 @@ func MustAddMountMigrationSchema(r *schema.Resource, customStateUpgrade bool) *s return r } +func MustAddSecretsSyncCommonSchema(r *schema.Resource) *schema.Resource { + MustAddSchema(r, map[string]*schema.Schema{ + consts.FieldType: { + Type: schema.TypeString, + Computed: true, + Description: "Type of secrets destination.", + ForceNew: true, + }, + consts.FieldSecretNameTemplate: { + Type: schema.TypeString, + Optional: true, + Description: "Template describing how to generate external secret names.", + // @TODO can this be updated? + ForceNew: true, + }, + }) + + return r +} + +func MustAddSecretsSyncCloudSchema(r *schema.Resource) *schema.Resource { + MustAddSchema(r, map[string]*schema.Schema{ + consts.FieldCustomTags: { + Type: schema.TypeMap, + Optional: true, + Description: "Custom tags to set on the secret managed at the destination.", + // @TODO can this be updated? + ForceNew: true, + }, + }) + + return MustAddSecretsSyncCommonSchema(r) +} + func GetNamespaceSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ consts.FieldNamespace: { diff --git a/vault/provider.go b/vault/provider.go index f33caf8e5..3921139cb 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -729,23 +729,23 @@ var ( Resource: UpdateSchemaResource(samlAuthBackendRoleResource()), PathInventory: []string{"/auth/saml/role/{name}"}, }, - "vault_aws_secrets_sync_destination": { + "vault_secrets_sync_aws_destination": { Resource: UpdateSchemaResource(awsSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/aws-sm/{name}"}, }, - "vault_gh_secrets_sync_destination": { - Resource: UpdateSchemaResource(githubSecretsSyncDestinationResource()), - PathInventory: []string{"/sys/sync/destinations/gh/{name}"}, + "vault_secrets_sync_azure_destination": { + Resource: UpdateSchemaResource(azureSecretsSyncDestinationResource()), + PathInventory: []string{"/sys/sync/destinations/azure-kv/{name}"}, }, - "vault_gcp_secrets_sync_destination": { + "vault_secrets_sync_gcp_destination": { Resource: UpdateSchemaResource(gcpSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/gcp-sm/{name}"}, }, - "vault_azure_secrets_sync_destination": { - Resource: UpdateSchemaResource(azureSecretsSyncDestinationResource()), - PathInventory: []string{"/sys/sync/destinations/azure-kv/{name}"}, + "vault_secrets_sync_gh_destination": { + Resource: UpdateSchemaResource(githubSecretsSyncDestinationResource()), + PathInventory: []string{"/sys/sync/destinations/gh/{name}"}, }, - "vault_vercel_secrets_sync_destination": { + "vault_secrets_sync_vercel_destination": { Resource: UpdateSchemaResource(vercelSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/vercel-project/{name}"}, }, diff --git a/vault/resource_aws_secrets_sync_destination.go b/vault/resource_aws_secrets_sync_destination.go deleted file mode 100644 index 4019d9f29..000000000 --- a/vault/resource_aws_secrets_sync_destination.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package vault - -import ( - "context" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-provider-vault/internal/consts" - "github.com/hashicorp/terraform-provider-vault/internal/provider" -) - -const ( - fieldAccessKeyID = "access_key_id" - fieldSecretAccessKey = "secret_access_key" - - vaultFieldConnectionDetails = "connection_details" -) - -var awsSyncDestinationFields = []string{ - fieldAccessKeyID, - fieldSecretAccessKey, - consts.FieldAWSRegion, -} - -var awsSyncNonSensitiveFields = []string{ - consts.FieldAWSRegion, -} - -func awsSecretsSyncDestinationResource() *schema.Resource { - return &schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(awsSecretsSyncDestinationWrite, provider.VaultVersion115), - ReadContext: provider.ReadContextWrapper(awsSecretsSyncDestinationRead), - DeleteContext: awsSecretsSyncDestinationDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: map[string]*schema.Schema{ - consts.FieldName: { - Type: schema.TypeString, - Required: true, - Description: "Unique name of the AWS destination.", - ForceNew: true, - }, - fieldAccessKeyID: { - Type: schema.TypeString, - Optional: true, - Description: "Access key id to authenticate against the AWS secrets manager.", - ForceNew: true, - }, - fieldSecretAccessKey: { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - Description: "Secret access key to authenticate against the AWS secrets " + - "manager.", - ForceNew: true, - }, - consts.FieldRegion: { - Type: schema.TypeString, - Optional: true, - Description: "Region where to manage the secrets manager entries.", - ForceNew: true, - }, - }, - } -} - -func awsSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - name := d.Get(consts.FieldName).(string) - path := awsSecretsSyncDestinationPath(name) - - data := map[string]interface{}{} - - for _, k := range awsSyncDestinationFields { - data[k] = d.Get(k) - } - - log.Printf("[DEBUG] Writing AWS sync destination to %q", path) - _, err := client.Logical().WriteWithContext(ctx, path, data) - if err != nil { - return diag.Errorf("error enabling AWS sync destination %q: %s", path, err) - } - log.Printf("[DEBUG] Enabled AWS sync destination %q", path) - - d.SetId(name) - - return awsSecretsSyncDestinationRead(ctx, d, meta) -} - -func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - name := d.Id() - path := awsSecretsSyncDestinationPath(name) - - log.Printf("[DEBUG] Reading AWS sync destination") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err != nil { - return diag.Errorf("error reading AWS sync destination from %q: %s", path, err) - } - log.Printf("[DEBUG] Read AWS sync destination") - - if resp == nil { - log.Printf("[WARN] No info found at %q; removing from state.", path) - d.SetId("") - return nil - } - - if err := d.Set(consts.FieldName, name); err != nil { - return diag.FromErr(err) - } - - for _, k := range awsSyncNonSensitiveFields { - if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { - if m, ok := data.(map[string]interface{}); ok { - if v, ok := m[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.Errorf("error setting state key %q: err=%s", k, err) - } - } - } - } - } - - for _, k := range awsSyncDestinationFields { - if v, ok := resp.Data[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.Errorf("error setting state key %q: err=%s", k, err) - } - } - } - - return nil -} - -func awsSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - path := awsSecretsSyncDestinationPath(d.Id()) - - log.Printf("[DEBUG] Deleting AWS sync destination at %q", path) - _, err := client.Logical().DeleteWithContext(ctx, path) - if err != nil { - return diag.Errorf("error deleting AWS sync destination at %q: %s", path, err) - } - log.Printf("[DEBUG] Deleted AWS sync destination at %q", path) - - return nil -} - -func awsSecretsSyncDestinationPath(name string) string { - return "sys/sync/destinations/aws-sm/" + name -} diff --git a/vault/resource_gh_secrets_sync_destination.go b/vault/resource_gh_secrets_sync_destination.go deleted file mode 100644 index d1ff64383..000000000 --- a/vault/resource_gh_secrets_sync_destination.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package vault - -import ( - "context" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-provider-vault/internal/consts" - "github.com/hashicorp/terraform-provider-vault/internal/provider" -) - -const ( - fieldAccessToken = "access_token" - fieldRepositoryOwner = "repository_owner" - fieldRepositoryName = "repository_name" -) - -var githubSyncDestinationFields = []string{ - fieldAccessToken, - fieldRepositoryOwner, - fieldRepositoryName, -} - -var githubNonSensitiveFields = []string{ - fieldRepositoryOwner, - fieldRepositoryName, -} - -func githubSecretsSyncDestinationResource() *schema.Resource { - return &schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(githubSecretsSyncDestinationWrite, provider.VaultVersion115), - ReadContext: provider.ReadContextWrapper(githubSecretsSyncDestinationRead), - DeleteContext: githubSecretsSyncDestinationDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: map[string]*schema.Schema{ - consts.FieldName: { - Type: schema.TypeString, - Required: true, - Description: "Unique name of the github destination.", - ForceNew: true, - }, - fieldAccessToken: { - Type: schema.TypeString, - Required: true, - Sensitive: true, - Description: "Fine-grained or personal access token.", - ForceNew: true, - }, - fieldRepositoryOwner: { - Type: schema.TypeString, - Required: true, - Description: "GitHub organization or username that owns the repository.", - ForceNew: true, - }, - fieldRepositoryName: { - Type: schema.TypeString, - Required: true, - Description: "Name of the repository.", - ForceNew: true, - }, - }, - } -} - -func githubSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - name := d.Get(consts.FieldName).(string) - path := githubSecretsSyncDestinationPath(name) - - data := map[string]interface{}{} - - for _, k := range githubSyncDestinationFields { - data[k] = d.Get(k) - } - - log.Printf("[DEBUG] Writing Github sync destination to %q", path) - _, err := client.Logical().WriteWithContext(ctx, path, data) - if err != nil { - return diag.Errorf("error enabling Github sync destination %q: %s", path, err) - } - log.Printf("[DEBUG] Enabled Github sync destination %q", path) - - d.SetId(name) - - return githubSecretsSyncDestinationRead(ctx, d, meta) -} - -func githubSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - name := d.Id() - path := githubSecretsSyncDestinationPath(name) - - log.Printf("[DEBUG] Reading Github sync destination") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err != nil { - return diag.Errorf("error reading Github sync destination from %q: %s", path, err) - } - log.Printf("[DEBUG] Read Github sync destination") - - if resp == nil { - log.Printf("[WARN] No info found at %q; removing from state.", path) - d.SetId("") - return nil - } - - if err := d.Set(consts.FieldName, name); err != nil { - return diag.FromErr(err) - } - - for _, k := range githubNonSensitiveFields { - if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { - if m, ok := data.(map[string]interface{}); ok { - if v, ok := m[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.Errorf("error setting state key %q: err=%s", k, err) - } - } - } - } - } - - return nil -} - -func githubSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - path := githubSecretsSyncDestinationPath(d.Id()) - - log.Printf("[DEBUG] Deleting Github sync destination at %q", path) - _, err := client.Logical().DeleteWithContext(ctx, path) - if err != nil { - return diag.Errorf("error deleting Github sync destination at %q: %s", path, err) - } - log.Printf("[DEBUG] Deleted Github sync destination at %q", path) - - return nil -} - -func githubSecretsSyncDestinationPath(name string) string { - return "sys/sync/destinations/gh/" + name -} diff --git a/vault/resource_secrets_sync_aws_destination.go b/vault/resource_secrets_sync_aws_destination.go new file mode 100644 index 000000000..a6aa48cd1 --- /dev/null +++ b/vault/resource_secrets_sync_aws_destination.go @@ -0,0 +1,102 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/internal/sync" +) + +const ( + fieldAccessKeyID = "access_key_id" + fieldSecretAccessKey = "secret_access_key" + + vaultFieldConnectionDetails = "connection_details" + + awsSyncType = "aws-sm" +) + +// awsSyncWriteFields contains all fields that need to be written to the API +var awsSyncWriteFields = []string{ + fieldAccessKeyID, + fieldSecretAccessKey, + consts.FieldRegion, + consts.FieldCustomTags, + consts.FieldSecretNameTemplate, +} + +// awsSyncReadFields contains all fields that are returned on read from the API +var awsSyncReadFields = []string{ + consts.FieldRegion, + consts.FieldCustomTags, + consts.FieldSecretNameTemplate, +} + +// awsSyncUpdateFields contains all fields that can be updated via the API +var awsSyncUpdateFields = []string{ + fieldAccessKeyID, + fieldSecretAccessKey, +} + +func awsSecretsSyncDestinationResource() *schema.Resource { + return provider.MustAddSecretsSyncCloudSchema(&schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(awsSecretsSyncDestinationWrite, provider.VaultVersion116), + ReadContext: provider.ReadContextWrapper(awsSecretsSyncDestinationRead), + UpdateContext: awsSecretsSyncDestinationUpdate, + DeleteContext: awsSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the AWS destination.", + ForceNew: true, + }, + fieldAccessKeyID: { + Type: schema.TypeString, + Optional: true, + Description: "Access key id to authenticate against the AWS secrets manager.", + }, + fieldSecretAccessKey: { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Secret access key to authenticate against the AWS secrets " + + "manager.", + }, + consts.FieldRegion: { + Type: schema.TypeString, + Optional: true, + Description: "Region where to manage the secrets manager entries.", + ForceNew: true, + }, + }, + }) +} + +func awsSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationWrite(ctx, d, meta, awsSyncType, awsSyncWriteFields, awsSyncReadFields) +} + +func awsSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationUpdate(ctx, d, meta, awsSyncType, awsSyncUpdateFields, awsSyncReadFields) +} + +func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + // since other fields come back as '******', we only set the non-sensitive region fields + return syncutil.SyncDestinationRead(ctx, d, meta, awsSyncType, awsSyncReadFields) +} + +func awsSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationDelete(ctx, d, meta, awsSyncType) +} diff --git a/vault/resource_aws_secrets_sync_destination_test.go b/vault/resource_secrets_sync_aws_destination_test.go similarity index 65% rename from vault/resource_aws_secrets_sync_destination_test.go rename to vault/resource_secrets_sync_aws_destination_test.go index 168b41f2c..b819e0ef3 100644 --- a/vault/resource_aws_secrets_sync_destination_test.go +++ b/vault/resource_secrets_sync_aws_destination_test.go @@ -15,10 +15,14 @@ import ( "github.com/hashicorp/terraform-provider-vault/testutil" ) +const ( + defaultSecretsSyncTemplate = "new-name-template-{{ .SecretPath }}" +) + func TestAWSSecretsSyncDestination(t *testing.T) { destName := acctest.RandomWithPrefix("tf-sync-dest-aws") - resourceName := "vault_aws_secrets_sync_destination.test" + resourceName := "vault_secrets_sync_aws_destination.test" accessKey, secretKey := testutil.GetTestAWSCreds(t) region := testutil.GetTestAWSRegion(t) @@ -26,38 +30,43 @@ func TestAWSSecretsSyncDestination(t *testing.T) { ProviderFactories: providerFactories, PreCheck: func() { testutil.TestAccPreCheck(t) - SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) - }, PreventPostDestroyRefresh: true, + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion116) + }, Steps: []resource.TestStep{ { - Config: testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName), + Config: testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), resource.TestCheckResourceAttr(resourceName, fieldAccessKeyID, accessKey), resource.TestCheckResourceAttr(resourceName, fieldSecretAccessKey, secretKey), resource.TestCheckResourceAttr(resourceName, consts.FieldRegion, region), + resource.TestCheckResourceAttr(resourceName, consts.FieldType, awsSyncType), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, defaultSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), }, testutil.GetImportTestStep(resourceName, false, nil, fieldAccessKeyID, fieldSecretAccessKey, - // TODO confirm if region will not be returned in response - consts.FieldRegion, ), }, }) } -func testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName string) string { +func testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName, templ string) string { ret := fmt.Sprintf(` -resource "vault_aws_secrets_sync_destination" "test" { - name = "%s" - access_key_id = "%s" - secret_access_key = "%s" - region = "%s" +resource "vault_secrets_sync_aws_destination" "test" { + name = "%s" + access_key_id = "%s" + secret_access_key = "%s" + region = "%s" + secret_name_template = "%s" + custom_tags = { + "foo" = "bar" + } } -`, destName, accessKey, secretKey, region) +`, destName, accessKey, secretKey, region, templ) return ret } diff --git a/vault/resource_azure_secrets_sync_destination.go b/vault/resource_secrets_sync_azure_destination.go similarity index 100% rename from vault/resource_azure_secrets_sync_destination.go rename to vault/resource_secrets_sync_azure_destination.go diff --git a/vault/resource_azure_secrets_sync_destination_test.go b/vault/resource_secrets_sync_azure_destination_test.go similarity index 100% rename from vault/resource_azure_secrets_sync_destination_test.go rename to vault/resource_secrets_sync_azure_destination_test.go diff --git a/vault/resource_gcp_secrets_sync_destination.go b/vault/resource_secrets_sync_gcp_destination.go similarity index 100% rename from vault/resource_gcp_secrets_sync_destination.go rename to vault/resource_secrets_sync_gcp_destination.go diff --git a/vault/resource_gcp_secrets_sync_destination_test.go b/vault/resource_secrets_sync_gcp_destination_test.go similarity index 100% rename from vault/resource_gcp_secrets_sync_destination_test.go rename to vault/resource_secrets_sync_gcp_destination_test.go diff --git a/vault/resource_secrets_sync_gh_destination.go b/vault/resource_secrets_sync_gh_destination.go new file mode 100644 index 000000000..7e8b4fc6c --- /dev/null +++ b/vault/resource_secrets_sync_gh_destination.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + syncutil "github.com/hashicorp/terraform-provider-vault/internal/sync" +) + +const ( + fieldAccessToken = "access_token" + fieldRepositoryOwner = "repository_owner" + fieldRepositoryName = "repository_name" + ghSyncType = "gh" +) + +var githubSyncWriteFields = []string{ + fieldAccessToken, + fieldRepositoryOwner, + fieldRepositoryName, + consts.FieldSecretNameTemplate, +} + +var githubSyncUpdateFields = []string{ + fieldAccessToken, +} + +var githubSyncReadFields = []string{ + fieldRepositoryOwner, + fieldRepositoryName, + consts.FieldSecretNameTemplate, +} + +func githubSecretsSyncDestinationResource() *schema.Resource { + return provider.MustAddSecretsSyncCommonSchema(&schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(githubSecretsSyncDestinationWrite, provider.VaultVersion116), + ReadContext: provider.ReadContextWrapper(githubSecretsSyncDestinationRead), + UpdateContext: githubSecretsSyncDestinationUpdate, + DeleteContext: githubSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the github destination.", + ForceNew: true, + }, + fieldAccessToken: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Fine-grained or personal access token.", + }, + fieldRepositoryOwner: { + Type: schema.TypeString, + Required: true, + Description: "GitHub organization or username that owns the repository.", + ForceNew: true, + }, + fieldRepositoryName: { + Type: schema.TypeString, + Required: true, + Description: "Name of the repository.", + ForceNew: true, + }, + }, + }) +} + +func githubSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationWrite(ctx, d, meta, ghSyncType, githubSyncWriteFields, githubSyncReadFields) +} + +func githubSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationUpdate(ctx, d, meta, ghSyncType, githubSyncUpdateFields, githubSyncReadFields) +} + +func githubSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationRead(ctx, d, meta, ghSyncType, githubSyncReadFields) +} + +func githubSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationDelete(ctx, d, meta, ghSyncType) +} diff --git a/vault/resource_gh_secrets_sync_destination_test.go b/vault/resource_secrets_sync_gh_destination_test.go similarity index 75% rename from vault/resource_gh_secrets_sync_destination_test.go rename to vault/resource_secrets_sync_gh_destination_test.go index eda601b70..e3e7d60a1 100644 --- a/vault/resource_gh_secrets_sync_destination_test.go +++ b/vault/resource_secrets_sync_gh_destination_test.go @@ -18,7 +18,7 @@ import ( func TestGithubSecretsSyncDestination(t *testing.T) { destName := acctest.RandomWithPrefix("tf-sync-dest-gh") - resourceName := "vault_gh_secrets_sync_destination.test" + resourceName := "vault_secrets_sync_gh_destination.test" values := testutil.SkipTestEnvUnset(t, "GITHUB_ACCESS_TOKEN", @@ -34,17 +34,18 @@ func TestGithubSecretsSyncDestination(t *testing.T) { ProviderFactories: providerFactories, PreCheck: func() { testutil.TestAccPreCheck(t) - SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion116) }, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ { - Config: testGithubSecretsSyncDestinationConfig(accessToken, repoOwner, repoName, destName), + Config: testGithubSecretsSyncDestinationConfig(accessToken, repoOwner, repoName, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), resource.TestCheckResourceAttr(resourceName, fieldAccessToken, accessToken), resource.TestCheckResourceAttr(resourceName, fieldRepositoryOwner, repoOwner), resource.TestCheckResourceAttr(resourceName, fieldRepositoryName, repoName), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, defaultSecretsSyncTemplate), ), }, testutil.GetImportTestStep(resourceName, false, nil, @@ -54,15 +55,16 @@ func TestGithubSecretsSyncDestination(t *testing.T) { }) } -func testGithubSecretsSyncDestinationConfig(accessToken, repoOwner, repoName, destName string) string { +func testGithubSecretsSyncDestinationConfig(accessToken, repoOwner, repoName, destName, templ string) string { ret := fmt.Sprintf(` -resource "vault_gh_secrets_sync_destination" "test" { - name = "%s" - access_token = "%s" - repository_owner = "%s" - repository_name = "%s" +resource "vault_secrets_sync_gh_destination" "test" { + name = "%s" + access_token = "%s" + repository_owner = "%s" + repository_name = "%s" + secret_name_template = "%s" } -`, destName, accessToken, repoOwner, repoName) +`, destName, accessToken, repoOwner, repoName, templ) return ret } diff --git a/vault/resource_secrets_sync_vercel_destination.go b/vault/resource_secrets_sync_vercel_destination.go new file mode 100644 index 000000000..72f637653 --- /dev/null +++ b/vault/resource_secrets_sync_vercel_destination.go @@ -0,0 +1,106 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + syncutil "github.com/hashicorp/terraform-provider-vault/internal/sync" +) + +const ( + fieldProjectID = "project_id" + fieldTeamID = "team_id" + fieldDeploymentEnvironments = "deployment_environments" + vercelSyncType = "vercel-project" +) + +var vercelSyncWriteFields = []string{ + fieldAccessToken, + fieldProjectID, + fieldTeamID, + fieldDeploymentEnvironments, + consts.FieldSecretNameTemplate, +} + +var vercelSyncUpdateFields = []string{ + fieldAccessToken, + fieldDeploymentEnvironments, + fieldTeamID, +} + +var vercelSyncReadFields = []string{ + fieldProjectID, + fieldTeamID, + fieldDeploymentEnvironments, + consts.FieldSecretNameTemplate, +} + +func vercelSecretsSyncDestinationResource() *schema.Resource { + return provider.MustAddSecretsSyncCommonSchema(&schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(vercelSecretsSyncDestinationWrite, provider.VaultVersion116), + UpdateContext: vercelSecretsSyncDestinationUpdate, + ReadContext: provider.ReadContextWrapper(vercelSecretsSyncDestinationRead), + DeleteContext: vercelSecretsSyncDestinationDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + consts.FieldName: { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the Vercel destination.", + ForceNew: true, + }, + fieldAccessToken: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Vercel API access token with the permissions to manage " + + "environment variables.", + }, + fieldProjectID: { + Type: schema.TypeString, + Required: true, + Description: "Project ID where to manage environment variables.", + ForceNew: true, + }, + fieldTeamID: { + Type: schema.TypeString, + Optional: true, + Description: "Team ID the project belongs to.", + }, + fieldDeploymentEnvironments: { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "Deployment environments where the environment " + + "variables are available. Accepts 'development', " + + "'preview' & 'production'.", + }, + }, + }) +} + +func vercelSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationWrite(ctx, d, meta, vercelSyncType, vercelSyncWriteFields, vercelSyncReadFields) +} + +func vercelSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationUpdate(ctx, d, meta, vercelSyncType, vercelSyncUpdateFields, vercelSyncReadFields) +} + +func vercelSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationRead(ctx, d, meta, vercelSyncType, vercelSyncReadFields) +} + +func vercelSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationDelete(ctx, d, meta, vercelSyncType) +} diff --git a/vault/resource_vercel_secrets_sync_destination_test.go b/vault/resource_secrets_sync_vercel_destination_test.go similarity index 83% rename from vault/resource_vercel_secrets_sync_destination_test.go rename to vault/resource_secrets_sync_vercel_destination_test.go index c1b0f7457..e73e4d722 100644 --- a/vault/resource_vercel_secrets_sync_destination_test.go +++ b/vault/resource_secrets_sync_vercel_destination_test.go @@ -18,7 +18,7 @@ import ( func TestVercelSecretsSyncDestination(t *testing.T) { destName := acctest.RandomWithPrefix("tf-sync-dest-vercel") - resourceName := "vault_vercel_secrets_sync_destination.test" + resourceName := "vault_secrets_sync_vercel_destination.test" values := testutil.SkipTestEnvUnset(t, "VERCEL_ACCESS_TOKEN", @@ -30,11 +30,11 @@ func TestVercelSecretsSyncDestination(t *testing.T) { ProviderFactories: providerFactories, PreCheck: func() { testutil.TestAccPreCheck(t) - SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion116) }, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ { - Config: testVercelSecretsSyncDestinationConfig(accessToken, projectID, destName), + Config: testVercelSecretsSyncDestinationConfig(accessToken, projectID, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), @@ -44,6 +44,7 @@ func TestVercelSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "deployment_environments.0", "development"), resource.TestCheckResourceAttr(resourceName, "deployment_environments.1", "preview"), resource.TestCheckResourceAttr(resourceName, "deployment_environments.2", "production"), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, defaultSecretsSyncTemplate), ), }, testutil.GetImportTestStep(resourceName, false, nil, @@ -53,15 +54,16 @@ func TestVercelSecretsSyncDestination(t *testing.T) { }) } -func testVercelSecretsSyncDestinationConfig(accessToken, projectID, destName string) string { +func testVercelSecretsSyncDestinationConfig(accessToken, projectID, destName, templ string) string { ret := fmt.Sprintf(` -resource "vault_vercel_secrets_sync_destination" "test" { +resource "vault_secrets_sync_vercel_destination" "test" { name = "%s" access_token = "%s" project_id = "%s" deployment_environments = ["development", "preview", "production"] + secret_name_template = "%s" } -`, destName, accessToken, projectID) +`, destName, accessToken, projectID, templ) return ret } diff --git a/vault/resource_vercel_secrets_sync_destination.go b/vault/resource_vercel_secrets_sync_destination.go deleted file mode 100644 index 9761b059e..000000000 --- a/vault/resource_vercel_secrets_sync_destination.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package vault - -import ( - "context" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - "github.com/hashicorp/terraform-provider-vault/internal/consts" - "github.com/hashicorp/terraform-provider-vault/internal/provider" -) - -const ( - fieldProjectID = "project_id" - fieldTeamID = "team_id" - fieldDeploymentEnvironments = "deployment_environments" -) - -var vercelSyncDestinationFields = []string{ - fieldAccessToken, - fieldProjectID, - fieldTeamID, - fieldDeploymentEnvironments, -} - -var vercelNonSensitiveFields = []string{ - fieldProjectID, - fieldTeamID, - fieldDeploymentEnvironments, -} - -func vercelSecretsSyncDestinationResource() *schema.Resource { - return &schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(vercelSecretsSyncDestinationWrite, provider.VaultVersion115), - ReadContext: provider.ReadContextWrapper(vercelSecretsSyncDestinationRead), - DeleteContext: vercelSecretsSyncDestinationDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: map[string]*schema.Schema{ - consts.FieldName: { - Type: schema.TypeString, - Required: true, - Description: "Unique name of the Vercel destination.", - ForceNew: true, - }, - fieldAccessToken: { - Type: schema.TypeString, - Required: true, - Sensitive: true, - Description: "Vercel API access token with the permissions to manage " + - "environment variables.", - ForceNew: true, - }, - fieldProjectID: { - Type: schema.TypeString, - Required: true, - Description: "Project ID where to manage environment variables.", - ForceNew: true, - }, - fieldTeamID: { - Type: schema.TypeString, - Optional: true, - Description: "Vercel API access token with the permissions to manage " + - "environment variables.", - ForceNew: true, - }, - fieldDeploymentEnvironments: { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Required: true, - Description: "Deployment environments where the environment " + - "variables are available. Accepts 'development', " + - "'preview' & 'production'.", - ForceNew: true, - }, - }, - } -} - -func vercelSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - name := d.Get(consts.FieldName).(string) - path := vercelSecretsSyncDestinationPath(name) - - data := map[string]interface{}{} - - for _, k := range vercelSyncDestinationFields { - data[k] = d.Get(k) - } - - log.Printf("[DEBUG] Writing Vercel sync destination to %q", path) - _, err := client.Logical().WriteWithContext(ctx, path, data) - if err != nil { - return diag.Errorf("error enabling Vercel sync destination %q: %s", path, err) - } - log.Printf("[DEBUG] Enabled Vercel sync destination %q", path) - - d.SetId(name) - - return vercelSecretsSyncDestinationRead(ctx, d, meta) -} - -func vercelSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - name := d.Id() - path := vercelSecretsSyncDestinationPath(name) - - log.Printf("[DEBUG] Reading Vercel sync destination") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err != nil { - return diag.Errorf("error reading Vercel sync destination from %q: %s", path, err) - } - log.Printf("[DEBUG] Read Vercel sync destination") - - if resp == nil { - log.Printf("[WARN] No info found at %q; removing from state.", path) - d.SetId("") - return nil - } - - if err := d.Set(consts.FieldName, name); err != nil { - return diag.FromErr(err) - } - - for _, k := range vercelNonSensitiveFields { - if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { - if m, ok := data.(map[string]interface{}); ok { - if v, ok := m[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.Errorf("error setting state key %q: err=%s", k, err) - } - } - } - } - } - - return nil -} - -func vercelSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - path := vercelSecretsSyncDestinationPath(d.Id()) - - log.Printf("[DEBUG] Deleting Vercel sync destination at %q", path) - _, err := client.Logical().DeleteWithContext(ctx, path) - if err != nil { - return diag.Errorf("error deleting Vercel sync destination at %q: %s", path, err) - } - log.Printf("[DEBUG] Deleted Vercel sync destination at %q", path) - - return nil -} - -func vercelSecretsSyncDestinationPath(name string) string { - return "sys/sync/destinations/vercel-project/" + name -} From 53cbbe6021926fdd6d9ed062e46d613178b3a2c8 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 10 Jan 2024 11:49:47 -0800 Subject: [PATCH 12/24] add generalized method code --- internal/sync/secrets_sync.go | 147 ++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 internal/sync/secrets_sync.go diff --git a/internal/sync/secrets_sync.go b/internal/sync/secrets_sync.go new file mode 100644 index 000000000..f704188d6 --- /dev/null +++ b/internal/sync/secrets_sync.go @@ -0,0 +1,147 @@ +package syncutil + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-provider-vault/internal/consts" + "github.com/hashicorp/terraform-provider-vault/internal/provider" +) + +const ( + fieldConnectionDetails = "connection_details" + fieldOptions = "options" +) + +func SyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string, writeFields, readFields []string) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + name := d.Get(consts.FieldName).(string) + path := secretsSyncDestinationPath(name, typ) + + data := map[string]interface{}{} + + for _, k := range writeFields { + if v, ok := d.GetOk(k); ok { + data[k] = v + } + } + + log.Printf("[DEBUG] Writing sync destination to %q", path) + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error enabling sync destination %q: %s", path, err) + } + log.Printf("[DEBUG] Enabled sync destination %q", path) + + d.SetId(name) + + return SyncDestinationRead(ctx, d, meta, typ, readFields) +} + +func SyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string, fields []string) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + name := d.Id() + path := secretsSyncDestinationPath(name, typ) + + log.Printf("[DEBUG] Reading sync destination") + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading sync destination from %q: %s", path, err) + } + log.Printf("[DEBUG] Read sync destination") + + if resp == nil { + log.Printf("[WARN] No info found at %q; removing from state.", path) + d.SetId("") + return nil + } + + if err := d.Set(consts.FieldName, name); err != nil { + return diag.FromErr(err) + } + + // implicitly set type so it can be passed down to association resource for ease-of-use + if err := d.Set(consts.FieldType, typ); err != nil { + return diag.FromErr(err) + } + + connectionDetails := resp.Data[fieldConnectionDetails] + options := resp.Data[fieldOptions] + for _, k := range fields { + if connectionMap, ok := connectionDetails.(map[string]interface{}); ok { + if v, ok := connectionMap[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + + if optionsMap, ok := options.(map[string]interface{}); ok { + if v, ok := optionsMap[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + } + + return nil +} + +func SyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string, updateFields, readFields []string) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := secretsSyncDestinationPath(d.Id(), typ) + + data := map[string]interface{}{} + + for _, k := range updateFields { + data[k] = d.Get(k) + } + + log.Printf("[DEBUG] Updating sync destination at %q", path) + // @TODO confirm if this should be a JSONMergePatch instead + _, err := client.Logical().WriteWithContext(ctx, path, data) + if err != nil { + return diag.Errorf("error updating sync destination %q: %s", path, err) + } + log.Printf("[DEBUG] Updated sync destination %q", path) + + return SyncDestinationRead(ctx, d, meta, typ, readFields) +} + +func SyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := secretsSyncDestinationPath(d.Id(), typ) + + log.Printf("[DEBUG] Deleting sync destination at %q", path) + _, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.Errorf("error deleting sync destination at %q: %s", path, err) + } + log.Printf("[DEBUG] Deleted sync destination at %q", path) + + return nil +} + +func secretsSyncDestinationPath(name, typ string) string { + return fmt.Sprintf("sys/sync/destinations/%s/%s", typ, name) +} From d27daa86884e479eb3373b32e8d45a41acd6010a Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 10 Jan 2024 17:00:47 -0800 Subject: [PATCH 13/24] Add new parameters and generalized code to GCP and Azure --- .../resource_secrets_sync_aws_destination.go | 2 - ...ource_secrets_sync_aws_destination_test.go | 1 + ...resource_secrets_sync_azure_destination.go | 108 ++++-------------- ...rce_secrets_sync_azure_destination_test.go | 29 +++-- .../resource_secrets_sync_gcp_destination.go | 104 +++++------------ ...ource_secrets_sync_gcp_destination_test.go | 29 +++-- 6 files changed, 85 insertions(+), 188 deletions(-) diff --git a/vault/resource_secrets_sync_aws_destination.go b/vault/resource_secrets_sync_aws_destination.go index a6aa48cd1..b25b713d2 100644 --- a/vault/resource_secrets_sync_aws_destination.go +++ b/vault/resource_secrets_sync_aws_destination.go @@ -18,8 +18,6 @@ const ( fieldAccessKeyID = "access_key_id" fieldSecretAccessKey = "secret_access_key" - vaultFieldConnectionDetails = "connection_details" - awsSyncType = "aws-sm" ) diff --git a/vault/resource_secrets_sync_aws_destination_test.go b/vault/resource_secrets_sync_aws_destination_test.go index b819e0ef3..68ea9265a 100644 --- a/vault/resource_secrets_sync_aws_destination_test.go +++ b/vault/resource_secrets_sync_aws_destination_test.go @@ -43,6 +43,7 @@ func TestAWSSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, consts.FieldRegion, region), resource.TestCheckResourceAttr(resourceName, consts.FieldType, awsSyncType), resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, defaultSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, "custom_tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), }, diff --git a/vault/resource_secrets_sync_azure_destination.go b/vault/resource_secrets_sync_azure_destination.go index 543719bf9..410b382a5 100644 --- a/vault/resource_secrets_sync_azure_destination.go +++ b/vault/resource_secrets_sync_azure_destination.go @@ -5,37 +5,46 @@ package vault import ( "context" - "log" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-vault/internal/consts" "github.com/hashicorp/terraform-provider-vault/internal/provider" + syncutil "github.com/hashicorp/terraform-provider-vault/internal/sync" ) const ( fieldKeyVaultURI = "key_vault_uri" fieldCloud = "cloud" + azureSyncType = "azure-kv" ) -var azureSyncDestinationFields = []string{ +var azureSyncWriteFields = []string{ fieldKeyVaultURI, fieldCloud, - consts.FieldClientID, consts.FieldClientSecret, + consts.FieldClientID, consts.FieldTenantID, } -var azureNonSensitiveFields = []string{ - fieldKeyVaultURI, +var azureSyncUpdateFields = []string{ + consts.FieldClientSecret, + consts.FieldClientID, +} + +var azureSyncReadFields = []string{ fieldCloud, + consts.FieldClientID, + consts.FieldTenantID, } func azureSecretsSyncDestinationResource() *schema.Resource { return &schema.Resource{ CreateContext: provider.MountCreateContextWrapper(azureSecretsSyncDestinationWrite, provider.VaultVersion115), ReadContext: provider.ReadContextWrapper(azureSecretsSyncDestinationRead), + // @TODO confirm this is available + UpdateContext: azureSecretsSyncDestinationUpdate, DeleteContext: azureSecretsSyncDestinationDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -57,21 +66,17 @@ func azureSecretsSyncDestinationResource() *schema.Resource { consts.FieldClientID: { Type: schema.TypeString, Required: true, - Sensitive: true, Description: "Client ID of an Azure app registration.", - ForceNew: true, }, consts.FieldClientSecret: { Type: schema.TypeString, Required: true, Sensitive: true, Description: "Client Secret of an Azure app registration.", - ForceNew: true, }, consts.FieldTenantID: { Type: schema.TypeString, Required: true, - Sensitive: true, Description: "ID of the target Azure tenant.", ForceNew: true, }, @@ -86,90 +91,17 @@ func azureSecretsSyncDestinationResource() *schema.Resource { } func azureSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - name := d.Get(consts.FieldName).(string) - path := azureSecretsSyncDestinationPath(name) - - data := map[string]interface{}{} - - for _, k := range azureSyncDestinationFields { - data[k] = d.Get(k) - } - - log.Printf("[DEBUG] Writing Azure sync destination to %q", path) - _, err := client.Logical().WriteWithContext(ctx, path, data) - if err != nil { - return diag.Errorf("error enabling Azure sync destination %q: %s", path, err) - } - log.Printf("[DEBUG] Enabled Azure sync destination %q", path) - - d.SetId(name) + return syncutil.SyncDestinationWrite(ctx, d, meta, azureSyncType, azureSyncWriteFields, azureSyncReadFields) +} - return azureSecretsSyncDestinationRead(ctx, d, meta) +func azureSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationUpdate(ctx, d, meta, azureSyncType, azureSyncUpdateFields, azureSyncReadFields) } func azureSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - name := d.Id() - path := azureSecretsSyncDestinationPath(name) - - log.Printf("[DEBUG] Reading Azure sync destination") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err != nil { - return diag.Errorf("error reading Azure sync destination from %q: %s", path, err) - } - log.Printf("[DEBUG] Read Azure sync destination") - - if resp == nil { - log.Printf("[WARN] No info found at %q; removing from state.", path) - d.SetId("") - return nil - } - - if err := d.Set(consts.FieldName, name); err != nil { - return diag.FromErr(err) - } - - for _, k := range azureNonSensitiveFields { - if data, ok := resp.Data[vaultFieldConnectionDetails]; ok { - if m, ok := data.(map[string]interface{}); ok { - if v, ok := m[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.Errorf("error setting state key %q: err=%s", k, err) - } - } - } - } - } - - return nil + return syncutil.SyncDestinationRead(ctx, d, meta, azureSyncType, azureSyncReadFields) } func azureSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - path := azureSecretsSyncDestinationPath(d.Id()) - - log.Printf("[DEBUG] Deleting Azure sync destination at %q", path) - _, err := client.Logical().DeleteWithContext(ctx, path) - if err != nil { - return diag.Errorf("error deleting Azure sync destination at %q: %s", path, err) - } - log.Printf("[DEBUG] Deleted Azure sync destination at %q", path) - - return nil -} - -func azureSecretsSyncDestinationPath(name string) string { - return "sys/sync/destinations/azure-kv/" + name + return syncutil.SyncDestinationDelete(ctx, d, meta, azureSyncType) } diff --git a/vault/resource_secrets_sync_azure_destination_test.go b/vault/resource_secrets_sync_azure_destination_test.go index aecb627c1..f1d51ca8e 100644 --- a/vault/resource_secrets_sync_azure_destination_test.go +++ b/vault/resource_secrets_sync_azure_destination_test.go @@ -18,7 +18,7 @@ import ( func TestAzureSecretsSyncDestination(t *testing.T) { destName := acctest.RandomWithPrefix("tf-sync-dest-azure") - resourceName := "vault_azure_secrets_sync_destination.test" + resourceName := "vault_secrets_sync_azure_destination.test" values := testutil.SkipTestEnvUnset(t, "AZURE_KEY_VAULT_URI", @@ -39,7 +39,7 @@ func TestAzureSecretsSyncDestination(t *testing.T) { }, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ { - Config: testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName), + Config: testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), @@ -48,27 +48,32 @@ func TestAzureSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, consts.FieldTenantID, tenantID), resource.TestCheckResourceAttr(resourceName, fieldKeyVaultURI, keyVaultURI), resource.TestCheckResourceAttr(resourceName, fieldCloud, "cloud"), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, defaultSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, "custom_tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), }, testutil.GetImportTestStep(resourceName, false, nil, - consts.FieldClientID, consts.FieldClientSecret, - consts.FieldTenantID, ), }, }) } -func testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName string) string { +func testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName, templ string) string { ret := fmt.Sprintf(` -resource "vault_azure_secrets_sync_destination" "test" { - name = "%s" - key_vault_uri = "%s" - client_id = "%s" - client_secret = "%s" - tenant_id = "%s" +resource "vault_secrets_sync_azure_destination" "test" { + name = "%s" + key_vault_uri = "%s" + client_id = "%s" + client_secret = "%s" + tenant_id = "%s" + secret_name_template = "%s" + custom_tags = { + "foo" = "bar" + } } -`, destName, keyVaultURI, clientID, clientSecret, tenantID) +`, destName, keyVaultURI, clientID, clientSecret, tenantID, templ) return ret } diff --git a/vault/resource_secrets_sync_gcp_destination.go b/vault/resource_secrets_sync_gcp_destination.go index 316bfa660..556104175 100644 --- a/vault/resource_secrets_sync_gcp_destination.go +++ b/vault/resource_secrets_sync_gcp_destination.go @@ -5,22 +5,40 @@ package vault import ( "context" - "log" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-vault/internal/consts" "github.com/hashicorp/terraform-provider-vault/internal/provider" + syncutil "github.com/hashicorp/terraform-provider-vault/internal/sync" ) -var gcpSyncDestinationFields = []string{ +const ( + gcpSyncType = "gcp-sm" +) + +var gcpSyncWriteFields = []string{ + consts.FieldCredentials, + consts.FieldSecretNameTemplate, + consts.FieldCustomTags, +} + +var gcpSyncUpdateFields = []string{ consts.FieldCredentials, + // consts.FieldSecretNameTemplate, + // consts.FieldCustomTags, +} + +var gcpSyncReadFields = []string{ + consts.FieldSecretNameTemplate, + consts.FieldCustomTags, } func gcpSecretsSyncDestinationResource() *schema.Resource { - return &schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(gcpSecretsSyncDestinationWrite, provider.VaultVersion115), + return provider.MustAddSecretsSyncCloudSchema(&schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(gcpSecretsSyncDestinationWrite, provider.VaultVersion116), + UpdateContext: gcpSecretsSyncDestinationUpdate, ReadContext: provider.ReadContextWrapper(gcpSecretsSyncDestinationRead), DeleteContext: gcpSecretsSyncDestinationDelete, Importer: &schema.ResourceImporter{ @@ -38,86 +56,24 @@ func gcpSecretsSyncDestinationResource() *schema.Resource { Type: schema.TypeString, Required: true, Sensitive: true, - Description: "JSON credentials (either file contents or '@path/to/file').", - ForceNew: true, + Description: "JSON-encoded credentials to use to connect to GCP.", }, }, - } + }) } func gcpSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - name := d.Get(consts.FieldName).(string) - path := gcpSecretsSyncDestinationPath(name) - - data := map[string]interface{}{} - - for _, k := range gcpSyncDestinationFields { - data[k] = d.Get(k) - } - - log.Printf("[DEBUG] Writing GCP sync destination to %q", path) - _, err := client.Logical().WriteWithContext(ctx, path, data) - if err != nil { - return diag.Errorf("error enabling GCP sync destination %q: %s", path, err) - } - log.Printf("[DEBUG] Enabled GCP sync destination %q", path) - - d.SetId(name) + return syncutil.SyncDestinationWrite(ctx, d, meta, gcpSyncType, gcpSyncWriteFields, gcpSyncReadFields) +} - return gcpSecretsSyncDestinationRead(ctx, d, meta) +func gcpSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationUpdate(ctx, d, meta, gcpSyncType, gcpSyncUpdateFields, gcpSyncReadFields) } func gcpSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - name := d.Id() - path := gcpSecretsSyncDestinationPath(name) - - log.Printf("[DEBUG] Reading GCP sync destination") - resp, err := client.Logical().ReadWithContext(ctx, path) - if err != nil { - return diag.Errorf("error reading GCP sync destination from %q: %s", path, err) - } - log.Printf("[DEBUG] Read GCP sync destination") - - if resp == nil { - log.Printf("[WARN] No info found at %q; removing from state.", path) - d.SetId("") - return nil - } - - if err := d.Set(consts.FieldName, name); err != nil { - return diag.FromErr(err) - } - - return nil + return syncutil.SyncDestinationRead(ctx, d, meta, gcpSyncType, gcpSyncReadFields) } func gcpSecretsSyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - path := gcpSecretsSyncDestinationPath(d.Id()) - - log.Printf("[DEBUG] Deleting GCP sync destination at %q", path) - _, err := client.Logical().DeleteWithContext(ctx, path) - if err != nil { - return diag.Errorf("error deleting GCP sync destination at %q: %s", path, err) - } - log.Printf("[DEBUG] Deleted GCP sync destination at %q", path) - - return nil -} - -func gcpSecretsSyncDestinationPath(name string) string { - return "sys/sync/destinations/gcp-sm/" + name + return syncutil.SyncDestinationDelete(ctx, d, meta, gcpSyncType) } diff --git a/vault/resource_secrets_sync_gcp_destination_test.go b/vault/resource_secrets_sync_gcp_destination_test.go index 78d6f207d..0c05181ad 100644 --- a/vault/resource_secrets_sync_gcp_destination_test.go +++ b/vault/resource_secrets_sync_gcp_destination_test.go @@ -18,26 +18,26 @@ import ( func TestGCPSecretsSyncDestination(t *testing.T) { destName := acctest.RandomWithPrefix("tf-sync-dest-gcp") - resourceName := "vault_gcp_secrets_sync_destination.test" + resourceName := "vault_secrets_sync_gcp_destination.test" - values := testutil.SkipTestEnvUnset(t, - "GOOGLE_APPLICATION_CREDENTIALS", - ) - credentials := values[0] + credentials, _ := testutil.GetTestGCPCreds(t) resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, PreCheck: func() { testutil.TestAccPreCheck(t) - SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion116) }, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ { - Config: testGCPSecretsSyncDestinationConfig(credentials, destName), + Config: testGCPSecretsSyncDestinationConfig(credentials, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), resource.TestCheckResourceAttr(resourceName, consts.FieldCredentials, credentials), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, defaultSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, "custom_tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), }, testutil.GetImportTestStep(resourceName, false, nil, @@ -46,13 +46,18 @@ func TestGCPSecretsSyncDestination(t *testing.T) { }) } -func testGCPSecretsSyncDestinationConfig(credentials, destName string) string { +func testGCPSecretsSyncDestinationConfig(credentials, destName, templ string) string { ret := fmt.Sprintf(` -resource "vault_gcp_secrets_sync_destination" "test" { - name = "%s" - credentials = "%s" +resource "vault_secrets_sync_gcp_destination" "test" { + name = "%s" + credentials = < Date: Wed, 10 Jan 2024 17:30:25 -0800 Subject: [PATCH 14/24] fix association fields --- vault/resource_secrets_sync_association.go | 61 ++++++++++++------- ...ource_secrets_sync_aws_destination_test.go | 2 +- ...ource_secrets_sync_gcp_destination_test.go | 1 + 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/vault/resource_secrets_sync_association.go b/vault/resource_secrets_sync_association.go index aa5913671..5582c6d5b 100644 --- a/vault/resource_secrets_sync_association.go +++ b/vault/resource_secrets_sync_association.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "log" + "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -18,20 +19,18 @@ import ( const ( fieldSecretName = "secret_name" fieldSyncStatus = "sync_status" - fieldAccessor = "accessor" fieldUpdatedAt = "updated_at" ) var secretsSyncAssociationFields = []string{ fieldSecretName, fieldSyncStatus, - fieldAccessor, fieldUpdatedAt, } func secretsSyncAssociationResource() *schema.Resource { return &schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(secretsSyncAssociationWrite, provider.VaultVersion115), + CreateContext: provider.MountCreateContextWrapper(secretsSyncAssociationWrite, provider.VaultVersion116), ReadContext: provider.ReadContextWrapper(secretsSyncAssociationRead), DeleteContext: secretsSyncAssociationDelete, @@ -39,13 +38,13 @@ func secretsSyncAssociationResource() *schema.Resource { consts.FieldName: { Type: schema.TypeString, Required: true, - Description: "Name of the store/destination.", + Description: "Name of the destination.", ForceNew: true, }, consts.FieldType: { Type: schema.TypeString, Required: true, - Description: "Type of secrets store.", + Description: "Type of sync destination.", ForceNew: true, }, consts.FieldMount: { @@ -54,13 +53,6 @@ func secretsSyncAssociationResource() *schema.Resource { ForceNew: true, Description: "Specifies the mount where the secret is located.", }, - fieldAccessor: { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Specifies the unique accessor of the mount.", - ValidateFunc: provider.ValidateNoLeadingTrailingSlashes, - }, fieldSecretName: { Type: schema.TypeString, Required: true, @@ -89,15 +81,14 @@ func secretsSyncAssociationWrite(ctx context.Context, d *schema.ResourceData, me name := d.Get(consts.FieldName).(string) destType := d.Get(consts.FieldType).(string) - path := secretsSyncAssociationSetPath(name, destType) + secretName := d.Get(fieldSecretName).(string) + mount := d.Get(consts.FieldMount).(string) - data := map[string]interface{}{} + path := secretsSyncAssociationSetPath(name, destType) - for _, k := range []string{ - fieldSecretName, - consts.FieldMount, - } { - data[k] = d.Get(k) + data := map[string]interface{}{ + fieldSecretName: secretName, + consts.FieldMount: mount, } log.Printf("[DEBUG] Writing association to %q", path) @@ -108,11 +99,14 @@ func secretsSyncAssociationWrite(ctx context.Context, d *schema.ResourceData, me log.Printf("[DEBUG] Wrote association to %q", path) // expect accessor to be provided from mount - accessor := d.Get(fieldAccessor).(string) + accessor, err := getMountAccessor(ctx, d, meta) + if err != nil { + return diag.Errorf("could not obtain accessor from given mount; err=%s", err) + } // associations can't be read from Vault // set data that is received from Vault upon writes - vaultRespKey := fmt.Sprintf("%s/%s", accessor, name) + vaultRespKey := fmt.Sprintf("%s/%s", accessor, secretName) associatedSecrets := "associated_secrets" secretData, ok := resp.Data[associatedSecrets] if !ok { @@ -180,3 +174,28 @@ func secretsSyncAssociationSetPath(name, destType string) string { func secretsSyncAssociationDeletePath(name, destType string) string { return fmt.Sprintf("sys/sync/destinations/%s/%s/associations/remove", destType, name) } + +func getMountAccessor(ctx context.Context, d *schema.ResourceData, meta interface{}) (string, error) { + client, e := provider.GetClient(d, meta) + if e != nil { + return "", e + } + + mount := d.Get(consts.FieldMount).(string) + + log.Printf("[DEBUG] Reading mount %s from Vault", mount) + mounts, err := client.Sys().ListMountsWithContext(ctx) + if err != nil { + return "", err + } + + // path can have a trailing slash, but doesn't need to have one + // this standardises on having a trailing slash, which is how the + // API always responds. + m, ok := mounts[strings.Trim(mount, "/")+"/"] + if !ok { + return "", fmt.Errorf("expected mount at %s; no mount found", mount) + } + + return m.Accessor, nil +} diff --git a/vault/resource_secrets_sync_aws_destination_test.go b/vault/resource_secrets_sync_aws_destination_test.go index 68ea9265a..ca3132429 100644 --- a/vault/resource_secrets_sync_aws_destination_test.go +++ b/vault/resource_secrets_sync_aws_destination_test.go @@ -16,7 +16,7 @@ import ( ) const ( - defaultSecretsSyncTemplate = "new-name-template-{{ .SecretPath }}" + defaultSecretsSyncTemplate = "VAULT_{{ .MountAccessor | uppercase }}_{{ .SecretPath | uppercase }}" ) func TestAWSSecretsSyncDestination(t *testing.T) { diff --git a/vault/resource_secrets_sync_gcp_destination_test.go b/vault/resource_secrets_sync_gcp_destination_test.go index 0c05181ad..f34ca8e49 100644 --- a/vault/resource_secrets_sync_gcp_destination_test.go +++ b/vault/resource_secrets_sync_gcp_destination_test.go @@ -36,6 +36,7 @@ func TestGCPSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), resource.TestCheckResourceAttr(resourceName, consts.FieldCredentials, credentials), resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, defaultSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, consts.FieldType, gcpSyncType), resource.TestCheckResourceAttr(resourceName, "custom_tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), From fd2872945efb93008b482e1a80fb6c0968db210f Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 10 Jan 2024 17:48:31 -0800 Subject: [PATCH 15/24] fix incorrect 1.16 provider version --- internal/consts/consts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index a780e7904..e69539eb0 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -437,7 +437,7 @@ const ( VaultVersion113 = "1.13.0" VaultVersion114 = "1.14.0" VaultVersion115 = "1.15.0" - VaultVersion116 = "1.16.0-beta1+ent" + VaultVersion116 = "1.16.0" /* Vault auth methods From 3ec8ae8b7c6f308843a8d4ca9c2ac712a112cf29 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Thu, 11 Jan 2024 11:06:32 -0800 Subject: [PATCH 16/24] make rest of env var fields optional --- internal/provider/schema_util.go | 1 + vault/resource_secrets_sync_azure_destination.go | 9 ++++----- vault/resource_secrets_sync_gcp_destination.go | 2 +- vault/resource_secrets_sync_gh_destination.go | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/provider/schema_util.go b/internal/provider/schema_util.go index 93cb40110..71af69e0e 100644 --- a/internal/provider/schema_util.go +++ b/internal/provider/schema_util.go @@ -77,6 +77,7 @@ func MustAddSecretsSyncCommonSchema(r *schema.Resource) *schema.Resource { consts.FieldSecretNameTemplate: { Type: schema.TypeString, Optional: true, + Computed: true, Description: "Template describing how to generate external secret names.", // @TODO can this be updated? ForceNew: true, diff --git a/vault/resource_secrets_sync_azure_destination.go b/vault/resource_secrets_sync_azure_destination.go index 410b382a5..fce97a539 100644 --- a/vault/resource_secrets_sync_azure_destination.go +++ b/vault/resource_secrets_sync_azure_destination.go @@ -43,7 +43,6 @@ func azureSecretsSyncDestinationResource() *schema.Resource { return &schema.Resource{ CreateContext: provider.MountCreateContextWrapper(azureSecretsSyncDestinationWrite, provider.VaultVersion115), ReadContext: provider.ReadContextWrapper(azureSecretsSyncDestinationRead), - // @TODO confirm this is available UpdateContext: azureSecretsSyncDestinationUpdate, DeleteContext: azureSecretsSyncDestinationDelete, Importer: &schema.ResourceImporter{ @@ -59,24 +58,24 @@ func azureSecretsSyncDestinationResource() *schema.Resource { }, fieldKeyVaultURI: { Type: schema.TypeString, - Required: true, + Optional: true, Description: "URI of an existing Azure Key Vault instance.", ForceNew: true, }, consts.FieldClientID: { Type: schema.TypeString, - Required: true, + Optional: true, Description: "Client ID of an Azure app registration.", }, consts.FieldClientSecret: { Type: schema.TypeString, - Required: true, + Optional: true, Sensitive: true, Description: "Client Secret of an Azure app registration.", }, consts.FieldTenantID: { Type: schema.TypeString, - Required: true, + Optional: true, Description: "ID of the target Azure tenant.", ForceNew: true, }, diff --git a/vault/resource_secrets_sync_gcp_destination.go b/vault/resource_secrets_sync_gcp_destination.go index 556104175..dfe326e60 100644 --- a/vault/resource_secrets_sync_gcp_destination.go +++ b/vault/resource_secrets_sync_gcp_destination.go @@ -54,7 +54,7 @@ func gcpSecretsSyncDestinationResource() *schema.Resource { }, consts.FieldCredentials: { Type: schema.TypeString, - Required: true, + Optional: true, Sensitive: true, Description: "JSON-encoded credentials to use to connect to GCP.", }, diff --git a/vault/resource_secrets_sync_gh_destination.go b/vault/resource_secrets_sync_gh_destination.go index 7e8b4fc6c..c255883e3 100644 --- a/vault/resource_secrets_sync_gh_destination.go +++ b/vault/resource_secrets_sync_gh_destination.go @@ -57,19 +57,19 @@ func githubSecretsSyncDestinationResource() *schema.Resource { }, fieldAccessToken: { Type: schema.TypeString, - Required: true, + Optional: true, Sensitive: true, Description: "Fine-grained or personal access token.", }, fieldRepositoryOwner: { Type: schema.TypeString, - Required: true, + Optional: true, Description: "GitHub organization or username that owns the repository.", ForceNew: true, }, fieldRepositoryName: { Type: schema.TypeString, - Required: true, + Optional: true, Description: "Name of the repository.", ForceNew: true, }, From f9a5ed7b49b06f74eb3b490b2a1e957d2265a492 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Fri, 12 Jan 2024 10:22:59 -0800 Subject: [PATCH 17/24] format --- internal/consts/consts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/consts/consts.go b/internal/consts/consts.go index b2ed4ceef..3f4ad9219 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -373,7 +373,7 @@ const ( FieldPermanentlyDelete = "permanently_delete" FieldSignInAudience = "sign_in_audience" FieldTags = "tags" - FieldCustomTags = "custom_tags" + FieldCustomTags = "custom_tags" FieldSecretNameTemplate = "secret_name_template" /* From 20ad80acb9974f19377a83944fcdf419aa3637a3 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Tue, 16 Jan 2024 14:40:35 -0800 Subject: [PATCH 18/24] make tags and template editable --- internal/provider/schema_util.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/provider/schema_util.go b/internal/provider/schema_util.go index 71af69e0e..da60dd987 100644 --- a/internal/provider/schema_util.go +++ b/internal/provider/schema_util.go @@ -79,8 +79,6 @@ func MustAddSecretsSyncCommonSchema(r *schema.Resource) *schema.Resource { Optional: true, Computed: true, Description: "Template describing how to generate external secret names.", - // @TODO can this be updated? - ForceNew: true, }, }) @@ -93,8 +91,6 @@ func MustAddSecretsSyncCloudSchema(r *schema.Resource) *schema.Resource { Type: schema.TypeMap, Optional: true, Description: "Custom tags to set on the secret managed at the destination.", - // @TODO can this be updated? - ForceNew: true, }, }) From 86ed21bcb6f9ed3790b38bcbc9c6a0777efc53aa Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Tue, 16 Jan 2024 14:43:08 -0800 Subject: [PATCH 19/24] consolidate write ops to single method and add tests for updates --- internal/sync/secrets_sync.go | 37 ++--------- .../resource_secrets_sync_aws_destination.go | 18 ++---- ...ource_secrets_sync_aws_destination_test.go | 64 +++++++++++++++++-- ...resource_secrets_sync_azure_destination.go | 21 ++---- ...rce_secrets_sync_azure_destination_test.go | 42 ++++++++++-- .../resource_secrets_sync_gcp_destination.go | 18 ++---- ...ource_secrets_sync_gcp_destination_test.go | 37 +++++++++-- vault/resource_secrets_sync_gh_destination.go | 13 ++-- ...source_secrets_sync_gh_destination_test.go | 33 ++++++++-- ...esource_secrets_sync_vercel_destination.go | 18 ++---- ...ce_secrets_sync_vercel_destination_test.go | 36 +++++++++-- 11 files changed, 214 insertions(+), 123 deletions(-) diff --git a/internal/sync/secrets_sync.go b/internal/sync/secrets_sync.go index f704188d6..243248ab2 100644 --- a/internal/sync/secrets_sync.go +++ b/internal/sync/secrets_sync.go @@ -17,7 +17,7 @@ const ( fieldOptions = "options" ) -func SyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string, writeFields, readFields []string) diag.Diagnostics { +func SyncDestinationCreateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string, writeFields, readFields []string) diag.Diagnostics { client, e := provider.GetClient(d, meta) if e != nil { return diag.FromErr(e) @@ -34,14 +34,16 @@ func SyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta inte } } - log.Printf("[DEBUG] Writing sync destination to %q", path) + log.Printf("[DEBUG] Writing sync destination data to %q", path) _, err := client.Logical().WriteWithContext(ctx, path, data) if err != nil { - return diag.Errorf("error enabling sync destination %q: %s", path, err) + return diag.Errorf("error writing sync destination data to %q: %s", path, err) } - log.Printf("[DEBUG] Enabled sync destination %q", path) + log.Printf("[DEBUG] Wrote sync destination data to %q", path) - d.SetId(name) + if d.IsNewResource() { + d.SetId(name) + } return SyncDestinationRead(ctx, d, meta, typ, readFields) } @@ -99,31 +101,6 @@ func SyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta inter return nil } -func SyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string, updateFields, readFields []string) diag.Diagnostics { - client, e := provider.GetClient(d, meta) - if e != nil { - return diag.FromErr(e) - } - - path := secretsSyncDestinationPath(d.Id(), typ) - - data := map[string]interface{}{} - - for _, k := range updateFields { - data[k] = d.Get(k) - } - - log.Printf("[DEBUG] Updating sync destination at %q", path) - // @TODO confirm if this should be a JSONMergePatch instead - _, err := client.Logical().WriteWithContext(ctx, path, data) - if err != nil { - return diag.Errorf("error updating sync destination %q: %s", path, err) - } - log.Printf("[DEBUG] Updated sync destination %q", path) - - return SyncDestinationRead(ctx, d, meta, typ, readFields) -} - func SyncDestinationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}, typ string) diag.Diagnostics { client, e := provider.GetClient(d, meta) if e != nil { diff --git a/vault/resource_secrets_sync_aws_destination.go b/vault/resource_secrets_sync_aws_destination.go index b25b713d2..c3dc81b4a 100644 --- a/vault/resource_secrets_sync_aws_destination.go +++ b/vault/resource_secrets_sync_aws_destination.go @@ -37,17 +37,11 @@ var awsSyncReadFields = []string{ consts.FieldSecretNameTemplate, } -// awsSyncUpdateFields contains all fields that can be updated via the API -var awsSyncUpdateFields = []string{ - fieldAccessKeyID, - fieldSecretAccessKey, -} - func awsSecretsSyncDestinationResource() *schema.Resource { return provider.MustAddSecretsSyncCloudSchema(&schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(awsSecretsSyncDestinationWrite, provider.VaultVersion116), + CreateContext: provider.MountCreateContextWrapper(awsSecretsSyncDestinationCreateUpdate, provider.VaultVersion116), ReadContext: provider.ReadContextWrapper(awsSecretsSyncDestinationRead), - UpdateContext: awsSecretsSyncDestinationUpdate, + UpdateContext: awsSecretsSyncDestinationCreateUpdate, DeleteContext: awsSecretsSyncDestinationDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -82,12 +76,8 @@ func awsSecretsSyncDestinationResource() *schema.Resource { }) } -func awsSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return syncutil.SyncDestinationWrite(ctx, d, meta, awsSyncType, awsSyncWriteFields, awsSyncReadFields) -} - -func awsSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return syncutil.SyncDestinationUpdate(ctx, d, meta, awsSyncType, awsSyncUpdateFields, awsSyncReadFields) +func awsSecretsSyncDestinationCreateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationCreateUpdate(ctx, d, meta, awsSyncType, awsSyncWriteFields, awsSyncReadFields) } func awsSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/vault/resource_secrets_sync_aws_destination_test.go b/vault/resource_secrets_sync_aws_destination_test.go index ca3132429..5d71ec6b4 100644 --- a/vault/resource_secrets_sync_aws_destination_test.go +++ b/vault/resource_secrets_sync_aws_destination_test.go @@ -16,7 +16,8 @@ import ( ) const ( - defaultSecretsSyncTemplate = "VAULT_{{ .MountAccessor | uppercase }}_{{ .SecretPath | uppercase }}" + defaultSecretsSyncTemplate = "vault/{{ .MountAccessor }}/{{ .SecretPath }}" + updatedSecretsSyncTemplate = "VAULT_{{ .MountAccessor | uppercase }}_{{ .SecretPath | uppercase }}" ) func TestAWSSecretsSyncDestination(t *testing.T) { @@ -34,7 +35,7 @@ func TestAWSSecretsSyncDestination(t *testing.T) { }, Steps: []resource.TestStep{ { - Config: testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName, defaultSecretsSyncTemplate), + Config: testAWSSecretsSyncDestinationConfig_initial(accessKey, secretKey, region, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), @@ -47,6 +48,21 @@ func TestAWSSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), }, + { + Config: testAWSSecretsSyncDestinationConfig_updated(accessKey, secretKey, region, destName, updatedSecretsSyncTemplate), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, fieldAccessKeyID, accessKey), + resource.TestCheckResourceAttr(resourceName, fieldSecretAccessKey, secretKey), + resource.TestCheckResourceAttr(resourceName, consts.FieldRegion, region), + resource.TestCheckResourceAttr(resourceName, consts.FieldType, awsSyncType), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, updatedSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, "custom_tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.baz", "bux"), + ), + }, testutil.GetImportTestStep(resourceName, false, nil, fieldAccessKeyID, fieldSecretAccessKey, @@ -55,19 +71,55 @@ func TestAWSSecretsSyncDestination(t *testing.T) { }) } -func testAWSSecretsSyncDestinationConfig(accessKey, secretKey, region, destName, templ string) string { +func testAWSSecretsSyncDestinationConfig_initial(accessKey, secretKey, region, destName, templ string) string { ret := fmt.Sprintf(` resource "vault_secrets_sync_aws_destination" "test" { name = "%s" access_key_id = "%s" secret_access_key = "%s" region = "%s" + %s +} +`, destName, accessKey, secretKey, region, testSecretsSyncDestinationCommonConfig(templ, false, true, false)) + + return ret +} + +func testAWSSecretsSyncDestinationConfig_updated(accessKey, secretKey, region, destName, templ string) string { + ret := fmt.Sprintf(` +resource "vault_secrets_sync_aws_destination" "test" { + name = "%s" + access_key_id = "%s" + secret_access_key = "%s" + region = "%s" + %s +} +`, destName, accessKey, secretKey, region, testSecretsSyncDestinationCommonConfig(templ, true, true, true)) + + return ret +} + +func testSecretsSyncDestinationCommonConfig(templ string, withTemplate, withTags, update bool) string { + ret := "" + if withTemplate { + ret += fmt.Sprintf(` secret_name_template = "%s" +`, templ) + } + + if withTags && !update { + ret += fmt.Sprintf(` custom_tags = { "foo" = "bar" } -} -`, destName, accessKey, secretKey, region, templ) - +`) + } else if withTags && update { + ret += fmt.Sprintf(` + custom_tags = { + "foo" = "bar" + "baz" = "bux" + } +`) + } return ret } diff --git a/vault/resource_secrets_sync_azure_destination.go b/vault/resource_secrets_sync_azure_destination.go index fce97a539..552758c02 100644 --- a/vault/resource_secrets_sync_azure_destination.go +++ b/vault/resource_secrets_sync_azure_destination.go @@ -28,11 +28,6 @@ var azureSyncWriteFields = []string{ consts.FieldTenantID, } -var azureSyncUpdateFields = []string{ - consts.FieldClientSecret, - consts.FieldClientID, -} - var azureSyncReadFields = []string{ fieldCloud, consts.FieldClientID, @@ -40,10 +35,10 @@ var azureSyncReadFields = []string{ } func azureSecretsSyncDestinationResource() *schema.Resource { - return &schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(azureSecretsSyncDestinationWrite, provider.VaultVersion115), + return provider.MustAddSecretsSyncCloudSchema(&schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(azureSecretsSyncDestinationCreateUpdate, provider.VaultVersion115), ReadContext: provider.ReadContextWrapper(azureSecretsSyncDestinationRead), - UpdateContext: azureSecretsSyncDestinationUpdate, + UpdateContext: azureSecretsSyncDestinationCreateUpdate, DeleteContext: azureSecretsSyncDestinationDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -86,15 +81,11 @@ func azureSecretsSyncDestinationResource() *schema.Resource { ForceNew: true, }, }, - } -} - -func azureSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return syncutil.SyncDestinationWrite(ctx, d, meta, azureSyncType, azureSyncWriteFields, azureSyncReadFields) + }) } -func azureSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return syncutil.SyncDestinationUpdate(ctx, d, meta, azureSyncType, azureSyncUpdateFields, azureSyncReadFields) +func azureSecretsSyncDestinationCreateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationCreateUpdate(ctx, d, meta, azureSyncType, azureSyncWriteFields, azureSyncReadFields) } func azureSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/vault/resource_secrets_sync_azure_destination_test.go b/vault/resource_secrets_sync_azure_destination_test.go index f1d51ca8e..7e504ea9f 100644 --- a/vault/resource_secrets_sync_azure_destination_test.go +++ b/vault/resource_secrets_sync_azure_destination_test.go @@ -39,7 +39,7 @@ func TestAzureSecretsSyncDestination(t *testing.T) { }, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ { - Config: testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName, defaultSecretsSyncTemplate), + Config: testAzureSecretsSyncDestinationConfig_initial(keyVaultURI, clientID, clientSecret, tenantID, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), @@ -53,6 +53,22 @@ func TestAzureSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), }, + { + Config: testAzureSecretsSyncDestinationConfig_initial(keyVaultURI, clientID, clientSecret, tenantID, destName, updatedSecretsSyncTemplate), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, consts.FieldClientSecret, clientSecret), + resource.TestCheckResourceAttr(resourceName, consts.FieldClientID, clientID), + resource.TestCheckResourceAttr(resourceName, consts.FieldTenantID, tenantID), + resource.TestCheckResourceAttr(resourceName, fieldKeyVaultURI, keyVaultURI), + resource.TestCheckResourceAttr(resourceName, fieldCloud, "cloud"), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, updatedSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, "custom_tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.baz", "bux"), + ), + }, testutil.GetImportTestStep(resourceName, false, nil, consts.FieldClientSecret, ), @@ -60,7 +76,22 @@ func TestAzureSecretsSyncDestination(t *testing.T) { }) } -func testAzureSecretsSyncDestinationConfig(keyVaultURI, clientID, clientSecret, tenantID, destName, templ string) string { +func testAzureSecretsSyncDestinationConfig_initial(keyVaultURI, clientID, clientSecret, tenantID, destName, templ string) string { + ret := fmt.Sprintf(` +resource "vault_secrets_sync_azure_destination" "test" { + name = "%s" + key_vault_uri = "%s" + client_id = "%s" + client_secret = "%s" + tenant_id = "%s" + %s +} +`, destName, keyVaultURI, clientID, clientSecret, tenantID, testSecretsSyncDestinationCommonConfig(templ, true, true, false)) + + return ret +} + +func testAzureSecretsSyncDestinationConfig_updated(keyVaultURI, clientID, clientSecret, tenantID, destName, templ string) string { ret := fmt.Sprintf(` resource "vault_secrets_sync_azure_destination" "test" { name = "%s" @@ -68,12 +99,9 @@ resource "vault_secrets_sync_azure_destination" "test" { client_id = "%s" client_secret = "%s" tenant_id = "%s" - secret_name_template = "%s" - custom_tags = { - "foo" = "bar" - } + %s } -`, destName, keyVaultURI, clientID, clientSecret, tenantID, templ) +`, destName, keyVaultURI, clientID, clientSecret, tenantID, testSecretsSyncDestinationCommonConfig(templ, true, true, true)) return ret } diff --git a/vault/resource_secrets_sync_gcp_destination.go b/vault/resource_secrets_sync_gcp_destination.go index dfe326e60..8424db88b 100644 --- a/vault/resource_secrets_sync_gcp_destination.go +++ b/vault/resource_secrets_sync_gcp_destination.go @@ -24,12 +24,6 @@ var gcpSyncWriteFields = []string{ consts.FieldCustomTags, } -var gcpSyncUpdateFields = []string{ - consts.FieldCredentials, - // consts.FieldSecretNameTemplate, - // consts.FieldCustomTags, -} - var gcpSyncReadFields = []string{ consts.FieldSecretNameTemplate, consts.FieldCustomTags, @@ -37,8 +31,8 @@ var gcpSyncReadFields = []string{ func gcpSecretsSyncDestinationResource() *schema.Resource { return provider.MustAddSecretsSyncCloudSchema(&schema.Resource{ - CreateContext: provider.MountCreateContextWrapper(gcpSecretsSyncDestinationWrite, provider.VaultVersion116), - UpdateContext: gcpSecretsSyncDestinationUpdate, + CreateContext: provider.MountCreateContextWrapper(gcpSecretsSyncDestinationCreateUpdate, provider.VaultVersion116), + UpdateContext: gcpSecretsSyncDestinationCreateUpdate, ReadContext: provider.ReadContextWrapper(gcpSecretsSyncDestinationRead), DeleteContext: gcpSecretsSyncDestinationDelete, Importer: &schema.ResourceImporter{ @@ -62,12 +56,8 @@ func gcpSecretsSyncDestinationResource() *schema.Resource { }) } -func gcpSecretsSyncDestinationWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return syncutil.SyncDestinationWrite(ctx, d, meta, gcpSyncType, gcpSyncWriteFields, gcpSyncReadFields) -} - -func gcpSecretsSyncDestinationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return syncutil.SyncDestinationUpdate(ctx, d, meta, gcpSyncType, gcpSyncUpdateFields, gcpSyncReadFields) +func gcpSecretsSyncDestinationCreateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return syncutil.SyncDestinationCreateUpdate(ctx, d, meta, gcpSyncType, gcpSyncWriteFields, gcpSyncReadFields) } func gcpSecretsSyncDestinationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/vault/resource_secrets_sync_gcp_destination_test.go b/vault/resource_secrets_sync_gcp_destination_test.go index f34ca8e49..734ef3ddb 100644 --- a/vault/resource_secrets_sync_gcp_destination_test.go +++ b/vault/resource_secrets_sync_gcp_destination_test.go @@ -30,7 +30,7 @@ func TestGCPSecretsSyncDestination(t *testing.T) { }, PreventPostDestroyRefresh: true, Steps: []resource.TestStep{ { - Config: testGCPSecretsSyncDestinationConfig(credentials, destName, defaultSecretsSyncTemplate), + Config: testGCPSecretsSyncDestinationConfig_initial(credentials, destName, defaultSecretsSyncTemplate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), @@ -41,24 +41,47 @@ func TestGCPSecretsSyncDestination(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), ), }, + { + Config: testGCPSecretsSyncDestinationConfig_updated(credentials, destName, updatedSecretsSyncTemplate), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, consts.FieldName, destName), + resource.TestCheckResourceAttr(resourceName, consts.FieldCredentials, credentials), + resource.TestCheckResourceAttr(resourceName, consts.FieldSecretNameTemplate, updatedSecretsSyncTemplate), + resource.TestCheckResourceAttr(resourceName, consts.FieldType, gcpSyncType), + resource.TestCheckResourceAttr(resourceName, "custom_tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.foo", "bar"), + resource.TestCheckResourceAttr(resourceName, "custom_tags.baz", "bux"), + ), + }, testutil.GetImportTestStep(resourceName, false, nil, consts.FieldCredentials), }, }) } -func testGCPSecretsSyncDestinationConfig(credentials, destName, templ string) string { +func testGCPSecretsSyncDestinationConfig_initial(credentials, destName, templ string) string { + ret := fmt.Sprintf(` +resource "vault_secrets_sync_gcp_destination" "test" { + name = "%s" + credentials = < Date: Tue, 16 Jan 2024 15:12:08 -0800 Subject: [PATCH 20/24] set kv uri for azure on reads --- vault/resource_secrets_sync_azure_destination.go | 1 + 1 file changed, 1 insertion(+) diff --git a/vault/resource_secrets_sync_azure_destination.go b/vault/resource_secrets_sync_azure_destination.go index 552758c02..72a3c2f8b 100644 --- a/vault/resource_secrets_sync_azure_destination.go +++ b/vault/resource_secrets_sync_azure_destination.go @@ -29,6 +29,7 @@ var azureSyncWriteFields = []string{ } var azureSyncReadFields = []string{ + fieldKeyVaultURI, fieldCloud, consts.FieldClientID, consts.FieldTenantID, From cf042c85e0dcc8d0d2edfb12f8cfc19c9fa18aca Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Tue, 16 Jan 2024 15:19:08 -0800 Subject: [PATCH 21/24] add changelog entry with version requirement --- CHANGELOG.md | 1 + internal/sync/secrets_sync.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad38b68e..09e0d8be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ FEATURES: * Add support for `ext_key_usage_oids` in `vault_pki_secret_backend_role` ([#2108](https://github.com/hashicorp/terraform-provider-vault/pull/2108)) * Adds support to `vault_gcp_auth_backend` for common backend tune parameters ([#1997](https://github.com/terraform-providers/terraform-provider-vault/pull/1997)). +* Add destination and association resources to support Secrets Sync. *Requires Vault 1.16+* ([#2098](https://github.com/hashicorp/terraform-provider-vault/pull/2098)). BUGS: * fix `vault_kv_secret_v2` drift when "data" is in secret name/path ([#2104](https://github.com/hashicorp/terraform-provider-vault/pull/2104)) diff --git a/internal/sync/secrets_sync.go b/internal/sync/secrets_sync.go index 243248ab2..04960c979 100644 --- a/internal/sync/secrets_sync.go +++ b/internal/sync/secrets_sync.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package syncutil import ( From a0638efa0dc99d6d2df8d4d4dee5bf97739dc287 Mon Sep 17 00:00:00 2001 From: vinay-gopalan <86625824+vinay-gopalan@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:39:35 -0800 Subject: [PATCH 22/24] Add Secrets Sync documentation (#2127) --- .../docs/r/secrets_sync_association.html.md | 83 ++++++++++++++++++ .../r/secrets_sync_aws_destination.html.md | 78 +++++++++++++++++ .../r/secrets_sync_azure_destination.html.md | 85 +++++++++++++++++++ .../r/secrets_sync_gcp_destination.html.md | 68 +++++++++++++++ .../r/secrets_sync_gh_destination.html.md | 73 ++++++++++++++++ .../r/secrets_sync_vercel_destination.html.md | 71 ++++++++++++++++ website/vault.erb | 26 ++++++ 7 files changed, 484 insertions(+) create mode 100644 website/docs/r/secrets_sync_association.html.md create mode 100644 website/docs/r/secrets_sync_aws_destination.html.md create mode 100644 website/docs/r/secrets_sync_azure_destination.html.md create mode 100644 website/docs/r/secrets_sync_gcp_destination.html.md create mode 100644 website/docs/r/secrets_sync_gh_destination.html.md create mode 100644 website/docs/r/secrets_sync_vercel_destination.html.md diff --git a/website/docs/r/secrets_sync_association.html.md b/website/docs/r/secrets_sync_association.html.md new file mode 100644 index 000000000..c9f429b2c --- /dev/null +++ b/website/docs/r/secrets_sync_association.html.md @@ -0,0 +1,83 @@ +--- +layout: "vault" +page_title: "Vault: vault_secrets_sync_association resource" +sidebar_current: "docs-vault-resource-secrets-sync-association" +description: |- + Triggers a sync operation in Vault and links a secret to an existing destination +--- + +# vault\_secrets\_sync\_association + +Triggers a sync operation in Vault and links a secret to an existing destination. +Requires Vault 1.16+. *Available only for Vault Enterprise*. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +For more information on associations, please refer to the Vault +[documentation](https://developer.hashicorp.com/vault/docs/sync#associations). + +## Example Usage + +```hcl +resource "vault_mount" "kvv2" { + path = "kvv2" + type = "kv" + options = { version = "2" } + description = "KV Version 2 secret engine mount" +} + +resource "vault_kv_secret_v2" "token" { + mount = vault_mount.kvv2.path + name = "token" + data_json = jsonencode( + { + dev = "B!gS3cr3t", + prod = "S3cureP4$$" + } + ) +} + +resource "vault_secrets_sync_gh_destination" "gh" { + name = "gh-dest" + access_token = var.access_token + repository_owner = var.repo_owner + repository_name = "repo-name-example" + secret_name_template = "vault_{{ .MountAccessor | lowercase }}_{{ .SecretPath | lowercase }}" +} + +resource "vault_secrets_sync_association" "gh_token" { + name = vault_secrets_sync_gh_destination.gh.name + type = vault_secrets_sync_gh_destination.gh.type + mount = vault_mount.kvv2.path + secret_name = vault_kv_secret_v2.token.name +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + +* `name` - (Required) Specifies the name of the destination. + +* `type` - (Required) Specifies the destination type. + +* `mount` - (Required) Specifies the mount where the secret is located. + +* `secret_name` - (Required) Specifies the name of the secret to synchronize. + +## Attributes Reference + +The following attributes are exported in addition to the above: + +* `sync_status` - Specifies the status of the association (for eg. `SYNCED`). + +* `updated_at` - Duration string specifying when the secret was last updated. diff --git a/website/docs/r/secrets_sync_aws_destination.html.md b/website/docs/r/secrets_sync_aws_destination.html.md new file mode 100644 index 000000000..4eae0c588 --- /dev/null +++ b/website/docs/r/secrets_sync_aws_destination.html.md @@ -0,0 +1,78 @@ +--- +layout: "vault" +page_title: "Vault: vault_secrets_sync_aws_destination resource" +sidebar_current: "docs-vault-resource-secrets-sync-aws-destination" +description: |- + Creates an AWS destination to synchronize secrets in Vault +--- + +# vault\_secrets\_sync\_aws\_destination + +Creates an AWS destination to synchronize secrets in Vault. Requires Vault 1.16+. +*Available only for Vault Enterprise*. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +For more information on syncing secrets with AWS, please refer to the Vault +[documentation](https://developer.hashicorp.com/vault/docs/sync/awssm). + +## Example Usage + +```hcl +resource "vault_secrets_sync_aws_destination" "aws" { + name = "aws-dest" + access_key_id = var.access_key_id + secret_access_key = var.secret_access_key + region = "us-east-1" + secret_name_template = "vault_{{ .MountAccessor | lowercase }}_{{ .SecretPath | lowercase }}" + custom_tags = { + "foo" = "bar" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + +* `name` - (Required) Unique name of the AWS destination. + +* `access_key_id` - (Optional) Access key id to authenticate against the AWS secrets manager. + Can be omitted and directly provided to Vault using the `AWS_ACCESS_KEY_ID` environment + variable. + +* `secret_access_key` - (Optional) Secret access key to authenticate against the AWS secrets manager. + Can be omitted and directly provided to Vault using the `AWS_SECRET_ACCESS_KEY` environment + variable. + +* `region` - (Optional) Region where to manage the secrets manager entries. + Can be omitted and directly provided to Vault using the `AWS_REGION` environment + variable. + +* `custom_tags` - (Optional) Custom tags to set on the secret managed at the destination. + +* `secret_name_template` - (Optional) Template describing how to generate external secret names. + Supports a subset of the Go Template syntax. + +## Attributes Reference + +The following attributes are exported in addition to the above: + +* `type` - The type of the secrets destination (`aws-sm`). + +## Import + +AWS Secrets sync destinations can be imported using the `name`, e.g. + +``` +$ terraform import vault_secrets_sync_aws_destination.aws aws-dest +``` diff --git a/website/docs/r/secrets_sync_azure_destination.html.md b/website/docs/r/secrets_sync_azure_destination.html.md new file mode 100644 index 000000000..f6921d84b --- /dev/null +++ b/website/docs/r/secrets_sync_azure_destination.html.md @@ -0,0 +1,85 @@ +--- +layout: "vault" +page_title: "Vault: vault_secrets_sync_azure_destination resource" +sidebar_current: "docs-vault-resource-secrets-sync-azure-destination" +description: |- + Creates a Azure destination to synchronize secrets in Vault +--- + +# vault\_secrets\_sync\_azure\_destination + +Creates a Azure Key Vault destination to synchronize secrets in Vault. Requires Vault 1.16+. +*Available only for Vault Enterprise*. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +For more information on syncing secrets with Azure Key Vault, please refer to the Vault +[documentation](https://developer.hashicorp.com/vault/docs/sync/azurekv). + +## Example Usage + +```hcl +resource "vault_secrets_sync_azure_destination" "az" { + name = "az-dest" + key_vault_uri = var.key_vault_uri + client_id = var.client_id + client_secret = var.client_secret + tenant_id = var.tenant_id + secret_name_template = "vault_{{ .MountAccessor | lowercase }}_{{ .SecretPath | lowercase }}" + custom_tags = { + "foo" = "bar" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + +* `name` - (Required) Unique name of the Azure destination. + +* `key_vault_uri` - (Optional) URI of an existing Azure Key Vault instance. + Can be omitted and directly provided to Vault using the `KEY_VAULT_URI` environment + variable. + +* `tenant_id` - (Optional) ID of the target Azure tenant. + Can be omitted and directly provided to Vault using the `AZURE_TENANT_ID` environment + variable. + +* `client_id` - (Optional) Client ID of an Azure app registration. + Can be omitted and directly provided to Vault using the `AZURE_CLIENT_ID` environment + variable. + +* `client_secret` - (Optional) Client Secret of an Azure app registration. + Can be omitted and directly provided to Vault using the `AZURE_CLIENT_SECRET` environment + variable. + +* `cloud` - (Optional) Specifies a cloud for the client. The default is Azure Public Cloud. + +* `custom_tags` - (Optional) Custom tags to set on the secret managed at the destination. + +* `secret_name_template` - (Optional) Template describing how to generate external secret names. + Supports a subset of the Go Template syntax. + +## Attributes Reference + +The following attributes are exported in addition to the above: + +* `type` - The type of the secrets destination (`azure-kv`). + +## Import + +Azure Secrets sync destinations can be imported using the `name`, e.g. + +``` +$ terraform import vault_secrets_sync_azure_destination.az az-dest +``` diff --git a/website/docs/r/secrets_sync_gcp_destination.html.md b/website/docs/r/secrets_sync_gcp_destination.html.md new file mode 100644 index 000000000..d619e0145 --- /dev/null +++ b/website/docs/r/secrets_sync_gcp_destination.html.md @@ -0,0 +1,68 @@ +--- +layout: "vault" +page_title: "Vault: vault_secrets_sync_gcp_destination resource" +sidebar_current: "docs-vault-resource-secrets-sync-gcp-destination" +description: |- + Creates a GCP destination to synchronize secrets in Vault +--- + +# vault\_secrets\_sync\_gcp\_destination + +Creates a GCP destination to synchronize secrets in Vault. Requires Vault 1.16+. +*Available only for Vault Enterprise*. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +For more information on syncing secrets with GCP, please refer to the Vault +[documentation](https://developer.hashicorp.com/vault/docs/sync/gcpsm). + +## Example Usage + +```hcl +resource "vault_secrets_sync_gcp_destination" "gcp" { + name = "gcp-dest" + credentials = file(var.credentials_file) + secret_name_template = "vault_{{ .MountAccessor | lowercase }}_{{ .SecretPath | lowercase }}" + custom_tags = { + "foo" = "bar" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + +* `name` - (Required) Unique name of the GCP destination. + +* `credentials` - (Optional) JSON-encoded credentials to use to connect to GCP. + Can be omitted and directly provided to Vault using the `GOOGLE_APPLICATION_CREDENTIALS` environment + variable. + +* `custom_tags` - (Optional) Custom tags to set on the secret managed at the destination. + +* `secret_name_template` - (Optional) Template describing how to generate external secret names. + Supports a subset of the Go Template syntax. + +## Attributes Reference + +The following attributes are exported in addition to the above: + +* `type` - The type of the secrets destination (`gcp-sm`). + +## Import + +GCP Secrets sync destinations can be imported using the `name`, e.g. + +``` +$ terraform import vault_secrets_sync_gcp_destination.gcp gcp-dest +``` diff --git a/website/docs/r/secrets_sync_gh_destination.html.md b/website/docs/r/secrets_sync_gh_destination.html.md new file mode 100644 index 000000000..31522e50a --- /dev/null +++ b/website/docs/r/secrets_sync_gh_destination.html.md @@ -0,0 +1,73 @@ +--- +layout: "vault" +page_title: "Vault: vault_secrets_sync_gh_destination resource" +sidebar_current: "docs-vault-resource-secrets-sync-gh-destination" +description: |- + Creates a GitHub destination to synchronize secrets in Vault +--- + +# vault\_secrets\_sync\_gh\_destination + +Creates a GitHub destination to synchronize secrets in Vault. Requires Vault 1.16+. +*Available only for Vault Enterprise*. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +For more information on syncing secrets with GitHub, please refer to the Vault +[documentation](https://developer.hashicorp.com/vault/docs/sync/github). + +## Example Usage + +```hcl +resource "vault_secrets_sync_gh_destination" "gh" { + name = "gh-dest" + access_token = var.access_token + repository_owner = var.repo_owner + repository_name = "repo-name-example" + secret_name_template = "vault_{{ .MountAccessor | lowercase }}_{{ .SecretPath | lowercase }}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + +* `name` - (Required) Unique name of the GitHub destination. + +* `access_token` - (Optional) Fine-grained or personal access token. + Can be omitted and directly provided to Vault using the `GITHUB_ACCESS_TOKEN` environment + variable. + +* `repository_owner` - (Optional) GitHub organization or username that owns the repository. + Can be omitted and directly provided to Vault using the `GITHUB_REPOSITORY_OWNER` environment + variable. + +* `repository_name` - (Optional) Name of the repository. + Can be omitted and directly provided to Vault using the `GITHUB_REPOSITORY_NAME` environment + variable. + +* `secret_name_template` - (Optional) Template describing how to generate external secret names. + Supports a subset of the Go Template syntax. + +## Attributes Reference + +The following attributes are exported in addition to the above: + +* `type` - The type of the secrets destination (`gh`). + +## Import + +GitHub Secrets sync destinations can be imported using the `name`, e.g. + +``` +$ terraform import vault_secrets_sync_gh_destination.gh gh-dest +``` diff --git a/website/docs/r/secrets_sync_vercel_destination.html.md b/website/docs/r/secrets_sync_vercel_destination.html.md new file mode 100644 index 000000000..2bea887b1 --- /dev/null +++ b/website/docs/r/secrets_sync_vercel_destination.html.md @@ -0,0 +1,71 @@ +--- +layout: "vault" +page_title: "Vault: vault_secrets_sync_vercel_destination resource" +sidebar_current: "docs-vault-resource-secrets-sync-vercel-destination" +description: |- + Creates a GitHub destination to synchronize secrets in Vault +--- + +# vault\_secrets\_sync\_vercel\_destination + +Creates a GitHub destination to synchronize secrets in Vault. Requires Vault 1.16+. +*Available only for Vault Enterprise*. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +For more information on syncing secrets with GitHub, please refer to the Vault +[documentation](https://developer.hashicorp.com/vault/docs/sync/github). + +## Example Usage + +```hcl +resource "vault_secrets_sync_vercel_destination" "vercel" { + name = "vercel-dest" + access_token = var.access_token + project_id = var.project_id + deployment_environments = ["development", "preview", "production"] + secret_name_template = "vault_{{ .MountAccessor | lowercase }}_{{ .SecretPath | lowercase }}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + The value should not contain leading or trailing forward slashes. + The `namespace` is always relative to the provider's configured [namespace](/docs/providers/vault#namespace). + +* `name` - (Required) Unique name of the GitHub destination. + +* `access_token` - (Required) Vercel API access token with the permissions to manage environment + variables. + +* `project_id` - (Required) Project ID where to manage environment variables. + +* `deployment_environments` - (Required) Deployment environments where the environment variables + are available. Accepts `development`, `preview` and `production`. + +* `team_id` - (Optional) Team ID where to manage environment variables. + +* `secret_name_template` - (Optional) Template describing how to generate external secret names. + Supports a subset of the Go Template syntax. + +## Attributes Reference + +The following attributes are exported in addition to the above: + +* `type` - The type of the secrets destination (`vercel-project`). + +## Import + +GitHub Secrets sync destinations can be imported using the `name`, e.g. + +``` +$ terraform import vault_secrets_sync_vercel_destination.vercel vercel-dest +``` diff --git a/website/vault.erb b/website/vault.erb index 289473b34..d2a5e8946 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -633,6 +633,32 @@ vault_transit_secret_backend_key + > + vault_secrets_sync_aws_destination + + + > + vault_secrets_sync_gcp_destination + + + > + vault_secrets_sync_azure_destination + + + > + vault_secrets_sync_gh_destination + + + > + vault_secrets_sync_vercel_destination + + + > + vault_secrets_sync_association + + + + From 4da4fbd68cc957dddcd62d7fbcdf47dcdad231f7 Mon Sep 17 00:00:00 2001 From: Max Coulombe <109547106+maxcoulombe@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:10:56 -0500 Subject: [PATCH 23/24] Vault 22912/sync config (#2125) + added sync config resource --- vault/provider.go | 4 + vault/resource_secrets_sync_config.go | 137 +++++++++++++++++++++ vault/resource_secrets_sync_config_test.go | 83 +++++++++++++ website/docs/r/secrets_sync_config.html.md | 48 ++++++++ website/vault.erb | 4 + 5 files changed, 276 insertions(+) create mode 100644 vault/resource_secrets_sync_config.go create mode 100644 vault/resource_secrets_sync_config_test.go create mode 100644 website/docs/r/secrets_sync_config.html.md diff --git a/vault/provider.go b/vault/provider.go index 3921139cb..e7eff96e9 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -729,6 +729,10 @@ var ( Resource: UpdateSchemaResource(samlAuthBackendRoleResource()), PathInventory: []string{"/auth/saml/role/{name}"}, }, + "vault_secrets_sync_config": { + Resource: UpdateSchemaResource(secretsSyncConfigResource()), + PathInventory: []string{"/sys/sync/config"}, + }, "vault_secrets_sync_aws_destination": { Resource: UpdateSchemaResource(awsSecretsSyncDestinationResource()), PathInventory: []string{"/sys/sync/destinations/aws-sm/{name}"}, diff --git a/vault/resource_secrets_sync_config.go b/vault/resource_secrets_sync_config.go new file mode 100644 index 000000000..fce0ec62a --- /dev/null +++ b/vault/resource_secrets_sync_config.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/vault/helper/namespace" +) + +const ( + fieldDisabled = "disabled" + fieldQueueCapacity = "queue_capacity" +) + +var syncConfigFields = []string{ + fieldDisabled, + fieldQueueCapacity, +} + +func secretsSyncConfigResource() *schema.Resource { + return &schema.Resource{ + CreateContext: provider.MountCreateContextWrapper(secretsSyncConfigWrite, provider.VaultVersion116), + UpdateContext: secretsSyncConfigWrite, + ReadContext: secretsSyncConfigRead, + DeleteContext: secretsSyncConfigDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + fieldDisabled: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Disables the syncing process between Vault and external destinations.", + }, + fieldQueueCapacity: { + Type: schema.TypeInt, + Optional: true, + Default: 1_000_000, + Description: "Maximum number of pending sync operations allowed on the queue.", + }, + }, + } +} + +func secretsSyncConfigWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + if client.Namespace() != namespace.RootNamespaceID && client.Namespace() != "" { + return diag.Errorf("error writing sync config, this API is reserved to the root namespace and cannot be used with %q", client.Namespace()) + } + + path := secretsSyncConfigPath() + + data := map[string]interface{}{} + for _, k := range syncConfigFields { + if k == fieldDisabled { // GetOk evaluates the value so a false bool field would be skipped + data[k] = d.Get(k) + } else if value, ok := d.GetOk(k); ok { + data[k] = value + } + } + + log.Printf("[DEBUG] Writing sync config at %q", path) + _, err := client.Logical().JSONMergePatch(ctx, path, data) + if err != nil { + return diag.Errorf("error writing sync config at %q: %s", path, err) + } + + d.SetId("global_config") // Config is global and doesn't have a human-defined identifier, so we use a placeholder ID + + return secretsSyncConfigRead(ctx, d, meta) +} + +func secretsSyncConfigRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := secretsSyncConfigPath() + + log.Printf("[DEBUG] Reading sync config from %q", path) + resp, err := client.Logical().ReadWithContext(ctx, path) + if err != nil { + return diag.Errorf("error reading sync config from %q: %s", path, err) + } + + if resp == nil { + log.Printf("[WARN] No config found at %q; removing from state.", path) + d.SetId("") + return nil + } + + for _, k := range syncConfigFields { + if v, ok := resp.Data[k]; ok { + if err := d.Set(k, v); err != nil { + return diag.Errorf("error setting state key %q: err=%s", k, err) + } + } + } + + return nil +} + +func secretsSyncConfigDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, e := provider.GetClient(d, meta) + if e != nil { + return diag.FromErr(e) + } + + path := secretsSyncConfigPath() + + log.Printf("[DEBUG] Resetting sync config to default values at %q", path) + _, err := client.Logical().DeleteWithContext(ctx, path) + if err != nil { + return diag.Errorf("error deleting sync config at %q: %s", path, err) + } + + d.SetId("") + + return nil +} + +func secretsSyncConfigPath() string { + return "sys/sync/config" +} diff --git a/vault/resource_secrets_sync_config_test.go b/vault/resource_secrets_sync_config_test.go new file mode 100644 index 000000000..89ad5f7bd --- /dev/null +++ b/vault/resource_secrets_sync_config_test.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vault + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-vault/internal/provider" + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +func TestSecretsSyncConfig(t *testing.T) { + resourceName := "vault_secrets_sync_config.test" + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion116) + }, PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testSecretsSyncConfig("root", true, 1), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, fieldDisabled, "true"), + resource.TestCheckResourceAttr(resourceName, fieldQueueCapacity, "1"), + ), + }, + { + Config: testSecretsSyncConfigEmpty(), + Check: resource.ComposeTestCheckFunc( + + resource.TestCheckResourceAttr(resourceName, fieldDisabled, "false"), + resource.TestCheckResourceAttr(resourceName, fieldQueueCapacity, "1000000"), + ), + }, + testutil.GetImportTestStep(resourceName, false, nil, + fieldDisabled, + fieldQueueCapacity, + ), + }, + }) + + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + PreCheck: func() { + testutil.TestAccPreCheck(t) + SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115) + }, PreventPostDestroyRefresh: true, + Steps: []resource.TestStep{ + { + Config: testSecretsSyncConfig("non-root-namespace", false, 100000), + ExpectError: regexp.MustCompile(".*this API is reserved to the root namespace.*"), + }, + }, + }) +} + +func testSecretsSyncConfig(namespace string, disabled bool, queueCapacity int) string { + ret := fmt.Sprintf(` +resource "vault_secrets_sync_config" "test" { + namespace = "%s" + disabled = %t + queue_capacity = %d +} +`, namespace, disabled, queueCapacity) + + return ret +} + +func testSecretsSyncConfigEmpty() string { + ret := fmt.Sprintf(` +resource "vault_secrets_sync_config" "test" { +} +`) + + return ret +} diff --git a/website/docs/r/secrets_sync_config.html.md b/website/docs/r/secrets_sync_config.html.md new file mode 100644 index 000000000..7d53ca93e --- /dev/null +++ b/website/docs/r/secrets_sync_config.html.md @@ -0,0 +1,48 @@ +--- +layout: "vault" +page_title: "Vault: vault_secrets_sync_config resource" +sidebar_current: "docs-vault-secrets-sync-config" +description: |- + Configures the secret sync global config. +--- + +# vault\_secrets\_sync\_config + +Configures the secret sync global config. +The config is global and can only be managed in the root namespace. + +~> **Important** The config is global so the vault_secrets_sync_config resource must not be defined +multiple times for the same Vault server. If multiple definition exists, the last one applied will be +effective. + +## Example Usage + +```hcl +resource "vault_secrets_sync_config" "global_config" { + disabled = true + queue_capacity = 500000 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `namespace` - (Optional) The namespace to provision the resource in. + This resource can only be configured in the root namespace. + *Available only for Vault Enterprise*. + +* `disabled` - (Optional) Disables the syncing process between Vault and external destinations. Defaults to `false`. + +* `queue_capacity` - (Optional) Maximum number of pending sync operations allowed on the queue. Defaults to `1000000`. + + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +``` +$ terraform import vault_secrets_sync_config.config global_config +``` diff --git a/website/vault.erb b/website/vault.erb index d2a5e8946..970ee25ca 100644 --- a/website/vault.erb +++ b/website/vault.erb @@ -633,6 +633,10 @@ vault_transit_secret_backend_key + > + vault_secrets_sync_config + + > vault_secrets_sync_aws_destination From f4ea26863d8178649ec0f3dbe50465ffe000e0f3 Mon Sep 17 00:00:00 2001 From: Vinay Gopalan Date: Wed, 17 Jan 2024 11:59:28 -0800 Subject: [PATCH 24/24] add sync association struct model to avoid deep nesting --- vault/resource_secrets_sync_association.go | 75 ++++++++++++++-------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/vault/resource_secrets_sync_association.go b/vault/resource_secrets_sync_association.go index 5582c6d5b..1514cad72 100644 --- a/vault/resource_secrets_sync_association.go +++ b/vault/resource_secrets_sync_association.go @@ -5,12 +5,14 @@ package vault import ( "context" + "encoding/json" "fmt" "log" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/vault/api" "github.com/hashicorp/terraform-provider-vault/internal/consts" "github.com/hashicorp/terraform-provider-vault/internal/provider" @@ -22,12 +24,6 @@ const ( fieldUpdatedAt = "updated_at" ) -var secretsSyncAssociationFields = []string{ - fieldSecretName, - fieldSyncStatus, - fieldUpdatedAt, -} - func secretsSyncAssociationResource() *schema.Resource { return &schema.Resource{ CreateContext: provider.MountCreateContextWrapper(secretsSyncAssociationWrite, provider.VaultVersion116), @@ -103,30 +99,29 @@ func secretsSyncAssociationWrite(ctx context.Context, d *schema.ResourceData, me if err != nil { return diag.Errorf("could not obtain accessor from given mount; err=%s", err) } - - // associations can't be read from Vault - // set data that is received from Vault upon writes vaultRespKey := fmt.Sprintf("%s/%s", accessor, secretName) - associatedSecrets := "associated_secrets" - secretData, ok := resp.Data[associatedSecrets] + + saModel, err := getSyncAssociationModelFromResponse(resp) + if err != nil { + return diag.FromErr(err) + } + + syncData, ok := saModel.AssociatedSecrets[vaultRespKey] if !ok { - // did not find any associated secrets for this mount - // we expect to see data here since we just wrote an association - return diag.Errorf("expected associated secrets; received no secret associations in mount") + return diag.Errorf("no associated secrets found for given mount accessor and secret name %s", vaultRespKey) + } + + // set data that is received from Vault upon writes to avoid extra sync association reads + if err := d.Set(fieldSecretName, syncData.SecretName); err != nil { + return diag.FromErr(err) } - for _, k := range secretsSyncAssociationFields { - if secretsMap, ok := secretData.(map[string]interface{}); ok { - if val, ok := secretsMap[vaultRespKey]; ok { - if valMap, ok := val.(map[string]interface{}); ok { - if v, ok := valMap[k]; ok { - if err := d.Set(k, v); err != nil { - return diag.FromErr(err) - } - } - } - } - } + if err := d.Set(fieldSyncStatus, syncData.SyncStatus); err != nil { + return diag.FromErr(err) + } + + if err := d.Set(fieldUpdatedAt, syncData.UpdatedAt); err != nil { + return diag.FromErr(err) } d.SetId(path) @@ -199,3 +194,31 @@ func getMountAccessor(ctx context.Context, d *schema.ResourceData, meta interfac return m.Accessor, nil } + +type syncAssociationModel struct { + AssociatedSecrets map[string]syncAssociationData `json:"associated_secrets"` +} + +type syncAssociationData struct { + Accessor string `json:"accessor"` + SecretName string `json:"secret_name"` + SyncStatus string `json:"sync_status"` + UpdatedAt string `json:"updated_at"` +} + +func getSyncAssociationModelFromResponse(resp *api.Secret) (*syncAssociationModel, error) { + // convert resp data to JSON + b, err := json.Marshal(resp.Data) + if err != nil { + return nil, fmt.Errorf("error converting vault response to JSON; err=%s", err) + } + + // convert JSON to struct + var model *syncAssociationModel + err = json.Unmarshal(b, &model) + if err != nil { + return nil, fmt.Errorf("error converting JSON to sync association model; err=%s", err) + } + + return model, nil +}