Skip to content

Commit e15c104

Browse files
impl(bigquery_table): implementation of ignore_schema_changes virtual… (#13576) (#23495)
[upstream:2a22759ec0725321ddcb6ae69a41d133ef0db933] Signed-off-by: Modular Magician <[email protected]>
1 parent 5b5c0b4 commit e15c104

File tree

4 files changed

+172
-9
lines changed

4 files changed

+172
-9
lines changed

.changelog/13576.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
bigquery: added `ignore_schema_changes` virtual field to `google_bigquery_table` resource. Only `dataPolicies` field is supported in `ignore_schema_changes` for now.
3+
```

google/services/bigquery/resource_bigquery_table.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"strconv"
2929
"strings"
3030

31+
"golang.org/x/exp/slices"
32+
3133
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
3234
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
3335
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure"
@@ -66,7 +68,7 @@ func bigQueryTablecheckNameExists(jsonList []interface{}) error {
6668
// Compares two json's while optionally taking in a compareMapKeyVal function.
6769
// This function will override any comparison of a given map[string]interface{}
6870
// on a specific key value allowing for a separate equality in specific scenarios
69-
func jsonCompareWithMapKeyOverride(key string, a, b interface{}, compareMapKeyVal func(key string, val1, val2 map[string]interface{}) bool) (bool, error) {
71+
func jsonCompareWithMapKeyOverride(key string, a, b interface{}, compareMapKeyVal func(key string, val1, val2 map[string]interface{}, d *schema.ResourceData) bool, d *schema.ResourceData) (bool, error) {
7072
switch a.(type) {
7173
case []interface{}:
7274
arrayA := a.([]interface{})
@@ -89,7 +91,7 @@ func jsonCompareWithMapKeyOverride(key string, a, b interface{}, compareMapKeyVa
8991
bigQueryTableSortArrayByName(arrayB)
9092
}
9193
for i := range arrayA {
92-
eq, err := jsonCompareWithMapKeyOverride(strconv.Itoa(i), arrayA[i], arrayB[i], compareMapKeyVal)
94+
eq, err := jsonCompareWithMapKeyOverride(strconv.Itoa(i), arrayA[i], arrayB[i], compareMapKeyVal, d)
9395
if err != nil {
9496
return false, err
9597
} else if !eq {
@@ -119,14 +121,14 @@ func jsonCompareWithMapKeyOverride(key string, a, b interface{}, compareMapKeyVa
119121
}
120122

121123
for subKey := range unionOfKeys {
122-
eq := compareMapKeyVal(subKey, objectA, objectB)
124+
eq := compareMapKeyVal(subKey, objectA, objectB, d)
123125
if !eq {
124126
valA, ok1 := objectA[subKey]
125127
valB, ok2 := objectB[subKey]
126128
if !ok1 || !ok2 {
127129
return false, nil
128130
}
129-
eq, err := jsonCompareWithMapKeyOverride(subKey, valA, valB, compareMapKeyVal)
131+
eq, err := jsonCompareWithMapKeyOverride(subKey, valA, valB, compareMapKeyVal, d)
130132
if err != nil || !eq {
131133
return false, err
132134
}
@@ -152,7 +154,7 @@ func valueIsInArray(value interface{}, array []interface{}) bool {
152154
return false
153155
}
154156

155-
func bigQueryTableMapKeyOverride(key string, objectA, objectB map[string]interface{}) bool {
157+
func bigQueryTableMapKeyOverride(key string, objectA, objectB map[string]interface{}, d *schema.ResourceData) bool {
156158
// we rely on the fallback to nil if the object does not have the key
157159
valA := objectA[key]
158160
valB := objectB[key]
@@ -172,14 +174,22 @@ func bigQueryTableMapKeyOverride(key string, objectA, objectB map[string]interfa
172174
case "policyTags":
173175
eq := bigQueryTableNormalizePolicyTags(valA) == nil && bigQueryTableNormalizePolicyTags(valB) == nil
174176
return eq
177+
case "dataPolicies":
178+
if d == nil {
179+
return false
180+
}
181+
// Access the ignore_schema_changes list from the Terraform configuration
182+
ignoreSchemaChanges := d.Get("ignore_schema_changes").([]interface{})
183+
// Suppress diffs for the "dataPolicies" field if it was present in "ignore_schema_changes"
184+
return slices.Contains(ignoreSchemaChanges, "dataPolicies")
175185
}
176186

177187
// otherwise rely on default behavior
178188
return false
179189
}
180190

181191
// Compare the JSON strings are equal
182-
func bigQueryTableSchemaDiffSuppress(name, old, new string, _ *schema.ResourceData) bool {
192+
func bigQueryTableSchemaDiffSuppress(name, old, new string, d *schema.ResourceData) bool {
183193
// The API can return an empty schema which gets encoded to "null" during read.
184194
if old == "null" {
185195
old = "[]"
@@ -192,7 +202,7 @@ func bigQueryTableSchemaDiffSuppress(name, old, new string, _ *schema.ResourceDa
192202
log.Printf("[DEBUG] unable to unmarshal new json - %v", err)
193203
}
194204

195-
eq, err := jsonCompareWithMapKeyOverride(name, a, b, bigQueryTableMapKeyOverride)
205+
eq, err := jsonCompareWithMapKeyOverride(name, a, b, bigQueryTableMapKeyOverride, d)
196206
if err != nil {
197207
log.Printf("[DEBUG] %v", err)
198208
log.Printf("[DEBUG] Error comparing JSON: %v, %v", old, new)
@@ -1278,7 +1288,13 @@ func ResourceBigQueryTable() *schema.Resource {
12781288
Computed: true,
12791289
Description: `A hash of the resource.`,
12801290
},
1281-
1291+
"ignore_schema_changes": {
1292+
Type: schema.TypeList,
1293+
Optional: true,
1294+
MaxItems: 10,
1295+
Description: `Mention which fields in schema are to be ignored`,
1296+
Elem: &schema.Schema{Type: schema.TypeString},
1297+
},
12821298
// LastModifiedTime: [Output-only] The time when this table was last
12831299
// modified, in milliseconds since the epoch.
12841300
"last_modified_time": {
@@ -2070,7 +2086,7 @@ type TableReference struct {
20702086

20712087
func resourceBigQueryTableUpdate(d *schema.ResourceData, meta interface{}) error {
20722088
// If only client-side fields were modified, short-circuit the Update function to avoid sending an update API request.
2073-
clientSideFields := map[string]bool{"deletion_protection": true, "table_metadata_view": true}
2089+
clientSideFields := map[string]bool{"deletion_protection": true, "ignore_schema_changes": true, "table_metadata_view": true}
20742090
clientSideOnly := true
20752091
for field := range ResourceBigQueryTable().Schema {
20762092
if d.HasChange(field) && !clientSideFields[field] {

google/services/bigquery/resource_bigquery_table_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,51 @@ func TestAccBigQueryTable_Basic(t *testing.T) {
6161
})
6262
}
6363

64+
func TestAccBigQueryTable_IgnoreSchemaDataPoliciesChanges(t *testing.T) {
65+
t.Parallel()
66+
67+
projectID := envvar.GetTestProjectFromEnv()
68+
random_suffix := acctest.RandString(t, 10)
69+
datasetID := fmt.Sprintf("tf_test_dataset_%s", random_suffix)
70+
tableID := fmt.Sprintf("tf_test_table_%s", random_suffix)
71+
dataPolicyID1 := fmt.Sprintf("tf_test_data_policy_%s", random_suffix)
72+
dataPolicyName1 := fmt.Sprintf("projects/%s/locations/us-central1/dataPolicies/%s", projectID, dataPolicyID1)
73+
dataPolicyID2 := fmt.Sprintf("tf_test_data_policy_%s", acctest.RandString(t, 10))
74+
dataPolicyName2 := fmt.Sprintf("projects/%s/locations/us-central1/dataPolicies/%s", projectID, dataPolicyID2)
75+
dataCatTaxonomy := fmt.Sprintf("tf_test_taxonomy_%s", random_suffix)
76+
77+
acctest.VcrTest(t, resource.TestCase{
78+
PreCheck: func() { acctest.AccTestPreCheck(t) },
79+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
80+
CheckDestroy: testAccCheckBigQueryTableDestroyProducer(t),
81+
Steps: []resource.TestStep{
82+
{
83+
Config: testAccBigQueryTableDataPolicies(datasetID, tableID, dataPolicyID1, dataPolicyID2, dataCatTaxonomy, dataPolicyName1),
84+
},
85+
{
86+
ResourceName: "google_bigquery_table.test",
87+
ImportState: true,
88+
ImportStateVerify: true,
89+
ImportStateVerifyIgnore: []string{"deletion_protection", "ignore_schema_changes"},
90+
},
91+
{
92+
Config: testAccBigQueryTableDataPolicies(datasetID, tableID, dataPolicyID1, dataPolicyID2, dataCatTaxonomy, dataPolicyName2),
93+
PlanOnly: true,
94+
ExpectNonEmptyPlan: false,
95+
},
96+
{
97+
Config: testAccBigQueryTableUpdated(datasetID, tableID),
98+
},
99+
{
100+
ResourceName: "google_bigquery_table.test",
101+
ImportState: true,
102+
ImportStateVerify: true,
103+
ImportStateVerifyIgnore: []string{"deletion_protection", "ignore_schema_changes"},
104+
},
105+
},
106+
})
107+
}
108+
64109
func TestAccBigQueryTable_TableMetadataView(t *testing.T) {
65110
t.Parallel()
66111

@@ -1997,6 +2042,102 @@ EOH
19972042
`, datasetID, tableID)
19982043
}
19992044

