diff --git a/.changelog/8658.txt b/.changelog/8658.txt new file mode 100644 index 00000000000..6269662fb32 --- /dev/null +++ b/.changelog/8658.txt @@ -0,0 +1,3 @@ +```release-note:bug +bigquery: added support to unset policy tags in table schema +``` diff --git a/google/services/bigquery/resource_bigquery_table.go b/google/services/bigquery/resource_bigquery_table.go index faec2b56c42..d508cb46653 100644 --- a/google/services/bigquery/resource_bigquery_table.go +++ b/google/services/bigquery/resource_bigquery_table.go @@ -149,6 +149,9 @@ func bigQueryTableMapKeyOverride(key string, objectA, objectB map[string]interfa return false } return bigQueryTableTypeEq(valA.(string), valB.(string)) + case "policyTags": + eq := bigQueryTableNormalizePolicyTags(valA) == nil && bigQueryTableNormalizePolicyTags(valB) == nil + return eq } // otherwise rely on default behavior @@ -225,6 +228,23 @@ func bigQueryTableModeIsForceNew(old, new string) bool { return !eq && !reqToNull } +func bigQueryTableNormalizePolicyTags(val interface{}) interface{} { + if val == nil { + return nil + } + if policyTags, ok := val.(map[string]interface{}); ok { + // policyTags = {} is same as nil. + if len(policyTags) == 0 { + return nil + } + // policyTags = {names = []} is same as nil. + if names, ok := policyTags["names"].([]interface{}); ok && len(names) == 0 { + return nil + } + } + return val +} + // Compares two existing schema implementations and decides if // it is changeable.. pairs with a force new on not changeable func resourceBigQueryTableSchemaIsChangeable(old, new interface{}) (bool, error) { @@ -1114,7 +1134,10 @@ func resourceTable(d *schema.ResourceData, meta interface{}) (*bigquery.Table, e } if v, ok := d.GetOk("schema"); ok { - schema, err := expandSchema(v) + _, viewPresent := d.GetOk("view") + _, materializedViewPresent := d.GetOk("materialized_view") + managePolicyTags := !viewPresent && !materializedViewPresent + schema, err := expandSchema(v, managePolicyTags) if err != nil { return nil, err } @@ -1462,7 +1485,8 @@ func expandExternalDataConfiguration(cfg interface{}) (*bigquery.ExternalDataCon edc.MaxBadRecords = int64(v.(int)) } if v, ok := raw["schema"]; ok { - schema, err := expandSchema(v) + managePolicyTags := true + schema, err := expandSchema(v, managePolicyTags) if err != nil { return nil, err } @@ -1781,7 +1805,7 @@ func flattenJsonOptions(opts *bigquery.JsonOptions) []map[string]interface{} { return []map[string]interface{}{result} } -func expandSchema(raw interface{}) (*bigquery.TableSchema, error) { +func expandSchema(raw interface{}, managePolicyTags bool) (*bigquery.TableSchema, error) { var fields []*bigquery.TableFieldSchema if len(raw.(string)) == 0 { @@ -1792,6 +1816,12 @@ func expandSchema(raw interface{}) (*bigquery.TableSchema, error) { return nil, err } + if managePolicyTags { + for _, field := range fields { + setEmptyPolicyTagsInSchema(field) + } + } + return &bigquery.TableSchema{Fields: fields}, nil } @@ -1804,6 +1834,21 @@ func flattenSchema(tableSchema *bigquery.TableSchema) (string, error) { return string(schema), nil } +// Explicitly set empty PolicyTags unless the PolicyTags field is specified in the schema. +func setEmptyPolicyTagsInSchema(field *bigquery.TableFieldSchema) { + // Field has children fields. + if len(field.Fields) > 0 { + for _, subField := range field.Fields { + setEmptyPolicyTagsInSchema(subField) + } + return + } + // Field is a leaf. + if field.PolicyTags == nil { + field.PolicyTags = &bigquery.TableFieldSchemaPolicyTags{Names: []string{}} + } +} + func expandTimePartitioning(configured interface{}) *bigquery.TimePartitioning { raw := configured.([]interface{})[0].(map[string]interface{}) tp := &bigquery.TimePartitioning{Type: raw["type"].(string)} diff --git a/google/services/bigquery/resource_bigquery_table_test.go b/google/services/bigquery/resource_bigquery_table_test.go index da5aec99957..2706a53c2ca 100644 --- a/google/services/bigquery/resource_bigquery_table_test.go +++ b/google/services/bigquery/resource_bigquery_table_test.go @@ -991,6 +991,142 @@ func TestAccBigQueryTable_emptySchema(t *testing.T) { }) } +func TestAccBigQueryTable_Update_SchemaWithoutPolicyTagsToWithPolicyTags(t *testing.T) { + t.Parallel() + + datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + projectID := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigQueryTableBasicSchema(datasetID, tableID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccBigQueryTableBasicSchemaWithPolicyTags(datasetID, tableID, projectID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccBigQueryTable_Update_SchemaWithPolicyTagsToNoPolicyTag(t *testing.T) { + t.Parallel() + + datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + projectID := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigQueryTableBasicSchemaWithPolicyTags(datasetID, tableID, projectID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccBigQueryTableBasicSchema(datasetID, tableID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccBigQueryTable_Update_SchemaWithPolicyTagsToEmptyPolicyTag(t *testing.T) { + t.Parallel() + + datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + projectID := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigQueryTableBasicSchemaWithPolicyTags(datasetID, tableID, projectID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccBigQueryTableBasicSchemaWithEmptyPolicyTags(datasetID, tableID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccBigQueryTable_Update_SchemaWithPolicyTagsToEmptyPolicyTagNames(t *testing.T) { + t.Parallel() + + datasetID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + tableID := fmt.Sprintf("tf_test_%s", acctest.RandString(t, 10)) + projectID := envvar.GetTestProjectFromEnv() + + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccBigQueryTableBasicSchemaWithPolicyTags(datasetID, tableID, projectID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccBigQueryTableBasicSchemaWithEmptyPolicyTagNames(datasetID, tableID), + }, + { + ResourceName: "google_bigquery_table.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + func testAccCheckBigQueryExtData(t *testing.T, expectedQuoteChar string) resource.TestCheckFunc { return func(s *terraform.State) error { for _, rs := range s.RootModule().Resources { @@ -1037,6 +1173,141 @@ func testAccCheckBigQueryTableDestroyProducer(t *testing.T) func(s *terraform.St } } +func testAccBigQueryTableBasicSchema(datasetID, tableID string) string { + return fmt.Sprintf(` +resource "google_bigquery_dataset" "test" { + dataset_id = "%s" +} + +resource "google_bigquery_table" "test" { + deletion_protection = false + table_id = "%s" + dataset_id = google_bigquery_dataset.test.dataset_id + + schema = <