From 3433c55b7ef98441238a44b4601fdfdc0ffb59c6 Mon Sep 17 00:00:00 2001 From: The Magician Date: Sun, 11 Dec 2022 20:08:14 -0800 Subject: [PATCH] Adding Support to update DataCatalog TagTemplate fields (#6803) (#13216) fixes https://github.com/hashicorp/terraform-provider-google/issues/6574 Signed-off-by: Modular Magician Signed-off-by: Modular Magician --- .changelog/6803.txt | 3 + google/resource_data_catalog_tag_template.go | 202 +++++++++++++++++- ...resource_data_catalog_tag_template_test.go | 172 +++++++++++++++ .../r/data_catalog_tag_template.html.markdown | 2 +- 4 files changed, 367 insertions(+), 12 deletions(-) create mode 100644 .changelog/6803.txt create mode 100644 google/resource_data_catalog_tag_template_test.go diff --git a/.changelog/6803.txt b/.changelog/6803.txt new file mode 100644 index 00000000000..6e286a4f6b7 --- /dev/null +++ b/.changelog/6803.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +datacatalog: added update support for `fields` in `google_data_catalog_tag_template` +``` diff --git a/google/resource_data_catalog_tag_template.go b/google/resource_data_catalog_tag_template.go index 99df07189ad..63a8d830453 100644 --- a/google/resource_data_catalog_tag_template.go +++ b/google/resource_data_catalog_tag_template.go @@ -25,6 +25,50 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +//Use it to delete TagTemplate Field +func deleteTagTemplateField(d *schema.ResourceData, config *Config, name, billingProject, userAgent string) error { + + url_delete, err := replaceVars(d, config, "{{DataCatalogBasePath}}{{name}}/fields/"+name+"?force={{force_delete}}") + if err != nil { + return err + } + var obj map[string]interface{} + res, err := sendRequestWithTimeout(config, "DELETE", billingProject, url_delete, userAgent, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return fmt.Errorf("Error deleting TagTemplate Field %v: %s", name, err) + } + + log.Printf("[DEBUG] Finished deleting TagTemplate Field %q: %#v", name, res) + return nil +} + +//Use it to create TagTemplate Field +func createTagTemplateField(d *schema.ResourceData, config *Config, body map[string]interface{}, name, billingProject, userAgent string) error { + + url_create, err := replaceVars(d, config, "{{DataCatalogBasePath}}{{name}}/fields") + if err != nil { + return err + } + + url_create, err = addQueryParams(url_create, map[string]string{"tagTemplateFieldId": name}) + if err != nil { + return err + } + + res_create, err := sendRequestWithTimeout(config, "POST", billingProject, url_create, userAgent, body, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating TagTemplate Field: %s", err) + } + + if err != nil { + return fmt.Errorf("Error creating TagTemplate Field %v: %s", name, err) + } else { + log.Printf("[DEBUG] Finished creating TagTemplate Field %v: %#v", name, res_create) + } + + return nil +} + func resourceDataCatalogTagTemplate() *schema.Resource { return &schema.Resource{ Create: resourceDataCatalogTagTemplateCreate, @@ -46,14 +90,12 @@ func resourceDataCatalogTagTemplate() *schema.Resource { "fields": { Type: schema.TypeSet, Required: true, - ForceNew: true, - Description: `Set of tag template field IDs and the settings for the field. This set is an exhaustive list of the allowed fields. This set must contain at least one field and at most 500 fields.`, + Description: `Set of tag template field IDs and the settings for the field. This set is an exhaustive list of the allowed fields. This set must contain at least one field and at most 500 fields. The change of field_id will be resulting in re-creating of field. The change of primitive_type will be resulting in re-creating of field, however if the field is a required, you cannot update it.`, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "field_id": { Type: schema.TypeString, Required: true, - ForceNew: true, }, "type": { Type: schema.TypeList, @@ -86,6 +128,7 @@ Can have up to 500 allowed values.`, }, "primitive_type": { Type: schema.TypeString, + Computed: true, Optional: true, ValidateFunc: validateEnum([]string{"DOUBLE", "STRING", "BOOL", "TIMESTAMP", ""}), Description: `Represents primitive types - string, bool etc. @@ -96,21 +139,25 @@ Can have up to 500 allowed values.`, }, "description": { Type: schema.TypeString, + Computed: true, Optional: true, Description: `A description for this field.`, }, "display_name": { Type: schema.TypeString, + Computed: true, Optional: true, Description: `The display name for this field.`, }, "is_required": { Type: schema.TypeBool, + Computed: true, Optional: true, Description: `Whether this is a required field. Defaults to false.`, }, "order": { Type: schema.TypeInt, + Computed: true, Optional: true, Description: `The order of this field with respect to other fields in this tag template. A higher value indicates a more important field. The value can be negative. @@ -314,6 +361,12 @@ func resourceDataCatalogTagTemplateUpdate(d *schema.ResourceData, meta interface } else if v, ok := d.GetOkExists("display_name"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, displayNameProp)) { obj["displayName"] = displayNameProp } + fieldsProp, err := expandDataCatalogTagTemplateFields(d.Get("fields"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("fields"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, fieldsProp)) { + obj["fields"] = fieldsProp + } url, err := replaceVars(d, config, "{{DataCatalogBasePath}}{{name}}") if err != nil { @@ -326,6 +379,7 @@ func resourceDataCatalogTagTemplateUpdate(d *schema.ResourceData, meta interface if d.HasChange("display_name") { updateMask = append(updateMask, "displayName") } + // updateMask is a URL parameter but not present in the schema, so replaceVars // won't set it url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) @@ -333,19 +387,145 @@ func resourceDataCatalogTagTemplateUpdate(d *schema.ResourceData, meta interface return err } - // err == nil indicates that the billing_project value was found - if bp, err := getBillingProject(d, config); err == nil { - billingProject = bp + if len(updateMask) > 0 { + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "PATCH", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating TagTemplate %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating TagTemplate %q: %#v", d.Id(), res) + } + } - res, err := sendRequestWithTimeout(config, "PATCH", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + // since fields have a separate endpoint, + // we need to handle it manually - if err != nil { - return fmt.Errorf("Error updating TagTemplate %q: %s", d.Id(), err) - } else { - log.Printf("[DEBUG] Finished updating TagTemplate %q: %#v", d.Id(), res) + type FieldChange struct { + Old, New map[string]interface{} } + o, n := d.GetChange("fields") + vals := make(map[string]*FieldChange) + + // this will create a dictionary with the value + // of field_id as the key that will contain the + // maps of old and new values + for _, raw := range o.(*schema.Set).List() { + obj := raw.(map[string]interface{}) + k := obj["field_id"].(string) + vals[k] = &FieldChange{Old: obj} + } + + for _, raw := range n.(*schema.Set).List() { + obj := raw.(map[string]interface{}) + k := obj["field_id"].(string) + if _, ok := vals[k]; !ok { + // if key is not present in the vals, + // then create an empty object to hold the new value + vals[k] = &FieldChange{} + } + vals[k].New = obj + } + + // fields schema to create schema.set below + dataCatalogTagTemplateFieldsSchema := &schema.Resource{ + Schema: resourceDataCatalogTagTemplate().Schema["fields"].Elem.(*schema.Resource).Schema, + } + + for name, change := range vals { + // A few different situations to deal with in here: + // - change.Old is nil: create a new role + // - change.New is nil: remove an existing role + // - both are set: test if New is different than Old and update if so + + changeOldSet := schema.NewSet(schema.HashResource(dataCatalogTagTemplateFieldsSchema), []interface{}{}) + changeOldSet.Add(change.Old) + var changeOldProp map[string]interface{} + if len(change.Old) != 0 { + changeOldProp, _ = expandDataCatalogTagTemplateFields(changeOldSet, nil, nil) + changeOldProp = changeOldProp[name].(map[string]interface{}) + } + + changeNewSet := schema.NewSet(schema.HashResource(dataCatalogTagTemplateFieldsSchema), []interface{}{}) + changeNewSet.Add(change.New) + var changeNewProp map[string]interface{} + if len(change.New) != 0 { + changeNewProp, _ = expandDataCatalogTagTemplateFields(changeNewSet, nil, nil) + changeNewProp = changeNewProp[name].(map[string]interface{}) + } + + // if old state is empty, then we have a new field to create + if len(change.Old) == 0 { + err := createTagTemplateField(d, config, changeNewProp, name, billingProject, userAgent) + if err != nil { + return err + } + + continue + } + + // if new state is empty, then we need to delete the current field + if len(change.New) == 0 { + err := deleteTagTemplateField(d, config, name, billingProject, userAgent) + if err != nil { + return err + } + + continue + } + + // if we have old and new values, but are not equal, update with the new state + if !reflect.DeepEqual(changeOldProp, changeNewProp) { + url1, err := replaceVars(d, config, "{{DataCatalogBasePath}}{{name}}/fields/"+name) + if err != nil { + return err + } + + oldType := changeOldProp["type"].(map[string]interface{}) + newType := changeNewProp["type"].(map[string]interface{}) + + if oldType["primitiveType"] != newType["primitiveType"] { + // As primitiveType can't be changed, it is considered as ForceNew which triggers the deletion of old field and recreation of a new field + // Before that, we need to check that is_required is True for the newType or not, as we don't have support to add new required field in the existing TagTemplate, + // So in such cases, we can simply return the error + + // Reason for checking the isRequired in changeNewProp - + // Because this changeNewProp check should be ignored when the user wants to update the primitive type and make it optional rather than keeping it required. + if changeNewProp["isRequired"] != nil && changeNewProp["isRequired"].(bool) { + return fmt.Errorf("Updating the primitive type for a required field on an existing tag template is not supported as TagTemplateField %q is required", name) + } + + // delete changeOldProp + err_delete := deleteTagTemplateField(d, config, name, billingProject, userAgent) + if err_delete != nil { + return err_delete + } + + // recreate changeNewProp + err_create := createTagTemplateField(d, config, changeNewProp, name, billingProject, userAgent) + if err_create != nil { + return err_create + } + + log.Printf("[DEBUG] Finished updating TagTemplate Field %q", name) + return resourceDataCatalogTagTemplateRead(d, meta) + } + + res, err := sendRequestWithTimeout(config, "PATCH", billingProject, url1, userAgent, changeNewProp, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return fmt.Errorf("Error updating TagTemplate Field %v: %s", name, err) + } + + log.Printf("[DEBUG] Finished updating TagTemplate Field %q: %#v", name, res) + } + } return resourceDataCatalogTagTemplateRead(d, meta) } diff --git a/google/resource_data_catalog_tag_template_test.go b/google/resource_data_catalog_tag_template_test.go new file mode 100644 index 00000000000..79cace0e38a --- /dev/null +++ b/google/resource_data_catalog_tag_template_test.go @@ -0,0 +1,172 @@ +package google + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataCatalogTagTemplate_dataCatalogTagTemplate_updateFields(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "force_delete": true, + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDataCatalogTagTemplateDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccDataCatalogTagTemplate_dataCatalogTagTemplateBasicExample(context), + }, + { + ResourceName: "google_data_catalog_tag_template.basic_tag_template", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"region", "tag_template_id", "force_delete"}, + }, + { + Config: testAccDataCatalogTagTemplate_dataCatalogTagTemplateUpdateFields(context), + }, + { + ResourceName: "google_data_catalog_tag_template.basic_tag_template", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"region", "tag_template_id", "force_delete"}, + }, + { + Config: testAccDataCatalogTagTemplate_dataCatalogTagTemplateUpdatePrimitiveTypeOfFieldsWithRequired(context), + ExpectError: regexp.MustCompile("Updating the primitive type for a required field on an existing tag template is not supported"), + }, + { + Config: testAccDataCatalogTagTemplate_dataCatalogTagTemplateUpdatePrimitiveTypeOfFieldsWithOptional(context), + }, + { + ResourceName: "google_data_catalog_tag_template.basic_tag_template", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"region", "tag_template_id", "force_delete"}, + }, + }, + }) +} + +func testAccDataCatalogTagTemplate_dataCatalogTagTemplateUpdateFields(context map[string]interface{}) string { + return Nprintf(` +resource "google_data_catalog_tag_template" "basic_tag_template" { + tag_template_id = "tf_test_my_template%{random_suffix}" + region = "us-central1" + display_name = "Demo Tag Template Test Update" + + fields { + field_id = "source" + display_name = "Source of data asset test update" + type { + primitive_type = "STRING" + } + is_required = true + } + + fields { + field_id = "pii_type" + display_name = "PII type" + type { + enum_type { + allowed_values { + display_name = "EMAIL" + } + allowed_values { + display_name = "SOCIAL SECURITY NUMBER" + } + allowed_values { + display_name = "NONE" + } + } + } + } + + force_delete = "%{force_delete}" +} +`, context) +} + +func testAccDataCatalogTagTemplate_dataCatalogTagTemplateUpdatePrimitiveTypeOfFieldsWithRequired(context map[string]interface{}) string { + return Nprintf(` +resource "google_data_catalog_tag_template" "basic_tag_template" { + tag_template_id = "tf_test_my_template%{random_suffix}" + region = "us-central1" + display_name = "Demo Tag Template Test Update" + + fields { + field_id = "source" + display_name = "Source of data asset test update" + type { + primitive_type = "DOUBLE" + } + is_required = true + } + + fields { + field_id = "pii_type" + display_name = "PII type" + type { + enum_type { + allowed_values { + display_name = "EMAIL" + } + allowed_values { + display_name = "SOCIAL SECURITY NUMBER" + } + allowed_values { + display_name = "NONE" + } + } + } + } + + force_delete = "%{force_delete}" +} +`, context) +} + +func testAccDataCatalogTagTemplate_dataCatalogTagTemplateUpdatePrimitiveTypeOfFieldsWithOptional(context map[string]interface{}) string { + return Nprintf(` +resource "google_data_catalog_tag_template" "basic_tag_template" { + tag_template_id = "tf_test_my_template%{random_suffix}" + region = "us-central1" + display_name = "Demo Tag Template Test Update" + + fields { + field_id = "source" + display_name = "Source of data asset test update" + type { + primitive_type = "DOUBLE" + } + } + + fields { + field_id = "pii_type" + display_name = "PII type" + type { + enum_type { + allowed_values { + display_name = "EMAIL" + } + allowed_values { + display_name = "SOCIAL SECURITY NUMBER" + } + allowed_values { + display_name = "NONE" + } + } + } + } + + force_delete = "%{force_delete}" +} +`, context) +} diff --git a/website/docs/r/data_catalog_tag_template.html.markdown b/website/docs/r/data_catalog_tag_template.html.markdown index f93d4988331..bb23a6ecab5 100644 --- a/website/docs/r/data_catalog_tag_template.html.markdown +++ b/website/docs/r/data_catalog_tag_template.html.markdown @@ -90,7 +90,7 @@ The following arguments are supported: * `fields` - (Required) - Set of tag template field IDs and the settings for the field. This set is an exhaustive list of the allowed fields. This set must contain at least one field and at most 500 fields. + Set of tag template field IDs and the settings for the field. This set is an exhaustive list of the allowed fields. This set must contain at least one field and at most 500 fields. The change of field_id will be resulting in re-creating of field. The change of primitive_type will be resulting in re-creating of field, however if the field is a required, you cannot update it. Structure is [documented below](#nested_fields). * `tag_template_id` -