2045+
func testAccBigQueryTableDataPolicies(datasetID, tableID, dataPolicyID1, dataPolicyID2, dataCatTaxonomy, dataPolicyName string) string {
2046+
return fmt.Sprintf(`
2047+
resource "google_bigquery_dataset" "test" {
2048+
location = "us-central1"
2049+
dataset_id = "%s"
2050+
}
2051+
2052+
resource "google_bigquery_datapolicy_data_policy" "data_policy1" {
2053+
location = "us-central1"
2054+
data_policy_id = "%s"
2055+
policy_tag = google_data_catalog_policy_tag.policy_tag.name
2056+
data_policy_type = "DATA_MASKING_POLICY"
2057+
data_masking_policy {
2058+
predefined_expression = "SHA256"
2059+
}
2060+
}
2061+
2062+
resource "google_bigquery_datapolicy_data_policy" "data_policy2" {
2063+
location = "us-central1"
2064+
data_policy_id = "%s"
2065+
policy_tag = google_data_catalog_policy_tag.policy_tag.name
2066+
data_policy_type = "DATA_MASKING_POLICY"
2067+
data_masking_policy {
2068+
predefined_expression = "FIRST_FOUR_CHARACTERS"
2069+
}
2070+
}
2071+
2072+
resource "google_data_catalog_policy_tag" "policy_tag" {
2073+
taxonomy = google_data_catalog_taxonomy.taxonomy.id
2074+
display_name = "Low security"
2075+
description = "A policy tag normally associated with low security items"
2076+
}
2077+
2078+
resource "google_data_catalog_taxonomy" "taxonomy" {
2079+
region = "us-central1"
2080+
display_name = "%s"
2081+
description = "A collection of policy tags"
2082+
activated_policy_types = ["FINE_GRAINED_ACCESS_CONTROL"]
2083+
}
2084+
2085+
resource "google_bigquery_table" "test" {
2086+
depends_on = [google_bigquery_datapolicy_data_policy.data_policy1, google_bigquery_datapolicy_data_policy.data_policy2]
2087+
deletion_protection = false
2088+
table_id = "%s"
2089+
dataset_id = google_bigquery_dataset.test.dataset_id
2090+
2091+
ignore_schema_changes = [
2092+
"dataPolicies"
2093+
]
2094+
2095+
schema = <<EOH
2096+
[
2097+
{
2098+
"name": "ts",
2099+
"type": "TIMESTAMP"
2100+
},
2101+
{
2102+
"name": "some_string",
2103+
"type": "STRING",
2104+
"dataPolicies": [
2105+
{
2106+
"name": "%s"
2107+
}
2108+
]
2109+
},
2110+
{
2111+
"name": "some_int",
2112+
"type": "INTEGER"
2113+
},
2114+
{
2115+
"name": "city",
2116+
"type": "RECORD",
2117+
"fields": [
2118+
{
2119+
"name": "id",
2120+
"type": "INTEGER"
2121+
},
2122+
{
2123+
"name": "coord",
2124+
"type": "RECORD",
2125+
"fields": [
2126+
{
2127+
"name": "lon",
2128+
"type": "FLOAT"
2129+
}
2130+
]
2131+
}
2132+
]
2133+
}
2134+
]
2135+
EOH
2136+
2137+
}
2138+
`, datasetID, dataPolicyID1, dataPolicyID2, dataCatTaxonomy, tableID, dataPolicyName)
2139+
}
2140+
20002141
func testAccBigQueryTableBasicWithTableMetadataView(datasetID, tableID string) string {
20012142
return fmt.Sprintf(`
20022143
resource "google_bigquery_dataset" "test" {

website/docs/r/bigquery_table.html.markdown

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ The following arguments are supported:
158158
with `external_data_configuration.schema`. Otherwise, schemas must be
159159
specified with this top-level field.
160160

161+
* `ignore_schema_changes` - (Optional) A list of fields which should be ignored for each column in schema.
162+
**NOTE:** Right now only `dataPolicies` field is supported. We might support others in the future.
163+
161164
* `schema_foreign_type_info` - (Optional) Specifies metadata of the foreign data
162165
type definition in field schema. Structure is [documented below](#nested_schema_foreign_type_info).
163166

0 commit comments

Comments
 (0)