From fdd3ca8a26aa6329b34056fc56c61fbcc118c7f7 Mon Sep 17 00:00:00 2001 From: The Magician Date: Mon, 30 Mar 2020 11:45:19 -0700 Subject: [PATCH] Add deadLetterPolicy to Pub/Sub Subscription resource (#3305) (#1913) * Add deadLetterPolicy to Pub/Sub subscription resource * fix: disable allow_empty_objects, fix docstring, add example * fix: set max_delivery_attempts * fix: block and topic name Signed-off-by: Modular Magician --- .changelog/3305.txt | 3 + google-beta/resource_pubsub_subscription.go | 137 ++++++++++++++++++ ...urce_pubsub_subscription_generated_test.go | 46 ++++++ .../docs/r/pubsub_subscription.html.markdown | 62 ++++++++ 4 files changed, 248 insertions(+) create mode 100644 .changelog/3305.txt diff --git a/.changelog/3305.txt b/.changelog/3305.txt new file mode 100644 index 0000000000..d64f480fa7 --- /dev/null +++ b/.changelog/3305.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +pubsub: Added `dead_letter_policy` support to `google_pubsub_subscription` +``` diff --git a/google-beta/resource_pubsub_subscription.go b/google-beta/resource_pubsub_subscription.go index 9696200b16..60a15097b2 100644 --- a/google-beta/resource_pubsub_subscription.go +++ b/google-beta/resource_pubsub_subscription.go @@ -93,6 +93,54 @@ for the call to the push endpoint. If the subscriber never acknowledges the message, the Pub/Sub system will eventually redeliver the message.`, }, + "dead_letter_policy": { + Type: schema.TypeList, + Optional: true, + Description: `A policy that specifies the conditions for dead lettering messages in +this subscription. If dead_letter_policy is not set, dead lettering +is disabled. + +The Cloud Pub/Sub service account associated with this subscriptions's +parent project (i.e., +service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com) must have +permission to Acknowledge() messages on this subscription.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dead_letter_topic": { + Type: schema.TypeString, + Optional: true, + Description: `The name of the topic to which dead letter messages should be published. +Format is 'projects/{project}/topics/{topic}'. + +The Cloud Pub/Sub service\naccount associated with the enclosing subscription's +parent project (i.e., +service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com) must have +permission to Publish() to this topic. + +The operation will fail if the topic does not exist. +Users should ensure that there is a subscription attached to this topic +since messages published to a topic with no subscriptions are lost.`, + }, + "max_delivery_attempts": { + Type: schema.TypeInt, + Optional: true, + Description: `The maximum number of delivery attempts for any message. The value must be +between 5 and 100. + +The number of delivery attempts is defined as 1 + (the sum of number of +NACKs and number of times the acknowledgement deadline has been exceeded for the message). + +A NACK is any call to ModifyAckDeadline with a 0 deadline. Note that +client libraries may automatically extend ack_deadlines. + +This field will be honored on a best effort basis. + +If this parameter is 0, a default value of 5 is used.`, + }, + }, + }, + }, "expiration_policy": { Type: schema.TypeList, Computed: true, @@ -290,6 +338,12 @@ func resourcePubsubSubscriptionCreate(d *schema.ResourceData, meta interface{}) } else if v, ok := d.GetOkExists("expiration_policy"); ok || !reflect.DeepEqual(v, expirationPolicyProp) { obj["expirationPolicy"] = expirationPolicyProp } + deadLetterPolicyProp, err := expandPubsubSubscriptionDeadLetterPolicy(d.Get("dead_letter_policy"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("dead_letter_policy"); ok || !reflect.DeepEqual(v, deadLetterPolicyProp) { + obj["deadLetterPolicy"] = deadLetterPolicyProp + } obj, err = resourcePubsubSubscriptionEncoder(d, meta, obj) if err != nil { @@ -418,6 +472,9 @@ func resourcePubsubSubscriptionRead(d *schema.ResourceData, meta interface{}) er if err := d.Set("expiration_policy", flattenPubsubSubscriptionExpirationPolicy(res["expirationPolicy"], d, config)); err != nil { return fmt.Errorf("Error reading Subscription: %s", err) } + if err := d.Set("dead_letter_policy", flattenPubsubSubscriptionDeadLetterPolicy(res["deadLetterPolicy"], d, config)); err != nil { + return fmt.Errorf("Error reading Subscription: %s", err) + } return nil } @@ -467,6 +524,12 @@ func resourcePubsubSubscriptionUpdate(d *schema.ResourceData, meta interface{}) } else if v, ok := d.GetOkExists("expiration_policy"); ok || !reflect.DeepEqual(v, expirationPolicyProp) { obj["expirationPolicy"] = expirationPolicyProp } + deadLetterPolicyProp, err := expandPubsubSubscriptionDeadLetterPolicy(d.Get("dead_letter_policy"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("dead_letter_policy"); ok || !reflect.DeepEqual(v, deadLetterPolicyProp) { + obj["deadLetterPolicy"] = deadLetterPolicyProp + } obj, err = resourcePubsubSubscriptionUpdateEncoder(d, meta, obj) if err != nil { @@ -504,6 +567,10 @@ func resourcePubsubSubscriptionUpdate(d *schema.ResourceData, meta interface{}) if d.HasChange("expiration_policy") { updateMask = append(updateMask, "expirationPolicy") } + + if d.HasChange("dead_letter_policy") { + updateMask = append(updateMask, "deadLetterPolicy") + } // 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, ",")}) @@ -669,6 +736,42 @@ func flattenPubsubSubscriptionExpirationPolicyTtl(v interface{}, d *schema.Resou return v } +func flattenPubsubSubscriptionDeadLetterPolicy(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["dead_letter_topic"] = + flattenPubsubSubscriptionDeadLetterPolicyDeadLetterTopic(original["deadLetterTopic"], d, config) + transformed["max_delivery_attempts"] = + flattenPubsubSubscriptionDeadLetterPolicyMaxDeliveryAttempts(original["maxDeliveryAttempts"], d, config) + return []interface{}{transformed} +} +func flattenPubsubSubscriptionDeadLetterPolicyDeadLetterTopic(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenPubsubSubscriptionDeadLetterPolicyMaxDeliveryAttempts(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + func expandPubsubSubscriptionName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { return replaceVars(d, config, "projects/{{project}}/subscriptions/{{name}}") } @@ -826,6 +929,40 @@ func expandPubsubSubscriptionExpirationPolicyTtl(v interface{}, d TerraformResou return v, nil } +func expandPubsubSubscriptionDeadLetterPolicy(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedDeadLetterTopic, err := expandPubsubSubscriptionDeadLetterPolicyDeadLetterTopic(original["dead_letter_topic"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedDeadLetterTopic); val.IsValid() && !isEmptyValue(val) { + transformed["deadLetterTopic"] = transformedDeadLetterTopic + } + + transformedMaxDeliveryAttempts, err := expandPubsubSubscriptionDeadLetterPolicyMaxDeliveryAttempts(original["max_delivery_attempts"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxDeliveryAttempts); val.IsValid() && !isEmptyValue(val) { + transformed["maxDeliveryAttempts"] = transformedMaxDeliveryAttempts + } + + return transformed, nil +} + +func expandPubsubSubscriptionDeadLetterPolicyDeadLetterTopic(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandPubsubSubscriptionDeadLetterPolicyMaxDeliveryAttempts(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + func resourcePubsubSubscriptionEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { delete(obj, "name") return obj, nil diff --git a/google-beta/resource_pubsub_subscription_generated_test.go b/google-beta/resource_pubsub_subscription_generated_test.go index 4f11aaca0b..56c3bf19b7 100644 --- a/google-beta/resource_pubsub_subscription_generated_test.go +++ b/google-beta/resource_pubsub_subscription_generated_test.go @@ -75,6 +75,52 @@ resource "google_pubsub_subscription" "example" { `, context) } +func TestAccPubsubSubscription_pubsubSubscriptionDeadLetterExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": acctest.RandString(10), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPubsubSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccPubsubSubscription_pubsubSubscriptionDeadLetterExample(context), + }, + { + ResourceName: "google_pubsub_subscription.example", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccPubsubSubscription_pubsubSubscriptionDeadLetterExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_pubsub_topic" "example" { + name = "tf-test-example-topic%{random_suffix}" +} + +resource "google_pubsub_topic" "example_dead_letter" { + name = "tf-test-example-topic%{random_suffix}-dead-letter" +} + +resource "google_pubsub_subscription" "example" { + name = "tf-test-example-subscription%{random_suffix}" + topic = google_pubsub_topic.example.name + + dead_letter_policy { + dead_letter_topic = google_pubsub_topic.example_dead_letter.id + max_delivery_attempts = 10 + } +} +`, context) +} + func testAccCheckPubsubSubscriptionDestroy(s *terraform.State) error { for name, rs := range s.RootModule().Resources { if rs.Type != "google_pubsub_subscription" { diff --git a/website/docs/r/pubsub_subscription.html.markdown b/website/docs/r/pubsub_subscription.html.markdown index 8c94115845..fd540351c0 100644 --- a/website/docs/r/pubsub_subscription.html.markdown +++ b/website/docs/r/pubsub_subscription.html.markdown @@ -107,6 +107,33 @@ resource "google_pubsub_subscription" "example" { topic = google_pubsub_topic.example.name } ``` + +## Example Usage - Pubsub Subscription Dead Letter + + +```hcl +resource "google_pubsub_topic" "example" { + name = "example-topic" +} + +resource "google_pubsub_topic" "example_dead_letter" { + name = "example-topic-dead-letter" +} + +resource "google_pubsub_subscription" "example" { + name = "example-subscription" + topic = google_pubsub_topic.example.name + + dead_letter_policy { + dead_letter_topic = google_pubsub_topic.example_dead_letter.id + max_delivery_attempts = 10 + } +} +``` ## Argument Reference @@ -181,6 +208,16 @@ The following arguments are supported: resource never expires. The minimum allowed value for expirationPolicy.ttl is 1 day. Structure is documented below. +* `dead_letter_policy` - + (Optional) + A policy that specifies the conditions for dead lettering messages in + this subscription. If dead_letter_policy is not set, dead lettering + is disabled. + The Cloud Pub/Sub service account associated with this subscriptions's + parent project (i.e., + service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com) must have + permission to Acknowledge() messages on this subscription. Structure is documented below. + * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. @@ -248,6 +285,31 @@ The `expiration_policy` block supports: A duration in seconds with up to nine fractional digits, terminated by 's'. Example - "3.5s". +The `dead_letter_policy` block supports: + +* `dead_letter_topic` - + (Optional) + The name of the topic to which dead letter messages should be published. + Format is `projects/{project}/topics/{topic}`. + The Cloud Pub/Sub service\naccount associated with the enclosing subscription's + parent project (i.e., + service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com) must have + permission to Publish() to this topic. + The operation will fail if the topic does not exist. + Users should ensure that there is a subscription attached to this topic + since messages published to a topic with no subscriptions are lost. + +* `max_delivery_attempts` - + (Optional) + The maximum number of delivery attempts for any message. The value must be + between 5 and 100. + The number of delivery attempts is defined as 1 + (the sum of number of + NACKs and number of times the acknowledgement deadline has been exceeded for the message). + A NACK is any call to ModifyAckDeadline with a 0 deadline. Note that + client libraries may automatically extend ack_deadlines. + This field will be honored on a best effort basis. + If this parameter is 0, a default value of 5 is used. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are exported: