diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 9e0f928a42..f287ca681f 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -85,6 +85,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), + "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_db_instance": resourceAwsDbInstance(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), diff --git a/builtin/providers/aws/resource_aws_autoscaling_notification.go b/builtin/providers/aws/resource_aws_autoscaling_notification.go new file mode 100644 index 0000000000..1eedeef75c --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_notification.go @@ -0,0 +1,224 @@ +package aws + +import ( + "fmt" + "sort" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsAutoscalingNotification() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoscalingNotificationCreate, + Read: resourceAwsAutoscalingNotificationRead, + Update: resourceAwsAutoscalingNotificationUpdate, + Delete: resourceAwsAutoscalingNotificationDelete, + + Schema: map[string]*schema.Schema{ + "topic_arn": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "group_names": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "notifications": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceAwsAutoscalingNotificationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + gl := d.Get("group_names").(*schema.Set).List() + var groups []interface{} + for _, g := range gl { + groups = append(groups, g) + } + + nl := getNofiticationList(d.Get("notifications").([]interface{})) + + topic := d.Get("topic_arn").(string) + if err := addNotificationConfigToGroupsWithTopic(conn, groups, nl, topic); err != nil { + return err + } + + // ARNs are unique, and these notifications are per ARN, so we re-use the ARN + // here as the ID + d.SetId(topic) + return resourceAwsAutoscalingNotificationRead(d, meta) +} + +func resourceAwsAutoscalingNotificationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + gl := d.Get("group_names").(*schema.Set).List() + var groups []*string + for _, g := range gl { + groups = append(groups, aws.String(g.(string))) + } + + opts := &autoscaling.DescribeNotificationConfigurationsInput{ + AutoScalingGroupNames: groups, + } + + resp, err := conn.DescribeNotificationConfigurations(opts) + if err != nil { + return fmt.Errorf("Error describing notifications") + } + + // grab all applicable notifcation configurations for this Topic + gRaw := make(map[string]bool) + nRaw := make(map[string]bool) + topic := d.Get("topic_arn").(string) + for _, n := range resp.NotificationConfigurations { + if *n.TopicARN == topic { + gRaw[*n.AutoScalingGroupName] = true + nRaw[*n.NotificationType] = true + } + } + + var gList []string + for k, _ := range gRaw { + gList = append(gList, k) + } + var nList []string + for k, _ := range nRaw { + nList = append(nList, k) + } + + sort.Strings(gList) + sort.Strings(nList) + + if err := d.Set("group_names", gList); err != nil { + return err + } + if err := d.Set("notifications", nList); err != nil { + return err + } + + return nil +} + +func resourceAwsAutoscalingNotificationUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + + nl := getNofiticationList(d.Get("notifications").([]interface{})) + + o, n := d.GetChange("group_names") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := os.Difference(ns).List() + add := ns.Difference(os).List() + + topic := d.Get("topic_arn").(string) + + if err := removeNotificationConfigToGroupsWithTopic(conn, remove, topic); err != nil { + return err + } + + var update []interface{} + if d.HasChange("notifications") { + for _, g := range d.Get("group_names").(*schema.Set).List() { + update = append(update, g) + } + } else { + update = add + } + + if err := addNotificationConfigToGroupsWithTopic(conn, update, nl, topic); err != nil { + return err + } + + return resourceAwsAutoscalingNotificationRead(d, meta) +} + +func addNotificationConfigToGroupsWithTopic(conn *autoscaling.AutoScaling, groups []interface{}, nl []*string, topic string) error { + for _, a := range groups { + opts := &autoscaling.PutNotificationConfigurationInput{ + AutoScalingGroupName: aws.String(a.(string)), + NotificationTypes: nl, + TopicARN: aws.String(topic), + } + + _, err := conn.PutNotificationConfiguration(opts) + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + return fmt.Errorf("[WARN] Error creating Autoscaling Group Notification for Group %s, error: \"%s\", code: \"%s\"", a.(string), awsErr.Message(), awsErr.Code()) + } + return err + } + } + return nil +} + +func removeNotificationConfigToGroupsWithTopic(conn *autoscaling.AutoScaling, groups []interface{}, topic string) error { + for _, r := range groups { + opts := &autoscaling.DeleteNotificationConfigurationInput{ + AutoScalingGroupName: aws.String(r.(string)), + TopicARN: aws.String(topic), + } + + _, err := conn.DeleteNotificationConfiguration(opts) + if err != nil { + return fmt.Errorf("[WARN] Error deleting notification configuration for ASG \"%s\", Topic ARN \"%s\"", r.(string), topic) + } + } + return nil +} + +func resourceAwsAutoscalingNotificationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).autoscalingconn + gl := d.Get("group_names").(*schema.Set).List() + var groups []interface{} + for _, g := range gl { + groups = append(groups, g) + } + + topic := d.Get("topic_arn").(string) + if err := removeNotificationConfigToGroupsWithTopic(conn, groups, topic); err != nil { + return err + } + + return nil +} + +func buildNotificationTypesSlice(l []string) (nl []*string) { + for _, n := range l { + if !strings.HasPrefix(n, "autoscaling:") { + nl = append(nl, aws.String("autoscaling:"+n)) + } else { + nl = append(nl, aws.String(n)) + } + } + return nl +} + +func getNofiticationList(l []interface{}) (nl []*string) { + var notifications []string + for _, n := range l { + notifications = append(notifications, n.(string)) + } + + return buildNotificationTypesSlice(notifications) +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_notification_test.go b/builtin/providers/aws/resource_aws_autoscaling_notification_test.go new file mode 100644 index 0000000000..4b2ceed56e --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_notification_test.go @@ -0,0 +1,256 @@ +package aws + +import ( + "fmt" + "strconv" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestBuildNotificationSlice(t *testing.T) { + a := "autoscaling:one" + b := "autoscaling:two" + + cases := []struct { + Input []string + Output []*string + }{ + {[]string{"one", "two"}, []*string{&a, &b}}, + {[]string{"autoscaling:one", "two"}, []*string{&a, &b}}, + } + + for _, tc := range cases { + actual := buildNotificationTypesSlice(tc.Input) + for i, a := range actual { + if *tc.Output[i] != *a { + t.Fatalf("bad converstion:\n\tinput: %s\n\toutput: %s", *tc.Output[i], *a) + } + } + } +} + +func TestAccASGNotification_basic(t *testing.T) { + var asgn autoscaling.DescribeNotificationConfigurationsOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckASGNDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccASGNotificationConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckASGNotificationExists("aws_autoscaling_notification.example", &asgn), + testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn), + ), + }, + }, + }) +} + +func TestAccASGNotification_update(t *testing.T) { + var asgn autoscaling.DescribeNotificationConfigurationsOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckASGNDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccASGNotificationConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckASGNotificationExists("aws_autoscaling_notification.example", &asgn), + testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn), + ), + }, + + resource.TestStep{ + Config: testAccASGNotificationConfig_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckASGNotificationExists("aws_autoscaling_notification.example", &asgn), + testAccCheckAWSASGNotificationAttributes("aws_autoscaling_notification.example", &asgn), + ), + }, + }, + }) +} + +func testAccCheckASGNotificationExists(n string, asgn *autoscaling.DescribeNotificationConfigurationsOutput) 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 ASG Notification ID is set") + } + + // var groups []*string + // groupCount, _ := strconv.Atoi(rs.Primary.Attributes["group_names.#"]) + // for i := 0; i < groupCount; i++ { + // key := fmt.Sprintf("group_names.%d", i) + // groups = append(groups, aws.String(rs.Primary.Attributes[key])) + // } + groups := []*string{aws.String("foobar1-terraform-test")} + + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + opts := &autoscaling.DescribeNotificationConfigurationsInput{ + AutoScalingGroupNames: groups, + } + + resp, err := conn.DescribeNotificationConfigurations(opts) + if err != nil { + return fmt.Errorf("Error describing notifications") + } + + *asgn = *resp + + return nil + } +} + +func testAccCheckASGNDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscaling_notification" { + continue + } + + groups := []*string{aws.String("foobar1-terraform-test")} + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + opts := &autoscaling.DescribeNotificationConfigurationsInput{ + AutoScalingGroupNames: groups, + } + + resp, err := conn.DescribeNotificationConfigurations(opts) + if err != nil { + return fmt.Errorf("Error describing notifications") + } + + if len(resp.NotificationConfigurations) != 0 { + fmt.Errorf("Error finding notification descriptions") + } + + } + return nil +} + +func testAccCheckAWSASGNotificationAttributes(n string, asgn *autoscaling.DescribeNotificationConfigurationsOutput) 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 ASG Notification ID is set") + } + + if len(asgn.NotificationConfigurations) == 0 { + return fmt.Errorf("Error: no ASG Notifications found") + } + + var notifications []*autoscaling.NotificationConfiguration + for _, n := range asgn.NotificationConfigurations { + if *n.TopicARN == rs.Primary.Attributes["topic_arn"] { + notifications = append(notifications, n) + } + } + + typeCount, _ := strconv.Atoi(rs.Primary.Attributes["notifications.#"]) + + if len(notifications) != typeCount { + return fmt.Errorf("Error: Bad ASG Notification count, expected (%d), got (%d)", typeCount, len(notifications)) + } + + return nil + } +} + +const testAccASGNotificationConfig_basic = ` +resource "aws_sns_topic" "topic_example" { + name = "user-updates-topic" +} + +resource "aws_launch_configuration" "foobar" { + name = "foobarautoscaling-terraform-test" + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-west-2a"] + name = "foobar1-terraform-test" + max_size = 1 + min_size = 1 + health_check_grace_period = 100 + health_check_type = "ELB" + desired_capacity = 1 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" +} + +resource "aws_autoscaling_notification" "example" { + group_names = ["${aws_autoscaling_group.bar.name}"] + notifications = [ + "autoscaling:EC2_INSTANCE_LAUNCH", + "autoscaling:EC2_INSTANCE_TERMINATE", + ] + topic_arn = "${aws_sns_topic.topic_example.arn}" +} +` + +const testAccASGNotificationConfig_update = ` +resource "aws_sns_topic" "user_updates" { + name = "user-updates-topic" +} + +resource "aws_launch_configuration" "foobar" { + name = "foobarautoscaling-terraform-test" + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-west-2a"] + name = "foobar1-terraform-test" + max_size = 1 + min_size = 1 + health_check_grace_period = 100 + health_check_type = "ELB" + desired_capacity = 1 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" +} + +resource "aws_autoscaling_group" "foo" { + availability_zones = ["us-west-2b"] + name = "barfoo-terraform-test" + max_size = 1 + min_size = 1 + health_check_grace_period = 200 + health_check_type = "ELB" + desired_capacity = 1 + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" +} + +resource "aws_autoscaling_notification" "example" { + group_names = [ + "${aws_autoscaling_group.bar.name}", + "${aws_autoscaling_group.foo.name}", + ] + notifications = [ + "EC2_INSTANCE_LAUNCH", + "EC2_INSTANCE_TERMINATE", + "EC2_INSTANCE_LAUNCH_ERROR" + ] + topic_arn = "${aws_sns_topic.user_updates.arn}" +}`