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

Add support for SNS topics #1952

Closed
wants to merge 4 commits into from
Closed
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 @@ -17,6 +17,7 @@ import (
"github.com/awslabs/aws-sdk-go/service/rds"
"github.com/awslabs/aws-sdk-go/service/route53"
"github.com/awslabs/aws-sdk-go/service/s3"
"github.com/awslabs/aws-sdk-go/service/sns"
)

type Config struct {
Expand All @@ -40,6 +41,7 @@ type AWSClient struct {
rdsconn *rds.RDS
iamconn *iam.IAM
elasticacheconn *elasticache.ElastiCache
snsconn *sns.SNS
}

// Client configures and returns a fully initailized AWSClient
Expand Down Expand Up @@ -113,6 +115,9 @@ func (c *Config) Client() (interface{}, error) {

log.Println("[INFO] Initializing Elasticache Connection")
client.elasticacheconn = elasticache.New(awsConfig)

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

if len(errs) > 0 {
Expand Down
1 change: 1 addition & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func Provider() terraform.ResourceProvider {
"aws_route_table_association": resourceAwsRouteTableAssociation(),
"aws_route_table": resourceAwsRouteTable(),
"aws_s3_bucket": resourceAwsS3Bucket(),
"aws_sns_topic": resourceAwsSnsTopic(),
"aws_security_group": resourceAwsSecurityGroup(),
"aws_security_group_rule": resourceAwsSecurityGroupRule(),
"aws_subnet": resourceAwsSubnet(),
Expand Down
92 changes: 92 additions & 0 deletions builtin/providers/aws/resource_aws_sns_topic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package aws

import (
"fmt"
"log"
"strings"

"github.com/awslabs/aws-sdk-go/aws"
"github.com/awslabs/aws-sdk-go/service/sns"

"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsSnsTopic() *schema.Resource {
return &schema.Resource{
// Topic updates are idempotent.
Create: resourceAwsSnsTopicCreate,
Update: resourceAwsSnsTopicCreate,

Read: resourceAwsSnsTopicRead,
Delete: resourceAwsSnsTopicDelete,

Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceAwsSnsTopicCreate(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn

createOpts := &sns.CreateTopicInput{
Name: aws.String(d.Get("name").(string)),
}

log.Printf("[DEBUG] Creating SNS topic")
resp, err := snsconn.CreateTopic(createOpts)
if err != nil {
return fmt.Errorf("Error creating SNS topic: %s", err)
}

// Store the ID, in this case the ARN.
topicArn := resp.TopicARN
d.SetId(*topicArn)
log.Printf("[INFO] SNS topic ID: %s", *topicArn)

return resourceAwsSnsTopicRead(d, meta)
}

func resourceAwsSnsTopicRead(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn

match, err := seekSnsTopic(d.Id(), snsconn)
if err != nil {
return err
}

if match == "" {
d.SetId("")
} else {
d.Set("arn", match)
d.Set("name", parseSnsTopicArn(match))
}

return nil
}

func resourceAwsSnsTopicDelete(d *schema.ResourceData, meta interface{}) error {
snsconn := meta.(*AWSClient).snsconn

_, err := snsconn.DeleteTopic(&sns.DeleteTopicInput{
TopicARN: aws.String(d.Id()),
})
if err != nil {
return fmt.Errorf("Error deleting SNS topic: %#v", err)
}
return nil
}

// parseSnsTopicArn extracts the topic's name from its amazon resource number.
func parseSnsTopicArn(arn string) string {
parts := strings.Split(arn, ":")
return parts[len(parts)-1]
}
101 changes: 101 additions & 0 deletions builtin/providers/aws/resource_aws_sns_topic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package aws

import (
"fmt"
"testing"

"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccSnsTopic(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccAwsSnsTopicDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAwsSnsTopicConfig,
Check: resource.ComposeTestCheckFunc(
testAccAwsSnsTopic(
"aws_sns_topic.foo",
),
),
},
resource.TestStep{
Config: testAccAwsSnsTopicConfigUpdate,
Check: resource.ComposeTestCheckFunc(
testAccAwsSnsTopic(
"aws_sns_topic.foo",
),
),
},
},
})
}

func testAccAwsSnsTopicDestroy(s *terraform.State) error {
if len(s.RootModule().Resources) > 0 {
return fmt.Errorf("Expected all resources to be gone, but found: %#v", s.RootModule().Resources)
}

return nil
}

func testAccAwsSnsTopic(snsTopicResource string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[snsTopicResource]
if !ok {
return fmt.Errorf("Not found: %s", snsTopicResource)
}

if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}
topic, ok := s.RootModule().Resources[snsTopicResource]
if !ok {
return fmt.Errorf("Not found: %s", snsTopicResource)
}

snsconn := testAccProvider.Meta().(*AWSClient).snsconn

match, err := seekSnsTopic(topic.Primary.ID, snsconn)
if err != nil {
return err
}
if match == "" {
return fmt.Errorf("Not found in AWS: %s", topic)
}

return nil
}
}

