diff --git a/.changelog/12868.txt b/.changelog/12868.txt new file mode 100644 index 00000000000..6959a41a7ad --- /dev/null +++ b/.changelog/12868.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +spanner: added `encryption_config` field to `google_spanner_backup_schedule` +``` \ No newline at end of file diff --git a/google/services/spanner/resource_spanner_backup_schedule.go b/google/services/spanner/resource_spanner_backup_schedule.go index d37f67a53d7..860b623d755 100644 --- a/google/services/spanner/resource_spanner_backup_schedule.go +++ b/google/services/spanner/resource_spanner_backup_schedule.go @@ -77,6 +77,32 @@ func ResourceSpannerBackupSchedule() *schema.Resource { A duration in seconds with up to nine fractional digits, ending with 's'. Example: '3.5s'. You can set this to a value up to 366 days.`, }, + "encryption_config": { + Type: schema.TypeList, + Computed: true, + Optional: true, + Description: `Configuration for the encryption of the backup schedule.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "encryption_type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidateEnum([]string{"USE_DATABASE_ENCRYPTION", "GOOGLE_DEFAULT_ENCRYPTION", "CUSTOMER_MANAGED_ENCRYPTION"}), + Description: `The encryption type of backups created by the backup schedule. +Possible values are USE_DATABASE_ENCRYPTION, GOOGLE_DEFAULT_ENCRYPTION, or CUSTOMER_MANAGED_ENCRYPTION. +If you use CUSTOMER_MANAGED_ENCRYPTION, you must specify a kmsKeyName. +If your backup type is incremental-backup, the encryption type must be GOOGLE_DEFAULT_ENCRYPTION. Possible values: ["USE_DATABASE_ENCRYPTION", "GOOGLE_DEFAULT_ENCRYPTION", "CUSTOMER_MANAGED_ENCRYPTION"]`, + }, + "kms_key_name": { + Type: schema.TypeString, + Optional: true, + Description: `The resource name of the Cloud KMS key to use for encryption. +Format: 'projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{cryptoKey}'`, + }, + }, + }, + }, "full_backup_spec": { Type: schema.TypeList, Optional: true, @@ -191,6 +217,12 @@ func resourceSpannerBackupScheduleCreate(d *schema.ResourceData, meta interface{ } else if v, ok := d.GetOkExists("incremental_backup_spec"); ok || !reflect.DeepEqual(v, incrementalBackupSpecProp) { obj["incrementalBackupSpec"] = incrementalBackupSpecProp } + encryptionConfigProp, err := expandSpannerBackupScheduleEncryptionConfig(d.Get("encryption_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("encryption_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(encryptionConfigProp)) && (ok || !reflect.DeepEqual(v, encryptionConfigProp)) { + obj["encryptionConfig"] = encryptionConfigProp + } obj, err = resourceSpannerBackupScheduleEncoder(d, meta, obj) if err != nil { @@ -312,6 +344,9 @@ func resourceSpannerBackupScheduleRead(d *schema.ResourceData, meta interface{}) if err := d.Set("incremental_backup_spec", flattenSpannerBackupScheduleIncrementalBackupSpec(res["incrementalBackupSpec"], d, config)); err != nil { return fmt.Errorf("Error reading BackupSchedule: %s", err) } + if err := d.Set("encryption_config", flattenSpannerBackupScheduleEncryptionConfig(res["encryptionConfig"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupSchedule: %s", err) + } return nil } @@ -344,6 +379,12 @@ func resourceSpannerBackupScheduleUpdate(d *schema.ResourceData, meta interface{ } else if v, ok := d.GetOkExists("spec"); ok || !reflect.DeepEqual(v, specProp) { obj["spec"] = specProp } + encryptionConfigProp, err := expandSpannerBackupScheduleEncryptionConfig(d.Get("encryption_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("encryption_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, encryptionConfigProp)) { + obj["encryptionConfig"] = encryptionConfigProp + } obj, err = resourceSpannerBackupScheduleEncoder(d, meta, obj) if err != nil { @@ -366,6 +407,10 @@ func resourceSpannerBackupScheduleUpdate(d *schema.ResourceData, meta interface{ if d.HasChange("spec") { updateMask = append(updateMask, "spec") } + + if d.HasChange("encryption_config") { + updateMask = append(updateMask, "encryptionConfig") + } // updateMask is a URL parameter but not present in the schema, so ReplaceVars // won't set it url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) @@ -541,6 +586,29 @@ func flattenSpannerBackupScheduleIncrementalBackupSpec(v interface{}, d *schema. return []interface{}{transformed} } +func flattenSpannerBackupScheduleEncryptionConfig(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["encryption_type"] = + flattenSpannerBackupScheduleEncryptionConfigEncryptionType(original["encryptionType"], d, config) + transformed["kms_key_name"] = + flattenSpannerBackupScheduleEncryptionConfigKmsKeyName(original["kmsKeyName"], d, config) + return []interface{}{transformed} +} +func flattenSpannerBackupScheduleEncryptionConfigEncryptionType(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + +func flattenSpannerBackupScheduleEncryptionConfigKmsKeyName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + func expandSpannerBackupScheduleName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { return v, nil } @@ -626,6 +694,40 @@ func expandSpannerBackupScheduleIncrementalBackupSpec(v interface{}, d tpgresour return transformed, nil } +func expandSpannerBackupScheduleEncryptionConfig(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.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{}) + + transformedEncryptionType, err := expandSpannerBackupScheduleEncryptionConfigEncryptionType(original["encryption_type"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedEncryptionType); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["encryptionType"] = transformedEncryptionType + } + + transformedKmsKeyName, err := expandSpannerBackupScheduleEncryptionConfigKmsKeyName(original["kms_key_name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedKmsKeyName); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["kmsKeyName"] = transformedKmsKeyName + } + + return transformed, nil +} + +func expandSpannerBackupScheduleEncryptionConfigEncryptionType(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandSpannerBackupScheduleEncryptionConfigKmsKeyName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + func resourceSpannerBackupScheduleEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { obj["name"] = d.Get("name").(string) if obj["name"] == nil || obj["name"] == "" { diff --git a/google/services/spanner/resource_spanner_backup_schedule_generated_meta.yaml b/google/services/spanner/resource_spanner_backup_schedule_generated_meta.yaml index 0305486b758..69d665acc8c 100644 --- a/google/services/spanner/resource_spanner_backup_schedule_generated_meta.yaml +++ b/google/services/spanner/resource_spanner_backup_schedule_generated_meta.yaml @@ -6,6 +6,8 @@ api_resource_type_kind: 'BackupSchedule' fields: - field: 'database' provider_only: true + - field: 'encryption_config.encryption_type' + - field: 'encryption_config.kms_key_name' - field: 'full_backup_spec' - field: 'incremental_backup_spec' - field: 'instance' diff --git a/google/services/spanner/resource_spanner_backup_schedule_generated_test.go b/google/services/spanner/resource_spanner_backup_schedule_generated_test.go index ee586f8bfa6..d29ec2bc50d 100644 --- a/google/services/spanner/resource_spanner_backup_schedule_generated_test.go +++ b/google/services/spanner/resource_spanner_backup_schedule_generated_test.go @@ -97,6 +97,10 @@ resource "google_spanner_backup_schedule" "full-backup" { } // The schedule creates only full backups. full_backup_spec {} + + encryption_config { + encryption_type = "USE_DATABASE_ENCRYPTION" + } } `, context) } @@ -169,6 +173,10 @@ resource "google_spanner_backup_schedule" "incremental-backup" { } // The schedule creates incremental backup chains. incremental_backup_spec {} + + encryption_config { + encryption_type = "GOOGLE_DEFAULT_ENCRYPTION" + } } `, context) } diff --git a/google/services/spanner/resource_spanner_schedule_backup_test.go b/google/services/spanner/resource_spanner_schedule_backup_test.go index 0df41e71f33..62bdbfc4b24 100644 --- a/google/services/spanner/resource_spanner_schedule_backup_test.go +++ b/google/services/spanner/resource_spanner_schedule_backup_test.go @@ -44,6 +44,68 @@ func TestAccSpannerBackupSchedule_update(t *testing.T) { }) } +func TestAccSpannerBackupSchedule_CMEKIncrementalBackup(t *testing.T) { + t.Parallel() + suffix := acctest.RandString(t, 10) + kms := acctest.BootstrapKMSKeyWithPurposeInLocationAndName(t, "ENCRYPT_DECRYPT", "us-central1", "tf-bootstrap-spanner-key") + + context := map[string]interface{}{ + "random_suffix": suffix, + "key_name": kms.CryptoKey.Name, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckSpannerBackupScheduleDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSpannerBackupSchedule_CMEKIncremental(context), + }, + { + ResourceName: "google_spanner_backup_schedule.backup_schedule", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSpannerBackupSchedule_CMEKFullBackup(t *testing.T) { + t.Parallel() + suffix := acctest.RandString(t, 10) + kms := acctest.BootstrapKMSKeyWithPurposeInLocationAndName(t, "ENCRYPT_DECRYPT", "us-central1", "tf-bootstrap-spanner-key") + + context := map[string]interface{}{ + "random_suffix": suffix, + "key_name": kms.CryptoKey.Name, + } + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckSpannerBackupScheduleDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSpannerBackupSchedule_basic(context), + }, + { + ResourceName: "google_spanner_backup_schedule.backup_schedule", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccSpannerBackupSchedule_CMEKFull(context), + }, + { + ResourceName: "google_spanner_backup_schedule.backup_schedule", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccSpannerBackupSchedule_basic(context map[string]interface{}) string { return acctest.Nprintf(` resource "google_spanner_instance" "instance" { @@ -51,6 +113,7 @@ resource "google_spanner_instance" "instance" { config = "regional-us-central1" display_name = "My Instance" num_nodes = 1 + edition = "ENTERPRISE" } resource "google_spanner_database" "database" { @@ -87,6 +150,7 @@ resource "google_spanner_instance" "instance" { config = "regional-us-central1" display_name = "My Instance" num_nodes = 1 + edition = "ENTERPRISE" } resource "google_spanner_database" "database" { @@ -115,3 +179,90 @@ resource "google_spanner_backup_schedule" "backup_schedule" { } `, context) } + +func testAccSpannerBackupSchedule_CMEKIncremental(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_spanner_instance" "instance" { + name = "my-instance-%{random_suffix}" + config = "regional-us-central1" + display_name = "My Instance" + num_nodes = 1 + edition = "ENTERPRISE" +} + +resource "google_spanner_database" "database" { + instance = google_spanner_instance.instance.name + name = "my-database-%{random_suffix}" + ddl = [ + "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)", + ] + deletion_protection = false + + encryption_config { + kms_key_name = "%{key_name}" + } +} + +resource "google_spanner_backup_schedule" "backup_schedule" { + instance = google_spanner_instance.instance.name + database = google_spanner_database.database.name + name = "my-backup-schedule-%{random_suffix}" + + retention_duration = "172800s" + + spec { + cron_spec { + text = "0 12 * * *" + } + } + + incremental_backup_spec {} + + encryption_config { + encryption_type = "GOOGLE_DEFAULT_ENCRYPTION" + } +} +`, context) +} + +func testAccSpannerBackupSchedule_CMEKFull(context map[string]interface{}) string { + return acctest.Nprintf(` +resource "google_spanner_instance" "instance" { + name = "my-instance-%{random_suffix}" + config = "regional-us-central1" + display_name = "My Instance" + num_nodes = 1 + edition = "ENTERPRISE" +} + +resource "google_spanner_database" "database" { + instance = google_spanner_instance.instance.name + name = "my-database-%{random_suffix}" + ddl = [ + "CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)", + ] + deletion_protection = false +} + +resource "google_spanner_backup_schedule" "backup_schedule" { + instance = google_spanner_instance.instance.name + database = google_spanner_database.database.name + name = "my-backup-schedule-%{random_suffix}" + + retention_duration = "172800s" + + spec { + cron_spec { + text = "0 12 * * *" + } + } + + full_backup_spec {} + + encryption_config { + encryption_type = "CUSTOMER_MANAGED_ENCRYPTION" + kms_key_name = "%{key_name}" + } +} +`, context) +} diff --git a/website/docs/r/spanner_backup_schedule.html.markdown b/website/docs/r/spanner_backup_schedule.html.markdown index 4bd125be86e..0d007849b4f 100644 --- a/website/docs/r/spanner_backup_schedule.html.markdown +++ b/website/docs/r/spanner_backup_schedule.html.markdown @@ -83,6 +83,10 @@ resource "google_spanner_backup_schedule" "full-backup" { } // The schedule creates only full backups. full_backup_spec {} + + encryption_config { + encryption_type = "USE_DATABASE_ENCRYPTION" + } } ```