diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 655c88fc457f..7b20d8395110 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -326,6 +326,7 @@ func Provider() terraform.ResourceProvider { "aws_spot_fleet_request": resourceAwsSpotFleetRequest(), "aws_sqs_queue": resourceAwsSqsQueue(), "aws_sns_topic": resourceAwsSnsTopic(), + "aws_sns_topic_policy": resourceAwsSnsTopicPolicy(), "aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(), "aws_subnet": resourceAwsSubnet(), "aws_volume_attachment": resourceAwsVolumeAttachment(), diff --git a/builtin/providers/aws/resource_aws_sns_topic_policy.go b/builtin/providers/aws/resource_aws_sns_topic_policy.go new file mode 100644 index 000000000000..d7bdaa6d8f51 --- /dev/null +++ b/builtin/providers/aws/resource_aws_sns_topic_policy.go @@ -0,0 +1,177 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/sns" +) + +func resourceAwsSnsTopicPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSnsTopicPolicyUpsert, + Read: resourceAwsSnsTopicPolicyRead, + Update: resourceAwsSnsTopicPolicyUpsert, + Delete: resourceAwsSnsTopicPolicyDelete, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "policy": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, + }, + }, + } +} + +func resourceAwsSnsTopicPolicyUpsert(d *schema.ResourceData, meta interface{}) error { + arn := d.Get("arn").(string) + req := sns.SetTopicAttributesInput{ + TopicArn: aws.String(arn), + AttributeName: aws.String("Policy"), + AttributeValue: aws.String(d.Get("policy").(string)), + } + + d.SetId(arn) + + // Retry the update in the event of an eventually consistent style of + // error, where say an IAM resource is successfully created but not + // actually available. See https://github.com/hashicorp/terraform/issues/3660 + log.Printf("[DEBUG] Updating SNS Topic Policy: %s", req) + stateConf := &resource.StateChangeConf{ + Pending: []string{"retrying"}, + Target: []string{"success"}, + Refresh: resourceAwsSNSUpdateRefreshFunc(meta, req), + Timeout: 3 * time.Minute, + MinTimeout: 3 * time.Second, + } + _, err := stateConf.WaitForState() + if err != nil { + return err + } + + return resourceAwsSnsTopicPolicyRead(d, meta) +} + +func resourceAwsSnsTopicPolicyRead(d *schema.ResourceData, meta interface{}) error { + snsconn := meta.(*AWSClient).snsconn + + attributeOutput, err := snsconn.GetTopicAttributes(&sns.GetTopicAttributesInput{ + TopicArn: aws.String(d.Id()), + }) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NotFound" { + log.Printf("[WARN] SNS Topic (%s) not found, error code (404)", d.Id()) + d.SetId("") + return nil + } + + return err + } + + if attributeOutput.Attributes == nil { + log.Printf("[WARN] SNS Topic (%q) attributes not found (nil)", d.Id()) + d.SetId("") + return nil + } + attrmap := attributeOutput.Attributes + + policy, ok := attrmap["Policy"] + if !ok { + log.Printf("[WARN] SNS Topic (%q) policy not found in attributes", d.Id()) + d.SetId("") + return nil + } + + d.Set("policy", policy) + + return nil +} + +func resourceAwsSnsTopicPolicyDelete(d *schema.ResourceData, meta interface{}) error { + accountId, err := getAccountIdFromSnsTopicArn(d.Id()) + if err != nil { + return err + } + + req := sns.SetTopicAttributesInput{ + TopicArn: aws.String(d.Id()), + AttributeName: aws.String("Policy"), + // It is impossible to delete a policy or set to empty + // (confirmed by AWS Support representative) + // so we instead set it back to the default one + AttributeValue: aws.String(buildDefaultSnsTopicPolicy(d.Id(), accountId)), + } + + // Retry the update in the event of an eventually consistent style of + // error, where say an IAM resource is successfully created but not + // actually available. See https://github.com/hashicorp/terraform/issues/3660 + log.Printf("[DEBUG] Resetting SNS Topic Policy to default: %s", req) + stateConf := &resource.StateChangeConf{ + Pending: []string{"retrying"}, + Target: []string{"success"}, + Refresh: resourceAwsSNSUpdateRefreshFunc(meta, req), + Timeout: 3 * time.Minute, + MinTimeout: 3 * time.Second, + } + _, err = stateConf.WaitForState() + if err != nil { + return err + } + return nil +} + +func getAccountIdFromSnsTopicArn(arn string) (string, error) { + // arn:aws:sns:us-west-2:123456789012:test-new + re := regexp.MustCompile("^arn:aws:sns:[^:]+:([0-9]{12}):.+") + matches := re.FindStringSubmatch(arn) + if len(matches) != 2 { + return "", fmt.Errorf("Unable to get account ID from ARN (%q)", arn) + } + return matches[1], nil +} + +func buildDefaultSnsTopicPolicy(topicArn, accountId string) string { + return fmt.Sprintf(`{ + "Version": "2008-10-17", + "Id": "__default_policy_ID", + "Statement": [ + { + "Sid": "__default_statement_ID", + "Effect": "Allow", + "Principal": { + "AWS": "*" + }, + "Action": [ + "SNS:GetTopicAttributes", + "SNS:SetTopicAttributes", + "SNS:AddPermission", + "SNS:RemovePermission", + "SNS:DeleteTopic", + "SNS:Subscribe", + "SNS:ListSubscriptionsByTopic", + "SNS:Publish", + "SNS:Receive" + ], + "Resource": "%s", + "Condition": { + "StringEquals": { + "AWS:SourceOwner": "%s" + } + } + } + ] +}`, topicArn, accountId) +} diff --git a/builtin/providers/aws/resource_aws_sns_topic_policy_test.go b/builtin/providers/aws/resource_aws_sns_topic_policy_test.go new file mode 100644 index 000000000000..e911f97c3329 --- /dev/null +++ b/builtin/providers/aws/resource_aws_sns_topic_policy_test.go @@ -0,0 +1,49 @@ +package aws + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAWSSNSTopicPolicy_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSNSTopicDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSSNSTopicConfig_withPolicy, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSNSTopicExists("aws_sns_topic.test"), + resource.TestMatchResourceAttr("aws_sns_topic_policy.custom", "policy", + regexp.MustCompile("^{\"Version\":\"2012-10-17\".+")), + ), + }, + }, + }) +} + +const testAccAWSSNSTopicConfig_withPolicy = ` +resource "aws_sns_topic" "test" { + name = "tf-acc-test-topic-with-policy" +} + +resource "aws_sns_topic_policy" "custom" { + arn = "${aws_sns_topic.test.arn}" + policy = <aws_sns_topic + > + aws_sns_topic_policy + + > aws_sns_topic_subscription