const testAccAwsSnsTopicConfig = `
resource "aws_sns_topic" "foo" {
name = "foo"
}
`

// Change the name but leave the resource name the same.
const testAccAwsSnsTopicConfigUpdate = `
resource "aws_sns_topic" "foo" {
name = "bar"
}
`

func Test_parseSnsTopicArn(t *testing.T) {
for _, ts := range []struct {
arn string
wanted string
}{
{"arn:aws:sns:us-east-1:123456789012:foo", "foo"},
{"arn:aws:sns:us-west-1:123456789012:bar", "bar"},
{"arn:aws:sns:us-east-1:123456789012:baz", "baz"},
} {
got := parseSnsTopicArn(ts.arn)
if got != ts.wanted {
t.Fatalf("got %s; wanted %s", got, ts.wanted)
}
}
}
104 changes: 104 additions & 0 deletions builtin/providers/aws/sns_topic_seeker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package aws

import (
"github.com/awslabs/aws-sdk-go/service/sns"
)

// seekSnsTopic starts a topic seeker and reads out the results, looking for
// a particular ARN.
func seekSnsTopic(soughtArn string, snsconn snsTopicLister) (string, error) {
s := &snsTopicSeeker{
lister: snsconn,
arns: make(chan string),
errc: make(chan error, 1),
}

// launch the seeker
go s.run()

for arn := range s.arns {
if arn == soughtArn {
return arn, nil
}
}
if err := <-s.errc; err != nil {
return "", err
}

// We never found the ARN.
return "", nil
}

// snsTopicLister implements ListTopics. It exists so we can mock out an SNS
// connection for the seeker in testing.
type snsTopicLister interface {
ListTopics(*sns.ListTopicsInput) (*sns.ListTopicsOutput, error)
}

// seekerStateFn represents the state of the pager as a function that returns
// the next state.
type snsTopicSeekerStateFn func(*snsTopicSeeker) snsTopicSeekerStateFn

// snsTopicSeeker holds the state of our SNS API scanner.
type snsTopicSeeker struct {
lister snsTopicLister // an SNS connection or mock
token *string // the token for the list topics request
respList []*sns.Topic // the list of topics in the AWS response
state snsTopicSeekerStateFn // the next state function
arns chan string // channel of topic ARNs
errc chan error // buffered error channel
}

// run the seeker
func (s *snsTopicSeeker) run() {
for s.state = listTopics; s.state != nil; {
s.state = s.state(s)
}
close(s.arns)
close(s.errc)
}

// emit a topic's ARN onto the arns channel
func (s *snsTopicSeeker) emit(topic *sns.Topic) {
s.arns <- *topic.TopicARN
}

// errorf sends an error on the error channel and returns nil, stopping the
// seeker.
func (s *snsTopicSeeker) errorf(err error) snsTopicSeekerStateFn {
s.errc <- err
return nil
}

// listTopics calls AWS for topics
func listTopics(s *snsTopicSeeker) snsTopicSeekerStateFn {
resp, err := s.lister.ListTopics(&sns.ListTopicsInput{
NextToken: s.token,
})
switch {
case err != nil:
return s.errorf(err)
case len(resp.Topics) == 0:
// We've no topics in SNS at all.
return nil
default:
s.respList = resp.Topics
s.token = resp.NextToken
return yieldTopic
}
}

// yieldTopics shifts the seeker's topic list and emits the first item.
func yieldTopic(s *snsTopicSeeker) snsTopicSeekerStateFn {
topic, remaining := s.respList[0], s.respList[1:]
s.emit(topic)
s.respList = remaining
switch {
case len(s.respList) > 0:
return yieldTopic
case s.token != nil:
return listTopics
default:
return nil
}
}
Loading