diff --git a/docs/content/develop/permadiff.md b/docs/content/develop/permadiff.md index ca1b94c48b04..2b4bb3b1d8ad 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) + rawConfigValue := d.Get("path.0.to.0.parent_field.0.nested_field") - sorted, err := tpgresource.SortStringsByConfigOrder(configValue, v.([]string)) + // 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 sorted.(interface{}) + 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) + 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 + } - sorted, err := tpgresource.SortStringsByConfigOrder(configValue, v.([]string)) + sortedStrings, err := tpgresource.SortStringsByConfigOrder(configValue, apiStringValue) if err != nil { log.Printf("[ERROR] Could not sort API response value: %s", err) return v } - return sorted.(interface{}) + return sortedStrings } ``` {{< /tab >}} diff --git a/mmv1/products/spanner/Database.yaml b/mmv1/products/spanner/Database.yaml index 5e0eca341608..ad4a0c45ecbd 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 keys to use to encrypt this database. The keys must exist + in the same locations 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..526fc566165c --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/spanner_database_kms_key_names.go.tmpl @@ -0,0 +1,31 @@ +func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Ignore `kms_key_names` if `kms_key_name` is set, because that field takes precedence. + _, kmsNameSet := d.GetOk("encryption_config.0.kms_key_name") + if kmsNameSet { + return nil + } + + 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..eba30f8dd202 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 @@ -539,7 +539,7 @@ func TestAccSpannerDatabase_cmek(t *testing.T) { ResourceName: "google_spanner_database.database", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"}, + ImportStateVerifyIgnore: []string{"ddl", "deletion_protection", "encryption_config.0.kms_key_names"}, }, }, }) @@ -605,4 +605,74 @@ resource "google_project_service_identity" "ck_sa" { `, context) } + +func TestAccSpannerDatabase_mrcmek(t *testing.T) { + acctest.SkipIfVcr(t) + t.Parallel() + + kms1 := acctest.BootstrapKMSKeyWithPurposeInLocationAndName(t, "ENCRYPT_DECRYPT", "us-central1", "tf-mr-cmek-test-key-us-central1") + kms2 := acctest.BootstrapKMSKeyWithPurposeInLocationAndName(t, "ENCRYPT_DECRYPT", "us-east1", "tf-mr-cmek-test-key-us-east1") + kms3 := acctest.BootstrapKMSKeyWithPurposeInLocationAndName(t, "ENCRYPT_DECRYPT", "us-east4", "tf-mr-cmek-test-key-us-east4") + context := map[string]interface{}{ + "random_suffix": acctest.RandString(t, 10), + "key_ring1": kms1.KeyRing.Name, + "key_name1": kms1.CryptoKey.Name, + "key_ring2": kms2.KeyRing.Name, + "key_name2": kms2.CryptoKey.Name, + "key_ring3": kms3.KeyRing.Name, + "key_name3": kms3.CryptoKey.Name, + } + + 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 = [ + "%{key_name1}", + "%{key_name2}", + "%{key_name3}", + ] + } + + deletion_protection = false + +} + + +`, 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.)