diff --git a/docs/content/develop/permadiff.md b/docs/content/develop/permadiff.md index ca1b94c48b04..560b39038589 100644 --- a/docs/content/develop/permadiff.md +++ b/docs/content/develop/permadiff.md @@ -234,15 +234,29 @@ Add a [custom flattener]({{< ref "/develop/custom-code#custom_flatten" >}}) for ```go func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { - configValue := d.Get("path.0.to.0.parent_field.0.nested_field").([]string) - - sorted, err := tpgresource.SortStringsByConfigOrder(configValue, v.([]string)) - if err != nil { - log.Printf("[ERROR] Could not sort API response value: %s", err) - return v - } - - return sorted.(interface{}) + rawConfigValue := d.Get("path.0.to.0.parent_field.0.nested_field") + + // Convert config value to []string + configValue, err := tpgresource.InterfaceSliceToStringSlice(rawConfigValue) + if err != nil { + log.Printf("[ERROR] Failed to convert config value: %s", err) + return v + } + + // Convert v to []string + apiStringValue, err := tpgresource.InterfaceSliceToStringSlice(v) + if err != nil { + log.Printf("[ERROR] Failed to convert API value: %s", err) + return v + } + + sortedStrings, err := tpgresource.SortStringsByConfigOrder(configValue, apiStringValue) + if err != nil { + log.Printf("[ERROR] Could not sort API response value: %s", err) + return v + } + + return sortedStrings } ``` {{< /tab >}} @@ -251,15 +265,29 @@ Define resource-specific functions in your service package, for example at the t ```go func flattenResourceNameFieldName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { - configValue := d.Get("path.0.to.0.parent_field.0.nested_field").([]string) - - sorted, err := tpgresource.SortStringsByConfigOrder(configValue, v.([]string)) - if err != nil { - log.Printf("[ERROR] Could not sort API response value: %s", err) - return v - } - - return sorted.(interface{}) + rawConfigValue := d.Get("path.0.to.0.parent_field.0.nested_field") + + // Convert config value to []string + configValue, err := tpgresource.InterfaceSliceToStringSlice(rawConfigValue) + if err != nil { + log.Printf("[ERROR] Failed to convert config value: %s", err) + return v + } + + // Convert v to []string + apiStringValue, err := tpgresource.InterfaceSliceToStringSlice(v) + if err != nil { + log.Printf("[ERROR] Failed to convert API value: %s", err) + return v + } + + sortedStrings, err := tpgresource.SortStringsByConfigOrder(configValue, apiStringValue) + if err != nil { + log.Printf("[ERROR] Could not sort API response value: %s", err) + return v + } + + return sortedStrings } ``` {{< /tab >}} diff --git a/mmv1/products/spanner/Database.yaml b/mmv1/products/spanner/Database.yaml index 5e0eca341608..b279f97319d4 100644 --- a/mmv1/products/spanner/Database.yaml +++ b/mmv1/products/spanner/Database.yaml @@ -150,8 +150,22 @@ properties: description: | Fully qualified name of the KMS key to use to encrypt this database. This key must exist in the same location as the Spanner Database. - required: true immutable: true + exactly_one_of: + - encryption_config.0.kms_key_name + - encryption_config.0.kms_key_names + - name: 'kmsKeyNames' + type: Array + description: | + Fully qualified name of the KMS key to use to encrypt this database. This key must exist + in the same location as the Spanner Database. + immutable: true + custom_flatten: templates/terraform/custom_flatten/spanner_database_kms_key_names.go.tmpl + item_type: + type: String + exactly_one_of: + - encryption_config.0.kms_key_name + - encryption_config.0.kms_key_names - name: 'databaseDialect' type: Enum description: | diff --git a/mmv1/templates/terraform/custom_flatten/spanner_database_kms_key_names.go.tmpl b/mmv1/templates/terraform/custom_flatten/spanner_database_kms_key_names.go.tmpl new file mode 100644 index 000000000000..03ae2ada7313 --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/spanner_database_kms_key_names.go.tmpl @@ -0,0 +1,25 @@ +func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + rawConfigValue := d.Get("encryption_config.0.kms_key_names") + + // Convert config value to []string + configValue, err := tpgresource.InterfaceSliceToStringSlice(rawConfigValue) + if err != nil { + log.Printf("[ERROR] Failed to convert config value: %s", err) + return v + } + + // Convert v to []string + apiStringValue, err := tpgresource.InterfaceSliceToStringSlice(v) + if err != nil { + log.Printf("[ERROR] Failed to convert API value: %s", err) + return v + } + + sortedStrings, err := tpgresource.SortStringsByConfigOrder(configValue, apiStringValue) + if err != nil { + log.Printf("[ERROR] Could not sort API response value: %s", err) + return v + } + + return sortedStrings +} diff --git a/mmv1/third_party/terraform/services/spanner/resource_spanner_database_test.go.tmpl b/mmv1/third_party/terraform/services/spanner/resource_spanner_database_test.go.tmpl index ebbd3e9db26a..d330b2a81a8d 100644 --- a/mmv1/third_party/terraform/services/spanner/resource_spanner_database_test.go.tmpl +++ b/mmv1/third_party/terraform/services/spanner/resource_spanner_database_test.go.tmpl @@ -605,4 +605,140 @@ resource "google_project_service_identity" "ck_sa" { `, context) } + +func TestAccSpannerDatabase_mrcmek(t *testing.T) { + acctest.SkipIfVcr(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.ProtoV5ProviderBetaFactories(t), + CheckDestroy: testAccCheckSpannerDatabaseDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSpannerDatabase_mrcmek(context), + }, + { + ResourceName: "google_spanner_database.database", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"}, + }, + }, + }) +} + +func testAccSpannerDatabase_mrcmek(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_spanner_instance" "main" { + provider = google-beta + config = "nam3" + display_name = "main-instance1" + num_nodes = 1 +} + +resource "google_spanner_database" "database" { + provider = google-beta + instance = google_spanner_instance.main.name + name = "tf-test-mrcmek-db%{random_suffix}" + ddl = [ + "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)", + "CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)", + ] + + encryption_config { + kms_key_names = [ + google_kms_crypto_key.example-key-us-central1.id, + google_kms_crypto_key.example-key-us-east1.id, + google_kms_crypto_key.example-key-us-east4.id, + ] + } + + deletion_protection = false + + depends_on = [google_kms_crypto_key_iam_member.crypto-key-binding-us-central1, + google_kms_crypto_key_iam_member.crypto-key-binding-us-east1, + google_kms_crypto_key_iam_member.crypto-key-binding-us-east4,] +} + +resource "google_kms_key_ring" "keyring-us-central1" { + provider = google-beta + name = "tf-test-ring%{random_suffix}" + location = "us-central1" +} + +resource "google_kms_crypto_key" "example-key-us-central1" { + provider = google-beta + name = "tf-test-key%{random_suffix}" + key_ring = google_kms_key_ring.keyring-us-central1.id + rotation_period = "100000s" +} + +resource "google_kms_crypto_key_iam_member" "crypto-key-binding-us-central1" { + provider = google-beta + crypto_key_id = google_kms_crypto_key.example-key-us-central1.id + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + + member = google_project_service_identity.ck_sa.member +} + +resource "google_kms_key_ring" "keyring-us-east1" { + provider = google-beta + name = "tf-test-ring%{random_suffix}" + location = "us-east1" +} + +resource "google_kms_crypto_key" "example-key-us-east1" { + provider = google-beta + name = "tf-test-key%{random_suffix}" + key_ring = google_kms_key_ring.keyring-us-east1.id + rotation_period = "100000s" +} + +resource "google_kms_crypto_key_iam_member" "crypto-key-binding-us-east1" { + provider = google-beta + crypto_key_id = google_kms_crypto_key.example-key-us-east1.id + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + + member = google_project_service_identity.ck_sa.member +} + +resource "google_kms_key_ring" "keyring-us-east4" { + provider = google-beta + name = "tf-test-ring%{random_suffix}" + location = "us-east4" +} + +resource "google_kms_crypto_key" "example-key-us-east4" { + provider = google-beta + name = "tf-test-key%{random_suffix}" + key_ring = google_kms_key_ring.keyring-us-east4.id + rotation_period = "100000s" +} + +resource "google_kms_crypto_key_iam_member" "crypto-key-binding-us-east4" { + provider = google-beta + crypto_key_id = google_kms_crypto_key.example-key-us-east4.id + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + + member = google_project_service_identity.ck_sa.member +} + +data "google_project" "project" { + provider = google-beta +} + +resource "google_project_service_identity" "ck_sa" { + provider = google-beta + project = data.google_project.project.project_id + service = "spanner.googleapis.com" +} + +`, context) +} + {{- end }} diff --git a/mmv1/third_party/terraform/tpgresource/utils.go b/mmv1/third_party/terraform/tpgresource/utils.go index 2f83608ad77f..30be51214971 100644 --- a/mmv1/third_party/terraform/tpgresource/utils.go +++ b/mmv1/third_party/terraform/tpgresource/utils.go @@ -238,6 +238,25 @@ func ExpandStringMap(d TerraformResourceData, key string) map[string]string { return ConvertStringMap(v.(map[string]interface{})) } +// InterfaceSliceToStringSlice converts a []interface{} containing strings to []string +func InterfaceSliceToStringSlice(v interface{}) ([]string, error) { + interfaceSlice, ok := v.([]interface{}) + if !ok { + return nil, fmt.Errorf("expected []interface{}, got %T", v) + } + + stringSlice := make([]string, len(interfaceSlice)) + for i, item := range interfaceSlice { + strItem, ok := item.(string) + if !ok { + return nil, fmt.Errorf("expected string, got %T at index %d", item, i) + } + stringSlice[i] = strItem + } + + return stringSlice, nil +} + // SortStringsByConfigOrder takes a slice of map[string]interface{} from a TF config // and API data, and returns a new slice containing the API data, reorderd to match // the TF config as closely as possible (with new items at the end of the list.)