Merge pull request #1974 from johnewart/sns

Initial SNS topic / subscription support
This commit is contained in:
Clint 2015-05-26 16:50:57 -05:00
commit 8764205bcf
11 changed files with 678 additions and 2 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/awslabs/aws-sdk-go/service/route53" "github.com/awslabs/aws-sdk-go/service/route53"
"github.com/awslabs/aws-sdk-go/service/s3" "github.com/awslabs/aws-sdk-go/service/s3"
"github.com/awslabs/aws-sdk-go/service/sqs" "github.com/awslabs/aws-sdk-go/service/sqs"
"github.com/awslabs/aws-sdk-go/service/sns"
) )
type Config struct { type Config struct {
@ -37,6 +38,7 @@ type AWSClient struct {
autoscalingconn *autoscaling.AutoScaling autoscalingconn *autoscaling.AutoScaling
s3conn *s3.S3 s3conn *s3.S3
sqsconn *sqs.SQS sqsconn *sqs.SQS
snsconn *sns.SNS
r53conn *route53.Route53 r53conn *route53.Route53
region string region string
rdsconn *rds.RDS rdsconn *rds.RDS
@ -89,6 +91,9 @@ func (c *Config) Client() (interface{}, error) {
log.Println("[INFO] Initializing SQS connection") log.Println("[INFO] Initializing SQS connection")
client.sqsconn = sqs.New(awsConfig) client.sqsconn = sqs.New(awsConfig)
log.Println("[INFO] Initializing SNS connection")
client.snsconn = sns.New(awsConfig)
log.Println("[INFO] Initializing RDS Connection") log.Println("[INFO] Initializing RDS Connection")
client.rdsconn = rds.New(awsConfig) client.rdsconn = rds.New(awsConfig)

View File

@ -123,6 +123,8 @@ func Provider() terraform.ResourceProvider {
"aws_security_group": resourceAwsSecurityGroup(), "aws_security_group": resourceAwsSecurityGroup(),
"aws_security_group_rule": resourceAwsSecurityGroupRule(), "aws_security_group_rule": resourceAwsSecurityGroupRule(),
"aws_sqs_queue": resourceAwsSqsQueue(), "aws_sqs_queue": resourceAwsSqsQueue(),
"aws_sns_topic": resourceAwsSnsTopic(),
"aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(),
"aws_subnet": resourceAwsSubnet(), "aws_subnet": resourceAwsSubnet(),
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(), "aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),
"aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(),

View File

@ -0,0 +1,153 @@
package aws
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/sns"
)
// Mutable attributes
var SNSAttributeMap = map[string]string{
"display_name" : "DisplayName",
"policy" : "Policy",
"delivery_policy": "DeliveryPolicy",
}
func resourceAwsSnsTopic() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSnsTopicCreate,
Read: resourceAwsSnsTopicRead,
Update: resourceAwsSnsTopicUpdate,
Delete: resourceAwsSnsTopicDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"display_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"policy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
Computed: true,
},
"delivery_policy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAwsSnsTopicCreate(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn
name := d.Get("name").(string)
log.Printf("[DEBUG] SNS create topic: %s", name)
req := &sns.CreateTopicInput{
Name: aws.String(name),
}
output, err := snsconn.CreateTopic(req)
if err != nil {
return fmt.Errorf("Error creating SNS topic: %s", err)
}
d.SetId(*output.TopicARN)
// Write the ARN to the 'arn' field for export
d.Set("arn", *output.TopicARN)
return resourceAwsSnsTopicUpdate(d, meta)
}
func resourceAwsSnsTopicUpdate(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn
resource := *resourceAwsSnsTopic()
for k, _ := range resource.Schema {
if attrKey, ok := SNSAttributeMap[k]; ok {
if d.HasChange(k) {
log.Printf("[DEBUG] Updating %s", attrKey)
_, n := d.GetChange(k)
// Ignore an empty policy
if !(k == "policy" && n == "") {
// Make API call to update attributes
req := &sns.SetTopicAttributesInput{
TopicARN: aws.String(d.Id()),
AttributeName: aws.String(attrKey),
AttributeValue: aws.String(n.(string)),
}
snsconn.SetTopicAttributes(req)
}
}
}
}
return resourceAwsSnsTopicRead(d, meta)
}
func resourceAwsSnsTopicRead(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn
attributeOutput, err := snsconn.GetTopicAttributes(&sns.GetTopicAttributesInput{
TopicARN: aws.String(d.Id()),
})
if err != nil {
return err
}
if attributeOutput.Attributes != nil && len(*attributeOutput.Attributes) > 0 {
attrmap := *attributeOutput.Attributes
resource := *resourceAwsSnsTopic()
// iKey = internal struct key, oKey = AWS Attribute Map key
for iKey, oKey := range SNSAttributeMap {
log.Printf("[DEBUG] Updating %s => %s", iKey, oKey)
if attrmap[oKey] != nil {
// Some of the fetched attributes are stateful properties such as
// the number of subscriptions, the owner, etc. skip those
if resource.Schema[iKey] != nil {
value := *attrmap[oKey]
log.Printf("[DEBUG] Updating %s => %s -> %s", iKey, oKey, value)
d.Set(iKey, *attrmap[oKey])
}
}
}
}
return nil
}
func resourceAwsSnsTopicDelete(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn
log.Printf("[DEBUG] SNS Delete Topic: %s", d.Id())
_, err := snsconn.DeleteTopic(&sns.DeleteTopicInput{
TopicARN: aws.String(d.Id()),
})
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,181 @@
package aws
import (
"fmt"
"log"
"github.com/hashicorp/terraform/helper/schema"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/sns"
)
func resourceAwsSnsTopicSubscription() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSnsTopicSubscriptionCreate,
Read: resourceAwsSnsTopicSubscriptionRead,
Update: resourceAwsSnsTopicSubscriptionUpdate,
Delete: resourceAwsSnsTopicSubscriptionDelete,
Schema: map[string]*schema.Schema{
"protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
"endpoint": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
"topic_arn": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: false,
},
"delivery_policy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: false,
},
"raw_message_delivery": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: false,
Default: false,
},
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAwsSnsTopicSubscriptionCreate(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn
if(d.Get("protocol") == "email") {
return fmt.Errorf("Email endpoints are not supported!")
}
output, err := subscribeToSNSTopic(d, snsconn)
if err != nil {
return err
}
log.Printf("New subscription ARN: %s", *output.SubscriptionARN)
d.SetId(*output.SubscriptionARN)
// Write the ARN to the 'arn' field for export
d.Set("arn", *output.SubscriptionARN)
return resourceAwsSnsTopicSubscriptionUpdate(d, meta)
}
func resourceAwsSnsTopicSubscriptionUpdate(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn
// If any changes happened, un-subscribe and re-subscribe
if d.HasChange("protocol") || d.HasChange("endpoint") || d.HasChange("topic_arn") {
log.Printf("[DEBUG] Updating subscription %s", d.Id())
// Unsubscribe
_, err := snsconn.Unsubscribe(&sns.UnsubscribeInput{
SubscriptionARN: aws.String(d.Id()),
})
if err != nil {
return fmt.Errorf("Error unsubscribing from SNS topic: %s", err)
}
// Re-subscribe and set id
output, err := subscribeToSNSTopic(d, snsconn)
d.SetId(*output.SubscriptionARN)
}
if d.HasChange("raw_message_delivery") {
_, n := d.GetChange("raw_message_delivery")
attrValue := "false"
if n.(bool) {
attrValue = "true"
}
req := &sns.SetSubscriptionAttributesInput{
SubscriptionARN: aws.String(d.Id()),
AttributeName: aws.String("RawMessageDelivery"),
AttributeValue: aws.String(attrValue),
}
_, err := snsconn.SetSubscriptionAttributes(req)
if err != nil {
return fmt.Errorf("Unable to set raw message delivery attribute on subscription")
}
}
return resourceAwsSnsTopicSubscriptionRead(d, meta)
}
func resourceAwsSnsTopicSubscriptionRead(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn
log.Printf("[DEBUG] Loading subscription %s", d.Id())
attributeOutput, err := snsconn.GetSubscriptionAttributes(&sns.GetSubscriptionAttributesInput{
SubscriptionARN: aws.String(d.Id()),
})
if err != nil {
return err
}
if attributeOutput.Attributes != nil && len(*attributeOutput.Attributes) > 0 {
attrHash := *attributeOutput.Attributes
log.Printf("[DEBUG] raw message delivery: %s", *attrHash["RawMessageDelivery"])
if *attrHash["RawMessageDelivery"] == "true" {
d.Set("raw_message_delivery", true)
} else {
d.Set("raw_message_delivery", false)
}
}
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()),
})
if err != nil {
return err
}
return nil
}
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)
log.Printf("[DEBUG] SNS create topic subscription: %s (%s) @ '%s'", endpoint, protocol, topic_arn)
req := &sns.SubscribeInput{
Protocol: aws.String(protocol),
Endpoint: aws.String(endpoint),
TopicARN: aws.String(topic_arn),
}
output, err = snsconn.Subscribe(req)
if err != nil {
return nil, fmt.Errorf("Error creating SNS topic: %s", err)
}
log.Printf("[DEBUG] Created new subscription!")
return output, nil
}

View File

@ -0,0 +1,103 @@
package aws
import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/sns"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/awslabs/aws-sdk-go/aws/awserr"
)
func TestAccAWSSNSTopicSubscription(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSNSTopicSubscriptionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSNSTopicSubscriptionConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"),
testAccCheckAWSSNSTopicSubscriptionExists("aws_sns_topic_subscription.test_subscription"),
),
},
},
})
}
func testAccCheckAWSSNSTopicSubscriptionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).snsconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_sns_topic" {
continue
}
// Try to find key pair
req := &sns.GetSubscriptionAttributesInput{
SubscriptionARN: aws.String(rs.Primary.ID),
}
_, err := conn.GetSubscriptionAttributes(req)
if err == nil {
return fmt.Errorf("Subscription still exists, can't continue.")
}
// Verify the error is an API error, not something else
_, ok := err.(awserr.Error)
if !ok {
return err
}
}
return nil
}
func testAccCheckAWSSNSTopicSubscriptionExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No SNS subscription with that ARN exists")
}
conn := testAccProvider.Meta().(*AWSClient).snsconn
params := &sns.GetSubscriptionAttributesInput{
SubscriptionARN: aws.String(rs.Primary.ID),
}
_, err := conn.GetSubscriptionAttributes(params)
if err != nil {
return err
}
return nil
}
}
const testAccAWSSNSTopicSubscriptionConfig = `
resource "aws_sns_topic" "test_topic" {
name = "terraform-test-topic"
}
resource "aws_sqs_queue" "test_queue" {
name = "terraform-subscription-test-queue"
}
resource "aws_sns_topic_subscription" "test_subscription" {
topic_arn = "${aws_sns_topic.test_topic.arn}"
protocol = "sqs"
endpoint = "${aws_sqs_queue.test_queue.arn}"
}
`

View File

@ -0,0 +1,89 @@
package aws
import (
"fmt"
"testing"
"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/sns"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/awslabs/aws-sdk-go/aws/awserr"
)
func TestAccAWSSNSTopic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSNSTopicDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSNSTopicConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSNSTopicExists("aws_sns_topic.test_topic"),
),
},
},
})
}
func testAccCheckAWSSNSTopicDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).snsconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_sns_topic" {
continue
}
// Check if the topic exists by fetching its attributes
params := &sns.GetTopicAttributesInput{
TopicARN: aws.String(rs.Primary.ID),
}
_, err := conn.GetTopicAttributes(params)
if err == nil {
return fmt.Errorf("Topic exists when it should be destroyed!")
}
// Verify the error is an API error, not something else
_, ok := err.(awserr.Error)
if !ok {
return err
}
}
return nil
}
func testAccCheckAWSSNSTopicExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No SNS topic with that ARN exists")
}
conn := testAccProvider.Meta().(*AWSClient).snsconn
params := &sns.GetTopicAttributesInput{
TopicARN: aws.String(rs.Primary.ID),
}
_, err := conn.GetTopicAttributes(params)
if err != nil {
return err
}
return nil
}
}
const testAccAWSSNSTopicConfig = `
resource "aws_sns_topic" "test_topic" {
name = "terraform-test-topic"
}
`

View File

@ -19,6 +19,7 @@ var AttributeMap = map[string]string{
"visibility_timeout_seconds": "VisibilityTimeout", "visibility_timeout_seconds": "VisibilityTimeout",
"policy": "Policy", "policy": "Policy",
"redrive_policy": "RedrivePolicy", "redrive_policy": "RedrivePolicy",
"arn": "QueueArn",
} }
// A number of these are marked as computed because if you don't // A number of these are marked as computed because if you don't
@ -70,6 +71,10 @@ func resourceAwsSqsQueue() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
}, },
} }
} }
@ -154,6 +159,7 @@ func resourceAwsSqsQueueRead(d *schema.ResourceData, meta interface{}) error {
QueueURL: aws.String(d.Id()), QueueURL: aws.String(d.Id()),
AttributeNames: []*string{aws.String("All")}, AttributeNames: []*string{aws.String("All")},
}) })
if err != nil { if err != nil {
return err return err
} }

View File

@ -0,0 +1,35 @@
---
layout: "aws"
page_title: "AWS: sns_topic"
sidebar_current: "docs-aws-resource-sns-topic"
description: |-
Provides an SNS topic resource.
---
# aws\_sns\_topic
Provides an SNS topic resource
## Example Usage
```
resource "aws_sns_topic" "user_updates" {
name = "user-updates-topic"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The friendly name for the SNS topic
* `policy` - (Optional) The fully-formed AWS policy as JSON
* `delivery_policy` - (Optional) The SNS delivery policy
## Attributes Reference
The following attributes are exported:
* `id` - The ARN of the SNS topic
* `arn` - The ARN of the SNS topic, as a more obvious property (clone of id)

View File

@ -0,0 +1,93 @@
---
layout: "aws"
page_title: "AWS: sns_topic_subscription"
sidebar_current: "docs-aws-resource-sns-topic-subscription"
description: |-
Provides a resource for subscribing to SNS topics.
---
# aws\_sns\_topic\_subscription
Provides a resource for subscribing to SNS topics. Requires that an SNS topic exist for the subscription to attach to.
This resource allows you to automatically place messages sent to SNS topics in SQS queues, send them as HTTP(S) POST requests
to a given endpoint, send SMS messages, or notify devices / applications. The most likely use case for Terraform users will
probably be SQS queues.
## Example Usage
You can directly supply a topic and ARN by hand in the `topic_arn` property along with the queue ARN:
```
resource "aws_sns_topic_subscription" "user_updates_sqs_target" {
topic_arn = "arn:aws:sns:us-west-2:432981146916:user-updates-topic"
protocol = "sqs"
endpoint = "arn:aws:sqs:us-west-2:432981146916:terraform-queue-too"
}
```
Alternatively you can use the ARN properties of a managed SNS topic and SQS queue:
```
resource "aws_sns_topic" "user_updates" {
name = "user-updates-topic"
}
resource "aws_sqs_queue" "user_updates_queue" {
name = "user-updates-queue"
}
resource "aws_sns_topic_subscription" "user_updates_sqs_target" {
topic_arn = "${aws_sns_topic.user_updates.arn}"
protocol = "sqs"
endpoint = "${aws_sqs_queue.user_updates_queue.arn}"
}
```
## 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`, `http`, `https`, `sms`, or `application`. (`email` is an option but unsupported, see below)
* `endpoint` - (Required) The endpoint to send data to, the contents will vary with the protocol. (see below for more information)
* `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).
### Protocols supported
Supported SNS protocols include:
* `http` -- delivery of JSON-encoded message via HTTP POST
* `https` -- delivery of JSON-encoded message via HTTPS POST
* `sms` -- delivery of message via SMS
* `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
Unsupported protocols include the following:
* `email` -- delivery of message via SMTP
* `email-json` -- delivery of JSON-encoded message via SMTP
These are unsupported because the email address 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.
### Specifying endpoints
Endpoints have different format requirements according to the protocol that is chosen.
* HTTP/HTTPS endpoints will require a URL to POST data to
* SMS endpoints are mobile numbers that are capable of receiving an SMS
* 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.
## Attributes Reference
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

View File

@ -11,7 +11,7 @@ description: |-
## Example Usage ## Example Usage
``` ```
resource "aws_sqs_queue" "terrform_queue" { resource "aws_sqs_queue" "terraform_queue" {
name = "terraform-example-queue" name = "terraform-example-queue"
delay_seconds = 90 delay_seconds = 90
max_message_size = 2048 max_message_size = 2048
@ -35,4 +35,5 @@ The following arguments are supported:
The following attributes are exported: The following attributes are exported:
* `id` - The URL for the created Amazon SQS queue. * `id` - The URL for the created Amazon SQS queue.
* `arn` - The ARN of the SQS queue

View File

@ -152,6 +152,14 @@
<a href="/docs/providers/aws/r/s3_bucket.html">aws_s3_bucket</a> <a href="/docs/providers/aws/r/s3_bucket.html">aws_s3_bucket</a>
</li> </li>
<li<%= sidebar_current("docs-aws-resource-sns-topic") %>>
<a href="/docs/providers/aws/r/sns_topic.html">aws_sns_topic</a>
</li>
<li<%= sidebar_current("docs-aws-resource-sns-topic-subscription") %>>
<a href="/docs/providers/aws/r/sns_topic_subscription.html">aws_sns_topic_subscription</a>
</li>
<li<%= sidebar_current("docs-aws-resource-security-group") %>> <li<%= sidebar_current("docs-aws-resource-security-group") %>>
<a href="/docs/providers/aws/r/security_group.html">aws_security_group</a> <a href="/docs/providers/aws/r/security_group.html">aws_security_group</a>
</li> </li>