From d898bd83aaa20479c54c2950d70ff2f1388d3c05 Mon Sep 17 00:00:00 2001 From: The Magician Date: Thu, 8 Jun 2023 12:49:37 -0700 Subject: [PATCH] feat: Add Pub/Sub Schema support for changing definitions via revisions (#8079) (#14857) * Add support for Pub/Sub schema evolution * Add example for Pub/Sub schema evolution * Remove changes not to be made in a single PR * Remove changes not to be made in a single PR * Remove changes not to be made in a single PR * Fix spacing * Test update, import * Reduce flakiness Signed-off-by: Modular Magician --- .changelog/8079.txt | 3 + .../resource_pubsub_schema_generated_test.go | 14 ++- google/resource_pubsub_schema_test.go | 86 ++++++++++++++++++ .../services/pubsub/resource_pubsub_schema.go | 90 ++++++++++++++++++- website/docs/r/pubsub_schema.html.markdown | 1 + 5 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 .changelog/8079.txt create mode 100644 google/resource_pubsub_schema_test.go diff --git a/.changelog/8079.txt b/.changelog/8079.txt new file mode 100644 index 00000000000..3057669b919 --- /dev/null +++ b/.changelog/8079.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +pubsub: Allowed `definition` of `google_pubsub_schema` to change without deleting and recreating the resource by using schema revisions (https://cloud.google.com/pubsub/docs/schemas#commit-schema-revision) +``` diff --git a/google/resource_pubsub_schema_generated_test.go b/google/resource_pubsub_schema_generated_test.go index 794cd6509ce..b27680b8b9a 100644 --- a/google/resource_pubsub_schema_generated_test.go +++ b/google/resource_pubsub_schema_generated_test.go @@ -46,10 +46,9 @@ func TestAccPubsubSchema_pubsubSchemaBasicExample(t *testing.T) { Config: testAccPubsubSchema_pubsubSchemaBasicExample(context), }, { - ResourceName: "google_pubsub_schema.example", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"definition"}, + ResourceName: "google_pubsub_schema.example", + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -82,10 +81,9 @@ func TestAccPubsubSchema_pubsubSchemaProtobufExample(t *testing.T) { Config: testAccPubsubSchema_pubsubSchemaProtobufExample(context), }, { - ResourceName: "google_pubsub_schema.example", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"definition"}, + ResourceName: "google_pubsub_schema.example", + ImportState: true, + ImportStateVerify: true, }, }, }) diff --git a/google/resource_pubsub_schema_test.go b/google/resource_pubsub_schema_test.go new file mode 100644 index 00000000000..9377140c640 --- /dev/null +++ b/google/resource_pubsub_schema_test.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-google/google/acctest" +) + +func TestAccPubsubSchema_update(t *testing.T) { + t.Parallel() + + schema := fmt.Sprintf("tf-test-schema-%s", RandString(t, 10)) + + VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckPubsubSubscriptionDestroyProducer(t), + ExternalProviders: map[string]resource.ExternalProvider{ + "time": {}, + }, + Steps: []resource.TestStep{ + { + Config: testAccPubsubSchema_basic(schema), + }, + { + ResourceName: "google_pubsub_schema.foo", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccPubsubSchema_updated(schema), + }, + { + ResourceName: "google_pubsub_schema.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccPubsubSchema_basic(schema string) string { + return fmt.Sprintf(` + resource "google_pubsub_schema" "foo" { + name = "%s" + type = "PROTOCOL_BUFFER" + definition = "syntax = \"proto3\";\nmessage Results {\nstring message_request = 1;\nstring message_response = 2;\n}" + } + + # Need to introduce delay for updates in order for tests to complete + # successfully due to caching effects. + resource "time_sleep" "wait_121_seconds" { + create_duration = "121s" + lifecycle { + replace_triggered_by = [ + google_pubsub_schema.foo + ] + } + } +`, schema) +} + +func testAccPubsubSchema_updated(schema string) string { + return fmt.Sprintf(` + resource "google_pubsub_schema" "foo" { + name = "%s" + type = "PROTOCOL_BUFFER" + definition = "syntax = \"proto3\";\nmessage Results {\nstring message_request = 1;\nstring message_response = 2;\nstring timestamp_request = 3;\n}" + } + + # Need to introduce delay for updates in order for tests to complete + # successfully due to caching effects. + resource "time_sleep" "wait_121_seconds" { + create_duration = "121s" + lifecycle { + replace_triggered_by = [ + google_pubsub_schema.foo + ] + } + } +`, schema) +} diff --git a/google/services/pubsub/resource_pubsub_schema.go b/google/services/pubsub/resource_pubsub_schema.go index eee512e5cba..49340a895cb 100644 --- a/google/services/pubsub/resource_pubsub_schema.go +++ b/google/services/pubsub/resource_pubsub_schema.go @@ -34,6 +34,7 @@ func ResourcePubsubSchema() *schema.Resource { return &schema.Resource{ Create: resourcePubsubSchemaCreate, Read: resourcePubsubSchemaRead, + Update: resourcePubsubSchemaUpdate, Delete: resourcePubsubSchemaDelete, Importer: &schema.ResourceImporter{ @@ -42,6 +43,7 @@ func ResourcePubsubSchema() *schema.Resource { Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), Delete: schema.DefaultTimeout(20 * time.Minute), }, @@ -56,7 +58,6 @@ func ResourcePubsubSchema() *schema.Resource { "definition": { Type: schema.TypeString, Optional: true, - ForceNew: true, Description: `The definition of the schema. This should contain a string representing the full definition of the schema that is a valid schema definition of the type specified in type.`, @@ -64,7 +65,6 @@ that is a valid schema definition of the type specified in type.`, "type": { Type: schema.TypeString, Optional: true, - ForceNew: true, ValidateFunc: verify.ValidateEnum([]string{"TYPE_UNSPECIFIED", "PROTOCOL_BUFFER", "AVRO", ""}), Description: `The type of the schema definition Default value: "TYPE_UNSPECIFIED" Possible values: ["TYPE_UNSPECIFIED", "PROTOCOL_BUFFER", "AVRO"]`, Default: "TYPE_UNSPECIFIED", @@ -235,6 +235,9 @@ func resourcePubsubSchemaRead(d *schema.ResourceData, meta interface{}) error { if err := d.Set("type", flattenPubsubSchemaType(res["type"], d, config)); err != nil { return fmt.Errorf("Error reading Schema: %s", err) } + if err := d.Set("definition", flattenPubsubSchemaDefinition(res["definition"], d, config)); err != nil { + return fmt.Errorf("Error reading Schema: %s", err) + } if err := d.Set("name", flattenPubsubSchemaName(res["name"], d, config)); err != nil { return fmt.Errorf("Error reading Schema: %s", err) } @@ -242,6 +245,77 @@ func resourcePubsubSchemaRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourcePubsubSchemaUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*transport_tpg.Config) + userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := tpgresource.GetProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for Schema: %s", err) + } + billingProject = project + + obj := make(map[string]interface{}) + typeProp, err := expandPubsubSchemaType(d.Get("type"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("type"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, typeProp)) { + obj["type"] = typeProp + } + definitionProp, err := expandPubsubSchemaDefinition(d.Get("definition"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("definition"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, definitionProp)) { + obj["definition"] = definitionProp + } + nameProp, err := expandPubsubSchemaName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + + obj, err = resourcePubsubSchemaUpdateEncoder(d, meta, obj) + if err != nil { + return err + } + + url, err := tpgresource.ReplaceVars(d, config, "{{PubsubBasePath}}projects/{{project}}/schemas/{{name}}:commit") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating Schema %q: %#v", d.Id(), obj) + + // err == nil indicates that the billing_project value was found + if bp, err := tpgresource.GetBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{ + Config: config, + Method: "POST", + Project: billingProject, + RawURL: url, + UserAgent: userAgent, + Body: obj, + Timeout: d.Timeout(schema.TimeoutUpdate), + }) + + if err != nil { + return fmt.Errorf("Error updating Schema %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating Schema %q: %#v", d.Id(), res) + } + + return resourcePubsubSchemaRead(d, meta) +} + func resourcePubsubSchemaDelete(d *schema.ResourceData, meta interface{}) error { config := meta.(*transport_tpg.Config) userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent) @@ -316,6 +390,10 @@ func flattenPubsubSchemaType(v interface{}, d *schema.ResourceData, config *tran return v } +func flattenPubsubSchemaDefinition(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + return v +} + func flattenPubsubSchemaName(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { if v == nil { return v @@ -334,3 +412,11 @@ func expandPubsubSchemaDefinition(v interface{}, d tpgresource.TerraformResource func expandPubsubSchemaName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { return tpgresource.GetResourceNameFromSelfLink(v.(string)), nil } + +func resourcePubsubSchemaUpdateEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { + newObj := make(map[string]interface{}) + newObj["name"] = d.Id() + obj["name"] = d.Id() + newObj["schema"] = obj + return newObj, nil +} diff --git a/website/docs/r/pubsub_schema.html.markdown b/website/docs/r/pubsub_schema.html.markdown index b8b3b342add..6febc34dfd1 100644 --- a/website/docs/r/pubsub_schema.html.markdown +++ b/website/docs/r/pubsub_schema.html.markdown @@ -108,6 +108,7 @@ This resource provides the following [Timeouts](https://developer.hashicorp.com/terraform/plugin/sdkv2/resources/retries-and-customizable-timeouts) configuration options: - `create` - Default is 20 minutes. +- `update` - Default is 20 minutes. - `delete` - Default is 20 minutes. ## Import