Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bigtable: table deletion protection support #13232

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/6722.txt
@@ -0,0 +1,4 @@
```release-note:enhancement
bigtable: supported table deletion protection in terraform

```
44 changes: 44 additions & 0 deletions google/resource_bigtable_table.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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{}))
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}

Expand Down
192 changes: 192 additions & 0 deletions google/resource_bigtable_table_test.go
Expand Up @@ -3,6 +3,7 @@ package google
import (
"context"
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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)
}