From fada168d140010f42e04de3612c607d70046a643 Mon Sep 17 00:00:00 2001 From: The Magician Date: Tue, 13 Dec 2022 10:35:59 -0800 Subject: [PATCH] Bigtable: table deletion protection support (#6722) (#4975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cameron Thornton Co-authored-by: Edward Sun Co-authored-by: Luca Prete Co-authored-by: Stephen Lewis (Burrows) Co-authored-by: Aleksandr Averbukh Co-authored-by: Edward Sun <42220489+edwardmedia@users.noreply.github.com> Co-authored-by: Jay Sanghani <111726632+sanghaniJ@users.noreply.github.com> Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> Co-authored-by: Stephen Lewis (Burrows) Co-authored-by: AlfatahB <111489009+AlfatahB@users.noreply.github.com> Co-authored-by: Kevin Si Co-authored-by: Zhenhua Li Co-authored-by: Mikołaj Siedlarek Co-authored-by: Avi Dave Co-authored-by: Grigory Solomin <5736535+gsolomin@users.noreply.github.com> Co-authored-by: Avi Dave Co-authored-by: Ryan Oaks Co-authored-by: Haruaki OTAKE Co-authored-by: x Co-authored-by: sakuya9t <4102161+sakuya9t@users.noreply.github.com> Co-authored-by: Luca Prete Co-authored-by: aniketkumarj <112697326+aniketkumarj@users.noreply.github.com> Co-authored-by: Terrence Ryan Co-authored-by: Sam Levenick Co-authored-by: Shotaro Kohama Co-authored-by: Francis (Feng) Liu Co-authored-by: bohengy <108434983+bohengy@users.noreply.github.com> Co-authored-by: Søren Hansen Co-authored-by: Ilia Lazebnik Co-authored-by: t-indumathy <30333884+t-indumathy@users.noreply.github.com> Co-authored-by: AarshDhokai <111489014+AarshDhokai@users.noreply.github.com> Co-authored-by: Joost Buskermolen Co-authored-by: Benjamin Berriot <40873921+IIBenII@users.noreply.github.com> Co-authored-by: Scott Suarez Co-authored-by: Carl Yeksigian Co-authored-by: Shuya Ma <87669292+shuyama1@users.noreply.github.com> Co-authored-by: iperetz-goo <108678927+iperetz-goo@users.noreply.github.com> Co-authored-by: Riley Karson Co-authored-by: Thomas Rodgers Co-authored-by: Daniel Vega-Myhre <105610547+danielvegamyhre@users.noreply.github.com> Co-authored-by: Neha Vellanki <35039892+neha-vellanki12@users.noreply.github.com> Co-authored-by: gfxcc Co-authored-by: Lingkai Shen Co-authored-by: Stenal P Jolly Co-authored-by: Mohamed Fouad <110571142+mraouffouad@users.noreply.github.com> Signed-off-by: Modular Magician Signed-off-by: Modular Magician Co-authored-by: Cameron Thornton Co-authored-by: Edward Sun Co-authored-by: Luca Prete Co-authored-by: Stephen Lewis (Burrows) Co-authored-by: Aleksandr Averbukh Co-authored-by: Edward Sun <42220489+edwardmedia@users.noreply.github.com> Co-authored-by: Jay Sanghani <111726632+sanghaniJ@users.noreply.github.com> Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> Co-authored-by: Stephen Lewis (Burrows) Co-authored-by: AlfatahB <111489009+AlfatahB@users.noreply.github.com> Co-authored-by: Kevin Si Co-authored-by: Zhenhua Li Co-authored-by: Mikołaj Siedlarek Co-authored-by: Avi Dave Co-authored-by: Grigory Solomin <5736535+gsolomin@users.noreply.github.com> Co-authored-by: Avi Dave Co-authored-by: Ryan Oaks Co-authored-by: Haruaki OTAKE Co-authored-by: x Co-authored-by: sakuya9t <4102161+sakuya9t@users.noreply.github.com> Co-authored-by: Luca Prete Co-authored-by: aniketkumarj <112697326+aniketkumarj@users.noreply.github.com> Co-authored-by: Terrence Ryan Co-authored-by: Sam Levenick Co-authored-by: Shotaro Kohama Co-authored-by: Francis (Feng) Liu Co-authored-by: bohengy <108434983+bohengy@users.noreply.github.com> Co-authored-by: Søren Hansen Co-authored-by: Ilia Lazebnik Co-authored-by: t-indumathy <30333884+t-indumathy@users.noreply.github.com> Co-authored-by: AarshDhokai <111489014+AarshDhokai@users.noreply.github.com> Co-authored-by: Joost Buskermolen Co-authored-by: Benjamin Berriot <40873921+IIBenII@users.noreply.github.com> Co-authored-by: Scott Suarez Co-authored-by: Carl Yeksigian Co-authored-by: Shuya Ma <87669292+shuyama1@users.noreply.github.com> Co-authored-by: iperetz-goo <108678927+iperetz-goo@users.noreply.github.com> Co-authored-by: Riley Karson Co-authored-by: Thomas Rodgers Co-authored-by: Daniel Vega-Myhre <105610547+danielvegamyhre@users.noreply.github.com> Co-authored-by: Neha Vellanki <35039892+neha-vellanki12@users.noreply.github.com> Co-authored-by: gfxcc Co-authored-by: Lingkai Shen Co-authored-by: Stenal P Jolly Co-authored-by: Mohamed Fouad <110571142+mraouffouad@users.noreply.github.com> --- .changelog/6722.txt | 4 + google-beta/resource_bigtable_table.go | 44 +++++ google-beta/resource_bigtable_table_test.go | 192 ++++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 .changelog/6722.txt diff --git a/.changelog/6722.txt b/.changelog/6722.txt new file mode 100644 index 0000000000..94333fb05f --- /dev/null +++ b/.changelog/6722.txt @@ -0,0 +1,4 @@ +```release-note:enhancement +bigtable: supported table deletion protection in terraform + +``` diff --git a/google-beta/resource_bigtable_table.go b/google-beta/resource_bigtable_table.go index 8944b1dacc..1619fceadb 100644 --- a/google-beta/resource_bigtable_table.go +++ b/google-beta/resource_bigtable_table.go @@ -8,6 +8,7 @@ import ( "cloud.google.com/go/bigtable" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceBigtableTable() *schema.Resource { @@ -76,6 +77,15 @@ func resourceBigtableTable() *schema.Resource { ForceNew: true, Description: `The ID of the project in which the resource belongs. If it is not provided, the provider project is used.`, }, + + "deletion_protection": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"PROTECTED", "UNPROTECTED"}, false), + Elem: &schema.Schema{Type: schema.TypeString}, + Description: `A field to make the table protected against data loss i.e. when set to PROTECTED, deleting the table, the column families in the table, and the instance containing the table would be prohibited. If not provided, currently deletion protection will be set to UNPROTECTED as it is the API default value.`, + }, }, UseJSONNumber: true, } @@ -109,6 +119,15 @@ func resourceBigtableTableCreate(d *schema.ResourceData, meta interface{}) error tableId := d.Get("name").(string) tblConf := bigtable.TableConf{TableID: tableId} + // Check if deletion protection is given + // If not given, currently tblConf.DeletionProtection will be set to false in the API + deletionProtection := d.Get("deletion_protection") + if deletionProtection == "PROTECTED" { + tblConf.DeletionProtection = bigtable.Protected + } else if deletionProtection == "UNPROTECTED" { + tblConf.DeletionProtection = bigtable.Unprotected + } + // Set the split keys if given. if v, ok := d.GetOk("split_keys"); ok { tblConf.SplitKeys = convertStringArr(v.([]interface{})) @@ -188,6 +207,18 @@ func resourceBigtableTableRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error setting column_family: %s", err) } + deletionProtection := table.DeletionProtection + if deletionProtection == bigtable.Protected { + if err := d.Set("deletion_protection", "PROTECTED"); err != nil { + return fmt.Errorf("Error setting deletion_protection: %s", err) + } + } else if deletionProtection == bigtable.Unprotected { + if err := d.Set("deletion_protection", "UNPROTECTED"); err != nil { + return fmt.Errorf("Error setting deletion_protection: %s", err) + } + } else { + return fmt.Errorf("Error setting deletion_protection, it should be either PROTECTED or UNPROTECTED") + } return nil } @@ -240,6 +271,19 @@ func resourceBigtableTableUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("deletion_protection") { + deletionProtection := d.Get("deletion_protection") + if deletionProtection == "PROTECTED" { + if err := c.UpdateTableWithDeletionProtection(ctx, name, bigtable.Protected); err != nil { + return fmt.Errorf("Error updating deletion protection in table %v: %s", name, err) + } + } else if deletionProtection == "UNPROTECTED" { + if err := c.UpdateTableWithDeletionProtection(ctx, name, bigtable.Unprotected); err != nil { + return fmt.Errorf("Error updating deletion protection in table %v: %s", name, err) + } + } + } + return resourceBigtableTableRead(d, meta) } diff --git a/google-beta/resource_bigtable_table_test.go b/google-beta/resource_bigtable_table_test.go index 69dbf2dffe..538dbb8625 100644 --- a/google-beta/resource_bigtable_table_test.go +++ b/google-beta/resource_bigtable_table_test.go @@ -3,6 +3,7 @@ package google import ( "context" "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -86,6 +87,125 @@ func TestAccBigtableTable_family(t *testing.T) { }) } +func TestAccBigtableTable_deletion_protection_protected(t *testing.T) { + // bigtable instance does not use the shared HTTP client, this test creates an instance + skipIfVcr(t) + t.Parallel() + + instanceName := fmt.Sprintf("tf-test-%s", randString(t, 10)) + tableName := fmt.Sprintf("tf-test-%s", randString(t, 10)) + family := fmt.Sprintf("tf-test-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBigtableTableDestroyProducer(t), + Steps: []resource.TestStep{ + // creating a table with a column family and deletion protection equals to protected + { + Config: testAccBigtableTable_deletion_protection(instanceName, tableName, "PROTECTED", family), + }, + { + ResourceName: "google_bigtable_table.table", + ImportState: true, + ImportStateVerify: true, + }, + // it is not possible to delete column families in the table with deletion protection equals to protected + { + Config: testAccBigtableTable(instanceName, tableName), + ExpectError: regexp.MustCompile(".*deletion protection field is set to true.*"), + }, + // it is not possible to delete the table because of deletion protection equals to protected + { + Config: testAccBigtableTable_destroyTable(instanceName), + ExpectError: regexp.MustCompile(".*deletion protection field is set to true.*"), + }, + // changing deletion protection field to unprotected without changing the column families + // checking if the table and the column family exists + { + Config: testAccBigtableTable_deletion_protection(instanceName, tableName, "UNPROTECTED", family), + Check: resource.ComposeTestCheckFunc( + testAccBigtableColumnFamilyExists(t, "google_bigtable_table.table", family), + ), + }, + { + ResourceName: "google_bigtable_table.table", + ImportState: true, + ImportStateVerify: true, + }, + // destroying the table is possible when deletion protection is equals to unprotected + { + Config: testAccBigtableTable_destroyTable(instanceName), + }, + { + ResourceName: "google_bigtable_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "instance_type"}, + }, + }, + }) +} + +func TestAccBigtableTable_deletion_protection_unprotected(t *testing.T) { + // bigtable instance does not use the shared HTTP client, this test creates an instance + skipIfVcr(t) + t.Parallel() + + instanceName := fmt.Sprintf("tf-test-%s", randString(t, 10)) + tableName := fmt.Sprintf("tf-test-%s", randString(t, 10)) + family := fmt.Sprintf("tf-test-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBigtableTableDestroyProducer(t), + Steps: []resource.TestStep{ + // creating a table with a column family and deletion protection equals to unprotected + { + Config: testAccBigtableTable_deletion_protection(instanceName, tableName, "UNPROTECTED", family), + }, + { + ResourceName: "google_bigtable_table.table", + ImportState: true, + ImportStateVerify: true, + }, + // removing the column family is possible because the deletion protection field is unprotected + { + Config: testAccBigtableTable(instanceName, tableName), + }, + { + ResourceName: "google_bigtable_table.table", + ImportState: true, + ImportStateVerify: true, + }, + // changing the deletion protection field to protected + { + Config: testAccBigtableTable_deletion_protection(instanceName, tableName, "PROTECTED", family), + }, + { + ResourceName: "google_bigtable_table.table", + ImportState: true, + ImportStateVerify: true, + }, + // it is not possible to delete the table because of deletion protection equals to protected + { + Config: testAccBigtableTable_destroyTable(instanceName), + ExpectError: regexp.MustCompile(".*deletion protection field is set to true.*"), + }, + // changing the deletion protection field to unprotected so that the sources can properly be destroyed + { + Config: testAccBigtableTable_deletion_protection(instanceName, tableName, "UNPROTECTED", family), + }, + { + ResourceName: "google_bigtable_table.table", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccBigtableTable_familyMany(t *testing.T) { // bigtable instance does not use the shared HTTP client, this test creates an instance skipIfVcr(t) @@ -173,6 +293,36 @@ func testAccCheckBigtableTableDestroyProducer(t *testing.T) func(s *terraform.St } } +func testAccBigtableColumnFamilyExists(t *testing.T, table_name_space, family string) resource.TestCheckFunc { + var ctx = context.Background() + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[table_name_space] + if !ok { + return fmt.Errorf("Table not found: %s", table_name_space) + } + + config := googleProviderConfig(t) + c, err := config.BigTableClientFactory(config.userAgent).NewAdminClient(config.Project, rs.Primary.Attributes["instance_name"]) + if err != nil { + return fmt.Errorf("Error starting admin client. %s", err) + } + + defer c.Close() + + table, err := c.TableInfo(ctx, rs.Primary.Attributes["name"]) + if err != nil { + return fmt.Errorf("Error retrieving table. Could not find %s in %s.", rs.Primary.Attributes["name"], rs.Primary.Attributes["instance_name"]) + } + for _, data := range flattenColumnFamily(table.Families) { + if data["family"] != family { + return fmt.Errorf("Error checking column family. Could not find column family %s in %s.", family, rs.Primary.Attributes["name"]) + } + } + + return nil + } +} + func testAccBigtableTable(instanceName, tableName string) string { return fmt.Sprintf(` resource "google_bigtable_instance" "instance" { @@ -239,6 +389,32 @@ resource "google_bigtable_table" "table" { `, instanceName, instanceName, tableName, family) } +func testAccBigtableTable_deletion_protection(instanceName, tableName, deletionProtection, family string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + + cluster { + cluster_id = "%s" + zone = "us-central1-b" + } + + instance_type = "DEVELOPMENT" + deletion_protection = false +} + +resource "google_bigtable_table" "table" { + name = "%s" + instance_name = google_bigtable_instance.instance.name + deletion_protection = "%s" + + column_family { + family = "%s" + } +} +`, instanceName, instanceName, tableName, deletionProtection, family) +} + func testAccBigtableTable_familyMany(instanceName, tableName, family string) string { return fmt.Sprintf(` resource "google_bigtable_instance" "instance" { @@ -300,3 +476,19 @@ resource "google_bigtable_table" "table" { } `, instanceName, instanceName, tableName, family, family, family) } + +func testAccBigtableTable_destroyTable(instanceName string) string { + return fmt.Sprintf(` +resource "google_bigtable_instance" "instance" { + name = "%s" + + cluster { + cluster_id = "%s" + zone = "us-central1-b" + } + + instance_type = "DEVELOPMENT" + deletion_protection = false +} +`, instanceName, instanceName) +}