From 441144d36266a1f90a4deed8607c86e15ba39a59 Mon Sep 17 00:00:00 2001 From: abheda-crest <105624942+abheda-crest@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:57:35 +0530 Subject: [PATCH] Add support for regional secrets list datasource `google_secret_manager_regional_secrets` (#11743) --- .../provider/provider_mmv1_resources.go.erb | 1 + ..._source_secret_manager_regional_secrets.go | 176 ++++++++++++ ...ce_secret_manager_regional_secrets_test.go | 257 ++++++++++++++++++ ...ret_manager_regional_secrets.html.markdown | 82 ++++++ 4 files changed, 516 insertions(+) create mode 100644 mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets.go create mode 100644 mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets_test.go create mode 100644 mmv1/third_party/terraform/website/docs/d/secret_manager_regional_secrets.html.markdown diff --git a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb index 847cc147f54d..e0031d4aa923 100644 --- a/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb +++ b/mmv1/third_party/terraform/provider/provider_mmv1_resources.go.erb @@ -182,6 +182,7 @@ var handwrittenDatasources = map[string]*schema.Resource{ <% end -%> "google_secret_manager_regional_secret_version": secretmanagerregional.DataSourceSecretManagerRegionalRegionalSecretVersion(), "google_secret_manager_regional_secret": secretmanagerregional.DataSourceSecretManagerRegionalRegionalSecret(), + "google_secret_manager_regional_secrets": secretmanagerregional.DataSourceSecretManagerRegionalRegionalSecrets(), "google_secret_manager_secret": secretmanager.DataSourceSecretManagerSecret(), "google_secret_manager_secrets": secretmanager.DataSourceSecretManagerSecrets(), "google_secret_manager_secret_version": secretmanager.DataSourceSecretManagerSecretVersion(), diff --git a/mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets.go b/mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets.go new file mode 100644 index 000000000000..b2cda48defee --- /dev/null +++ b/mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets.go @@ -0,0 +1,176 @@ +package secretmanagerregional + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-google/google/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport" +) + +func DataSourceSecretManagerRegionalRegionalSecrets() *schema.Resource { + dsSchema := tpgresource.DatasourceSchemaFromResourceSchema(ResourceSecretManagerRegionalRegionalSecret().Schema) + return &schema.Resource{ + Read: dataSourceSecretManagerRegionalRegionalSecretsRead, + Schema: map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "location": { + Type: schema.TypeString, + Required: true, + }, + "filter": { + Type: schema.TypeString, + Description: `Filter string, adhering to the rules in List-operation filtering (https://cloud.google.com/secret-manager/docs/filtering). +List only secrets matching the filter. If filter is empty, all regional secrets are listed from the specified location.`, + Optional: true, + }, + "secrets": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: dsSchema, + }, + }, + }, + } +} + +func dataSourceSecretManagerRegionalRegionalSecretsRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, "{{SecretManagerRegionalBasePath}}projects/{{project}}/locations/{{location}}/secrets") + if err != nil { + return err + } + + filter, has_filter := d.GetOk("filter") + + if has_filter { + url, err = transport_tpg.AddQueryParams(url, map[string]string{"filter": filter.(string)}) + if err != nil { + return err + } + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Secret: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + // To handle pagination locally + allSecrets := make([]interface{}, 0) + token := "" + for paginate := true; paginate; { + if token != "" { + url, err = transport_tpg.AddQueryParams(url, map[string]string{"pageToken": token}) + if err != nil { + return err + } + } + secrets, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "GET", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + }) + if err != nil { + return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("SecretManagerRegionalSecrets %q", d.Id())) + } + secretsInterface := secrets["secrets"] + if secretsInterface == nil { + break + } + allSecrets = append(allSecrets, secretsInterface.([]interface{})...) + tokenInterface := secrets["nextPageToken"] + if tokenInterface == nil { + paginate = false + } else { + paginate = true + token = tokenInterface.(string) + } + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("error setting project: %s", err) + } + + if err := d.Set("filter", filter); err != nil { + return fmt.Errorf("error setting filter: %s", err) + } + + if err := d.Set("secrets", flattenSecretManagerRegionalRegionalSecretsSecrets(allSecrets, d, config)); err != nil { + return fmt.Errorf("error setting secrets: %s", err) + } + + // Store the ID now + id, err := tpgresource.ReplaceVars(d, config, "projects/{{project}}/locations/{{location}}/secrets") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + if has_filter { + id += "/filter=" + filter.(string) + } + d.SetId(id) + + return nil +} + +func flattenSecretManagerRegionalRegionalSecretsSecrets(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + + transformed = append(transformed, map[string]interface{}{ + "annotations": flattenSecretManagerRegionalRegionalSecretEffectiveAnnotations(original["annotations"], d, config), + "effective_annotations": flattenSecretManagerRegionalRegionalSecretEffectiveAnnotations(original["annotations"], d, config), + "expire_time": flattenSecretManagerRegionalRegionalSecretExpireTime(original["expireTime"], d, config), + "labels": flattenSecretManagerRegionalRegionalSecretEffectiveLabels(original["labels"], d, config), + "effective_labels": flattenSecretManagerRegionalRegionalSecretEffectiveLabels(original["labels"], d, config), + "terraform_labels": flattenSecretManagerRegionalRegionalSecretEffectiveLabels(original["labels"], d, config), + "version_aliases": flattenSecretManagerRegionalRegionalSecretVersionAliases(original["versionAliases"], d, config), + "rotation": flattenSecretManagerRegionalRegionalSecretRotation(original["rotation"], d, config), + "topics": flattenSecretManagerRegionalRegionalSecretTopics(original["topics"], d, config), + "version_destroy_ttl": flattenSecretManagerRegionalRegionalSecretVersionDestroyTtl(original["versionDestroyTtl"], d, config), + "customer_managed_encryption": flattenSecretManagerRegionalRegionalSecretCustomerManagedEncryption(original["customerManagedEncryption"], d, config), + "create_time": flattenSecretManagerRegionalRegionalSecretCreateTime(original["createTime"], d, config), + "name": flattenSecretManagerRegionalRegionalSecretName(original["name"], d, config), + "project": getDataFromName(original["name"], 1), + "location": getDataFromName(original["name"], 3), + "secret_id": getDataFromName(original["name"], 5), + }) + } + return transformed +} + +func getDataFromName(v interface{}, part int) string { + name := v.(string) + split := strings.Split(name, "/") + return split[part] +} diff --git a/mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets_test.go b/mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets_test.go new file mode 100644 index 000000000000..a3ed4127b7ac --- /dev/null +++ b/mmv1/third_party/terraform/services/secretmanagerregional/data_source_secret_manager_regional_secrets_test.go @@ -0,0 +1,257 @@ +package secretmanagerregional_test + +import ( + "errors" + "fmt" + "strconv" + + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +func TestAccDataSourceSecretManagerRegionalRegionalSecrets_basic(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckSecretManagerRegionalRegionalSecretDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceSecretManagerRegionalRegionalSecrets_basic(context), + Check: resource.ComposeTestCheckFunc( + checkListDataSourceStateMatchesResourceStateWithIgnores( + "data.google_secret_manager_regional_secrets.foo", + "google_secret_manager_regional_secret.foo", + map[string]struct{}{ + "id": {}, + "project": {}, + }, + ), + ), + }, + }, + }) +} + +func testAccDataSourceSecretManagerRegionalRegionalSecrets_basic(context map[string]interface{}) string { + return acctest.Nprintf(` +provider "google" { + add_terraform_attribution_label = false +} + +resource "google_secret_manager_regional_secret" "foo" { + secret_id = "tf-test-secret-%{random_suffix}" + location = "us-central1" + + labels = { + label = "my-label" + } + + annotations = { + key1 = "value1" + } +} + +data "google_secret_manager_regional_secrets" "foo" { + location = "us-central1" + depends_on = [ + google_secret_manager_regional_secret.foo + ] +} +`, context) +} + +func TestAccDataSourceSecretManagerRegionalRegionalSecrets_filter(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckSecretManagerRegionalRegionalSecretDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccDataSourceSecretManagerRegionalRegionalSecrets_filter(context), + Check: resource.ComposeTestCheckFunc( + checkListDataSourceStateMatchesResourceStateWithIgnoresForAppliedFilter( + "data.google_secret_manager_regional_secrets.foo", + "google_secret_manager_regional_secret.foo", + "google_secret_manager_regional_secret.bar", + map[string]struct{}{ + "id": {}, + "project": {}, + }, + ), + ), + }, + }, + }) +} + +func testAccDataSourceSecretManagerRegionalRegionalSecrets_filter(context map[string]interface{}) string { + return acctest.Nprintf(` +provider "google" { + add_terraform_attribution_label = false +} + +resource "google_secret_manager_regional_secret" "foo" { + secret_id = "tf-test-secret-1-%{random_suffix}" + location = "us-central1" + + labels = { + label = "my-label1" + } + + annotations = { + key1 = "value1" + } +} + +resource "google_secret_manager_regional_secret" "bar" { + secret_id = "tf-test-secret-2-%{random_suffix}" + location = "us-central1" + + labels = { + label= "my-label2" + } + + annotations = { + key1 = "value1" + } +} + +data "google_secret_manager_regional_secrets" "foo" { + location = "us-central1" + filter = "labels.label=my-label1" + depends_on = [ + google_secret_manager_regional_secret.foo, + google_secret_manager_regional_secret.bar + ] +} +`, context) +} + +// This function checks data source state matches for resourceName secret manager regional secret state +func checkListDataSourceStateMatchesResourceStateWithIgnores(dataSourceName, resourceName string, ignoreFields map[string]struct{}) func(*terraform.State) error { + return func(s *terraform.State) error { + ds, ok := s.RootModule().Resources[dataSourceName] + if !ok { + return fmt.Errorf("can't find %s in state", dataSourceName) + } + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("can't find %s in state", resourceName) + } + + dsAttr := ds.Primary.Attributes + rsAttr := rs.Primary.Attributes + + err := checkFieldsMatchForDataSourceStateAndResourceState(dsAttr, rsAttr, ignoreFields) + if err != nil { + return err + } + return nil + } +} + +// This function checks whether all the attributes of the secret manager secret resource and the attributes of the secret manager secret inside the data source list are the same +func checkFieldsMatchForDataSourceStateAndResourceState(dsAttr, rsAttr map[string]string, ignoreFields map[string]struct{}) error { + totalSecrets, err := strconv.Atoi(dsAttr["secrets.#"]) + if err != nil { + return errors.New("Couldn't convert length of secrets list to integer") + } + index := "-1" + for i := 0; i < totalSecrets; i++ { + if dsAttr["secrets."+strconv.Itoa(i)+".name"] == rsAttr["name"] { + index = strconv.Itoa(i) + } + } + + if index == "-1" { + return errors.New("The newly created secret is not found in the data source") + } + + errMsg := "" + // Data sources are often derived from resources, so iterate over the resource fields to + // make sure all fields are accounted for in the data source. + // If a field exists in the data source but not in the resource, its expected value should + // be checked separately. + for k := range rsAttr { + if _, ok := ignoreFields[k]; ok { + continue + } + if k == "%" { + continue + } + if dsAttr["secrets."+index+"."+k] != rsAttr[k] { + // ignore data sources where an empty list is being compared against a null list. + if k[len(k)-1:] == "#" && (dsAttr["secrets."+index+"."+k] == "" || dsAttr["secrets."+index+"."+k] == "0") && (rsAttr[k] == "" || rsAttr[k] == "0") { + continue + } + errMsg += fmt.Sprintf("%s is %s; want %s\n", k, dsAttr["secrets."+index+"."+k], rsAttr[k]) + } + } + + if errMsg != "" { + return errors.New(errMsg) + } + + return nil +} + +// This function checks state match for resourceName and asserts the absence of resourceName2 in data source +func checkListDataSourceStateMatchesResourceStateWithIgnoresForAppliedFilter(dataSourceName, resourceName, resourceName2 string, ignoreFields map[string]struct{}) func(*terraform.State) error { + return func(s *terraform.State) error { + ds, ok := s.RootModule().Resources[dataSourceName] + if !ok { + return fmt.Errorf("can't find %s in state", dataSourceName) + } + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("can't find %s in state", resourceName) + } + + rs2, ok := s.RootModule().Resources[resourceName2] + if !ok { + return fmt.Errorf("can't find %s in state", resourceName2) + } + + dsAttr := ds.Primary.Attributes + rsAttr := rs.Primary.Attributes + rsAttr2 := rs2.Primary.Attributes + + err := checkFieldsMatchForDataSourceStateAndResourceState(dsAttr, rsAttr, ignoreFields) + if err != nil { + return err + } + err = checkResourceAbsentInDataSourceAfterFilterApplied(dsAttr, rsAttr2) + return err + } +} + +// This function asserts the absence of the secret manager secret resource which would not be included in the data source list due to the filter applied. +func checkResourceAbsentInDataSourceAfterFilterApplied(dsAttr, rsAttr map[string]string) error { + totalSecrets, err := strconv.Atoi(dsAttr["secrets.#"]) + if err != nil { + return errors.New("Couldn't convert length of secrets list to integer") + } + for i := 0; i < totalSecrets; i++ { + if dsAttr["secrets."+strconv.Itoa(i)+".name"] == rsAttr["name"] { + return errors.New("The resource is present in the data source even after the filter is applied") + } + } + return nil +} diff --git a/mmv1/third_party/terraform/website/docs/d/secret_manager_regional_secrets.html.markdown b/mmv1/third_party/terraform/website/docs/d/secret_manager_regional_secrets.html.markdown new file mode 100644 index 000000000000..5a0f2e9a1051 --- /dev/null +++ b/mmv1/third_party/terraform/website/docs/d/secret_manager_regional_secrets.html.markdown @@ -0,0 +1,82 @@ +--- +subcategory: "Secret Manager" +description: |- + List the Secret Manager Regional Secrets. +--- + +# google_secret_manager_regional_secrets + +Use this data source to list the Secret Manager Regional Secrets. + +## Example Usage + +```hcl +data "google_secret_manager_regional_secrets" "secrets" { + location = "us-central1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `project` - (optional) The ID of the project. + +* `filter` - (optional) Filter string, adhering to the rules in [List-operation filtering](https://cloud.google.com/secret-manager/docs/filtering). List only secrets matching the filter. If filter is empty, all regional secrets are listed from the specified location. + +* `location` - (Required) The location of the regional secret. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `secrets` - A list of regional secrets present in the specified location and matching the filter. Structure is [defined below](#nested_secrets). + +The `secrets` block supports: + +* `labels` - The labels assigned to this regional secret. + +* `annotations` - Custom metadata about the regional secret. + +* `version_aliases` - Mapping from version alias to version name. + +* `topics` - + A list of up to 10 Pub/Sub topics to which messages are published when control plane operations are called on the regional secret or its versions. + Structure is [documented below](#nested_topics). + +* `expire_time` - Timestamp in UTC when the regional secret is scheduled to expire. + +* `create_time` - The time at which the regional secret was created. + +* `rotation` - + The rotation time and period for a regional secret. + Structure is [documented below](#nested_rotation). + +* `project` - The ID of the project in which the resource belongs. + +* `location` - The location in which the resource belongs. + +* `secret_id` - The unique name of the resource. + +* `name` - The resource name of the regional secret. Format: `projects/{{project}}/locations/{{location}}/secrets/{{secret_id}}` + +* `version_destroy_ttl` - The version destroy ttl for the regional secret version. + +* `customer_managed_encryption` - + Customer Managed Encryption for the regional secret. + Structure is [documented below](#nested_customer_managed_encryption_user_managed). + +The `topics` block supports: + +* `name` - The resource name of the Pub/Sub topic that will be published to. + +The `rotation` block supports: + +* `next_rotation_time` - Timestamp in UTC at which the secret is scheduled to rotate. + +* `rotation_period` - The Duration between rotation notifications. + +The `customer_managed_encryption` block supports: + +* `kms_key_name` - + Describes the Cloud KMS encryption key that will be used to protect destination secret.