From 19dd6481f0cd0a76dbb19e33f1387aa45a321482 Mon Sep 17 00:00:00 2001 From: The Magician Date: Tue, 20 Apr 2021 18:38:10 -0700 Subject: [PATCH] Add cmek to spanner database (#4699) (#3181) * Add cmek to spanner database * Update timeout * Bump default timeout * Mark test as beta * Move to handwritten test Signed-off-by: Modular Magician --- .changelog/4699.txt | 6 ++ google-beta/config.go | 2 +- ...esource_dataflow_flex_template_job_test.go | 2 +- google-beta/resource_spanner_database.go | 67 ++++++++++++++ google-beta/resource_spanner_database_test.go | 88 +++++++++++++++++++ website/docs/r/spanner_database.html.markdown | 12 +++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 .changelog/4699.txt diff --git a/.changelog/4699.txt b/.changelog/4699.txt new file mode 100644 index 0000000000..2c44d865af --- /dev/null +++ b/.changelog/4699.txt @@ -0,0 +1,6 @@ +```release-note:enhancement +spanner: added support for setting a CMEK on `google_spanner_database` +``` +```release-note:note +all: changed default HTTP request timeout from 30 seconds to 120 seconds +``` diff --git a/google-beta/config.go b/google-beta/config.go index 3f5b6f4b8e..56d9a53372 100644 --- a/google-beta/config.go +++ b/google-beta/config.go @@ -350,7 +350,7 @@ func expandProviderBatchingConfig(v interface{}) (*batchingConfig, error) { func (c *Config) synchronousTimeout() time.Duration { if c.RequestTimeout == 0 { - return 30 * time.Second + return 120 * time.Second } return c.RequestTimeout } diff --git a/google-beta/resource_dataflow_flex_template_job_test.go b/google-beta/resource_dataflow_flex_template_job_test.go index 0d037482e0..df04d28bb0 100644 --- a/google-beta/resource_dataflow_flex_template_job_test.go +++ b/google-beta/resource_dataflow_flex_template_job_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - compute "google.golang.org/api/compute/v1" + "google.golang.org/api/compute/v1" ) func TestAccDataflowFlexTemplateJob_basic(t *testing.T) { diff --git a/google-beta/resource_spanner_database.go b/google-beta/resource_spanner_database.go index a3497aedc5..9572763b47 100644 --- a/google-beta/resource_spanner_database.go +++ b/google-beta/resource_spanner_database.go @@ -102,6 +102,24 @@ error in any statement, the database is not created.`, Type: schema.TypeString, }, }, + "encryption_config": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: `Encryption configuration for the database`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "kms_key_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + 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.`, + }, + }, + }, + }, "state": { Type: schema.TypeString, Computed: true, @@ -143,6 +161,12 @@ func resourceSpannerDatabaseCreate(d *schema.ResourceData, meta interface{}) err } else if v, ok := d.GetOkExists("ddl"); !isEmptyValue(reflect.ValueOf(extraStatementsProp)) && (ok || !reflect.DeepEqual(v, extraStatementsProp)) { obj["extraStatements"] = extraStatementsProp } + encryptionConfigProp, err := expandSpannerDatabaseEncryptionConfig(d.Get("encryption_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("encryption_config"); !isEmptyValue(reflect.ValueOf(encryptionConfigProp)) && (ok || !reflect.DeepEqual(v, encryptionConfigProp)) { + obj["encryptionConfig"] = encryptionConfigProp + } instanceProp, err := expandSpannerDatabaseInstance(d.Get("instance"), d, config) if err != nil { return err @@ -280,6 +304,9 @@ func resourceSpannerDatabaseRead(d *schema.ResourceData, meta interface{}) error if err := d.Set("state", flattenSpannerDatabaseState(res["state"], d, config)); err != nil { return fmt.Errorf("Error reading Database: %s", err) } + if err := d.Set("encryption_config", flattenSpannerDatabaseEncryptionConfig(res["encryptionConfig"], d, config)); err != nil { + return fmt.Errorf("Error reading Database: %s", err) + } if err := d.Set("instance", flattenSpannerDatabaseInstance(res["instance"], d, config)); err != nil { return fmt.Errorf("Error reading Database: %s", err) } @@ -434,6 +461,23 @@ func flattenSpannerDatabaseState(v interface{}, d *schema.ResourceData, config * return v } +func flattenSpannerDatabaseEncryptionConfig(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["kms_key_name"] = + flattenSpannerDatabaseEncryptionConfigKmsKeyName(original["kmsKeyName"], d, config) + return []interface{}{transformed} +} +func flattenSpannerDatabaseEncryptionConfigKmsKeyName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + func flattenSpannerDatabaseInstance(v interface{}, d *schema.ResourceData, config *Config) interface{} { if v == nil { return v @@ -449,6 +493,29 @@ func expandSpannerDatabaseDdl(v interface{}, d TerraformResourceData, config *Co return v, nil } +func expandSpannerDatabaseEncryptionConfig(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedKmsKeyName, err := expandSpannerDatabaseEncryptionConfigKmsKeyName(original["kms_key_name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedKmsKeyName); val.IsValid() && !isEmptyValue(val) { + transformed["kmsKeyName"] = transformedKmsKeyName + } + + return transformed, nil +} + +func expandSpannerDatabaseEncryptionConfigKmsKeyName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + func expandSpannerDatabaseInstance(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { f, err := parseGlobalFieldValue("instances", v.(string), "project", d, config, true) if err != nil { diff --git a/google-beta/resource_spanner_database_test.go b/google-beta/resource_spanner_database_test.go index 7f13f627a7..e39feab6cb 100644 --- a/google-beta/resource_spanner_database_test.go +++ b/google-beta/resource_spanner_database_test.go @@ -257,3 +257,91 @@ resource "google_spanner_database" "database" { } `, context) } + +func TestAccSpannerDatabase_cmek(t *testing.T) { + skipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckSpannerDatabaseDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSpannerDatabase_cmek(context), + }, + { + ResourceName: "google_spanner_database.database", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"}, + }, + }, + }) +} + +func testAccSpannerDatabase_cmek(context map[string]interface{}) string { + return Nprintf(` +resource "google_spanner_instance" "main" { + provider = google-beta + config = "regional-europe-west1" + display_name = "main-instance1" +} + +resource "google_spanner_database" "database" { + provider = google-beta + instance = google_spanner_instance.main.name + name = "tf-test-cmek-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_name = google_kms_crypto_key.example-key.id + } + + deletion_protection = false + + depends_on = [google_kms_crypto_key_iam_binding.crypto-key-binding] +} + +resource "google_kms_key_ring" "keyring" { + provider = google-beta + name = "tf-test-ring%{random_suffix}" + location = "europe-west1" +} + +resource "google_kms_crypto_key" "example-key" { + provider = google-beta + name = "tf-test-key%{random_suffix}" + key_ring = google_kms_key_ring.keyring.id + rotation_period = "100000s" +} + +resource "google_kms_crypto_key_iam_binding" "crypto-key-binding" { + provider = google-beta + crypto_key_id = google_kms_crypto_key.example-key.id + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + + members = [ + "serviceAccount:${google_project_service_identity.ck_sa.email}", + ] +} + +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) +} diff --git a/website/docs/r/spanner_database.html.markdown b/website/docs/r/spanner_database.html.markdown index 27f306e069..236681855c 100644 --- a/website/docs/r/spanner_database.html.markdown +++ b/website/docs/r/spanner_database.html.markdown @@ -88,6 +88,11 @@ The following arguments are supported: execute atomically with the creation of the database: if there is an error in any statement, the database is not created. +* `encryption_config` - + (Optional) + Encryption configuration for the database + Structure is documented below. + * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. @@ -95,6 +100,13 @@ The following arguments are supported: in Terraform state, a `terraform destroy` or `terraform apply` that would delete the instance will fail. +The `encryption_config` block supports: + +* `kms_key_name` - + (Required) + 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. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are exported: