Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial SNS topic / subscription support #1974

Merged
merged 4 commits into from
May 26, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions builtin/providers/aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/awslabs/aws-sdk-go/service/route53"
"github.com/awslabs/aws-sdk-go/service/s3"
"github.com/awslabs/aws-sdk-go/service/sqs"
"github.com/awslabs/aws-sdk-go/service/sns"
)

type Config struct {
Expand All @@ -37,6 +38,7 @@ type AWSClient struct {
autoscalingconn *autoscaling.AutoScaling
s3conn *s3.S3
sqsconn *sqs.SQS
snsconn *sns.SNS
r53conn *route53.Route53
region string
rdsconn *rds.RDS
Expand Down Expand Up @@ -89,6 +91,9 @@ func (c *Config) Client() (interface{}, error) {
log.Println("[INFO] Initializing SQS connection")
client.sqsconn = sqs.New(awsConfig)

log.Println("[INFO] Initializing SNS connection")
client.snsconn = sns.New(awsConfig)

log.Println("[INFO] Initializing RDS Connection")
client.rdsconn = rds.New(awsConfig)

Expand Down
2 changes: 2 additions & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ func Provider() terraform.ResourceProvider {
"aws_security_group": resourceAwsSecurityGroup(),
"aws_security_group_rule": resourceAwsSecurityGroupRule(),
"aws_sqs_queue": resourceAwsSqsQueue(),
"aws_sns_topic": resourceAwsSnsTopic(),
"aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(),
"aws_subnet": resourceAwsSubnet(),
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),
"aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(),
Expand Down
153 changes: 153 additions & 0 deletions builtin/providers/aws/resource_aws_sns_topic.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK id is being exported by default, therefore this seems to be a bit superfluous, or am I wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which part? Exporting ARN?

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
}
181 changes: 181 additions & 0 deletions builtin/providers/aws/resource_aws_sns_topic_subscription.go
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^ - if so, it's being exported to make recipes more readable (i.e topic_arn = ${aws_sns_topic.topic_name.arn} instead of topic_arn = ${aws_sns_topic.topic_name.id}, I'm okay with not exporting it, if it doesn't make sense to do so, but it is deliberate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see your point and I agree, that topic_arn = ${aws_sns_topic.topic_name.arn} probably makes more sense.

I was just thinking that we're allowing user to do it two ways (both id & arn), which may be a bit confusing for newcomers.

I'm leaving this up to @catsby to decide.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, I'm fine with the id and arn being separate field, but the same value.
I'd like to use arn going forward as much as possible, but some resources don't have that notion (that I can see) :/

I agree the mis-match of id and arn in some places is confusing, but I think it will settle as we use ARN more


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
}
Loading