diff --git a/.changelog/14923.txt b/.changelog/14923.txt new file mode 100644 index 000000000000..ce57b47ce2bb --- /dev/null +++ b/.changelog/14923.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_sns_topic_subscription: Add `email`, `email-json`, and `firehose` to protocol values. Add `subscription_role_arn` argument for Firehose support. Add `confirmation_was_authenticated`, `owner_id`, and `pending_confirmation` attributes. +``` \ No newline at end of file diff --git a/aws/internal/service/sns/waiter/status.go b/aws/internal/service/sns/waiter/status.go new file mode 100644 index 000000000000..310132194040 --- /dev/null +++ b/aws/internal/service/sns/waiter/status.go @@ -0,0 +1,30 @@ +package waiter + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func SubscriptionPendingConfirmation(conn *sns.SNS, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := conn.GetSubscriptionAttributes(&sns.GetSubscriptionAttributesInput{ + SubscriptionArn: aws.String(id), + }) + + if tfawserr.ErrCodeEquals(err, sns.ErrCodeResourceNotFoundException) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if output == nil { + return nil, "", nil + } + + return output, aws.StringValue(output.Attributes["PendingConfirmation"]), nil + } +} diff --git a/aws/internal/service/sns/waiter/waiter.go b/aws/internal/service/sns/waiter/waiter.go new file mode 100644 index 000000000000..fb7c5a850088 --- /dev/null +++ b/aws/internal/service/sns/waiter/waiter.go @@ -0,0 +1,28 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/sns" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + SubscriptionPendingConfirmationTimeout = 2 * time.Minute +) + +func SubscriptionConfirmed(conn *sns.SNS, id, expectedValue string, timeout time.Duration) (*sns.GetSubscriptionAttributesOutput, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{expectedValue}, + Refresh: SubscriptionPendingConfirmation(conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*sns.GetSubscriptionAttributesOutput); ok { + return output, err + } + + return nil, err +} diff --git a/aws/resource_aws_sns_topic_subscription.go b/aws/resource_aws_sns_topic_subscription.go index db3dd5ec76b6..1174bca2841e 100644 --- a/aws/resource_aws_sns_topic_subscription.go +++ b/aws/resource_aws_sns_topic_subscription.go @@ -5,23 +5,19 @@ import ( "encoding/json" "fmt" "log" - "net/url" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/service/sns" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/sns/waiter" ) -const awsSNSPendingConfirmationMessage = "pending confirmation" -const awsSNSPendingConfirmationMessageWithoutSpaces = "pendingconfirmation" -const awsSNSPasswordObfuscationPattern = "****" - func resourceAwsSnsTopicSubscription() *schema.Resource { return &schema.Resource{ Create: resourceAwsSnsTopicSubscriptionCreate, @@ -33,39 +29,18 @@ func resourceAwsSnsTopicSubscription() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "protocol": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - // email and email-json not supported - "application", - "http", - "https", - "lambda", - "sms", - "sqs", - }, true), - }, - "endpoint": { + "arn": { Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "endpoint_auto_confirms": { - Type: schema.TypeBool, - Optional: true, - Default: false, + Computed: true, }, "confirmation_timeout_in_minutes": { Type: schema.TypeInt, Optional: true, Default: 1, }, - "topic_arn": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + "confirmation_was_authenticated": { + Type: schema.TypeBool, + Computed: true, }, "delivery_policy": { Type: schema.TypeString, @@ -73,21 +48,16 @@ func resourceAwsSnsTopicSubscription() *schema.Resource { ValidateFunc: validation.StringIsJSON, DiffSuppressFunc: suppressEquivalentSnsTopicSubscriptionDeliveryPolicy, }, - "redrive_policy": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringIsJSON, - DiffSuppressFunc: suppressEquivalentJsonDiffs, + "endpoint": { + Type: schema.TypeString, + Required: true, + ForceNew: true, }, - "raw_message_delivery": { + "endpoint_auto_confirms": { Type: schema.TypeBool, Optional: true, Default: false, }, - "arn": { - Type: schema.TypeString, - Computed: true, - }, "filter_policy": { Type: schema.TypeString, Optional: true, @@ -98,64 +68,95 @@ func resourceAwsSnsTopicSubscription() *schema.Resource { return json }, }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + }, + "pending_confirmation": { + Type: schema.TypeBool, + Computed: true, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "application", + "email-json", + "email", + "firehose", + "http", + "https", + "lambda", + "sms", + "sqs", + }, true), + }, + "raw_message_delivery": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "redrive_policy": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringIsJSON, + DiffSuppressFunc: suppressEquivalentJsonDiffs, + }, + "subscription_role_arn": { + Type: schema.TypeString, + Optional: true, + }, + "topic_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, }, } } func resourceAwsSnsTopicSubscriptionCreate(d *schema.ResourceData, meta interface{}) error { - snsconn := meta.(*AWSClient).snsconn + conn := meta.(*AWSClient).snsconn + + input := &sns.SubscribeInput{ + Attributes: expandSNSSubscriptionAttributes(d), + Endpoint: aws.String(d.Get("endpoint").(string)), + Protocol: aws.String(d.Get("protocol").(string)), + ReturnSubscriptionArn: aws.Bool(true), // even if not confirmed, will get ARN + TopicArn: aws.String(d.Get("topic_arn").(string)), + } - output, err := subscribeToSNSTopic(d, snsconn) + output, err := conn.Subscribe(input) if err != nil { - return err + return fmt.Errorf("error creating SNS topic subscription: %w", err) } - if subscriptionHasPendingConfirmation(output.SubscriptionArn) { - log.Printf("[WARN] Invalid SNS Subscription, received a \"%s\" ARN", awsSNSPendingConfirmationMessage) - return nil + if output == nil || output.SubscriptionArn == nil || aws.StringValue(output.SubscriptionArn) == "" { + return fmt.Errorf("error creating SNS topic subscription: empty response") } - log.Printf("New subscription ARN: %s", *output.SubscriptionArn) d.SetId(aws.StringValue(output.SubscriptionArn)) - // Write the ARN to the 'arn' field for export - d.Set("arn", output.SubscriptionArn) - - return resourceAwsSnsTopicSubscriptionRead(d, meta) -} - -func resourceAwsSnsTopicSubscriptionUpdate(d *schema.ResourceData, meta interface{}) error { - snsconn := meta.(*AWSClient).snsconn + waitForConfirmation := true - if d.HasChange("raw_message_delivery") { - if err := snsSubscriptionAttributeUpdate(snsconn, d.Id(), "RawMessageDelivery", fmt.Sprintf("%t", d.Get("raw_message_delivery").(bool))); err != nil { - return err - } + if !d.Get("endpoint_auto_confirms").(bool) && strings.Contains(d.Get("protocol").(string), "http") { + waitForConfirmation = false } - if d.HasChange("filter_policy") { - filterPolicy := d.Get("filter_policy").(string) - - // https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html#message-filtering-policy-remove - if filterPolicy == "" { - filterPolicy = "{}" - } - - if err := snsSubscriptionAttributeUpdate(snsconn, d.Id(), "FilterPolicy", filterPolicy); err != nil { - return err - } + if strings.Contains(d.Get("protocol").(string), "email") { + waitForConfirmation = false } - if d.HasChange("delivery_policy") { - if err := snsSubscriptionAttributeUpdate(snsconn, d.Id(), "DeliveryPolicy", d.Get("delivery_policy").(string)); err != nil { - return err - } + timeout := waiter.SubscriptionPendingConfirmationTimeout + if strings.Contains(d.Get("protocol").(string), "http") { + timeout = time.Duration(d.Get("confirmation_timeout_in_minutes").(int)) * time.Minute } - if d.HasChange("redrive_policy") { - if err := snsSubscriptionAttributeUpdate(snsconn, d.Id(), "RedrivePolicy", d.Get("redrive_policy").(string)); err != nil { - return err + if waitForConfirmation { + if _, err := waiter.SubscriptionConfirmed(conn, d.Id(), "false", timeout); err != nil { + return fmt.Errorf("waiting for SNS topic subscription (%s) confirmation: %w", d.Id(), err) } } @@ -163,229 +164,162 @@ func resourceAwsSnsTopicSubscriptionUpdate(d *schema.ResourceData, meta interfac } func resourceAwsSnsTopicSubscriptionRead(d *schema.ResourceData, meta interface{}) error { - snsconn := meta.(*AWSClient).snsconn + conn := meta.(*AWSClient).snsconn log.Printf("[DEBUG] Loading subscription %s", d.Id()) - attributeOutput, err := snsconn.GetSubscriptionAttributes(&sns.GetSubscriptionAttributesInput{ + output, err := conn.GetSubscriptionAttributes(&sns.GetSubscriptionAttributesInput{ SubscriptionArn: aws.String(d.Id()), }) - if isAWSErr(err, sns.ErrCodeNotFoundException, "") { - log.Printf("[WARN] SNS Topic Subscription (%s) not found, removing from state", d.Id()) + if !d.IsNewResource() && (tfawserr.ErrCodeEquals(err, sns.ErrCodeResourceNotFoundException) || tfawserr.ErrCodeEquals(err, sns.ErrCodeNotFoundException)) { + log.Printf("[WARN] SNS subscription attributes (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error reading SNS Topic Subscription (%s) attributes: %s", d.Id(), err) + return fmt.Errorf("getting SNS subscription attributes (%s): %w", d.Id(), err) + } + + if output == nil || output.Attributes == nil || len(output.Attributes) == 0 { + return fmt.Errorf("getting SNS subscription attributes (%s): empty response", d.Id()) } - if attributeOutput == nil || len(attributeOutput.Attributes) == 0 { - return fmt.Errorf("error reading SNS Topic Subscription (%s) attributes: no attributes found", d.Id()) + attributes := output.Attributes + + d.Set("arn", attributes["SubscriptionArn"]) + d.Set("delivery_policy", attributes["DeliveryPolicy"]) + d.Set("endpoint", attributes["Endpoint"]) + d.Set("filter_policy", attributes["FilterPolicy"]) + d.Set("owner_id", attributes["Owner"]) + d.Set("protocol", attributes["Protocol"]) + d.Set("redrive_policy", attributes["RedrivePolicy"]) + d.Set("subscription_role_arn", attributes["SubscriptionRoleArn"]) + d.Set("topic_arn", attributes["TopicArn"]) + + d.Set("confirmation_was_authenticated", false) + if v, ok := attributes["ConfirmationWasAuthenticated"]; ok && aws.StringValue(v) == "true" { + d.Set("confirmation_was_authenticated", true) } - d.Set("arn", attributeOutput.Attributes["SubscriptionArn"]) - d.Set("delivery_policy", attributeOutput.Attributes["DeliveryPolicy"]) - d.Set("endpoint", attributeOutput.Attributes["Endpoint"]) - d.Set("filter_policy", attributeOutput.Attributes["FilterPolicy"]) - d.Set("protocol", attributeOutput.Attributes["Protocol"]) + d.Set("pending_confirmation", false) + if v, ok := attributes["PendingConfirmation"]; ok && aws.StringValue(v) == "true" { + d.Set("pending_confirmation", true) + } d.Set("raw_message_delivery", false) - if v, ok := attributeOutput.Attributes["RawMessageDelivery"]; ok && aws.StringValue(v) == "true" { + if v, ok := attributes["RawMessageDelivery"]; ok && aws.StringValue(v) == "true" { d.Set("raw_message_delivery", true) } - d.Set("redrive_policy", attributeOutput.Attributes["RedrivePolicy"]) - d.Set("topic_arn", attributeOutput.Attributes["TopicArn"]) - return nil } -func resourceAwsSnsTopicSubscriptionDelete(d *schema.ResourceData, meta interface{}) error { - snsconn := meta.(*AWSClient).snsconn - - log.Printf("[DEBUG] SNS delete topic subscription: %s", d.Id()) - _, err := snsconn.Unsubscribe(&sns.UnsubscribeInput{ - SubscriptionArn: aws.String(d.Id()), - }) - - return err -} - -// Assembles supplied attributes into a single map - empty/default values are excluded from the map -func getResourceAttributes(d *schema.ResourceData) (output map[string]*string) { - delivery_policy := d.Get("delivery_policy").(string) - filter_policy := d.Get("filter_policy").(string) - raw_message_delivery := d.Get("raw_message_delivery").(bool) - redrive_policy := d.Get("redrive_policy").(string) - - // Collect attributes if available - attributes := map[string]*string{} - - if delivery_policy != "" { - attributes["DeliveryPolicy"] = aws.String(delivery_policy) - } - - if filter_policy != "" { - attributes["FilterPolicy"] = aws.String(filter_policy) - } - - if raw_message_delivery { - attributes["RawMessageDelivery"] = aws.String(fmt.Sprintf("%t", raw_message_delivery)) - } +func resourceAwsSnsTopicSubscriptionUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).snsconn - if redrive_policy != "" { - attributes["RedrivePolicy"] = aws.String(redrive_policy) + if d.HasChange("raw_message_delivery") { + if err := snsSubscriptionAttributeUpdate(conn, d.Id(), "RawMessageDelivery", fmt.Sprintf("%t", d.Get("raw_message_delivery").(bool))); err != nil { + return err + } } - return attributes -} - -func subscribeToSNSTopic(d *schema.ResourceData, snsconn *sns.SNS) (output *sns.SubscribeOutput, err error) { - protocol := d.Get("protocol").(string) - endpoint := d.Get("endpoint").(string) - topic_arn := d.Get("topic_arn").(string) - endpoint_auto_confirms := d.Get("endpoint_auto_confirms").(bool) - confirmation_timeout_in_minutes := d.Get("confirmation_timeout_in_minutes").(int) - attributes := getResourceAttributes(d) - - if strings.Contains(protocol, "http") && !endpoint_auto_confirms { - return nil, fmt.Errorf("Protocol http/https is only supported for endpoints which auto confirms!") - } + if d.HasChange("filter_policy") { + filterPolicy := d.Get("filter_policy").(string) - log.Printf("[DEBUG] SNS create topic subscription: %s (%s) @ '%s'", endpoint, protocol, topic_arn) + // https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html#message-filtering-policy-remove + if filterPolicy == "" { + filterPolicy = "{}" + } - req := &sns.SubscribeInput{ - Protocol: aws.String(protocol), - Endpoint: aws.String(endpoint), - TopicArn: aws.String(topic_arn), - Attributes: attributes, + if err := snsSubscriptionAttributeUpdate(conn, d.Id(), "FilterPolicy", filterPolicy); err != nil { + return err + } } - output, err = snsconn.Subscribe(req) - if err != nil { - return nil, fmt.Errorf("Error creating SNS topic subscription: %s", err) + if d.HasChange("delivery_policy") { + if err := snsSubscriptionAttributeUpdate(conn, d.Id(), "DeliveryPolicy", d.Get("delivery_policy").(string)); err != nil { + return err + } } - log.Printf("[DEBUG] Finished subscribing to topic %s with subscription arn %s", topic_arn, *output.SubscriptionArn) - - if strings.Contains(protocol, "http") && subscriptionHasPendingConfirmation(output.SubscriptionArn) { - - log.Printf("[DEBUG] SNS create topic subscription is pending so fetching the subscription list for topic : %s (%s) @ '%s'", endpoint, protocol, topic_arn) - - err = resource.Retry(time.Duration(confirmation_timeout_in_minutes)*time.Minute, func() *resource.RetryError { - - subscription, err := findSubscriptionByNonID(d, snsconn) - - if err != nil { - return resource.NonRetryableError(err) - } - - if subscription == nil { - return resource.RetryableError(fmt.Errorf("Endpoint (%s) did not autoconfirm the subscription for topic %s", endpoint, topic_arn)) - } - - output.SubscriptionArn = subscription.SubscriptionArn - return nil - }) - - if isResourceTimeoutError(err) { - var subscription *sns.Subscription - subscription, err = findSubscriptionByNonID(d, snsconn) - - if subscription != nil { - output.SubscriptionArn = subscription.SubscriptionArn - } + if d.HasChange("subscription_role_arn") { + protocol := d.Get("protocol").(string) + subscriptionRoleARN := d.Get("subscription_role_arn").(string) + if strings.Contains(protocol, "firehose") && subscriptionRoleARN == "" { + return fmt.Errorf("protocol firehose must contain subscription_role_arn!") } - - if err != nil { - return nil, err + if !strings.Contains(protocol, "firehose") && subscriptionRoleARN != "" { + return fmt.Errorf("only protocol firehose supports subscription_role_arn!") } - } - - log.Printf("[DEBUG] Created new subscription! %s", *output.SubscriptionArn) - return output, nil -} -// finds a subscription using protocol, endpoint and topic_arn (which is a key in sns subscription) -func findSubscriptionByNonID(d *schema.ResourceData, conn *sns.SNS) (*sns.Subscription, error) { - protocol := d.Get("protocol").(string) - endpoint := d.Get("endpoint").(string) - topicARN := d.Get("topic_arn").(string) - obfuscatedEndpoint := obfuscateEndpoint(endpoint) - - input := &sns.ListSubscriptionsByTopicInput{ - TopicArn: aws.String(topicARN), + if err := snsSubscriptionAttributeUpdate(conn, d.Id(), "SubscriptionRoleArn", subscriptionRoleARN); err != nil { + return err + } } - var result *sns.Subscription - err := conn.ListSubscriptionsByTopicPages(input, func(page *sns.ListSubscriptionsByTopicOutput, lastPage bool) bool { - if page == nil { - return !lastPage + if d.HasChange("redrive_policy") { + if err := snsSubscriptionAttributeUpdate(conn, d.Id(), "RedrivePolicy", d.Get("redrive_policy").(string)); err != nil { + return err } + } - for _, subscription := range page.Subscriptions { - if subscription == nil { - continue - } - - if aws.StringValue(subscription.Endpoint) != obfuscatedEndpoint { - continue - } - - if aws.StringValue(subscription.Protocol) != protocol { - continue - } + return resourceAwsSnsTopicSubscriptionRead(d, meta) +} - if aws.StringValue(subscription.TopicArn) != topicARN { - continue - } +func resourceAwsSnsTopicSubscriptionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).snsconn - if subscriptionHasPendingConfirmation(subscription.SubscriptionArn) { - continue - } + log.Printf("[DEBUG] SNS delete topic subscription: %s", d.Id()) + _, err := conn.Unsubscribe(&sns.UnsubscribeInput{ + SubscriptionArn: aws.String(d.Id()), + }) - result = subscription + if tfawserr.ErrMessageContains(err, sns.ErrCodeInvalidParameterException, "Cannot unsubscribe a subscription that is pending confirmation") { + log.Printf("[WARN] Removing unconfirmed SNS topic subscription (%s) from Terraform state but failed to remove it from AWS!", d.Id()) + d.SetId("") + return nil + } - return false - } + return err +} - return !lastPage - }) +// Assembles supplied attributes into a single map - empty/default values are excluded from the map +func expandSNSSubscriptionAttributes(d *schema.ResourceData) (output map[string]*string) { + deliveryPolicy := d.Get("delivery_policy").(string) + filterPolicy := d.Get("filter_policy").(string) + rawMessageDelivery := d.Get("raw_message_delivery").(bool) + redrivePolicy := d.Get("redrive_policy").(string) + subscriptionRoleARN := d.Get("subscription_role_arn").(string) - return result, err -} + // Collect attributes if available + attributes := map[string]*string{} -// returns true if arn is nil or has both pending and confirmation words in the arn -func subscriptionHasPendingConfirmation(arn *string) bool { - if arn != nil && !strings.Contains(strings.Replace(strings.ToLower(*arn), " ", "", -1), awsSNSPendingConfirmationMessageWithoutSpaces) { - return false + if deliveryPolicy != "" { + attributes["DeliveryPolicy"] = aws.String(deliveryPolicy) } - return true -} + if filterPolicy != "" { + attributes["FilterPolicy"] = aws.String(filterPolicy) + } -// returns the endpoint with obfuscated password, if any -func obfuscateEndpoint(endpoint string) string { - res, err := url.Parse(endpoint) - if err != nil { - fmt.Println(err) + if rawMessageDelivery { + attributes["RawMessageDelivery"] = aws.String(fmt.Sprintf("%t", rawMessageDelivery)) } - var obfuscatedEndpoint = res.String() + if subscriptionRoleARN != "" { + attributes["SubscriptionRoleArn"] = aws.String(subscriptionRoleARN) + } - // If the user is defined, we try to get the username and password, if defined. - // Then, we update the user with the obfuscated version. - if res.User != nil { - if password, ok := res.User.Password(); ok { - obfuscatedEndpoint = strings.Replace(obfuscatedEndpoint, password, awsSNSPasswordObfuscationPattern, 1) - } + if redrivePolicy != "" { + attributes["RedrivePolicy"] = aws.String(redrivePolicy) } - return obfuscatedEndpoint + + return attributes } -func snsSubscriptionAttributeUpdate(snsconn *sns.SNS, subscriptionArn, attributeName, attributeValue string) error { +func snsSubscriptionAttributeUpdate(conn *sns.SNS, subscriptionArn, attributeName, attributeValue string) error { req := &sns.SetSubscriptionAttributesInput{ SubscriptionArn: aws.String(subscriptionArn), AttributeName: aws.String(attributeName), @@ -398,7 +332,7 @@ func snsSubscriptionAttributeUpdate(snsconn *sns.SNS, subscriptionArn, attribute req.AttributeValue = nil } - _, err := snsconn.SetSubscriptionAttributes(req) + _, err := conn.SetSubscriptionAttributes(req) if err != nil { return fmt.Errorf("error setting subscription (%s) attribute (%s): %s", subscriptionArn, attributeName, err) diff --git a/aws/resource_aws_sns_topic_subscription_test.go b/aws/resource_aws_sns_topic_subscription_test.go index 3ddc936a27ce..6e54f345ff8b 100644 --- a/aws/resource_aws_sns_topic_subscription_test.go +++ b/aws/resource_aws_sns_topic_subscription_test.go @@ -3,9 +3,11 @@ package aws import ( "encoding/json" "fmt" + "net/url" "reflect" "regexp" "strconv" + "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -61,7 +63,7 @@ func TestSuppressEquivalentSnsTopicSubscriptionDeliveryPolicy(t *testing.T) { func TestAccAWSSNSTopicSubscription_basic(t *testing.T) { attributes := make(map[string]string) resourceName := "aws_sns_topic_subscription.test_subscription" - ri := acctest.RandInt() + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -69,13 +71,15 @@ func TestAccAWSSNSTopicSubscription_basic(t *testing.T) { CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSNSTopicSubscriptionConfig(ri), + Config: testAccAWSSNSTopicSubscriptionConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), - testAccMatchResourceAttrRegionalARN(resourceName, "arn", "sns", regexp.MustCompile(fmt.Sprintf("terraform-test-topic-%d:.+", ri))), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", sns.ServiceName, regexp.MustCompile(fmt.Sprintf("%s:.+", rName))), + resource.TestCheckResourceAttr(resourceName, "confirmation_was_authenticated", "true"), resource.TestCheckResourceAttr(resourceName, "delivery_policy", ""), resource.TestCheckResourceAttrPair(resourceName, "endpoint", "aws_sqs_queue.test_queue", "arn"), resource.TestCheckResourceAttr(resourceName, "filter_policy", ""), + resource.TestCheckResourceAttr(resourceName, "pending_confirmation", "false"), resource.TestCheckResourceAttr(resourceName, "protocol", "sqs"), resource.TestCheckResourceAttr(resourceName, "raw_message_delivery", "false"), resource.TestCheckResourceAttrPair(resourceName, "topic_arn", "aws_sns_topic.test_topic", "arn"), @@ -97,9 +101,9 @@ func TestAccAWSSNSTopicSubscription_basic(t *testing.T) { func TestAccAWSSNSTopicSubscription_filterPolicy(t *testing.T) { attributes := make(map[string]string) resourceName := "aws_sns_topic_subscription.test_subscription" - ri := acctest.RandInt() filterPolicy1 := `{"key1": ["val1"], "key2": ["val2"]}` filterPolicy2 := `{"key3": ["val3"], "key4": ["val4"]}` + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -107,7 +111,7 @@ func TestAccAWSSNSTopicSubscription_filterPolicy(t *testing.T) { CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSNSTopicSubscriptionConfig_filterPolicy(ri, strconv.Quote(filterPolicy1)), + Config: testAccAWSSNSTopicSubscriptionConfig_filterPolicy(rName, strconv.Quote(filterPolicy1)), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "filter_policy", filterPolicy1), @@ -124,7 +128,7 @@ func TestAccAWSSNSTopicSubscription_filterPolicy(t *testing.T) { }, // Test attribute update { - Config: testAccAWSSNSTopicSubscriptionConfig_filterPolicy(ri, strconv.Quote(filterPolicy2)), + Config: testAccAWSSNSTopicSubscriptionConfig_filterPolicy(rName, strconv.Quote(filterPolicy2)), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "filter_policy", filterPolicy2), @@ -132,7 +136,7 @@ func TestAccAWSSNSTopicSubscription_filterPolicy(t *testing.T) { }, // Test attribute removal { - Config: testAccAWSSNSTopicSubscriptionConfig(ri), + Config: testAccAWSSNSTopicSubscriptionConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "filter_policy", ""), @@ -145,7 +149,7 @@ func TestAccAWSSNSTopicSubscription_filterPolicy(t *testing.T) { func TestAccAWSSNSTopicSubscription_deliveryPolicy(t *testing.T) { attributes := make(map[string]string) resourceName := "aws_sns_topic_subscription.test_subscription" - ri := acctest.RandInt() + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -153,7 +157,7 @@ func TestAccAWSSNSTopicSubscription_deliveryPolicy(t *testing.T) { CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSNSTopicSubscriptionConfig_deliveryPolicy(ri, strconv.Quote(`{"healthyRetryPolicy":{"minDelayTarget":5,"maxDelayTarget":20,"numRetries": 5}}`)), + Config: testAccAWSSNSTopicSubscriptionConfig_deliveryPolicy(rName, strconv.Quote(`{"healthyRetryPolicy":{"minDelayTarget":5,"maxDelayTarget":20,"numRetries": 5}}`)), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), testAccCheckAWSSNSTopicSubscriptionDeliveryPolicyAttribute(attributes, &snsTopicSubscriptionDeliveryPolicy{ @@ -176,7 +180,7 @@ func TestAccAWSSNSTopicSubscription_deliveryPolicy(t *testing.T) { }, // Test attribute update { - Config: testAccAWSSNSTopicSubscriptionConfig_deliveryPolicy(ri, strconv.Quote(`{"healthyRetryPolicy":{"minDelayTarget":3,"maxDelayTarget":78,"numRetries": 11}}`)), + Config: testAccAWSSNSTopicSubscriptionConfig_deliveryPolicy(rName, strconv.Quote(`{"healthyRetryPolicy":{"minDelayTarget":3,"maxDelayTarget":78,"numRetries": 11}}`)), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), testAccCheckAWSSNSTopicSubscriptionDeliveryPolicyAttribute(attributes, &snsTopicSubscriptionDeliveryPolicy{ @@ -190,7 +194,7 @@ func TestAccAWSSNSTopicSubscription_deliveryPolicy(t *testing.T) { }, // Test attribute removal { - Config: testAccAWSSNSTopicSubscriptionConfig(ri), + Config: testAccAWSSNSTopicSubscriptionConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "delivery_policy", ""), @@ -203,9 +207,9 @@ func TestAccAWSSNSTopicSubscription_deliveryPolicy(t *testing.T) { func TestAccAWSSNSTopicSubscription_redrivePolicy(t *testing.T) { attributes := make(map[string]string) resourceName := "aws_sns_topic_subscription.test_subscription" - ri := acctest.RandInt() - dlqName := fmt.Sprintf("tf-acc-test-queue-dlq-%d", ri) - updatedDlqName := fmt.Sprintf("tf-acc-test-queue-dlq-update-%d", ri) + dlqName := acctest.RandomWithPrefix("tf-acc-test") + updatedDlqName := acctest.RandomWithPrefix("tf-acc-test") + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -213,7 +217,7 @@ func TestAccAWSSNSTopicSubscription_redrivePolicy(t *testing.T) { CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSNSTopicSubscriptionConfig_redrivePolicy(ri, dlqName), + Config: testAccAWSSNSTopicSubscriptionConfig_redrivePolicy(rName, dlqName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), testAccCheckAWSSNSTopicSubscriptionRedrivePolicyAttribute(attributes, dlqName), @@ -230,7 +234,7 @@ func TestAccAWSSNSTopicSubscription_redrivePolicy(t *testing.T) { }, // Test attribute update { - Config: testAccAWSSNSTopicSubscriptionConfig_redrivePolicy(ri, updatedDlqName), + Config: testAccAWSSNSTopicSubscriptionConfig_redrivePolicy(rName, updatedDlqName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), testAccCheckAWSSNSTopicSubscriptionRedrivePolicyAttribute(attributes, updatedDlqName), @@ -247,7 +251,7 @@ func TestAccAWSSNSTopicSubscription_redrivePolicy(t *testing.T) { }, // Test attribute removal { - Config: testAccAWSSNSTopicSubscriptionConfig(ri), + Config: testAccAWSSNSTopicSubscriptionConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "redrive_policy", ""), @@ -260,7 +264,7 @@ func TestAccAWSSNSTopicSubscription_redrivePolicy(t *testing.T) { func TestAccAWSSNSTopicSubscription_rawMessageDelivery(t *testing.T) { attributes := make(map[string]string) resourceName := "aws_sns_topic_subscription.test_subscription" - ri := acctest.RandInt() + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -268,7 +272,7 @@ func TestAccAWSSNSTopicSubscription_rawMessageDelivery(t *testing.T) { CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSNSTopicSubscriptionConfig_rawMessageDelivery(ri, true), + Config: testAccAWSSNSTopicSubscriptionConfig_rawMessageDelivery(rName, true), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "raw_message_delivery", "true"), @@ -285,7 +289,7 @@ func TestAccAWSSNSTopicSubscription_rawMessageDelivery(t *testing.T) { }, // Test attribute update { - Config: testAccAWSSNSTopicSubscriptionConfig_rawMessageDelivery(ri, false), + Config: testAccAWSSNSTopicSubscriptionConfig_rawMessageDelivery(rName, false), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "raw_message_delivery", "false"), @@ -293,7 +297,7 @@ func TestAccAWSSNSTopicSubscription_rawMessageDelivery(t *testing.T) { }, // Test attribute removal { - Config: testAccAWSSNSTopicSubscriptionConfig(ri), + Config: testAccAWSSNSTopicSubscriptionConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), resource.TestCheckResourceAttr(resourceName, "raw_message_delivery", "false"), @@ -306,7 +310,7 @@ func TestAccAWSSNSTopicSubscription_rawMessageDelivery(t *testing.T) { func TestAccAWSSNSTopicSubscription_autoConfirmingEndpoint(t *testing.T) { attributes := make(map[string]string) resourceName := "aws_sns_topic_subscription.test_subscription" - ri := acctest.RandInt() + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, @@ -314,7 +318,7 @@ func TestAccAWSSNSTopicSubscription_autoConfirmingEndpoint(t *testing.T) { CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSNSTopicSubscriptionConfig_autoConfirmingEndpoint(ri), + Config: testAccAWSSNSTopicSubscriptionConfig_autoConfirmingEndpoint(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), ), @@ -335,7 +339,7 @@ func TestAccAWSSNSTopicSubscription_autoConfirmingEndpoint(t *testing.T) { func TestAccAWSSNSTopicSubscription_autoConfirmingSecuredEndpoint(t *testing.T) { attributes := make(map[string]string) resourceName := "aws_sns_topic_subscription.test_subscription" - ri := acctest.RandInt() + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccAPIGatewayTypeEDGEPreCheck(t) }, @@ -343,7 +347,7 @@ func TestAccAWSSNSTopicSubscription_autoConfirmingSecuredEndpoint(t *testing.T) CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSSNSTopicSubscriptionConfig_autoConfirmingSecuredEndpoint(ri, "john", "doe"), + Config: testAccAWSSNSTopicSubscriptionConfig_autoConfirmingSecuredEndpoint(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), ), @@ -361,6 +365,71 @@ func TestAccAWSSNSTopicSubscription_autoConfirmingSecuredEndpoint(t *testing.T) }) } +func TestAccAWSSNSTopicSubscription_email(t *testing.T) { + attributes := make(map[string]string) + resourceName := "aws_sns_topic_subscription.test_subscription" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSNSTopicSubscriptionEmailConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", sns.ServiceName, regexp.MustCompile(fmt.Sprintf("%s:.+", rName))), + resource.TestCheckResourceAttr(resourceName, "confirmation_was_authenticated", "false"), + resource.TestCheckResourceAttr(resourceName, "delivery_policy", ""), + resource.TestCheckResourceAttr(resourceName, "endpoint", "invalid_email@example.com"), + resource.TestCheckResourceAttr(resourceName, "filter_policy", ""), + resource.TestCheckResourceAttr(resourceName, "pending_confirmation", "true"), + resource.TestCheckResourceAttr(resourceName, "protocol", "email"), + resource.TestCheckResourceAttr(resourceName, "raw_message_delivery", "false"), + resource.TestCheckResourceAttrPair(resourceName, "topic_arn", "aws_sns_topic.test_topic", "arn"), + ), + }, + }, + }) +} + +func TestAccAWSSNSTopicSubscription_firehose(t *testing.T) { + attributes := make(map[string]string) + resourceName := "aws_sns_topic_subscription.test_subscription" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheckSkipSNS(t), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSNSTopicSubscriptionConfig_firehose(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSNSTopicSubscriptionExists(resourceName, attributes), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", sns.ServiceName, regexp.MustCompile(fmt.Sprintf("%s:.+", rName))), + resource.TestCheckResourceAttr(resourceName, "delivery_policy", ""), + resource.TestCheckResourceAttrPair(resourceName, "endpoint", "aws_kinesis_firehose_delivery_stream.test_stream", "arn"), + resource.TestCheckResourceAttr(resourceName, "filter_policy", ""), + resource.TestCheckResourceAttr(resourceName, "protocol", "firehose"), + resource.TestCheckResourceAttr(resourceName, "raw_message_delivery", "false"), + resource.TestCheckResourceAttrPair(resourceName, "topic_arn", "aws_sns_topic.test_topic", "arn"), + resource.TestCheckResourceAttrPair(resourceName, "subscription_role_arn", "aws_iam_role.firehose_role", "arn"), + ), + }, + }, + }) +} + +// testAccErrorCheckSkipSNS skips SNS tests that have error messages indicating unsupported features +func testAccErrorCheckSkipSNS(t *testing.T) resource.ErrorCheckFunc { + return testAccErrorCheckSkipMessagesContaining(t, + "Invalid protocol type: firehose", + ) +} + func testAccCheckAWSSNSTopicSubscriptionDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).snsconn @@ -468,6 +537,27 @@ func testAccCheckAWSSNSTopicSubscriptionRedrivePolicyAttribute(attributes map[st } } +const awsSNSPasswordObfuscationPattern = "****" + +// returns the endpoint with obfuscated password, if any +func obfuscateEndpoint(endpoint string) string { + res, err := url.Parse(endpoint) + if err != nil { + fmt.Println(err) + } + + var obfuscatedEndpoint = res.String() + + // If the user is defined, we try to get the username and password, if defined. + // Then, we update the user with the obfuscated version. + if res.User != nil { + if password, ok := res.User.Password(); ok { + obfuscatedEndpoint = strings.Replace(obfuscatedEndpoint, password, awsSNSPasswordObfuscationPattern, 1) + } + } + return obfuscatedEndpoint +} + func TestObfuscateEndpointPassword(t *testing.T) { checks := map[string]string{ "https://example.com/myroute": "https://example.com/myroute", @@ -483,14 +573,14 @@ func TestObfuscateEndpointPassword(t *testing.T) { } } -func testAccAWSSNSTopicSubscriptionConfig(i int) string { +func testAccAWSSNSTopicSubscriptionConfig(rName string) string { return fmt.Sprintf(` resource "aws_sns_topic" "test_topic" { - name = "terraform-test-topic-%d" + name = %[1]q } resource "aws_sqs_queue" "test_queue" { - name = "terraform-subscription-test-queue-%d" + name = %[1]q } resource "aws_sns_topic_subscription" "test_subscription" { @@ -498,17 +588,17 @@ resource "aws_sns_topic_subscription" "test_subscription" { protocol = "sqs" endpoint = aws_sqs_queue.test_queue.arn } -`, i, i) +`, rName) } -func testAccAWSSNSTopicSubscriptionConfig_filterPolicy(i int, policy string) string { +func testAccAWSSNSTopicSubscriptionConfig_filterPolicy(rName, policy string) string { return fmt.Sprintf(` resource "aws_sns_topic" "test_topic" { - name = "terraform-test-topic-%d" + name = %[1]q } resource "aws_sqs_queue" "test_queue" { - name = "terraform-subscription-test-queue-%d" + name = %[1]q } resource "aws_sns_topic_subscription" "test_subscription" { @@ -517,17 +607,17 @@ resource "aws_sns_topic_subscription" "test_subscription" { endpoint = aws_sqs_queue.test_queue.arn filter_policy = %s } -`, i, i, policy) +`, rName, policy) } -func testAccAWSSNSTopicSubscriptionConfig_deliveryPolicy(i int, policy string) string { +func testAccAWSSNSTopicSubscriptionConfig_deliveryPolicy(rName, policy string) string { return fmt.Sprintf(` resource "aws_sns_topic" "test_topic" { - name = "terraform-test-topic-%d" + name = %[1]q } resource "aws_sqs_queue" "test_queue" { - name = "terraform-subscription-test-queue-%d" + name = %[1]q } resource "aws_sns_topic_subscription" "test_subscription" { @@ -536,21 +626,21 @@ resource "aws_sns_topic_subscription" "test_subscription" { protocol = "sqs" topic_arn = aws_sns_topic.test_topic.arn } -`, i, i, policy) +`, rName, policy) } -func testAccAWSSNSTopicSubscriptionConfig_redrivePolicy(i int, dlqName string) string { +func testAccAWSSNSTopicSubscriptionConfig_redrivePolicy(rName, dlqName string) string { return fmt.Sprintf(` resource "aws_sns_topic" "test_topic" { - name = "terraform-test-topic-%[1]d" + name = %[1]q } resource "aws_sqs_queue" "test_queue" { - name = "terraform-subscription-test-queue-%[1]d" + name = %[1]q } resource "aws_sqs_queue" "test_queue_dlq" { - name = "%s" + name = %[2]q } resource "aws_sns_topic_subscription" "test_subscription" { @@ -559,17 +649,17 @@ resource "aws_sns_topic_subscription" "test_subscription" { protocol = "sqs" topic_arn = aws_sns_topic.test_topic.arn } -`, i, dlqName) +`, rName, dlqName) } -func testAccAWSSNSTopicSubscriptionConfig_rawMessageDelivery(i int, rawMessageDelivery bool) string { +func testAccAWSSNSTopicSubscriptionConfig_rawMessageDelivery(rName string, rawMessageDelivery bool) string { return fmt.Sprintf(` resource "aws_sns_topic" "test_topic" { - name = "terraform-test-topic-%d" + name = %[1]q } resource "aws_sqs_queue" "test_queue" { - name = "terraform-subscription-test-queue-%d" + name = %[1]q } resource "aws_sns_topic_subscription" "test_subscription" { @@ -578,17 +668,17 @@ resource "aws_sns_topic_subscription" "test_subscription" { raw_message_delivery = %t topic_arn = aws_sns_topic.test_topic.arn } -`, i, i, rawMessageDelivery) +`, rName, rawMessageDelivery) } -func testAccAWSSNSTopicSubscriptionConfig_autoConfirmingEndpoint(i int) string { +func testAccAWSSNSTopicSubscriptionConfig_autoConfirmingEndpoint(rName string) string { return fmt.Sprintf(` resource "aws_sns_topic" "test_topic" { - name = "tf-acc-test-sns-%d" + name = %[1]q } resource "aws_api_gateway_rest_api" "test" { - name = "tf-acc-test-sns-%d" + name = %[1]q description = "Terraform Acceptance test for SNS subscription" } @@ -631,8 +721,10 @@ resource "aws_api_gateway_integration_response" "test" { } } +data "aws_partition" "current" {} + resource "aws_iam_role" "iam_for_lambda" { - name = "tf-acc-test-sns-%d" + name = %[1]q assume_role_policy = < **NOTE:** If the SNS topic and SQS queue are in different AWS regions, it is important for the "aws_sns_topic_subscription" to use an AWS provider that is in the same region of the SNS topic. If the "aws_sns_topic_subscription" is using a provider with a different region than the SNS topic, terraform will fail to create the subscription. +~> **NOTE:** If the SNS topic and SQS queue are in different AWS regions, the `aws_sns_topic_subscription` must use an AWS provider that is in the same region as the SNS topic. If the `aws_sns_topic_subscription` uses a provider with a different region than the SNS topic, Terraform will fail to create the subscription. ~> **NOTE:** Setup of cross-account subscriptions from SNS topics to SQS queues requires Terraform to have access to BOTH accounts. -~> **NOTE:** If SNS topic and SQS queue are in different AWS accounts but the same region it is important for the "aws_sns_topic_subscription" to use the AWS provider of the account with the SQS queue. If "aws_sns_topic_subscription" is using a Provider with a different account than the SQS queue, terraform creates the subscriptions but does not keep state and tries to re-create the subscription at every apply. +~> **NOTE:** If an SNS topic and SQS queue are in different AWS accounts but the same region, the `aws_sns_topic_subscription` must use the AWS provider for the account with the SQS queue. If `aws_sns_topic_subscription` uses a Provider with a different account than the SQS queue, Terraform creates the subscription but does not keep state and tries to re-create the subscription at every `apply`. -~> **NOTE:** If SNS topic and SQS queue are in different AWS accounts and different AWS regions it is important to recognize that the subscription needs to be initiated from the account with the SQS queue but in the region of the SNS topic. +~> **NOTE:** If an SNS topic and SQS queue are in different AWS accounts and different AWS regions, the subscription needs to be initiated from the account with the SQS queue but in the region of the SNS topic. + +~> **NOTE:** You cannot unsubscribe to a subscription that is pending confirmation. If you use `email`, `email-json`, or `http`/`https` (without auto-confirmation enabled), until the subscription is confirmed (e.g., outside of Terraform), AWS does not allow Terraform to delete / unsubscribe the subscription. If you `destroy` an unconfirmed subscription, Terraform will remove the subscription from its state but the subscription will still exist in AWS. However, if you delete an SNS topic, SNS [deletes all the subscriptions](https://docs.aws.amazon.com/sns/latest/dg/sns-delete-subscription-topic.html) associated with the topic. Also, you can import a subscription after confirmation and then have the capability to delete it. ## Example Usage @@ -228,57 +227,51 @@ resource "aws_sns_topic_subscription" "sns-topic" { ## Argument Reference -The following arguments are supported: - -* `topic_arn` - (Required) The ARN of the SNS topic to subscribe to -* `protocol` - (Required) The protocol to use. The possible values for this are: `sqs`, `sms`, `lambda`, `application`. (`http` or `https` are partially supported, see below) (`email` is an option but is unsupported, see below). -* `endpoint` - (Required) The endpoint to send data to, the contents will vary with the protocol. (see below for more information) -* `endpoint_auto_confirms` - (Optional) Boolean indicating whether the end point is capable of [auto confirming subscription](http://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.html#SendMessageToHttp.prepare) e.g., PagerDuty (default is false) -* `confirmation_timeout_in_minutes` - (Optional) Integer indicating number of minutes to wait in retying mode for fetching subscription arn before marking it as failure. Only applicable for http and https protocols (default is 1 minute). -* `raw_message_delivery` - (Optional) Boolean indicating whether or not to enable raw message delivery (the original message is directly passed, not wrapped in JSON with the original message in the message property) (default is false). -* `filter_policy` - (Optional) JSON String with the filter policy that will be used in the subscription to filter messages seen by the target resource. Refer to the [SNS docs](https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html) for more details. -* `delivery_policy` - (Optional) JSON String with the delivery policy (retries, backoff, etc.) that will be used in the subscription - this only applies to HTTP/S subscriptions. Refer to the [SNS docs](https://docs.aws.amazon.com/sns/latest/dg/DeliveryPolicies.html) for more details. -* `redrive_policy` - (Optional) JSON String with the redrive policy that will be used in the subscription. Refer to the [SNS docs](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html#how-messages-moved-into-dead-letter-queue) for more details. - -### Protocols supported - -Supported SNS protocols include: +The following arguments are required: -* `lambda` -- delivery of JSON-encoded message to a lambda function -* `sqs` -- delivery of JSON-encoded message to an Amazon SQS queue -* `application` -- delivery of JSON-encoded message to an EndpointArn for a mobile app and device -* `sms` -- delivery text message +* `endpoint` - (Required) Endpoint to send data to. The contents vary with the protocol. See details below. +* `protocol` - (Required) Protocol to use. Valid values are: `sqs`, `sms`, `lambda`, `firehose`, and `application`. Protocols `email`, `email-json`, `http` and `https` are also valid but partially supported. See details below. +* `subscription_role_arn` - (Required if `protocol` is `firehose`) ARN of the IAM role to publish to Kinesis Data Firehose delivery stream. Refer to [SNS docs](https://docs.aws.amazon.com/sns/latest/dg/sns-firehose-as-subscriber.html). +* `topic_arn` - (Required) ARN of the SNS topic to subscribe to. -Partially supported SNS protocols include: +The following arguments are optional: -* `http` -- delivery of JSON-encoded messages via HTTP. Supported only for the end points that auto confirms the subscription. -* `https` -- delivery of JSON-encoded messages via HTTPS. Supported only for the end points that auto confirms the subscription. +* `confirmation_timeout_in_minutes` - (Optional) Integer indicating number of minutes to wait in retying mode for fetching subscription arn before marking it as failure. Only applicable for http and https protocols. Default is `1`. +* `delivery_policy` - (Optional) JSON String with the delivery policy (retries, backoff, etc.) that will be used in the subscription - this only applies to HTTP/S subscriptions. Refer to the [SNS docs](https://docs.aws.amazon.com/sns/latest/dg/DeliveryPolicies.html) for more details. +* `endpoint_auto_confirms` - (Optional) Whether the endpoint is capable of [auto confirming subscription](http://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.html#SendMessageToHttp.prepare) (e.g., PagerDuty). Default is `false`. +* `filter_policy` - (Optional) JSON String with the filter policy that will be used in the subscription to filter messages seen by the target resource. Refer to the [SNS docs](https://docs.aws.amazon.com/sns/latest/dg/message-filtering.html) for more details. +* `raw_message_delivery` - (Optional) Whether to enable raw message delivery (the original message is directly passed, not wrapped in JSON with the original message in the message property). Default is `false`. +* `redrive_policy` - (Optional) JSON String with the redrive policy that will be used in the subscription. Refer to the [SNS docs](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html#how-messages-moved-into-dead-letter-queue) for more details. -Unsupported protocols include the following: +### Protocol support -* `email` -- delivery of message via SMTP -* `email-json` -- delivery of JSON-encoded message via SMTP +Supported values for `protocol` include: -These are unsupported because the endpoint needs to be authorized and does not -generate an ARN until the target email address has been validated. This breaks -the Terraform model and as a result are not currently supported. +* `application` - Delivers JSON-encoded messages. `endpoint` is the endpoint ARN of a mobile app and device. +* `firehose` - Delivers JSON-encoded messages. `endpoint` is the ARN of an Amazon Kinesis Data Firehose delivery stream (e.g., +`arn:aws:firehose:us-east-1:123456789012:deliverystream/ticketUploadStream`). +* `lambda` - Delivers JSON-encoded messages. `endpoint` is the ARN of an AWS Lambda function. +* `sms` - Delivers text messages via SMS. `endpoint` is the phone number of an SMS-enabled device. +* `sqs` - Delivers JSON-encoded messages. `endpoint` is the ARN of an Amazon SQS queue (e.g., `arn:aws:sqs:us-west-2:123456789012:terraform-queue-too`). -### Specifying endpoints +Partially supported values for `protocol` include: -Endpoints have different format requirements according to the protocol that is chosen. +~> **NOTE:** If an `aws_sns_topic_subscription` uses a partially-supported protocol and the subscription is not confirmed, either through automatic confirmation or means outside of Terraform (e.g., clicking on a "Confirm Subscription" link in an email), Terraform cannot delete / unsubscribe the subscription. Attempting to `destroy` an unconfirmed subscription will remove the `aws_sns_topic_subscription` from Terraform's state but **_will not_** remove the subscription from AWS. The `pending_confirmation` attribute provides confirmation status. -* SQS endpoints come in the form of the SQS queue's ARN (not the URL of the queue) e.g: `arn:aws:sqs:us-west-2:432981146916:terraform-queue-too` -* Application endpoints are also the endpoint ARN for the mobile app and device. +* `email` - Delivers messages via SMTP. `endpoint` is an email address. +* `email-json` - Delivers JSON-encoded messages via SMTP. `endpoint` is an email address. +* `http` -- Delivers JSON-encoded messages via HTTP POST. `endpoint` is a URL beginning with `http://`. +* `https` -- Delivers JSON-encoded messages via HTTPS POST. `endpoint` is a URL beginning with `https://`. ## Attributes Reference In addition to all arguments above, the following attributes are exported: -* `id` - The ARN of the subscription -* `topic_arn` - The ARN of the topic the subscription belongs to -* `protocol` - The protocol being used -* `endpoint` - The full endpoint to send data to (SQS ARN, HTTP(S) URL, Application ARN, SMS number, etc.) -* `arn` - The ARN of the subscription stored as a more user-friendly property +* `arn` - ARN of the subscription. +* `confirmation_was_authenticated` - Whether the subscription confirmation request was authenticated. +* `id` - ARN of the subscription. +* `owner_id` - AWS account ID of the subscription's owner. +* `pending_confirmation` - Whether the subscription has not been confirmed. ## Import