diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d71af970..188df3abc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,10 @@ FEATURES: - * **New provider: `azure`** [GH-2052, GH-2053, GH-2372, GH-2380] + * **New provider: `azure`** [GH-2052, GH-2053, GH-2372, GH-2380, GH-2394] * **New resource: `aws_autoscaling_notification`** [GH-2197] + * **New resource: `aws_autoscaling_policy`** [GH-2201] + * **New resource: `aws_cloudwatch_metric_alarm`** [GH-2201] * **New resource: `aws_dynamodb_table`** [GH-2121] * **New resource: `aws_ecs_cluster`** [GH-1803] * **New resource: `aws_ecs_service`** [GH-1803] @@ -35,6 +37,8 @@ BUG FIXES: * command/apply: prevent output duplication when reporting errors [GH-2267] * provider/aws: fix panic when route has no cidr_block [GH-2215] * provider/aws: fix issue preventing destruction of IAM Roles [GH-2177] + * provider/aws: fix issue where Security Group Rules could collide and fail + to save to the state file correctly [GH-2376] * provider/aws: fix issue preventing destruction self referencing Securtity Group Rules [GH-2305] * provider/aws: fix issue causing perpetual diff on ELB listeners diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 658e0ec91..93f7db919 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" @@ -37,6 +38,7 @@ type Config struct { } type AWSClient struct { + cloudwatchconn *cloudwatch.CloudWatch dynamodbconn *dynamodb.DynamoDB ec2conn *ec2.EC2 ecsconn *ecs.ECS @@ -143,6 +145,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing Lambda Connection") client.lambdaconn = lambda.New(awsConfig) + + log.Println("[INFO] Initializing CloudWatch SDK connection") + client.cloudwatchconn = cloudwatch.New(awsConfig) } if len(errs) > 0 { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index f1f920f52..5d8e38537 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -86,6 +86,8 @@ func Provider() terraform.ResourceProvider { "aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(), "aws_autoscaling_group": resourceAwsAutoscalingGroup(), "aws_autoscaling_notification": resourceAwsAutoscalingNotification(), + "aws_autoscaling_policy": resourceAwsAutoscalingPolicy(), + "aws_cloudwatch_metric_alarm": resourceAwsCloudWatchMetricAlarm(), "aws_customer_gateway": resourceAwsCustomerGateway(), "aws_db_instance": resourceAwsDbInstance(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), diff --git a/builtin/providers/aws/resource_aws_autoscaling_group.go b/builtin/providers/aws/resource_aws_autoscaling_group.go index 0fc62b4b2..bf9b30c6a 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_group.go +++ b/builtin/providers/aws/resource_aws_autoscaling_group.go @@ -408,7 +408,7 @@ func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error { } wantELB := d.Get("min_elb_capacity").(int) - log.Printf("[DEBUG] Wanting for capacity: %d ASG, %d ELB", wantASG, wantELB) + log.Printf("[DEBUG] Waiting for capacity: %d ASG, %d ELB", wantASG, wantELB) return resource.Retry(waitForASGCapacityTimeout, func() error { g, err := getAwsAutoscalingGroup(d, meta) diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy.go b/builtin/providers/aws/resource_aws_autoscaling_policy.go new file mode 100644 index 000000000..d2f6d2d47 --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_policy.go @@ -0,0 +1,181 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsAutoscalingPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAutoscalingPolicyCreate, + Read: resourceAwsAutoscalingPolicyRead, + Update: resourceAwsAutoscalingPolicyUpdate, + Delete: resourceAwsAutoscalingPolicyDelete, + + Schema: map[string]*schema.Schema{ + "arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "adjustment_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "autoscaling_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cooldown": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "min_adjustment_step": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "scaling_adjustment": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + } +} + +func resourceAwsAutoscalingPolicyCreate(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := getAwsAutoscalingPutScalingPolicyInput(d) + + log.Printf("[DEBUG] AutoScaling PutScalingPolicy: %#v", params) + resp, err := autoscalingconn.PutScalingPolicy(¶ms) + if err != nil { + return fmt.Errorf("Error putting scaling policy: %s", err) + } + + d.Set("arn", resp.PolicyARN) + d.SetId(d.Get("name").(string)) + log.Printf("[INFO] AutoScaling Scaling PolicyARN: %s", d.Get("arn").(string)) + + return resourceAwsAutoscalingPolicyRead(d, meta) +} + +func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) error { + p, err := getAwsAutoscalingPolicy(d, meta) + if err != nil { + return err + } + if p == nil { + d.SetId("") + return nil + } + + log.Printf("[DEBUG] Read Scaling Policy: ASG: %s, SP: %s, Obj: %#v", d.Get("autoscaling_group_name"), d.Get("name"), p) + + d.Set("adjustment_type", p.AdjustmentType) + d.Set("autoscaling_group_name", p.AutoScalingGroupName) + d.Set("cooldown", p.Cooldown) + d.Set("min_adjustment_step", p.MinAdjustmentStep) + d.Set("arn", p.PolicyARN) + d.Set("name", p.PolicyName) + d.Set("scaling_adjustment", p.ScalingAdjustment) + + return nil +} + +func resourceAwsAutoscalingPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := getAwsAutoscalingPutScalingPolicyInput(d) + + log.Printf("[DEBUG] Autoscaling Update Scaling Policy: %#v", params) + _, err := autoscalingconn.PutScalingPolicy(¶ms) + if err != nil { + return err + } + + return resourceAwsAutoscalingPolicyRead(d, meta) +} + +func resourceAwsAutoscalingPolicyDelete(d *schema.ResourceData, meta interface{}) error { + autoscalingconn := meta.(*AWSClient).autoscalingconn + p, err := getAwsAutoscalingPolicy(d, meta) + if err != nil { + return err + } + if p == nil { + return nil + } + + params := autoscaling.DeletePolicyInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyName: aws.String(d.Get("name").(string)), + } + if _, err := autoscalingconn.DeletePolicy(¶ms); err != nil { + return fmt.Errorf("Autoscaling Scaling Policy: %s ", err) + } + + d.SetId("") + return nil +} + +// PutScalingPolicy seems to require all params to be resent, so create and update can share this common function +func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) autoscaling.PutScalingPolicyInput { + var params = autoscaling.PutScalingPolicyInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyName: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("adjustment_type"); ok { + params.AdjustmentType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("cooldown"); ok { + params.Cooldown = aws.Long(int64(v.(int))) + } + + if v, ok := d.GetOk("scaling_adjustment"); ok { + params.ScalingAdjustment = aws.Long(int64(v.(int))) + } + + if v, ok := d.GetOk("min_adjustment_step"); ok { + params.MinAdjustmentStep = aws.Long(int64(v.(int))) + } + + return params +} + +func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autoscaling.ScalingPolicy, error) { + autoscalingconn := meta.(*AWSClient).autoscalingconn + + params := autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), + PolicyNames: []*string{aws.String(d.Get("name").(string))}, + } + + log.Printf("[DEBUG] AutoScaling Scaling Policy Describe Params: %#v", params) + resp, err := autoscalingconn.DescribePolicies(¶ms) + if err != nil { + return nil, fmt.Errorf("Error retrieving scaling policies: %s", err) + } + + // find scaling policy + name := d.Get("name") + for idx, sp := range resp.ScalingPolicies { + if *sp.PolicyName == name { + return resp.ScalingPolicies[idx], nil + } + } + + // policy not found + return nil, nil +} diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy_test.go b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go new file mode 100644 index 000000000..0a7aeff91 --- /dev/null +++ b/builtin/providers/aws/resource_aws_autoscaling_policy_test.go @@ -0,0 +1,115 @@ +package aws + +import ( + "fmt" + "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 TestAccAWSAutoscalingPolicy_basic(t *testing.T) { + var policy autoscaling.ScalingPolicy + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAutoscalingPolicyDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSAutoscalingPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckScalingPolicyExists("aws_autoscaling_policy.foobar", &policy), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "adjustment_type", "ChangeInCapacity"), + resource.TestCheckResourceAttr("aws_autoscaling_policy.foobar", "cooldown", "300"), + ), + }, + }, + }) +} + +func testAccCheckScalingPolicyExists(n string, policy *autoscaling.ScalingPolicy) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + rs = rs + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + params := &autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]), + PolicyNames: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribePolicies(params) + if err != nil { + return err + } + if len(resp.ScalingPolicies) == 0 { + return fmt.Errorf("ScalingPolicy not found") + } + + return nil + } +} + +func testAccCheckAWSAutoscalingPolicyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).autoscalingconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_autoscaling_group" { + continue + } + + params := autoscaling.DescribePoliciesInput{ + AutoScalingGroupName: aws.String(rs.Primary.Attributes["autoscaling_group_name"]), + PolicyNames: []*string{aws.String(rs.Primary.ID)}, + } + + resp, err := conn.DescribePolicies(¶ms) + + if err == nil { + if len(resp.ScalingPolicies) != 0 && + *resp.ScalingPolicies[0].PolicyName == rs.Primary.ID { + return fmt.Errorf("Scaling Policy Still Exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +var testAccAWSAutoscalingPolicyConfig = fmt.Sprintf(` +resource "aws_launch_configuration" "foobar" { + name = "terraform-test-foobar5" + image_id = "ami-21f78e11" + instance_type = "t1.micro" +} + +resource "aws_autoscaling_group" "foobar" { + availability_zones = ["us-west-2a"] + name = "terraform-test-foobar5" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + force_delete = true + termination_policies = ["OldestInstance"] + launch_configuration = "${aws_launch_configuration.foobar.name}" + tag { + key = "Foo" + value = "foo-bar" + propagate_at_launch = true + } +} + +resource "aws_autoscaling_policy" "foobar" { + name = "foobar" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.foobar.name}" +} +`) diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go new file mode 100644 index 000000000..4c8b40140 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm.go @@ -0,0 +1,288 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" +) + +func resourceAwsCloudWatchMetricAlarm() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsCloudWatchMetricAlarmCreate, + Read: resourceAwsCloudWatchMetricAlarmRead, + Update: resourceAwsCloudWatchMetricAlarmUpdate, + Delete: resourceAwsCloudWatchMetricAlarmDelete, + + Schema: map[string]*schema.Schema{ + "alarm_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "comparison_operator": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "evaluation_periods": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "metric_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "namespace": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "period": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + "statistic": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "threshold": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + "actions_enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "alarm_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "alarm_description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "dimensions": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + }, + "insufficient_data_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "ok_actions": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "unit": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceAwsCloudWatchMetricAlarmCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchconn + + params := getAwsCloudWatchPutMetricAlarmInput(d) + + log.Printf("[DEBUG] Creating CloudWatch Metric Alarm: %#v", params) + _, err := conn.PutMetricAlarm(¶ms) + if err != nil { + return fmt.Errorf("Creating metric alarm failed: %s", err) + } + d.SetId(d.Get("alarm_name").(string)) + log.Println("[INFO] CloudWatch Metric Alarm created") + + return resourceAwsCloudWatchMetricAlarmRead(d, meta) +} + +func resourceAwsCloudWatchMetricAlarmRead(d *schema.ResourceData, meta interface{}) error { + a, err := getAwsCloudWatchMetricAlarm(d, meta) + if err != nil { + return err + } + if a == nil { + d.SetId("") + return nil + } + + log.Printf("[DEBUG] Reading CloudWatch Metric Alarm: %s", d.Get("alarm_name")) + + d.Set("actions_enabled", a.ActionsEnabled) + + if err := d.Set("alarm_actions", _strArrPtrToList(a.AlarmActions)); err != nil { + log.Printf("[WARN] Error setting Alarm Actions: %s", err) + } + d.Set("alarm_description", a.AlarmDescription) + d.Set("alarm_name", a.AlarmName) + d.Set("comparison_operator", a.ComparisonOperator) + d.Set("dimensions", a.Dimensions) + d.Set("evaluation_periods", a.EvaluationPeriods) + + if err := d.Set("insufficient_data_actions", _strArrPtrToList(a.InsufficientDataActions)); err != nil { + log.Printf("[WARN] Error setting Insufficient Data Actions: %s", err) + } + d.Set("metric_name", a.MetricName) + d.Set("namespace", a.Namespace) + + if err := d.Set("ok_actions", _strArrPtrToList(a.OKActions)); err != nil { + log.Printf("[WARN] Error setting OK Actions: %s", err) + } + d.Set("period", a.Period) + d.Set("statistic", a.Statistic) + d.Set("threshold", a.Threshold) + d.Set("unit", a.Unit) + + return nil +} + +func resourceAwsCloudWatchMetricAlarmUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).cloudwatchconn + params := getAwsCloudWatchPutMetricAlarmInput(d) + + log.Printf("[DEBUG] Updating CloudWatch Metric Alarm: %#v", params) + _, err := conn.PutMetricAlarm(¶ms) + if err != nil { + return fmt.Errorf("Updating metric alarm failed: %s", err) + } + log.Println("[INFO] CloudWatch Metric Alarm updated") + + return resourceAwsCloudWatchMetricAlarmRead(d, meta) +} + +func resourceAwsCloudWatchMetricAlarmDelete(d *schema.ResourceData, meta interface{}) error { + p, err := getAwsCloudWatchMetricAlarm(d, meta) + if err != nil { + return err + } + if p == nil { + log.Printf("[DEBUG] CloudWatch Metric Alarm %s is already gone", d.Id()) + return nil + } + + log.Printf("[INFO] Deleting CloudWatch Metric Alarm: %s", d.Id()) + + conn := meta.(*AWSClient).cloudwatchconn + params := cloudwatch.DeleteAlarmsInput{ + AlarmNames: []*string{aws.String(d.Id())}, + } + + if _, err := conn.DeleteAlarms(¶ms); err != nil { + return fmt.Errorf("Error deleting CloudWatch Metric Alarm: %s", err) + } + log.Println("[INFO] CloudWatch Metric Alarm deleted") + + d.SetId("") + return nil +} + +func getAwsCloudWatchPutMetricAlarmInput(d *schema.ResourceData) cloudwatch.PutMetricAlarmInput { + params := cloudwatch.PutMetricAlarmInput{ + AlarmName: aws.String(d.Get("alarm_name").(string)), + ComparisonOperator: aws.String(d.Get("comparison_operator").(string)), + EvaluationPeriods: aws.Long(int64(d.Get("evaluation_periods").(int))), + MetricName: aws.String(d.Get("metric_name").(string)), + Namespace: aws.String(d.Get("namespace").(string)), + Period: aws.Long(int64(d.Get("period").(int))), + Statistic: aws.String(d.Get("statistic").(string)), + Threshold: aws.Double(d.Get("threshold").(float64)), + } + + if v := d.Get("actions_enabled"); v != nil { + params.ActionsEnabled = aws.Boolean(v.(bool)) + } + + if v, ok := d.GetOk("alarm_description"); ok { + params.AlarmDescription = aws.String(v.(string)) + } + + if v, ok := d.GetOk("unit"); ok { + params.Unit = aws.String(v.(string)) + } + + var alarmActions []*string + if v := d.Get("alarm_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + alarmActions = append(alarmActions, aws.String(str)) + } + params.AlarmActions = alarmActions + } + + var insufficientDataActions []*string + if v := d.Get("insufficient_data_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + insufficientDataActions = append(insufficientDataActions, aws.String(str)) + } + params.InsufficientDataActions = insufficientDataActions + } + + var okActions []*string + if v := d.Get("ok_actions"); v != nil { + for _, v := range v.(*schema.Set).List() { + str := v.(string) + okActions = append(okActions, aws.String(str)) + } + params.OKActions = okActions + } + + a := d.Get("dimensions").(map[string]interface{}) + dimensions := make([]*cloudwatch.Dimension, 0, len(a)) + for k, v := range a { + dimensions = append(dimensions, &cloudwatch.Dimension{ + Name: aws.String(k), + Value: aws.String(v.(string)), + }) + } + params.Dimensions = dimensions + + return params +} + +func getAwsCloudWatchMetricAlarm(d *schema.ResourceData, meta interface{}) (*cloudwatch.MetricAlarm, error) { + conn := meta.(*AWSClient).cloudwatchconn + + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(d.Id())}, + } + + resp, err := conn.DescribeAlarms(¶ms) + if err != nil { + return nil, nil + } + + // Find it and return it + for idx, ma := range resp.MetricAlarms { + if *ma.AlarmName == d.Id() { + return resp.MetricAlarms[idx], nil + } + } + + return nil, nil +} + +func _strArrPtrToList(strArrPtr []*string) []string { + var result []string + for _, elem := range strArrPtr { + result = append(result, *elem) + } + return result +} diff --git a/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go new file mode 100644 index 000000000..8d9a53c36 --- /dev/null +++ b/builtin/providers/aws/resource_aws_cloudwatch_metric_alarm_test.go @@ -0,0 +1,95 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatch" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSCloudWatchMetricAlarm_basic(t *testing.T) { + var alarm cloudwatch.MetricAlarm + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudWatchMetricAlarmDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudWatchMetricAlarmConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudWatchMetricAlarmExists("aws_cloudwatch_metric_alarm.foobar", &alarm), + resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "metric_name", "CPUUtilization"), + resource.TestCheckResourceAttr("aws_cloudwatch_metric_alarm.foobar", "statistic", "Average"), + ), + }, + }, + }) +} + +func testAccCheckCloudWatchMetricAlarmExists(n string, alarm *cloudwatch.MetricAlarm) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeAlarms(¶ms) + if err != nil { + return err + } + if len(resp.MetricAlarms) == 0 { + return fmt.Errorf("Alarm not found") + } + *alarm = *resp.MetricAlarms[0] + + return nil + } +} + +func testAccCheckAWSCloudWatchMetricAlarmDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudwatchconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudwatch_metric_alarm" { + continue + } + + params := cloudwatch.DescribeAlarmsInput{ + AlarmNames: []*string{aws.String(rs.Primary.ID)}, + } + + resp, err := conn.DescribeAlarms(¶ms) + + if err == nil { + if len(resp.MetricAlarms) != 0 && + *resp.MetricAlarms[0].AlarmName == rs.Primary.ID { + return fmt.Errorf("Alarm Still Exists: %s", rs.Primary.ID) + } + } + } + + return nil +} + +var testAccAWSCloudWatchMetricAlarmConfig = fmt.Sprintf(` +resource "aws_cloudwatch_metric_alarm" "foobar" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + insufficient_data_actions = [] +} +`) diff --git a/builtin/providers/aws/resource_aws_security_group_rule.go b/builtin/providers/aws/resource_aws_security_group_rule.go index 270b41ed2..ce8b20498 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule.go +++ b/builtin/providers/aws/resource_aws_security_group_rule.go @@ -21,6 +21,9 @@ func resourceAwsSecurityGroupRule() *schema.Resource { Read: resourceAwsSecurityGroupRuleRead, Delete: resourceAwsSecurityGroupRuleDelete, + SchemaVersion: 1, + MigrateState: resourceAwsSecurityGroupRuleMigrateState, + Schema: map[string]*schema.Schema{ "type": &schema.Schema{ Type: schema.TypeString, @@ -61,10 +64,11 @@ func resourceAwsSecurityGroupRule() *schema.Resource { }, "source_security_group_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ConflictsWith: []string{"cidr_blocks"}, }, "self": &schema.Schema{ @@ -90,6 +94,7 @@ func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{} ruleType := d.Get("type").(string) + var autherr error switch ruleType { case "ingress": log.Printf("[DEBUG] Authorizing security group %s %s rule: %s", @@ -105,13 +110,7 @@ func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{} req.GroupName = sg.GroupName } - _, err := conn.AuthorizeSecurityGroupIngress(req) - - if err != nil { - return fmt.Errorf( - "Error authorizing security group %s rules: %s", - "rules", err) - } + _, autherr = conn.AuthorizeSecurityGroupIngress(req) case "egress": log.Printf("[DEBUG] Authorizing security group %s %s rule: %#v", @@ -122,18 +121,28 @@ func resourceAwsSecurityGroupRuleCreate(d *schema.ResourceData, meta interface{} IPPermissions: []*ec2.IPPermission{perm}, } - _, err = conn.AuthorizeSecurityGroupEgress(req) - - if err != nil { - return fmt.Errorf( - "Error authorizing security group %s rules: %s", - "rules", err) - } + _, autherr = conn.AuthorizeSecurityGroupEgress(req) default: return fmt.Errorf("Security Group Rule must be type 'ingress' or type 'egress'") } + if autherr != nil { + if awsErr, ok := autherr.(awserr.Error); ok { + if awsErr.Code() == "InvalidPermission.Duplicate" { + return fmt.Errorf(`[WARN] A duplicate Security Group rule was found. This may be +a side effect of a now-fixed Terraform issue causing two security groups with +identical attributes but different source_security_group_ids to overwrite each +other in the state. See https://github.com/hashicorp/terraform/pull/2376 for more +information and instructions for recovery. Error message: %s`, awsErr.Message()) + } + } + + return fmt.Errorf( + "Error authorizing security group rule type %s: %s", + ruleType, autherr) + } + d.SetId(ipPermissionIDHash(ruleType, perm)) return resourceAwsSecurityGroupRuleRead(d, meta) @@ -263,15 +272,32 @@ func findResourceSecurityGroup(conn *ec2.EC2, id string) (*ec2.SecurityGroup, er return resp.SecurityGroups[0], nil } +// ByGroupPair implements sort.Interface for []*ec2.UserIDGroupPairs based on +// GroupID or GroupName field (only one should be set). +type ByGroupPair []*ec2.UserIDGroupPair + +func (b ByGroupPair) Len() int { return len(b) } +func (b ByGroupPair) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b ByGroupPair) Less(i, j int) bool { + if b[i].GroupID != nil && b[j].GroupID != nil { + return *b[i].GroupID < *b[j].GroupID + } + if b[i].GroupName != nil && b[j].GroupName != nil { + return *b[i].GroupName < *b[j].GroupName + } + + panic("mismatched security group rules, may be a terraform bug") +} + func ipPermissionIDHash(ruleType string, ip *ec2.IPPermission) string { var buf bytes.Buffer - // for egress rules, an TCP rule of -1 is automatically added, in which case - // the to and from ports will be nil. We don't record this rule locally. - if ip.IPProtocol != nil && *ip.IPProtocol != "-1" { + if ip.FromPort != nil && *ip.FromPort > 0 { buf.WriteString(fmt.Sprintf("%d-", *ip.FromPort)) - buf.WriteString(fmt.Sprintf("%d-", *ip.ToPort)) - buf.WriteString(fmt.Sprintf("%s-", *ip.IPProtocol)) } + if ip.ToPort != nil && *ip.ToPort > 0 { + buf.WriteString(fmt.Sprintf("%d-", *ip.ToPort)) + } + buf.WriteString(fmt.Sprintf("%s-", *ip.IPProtocol)) buf.WriteString(fmt.Sprintf("%s-", ruleType)) // We need to make sure to sort the strings below so that we always @@ -288,6 +314,22 @@ func ipPermissionIDHash(ruleType string, ip *ec2.IPPermission) string { } } + if len(ip.UserIDGroupPairs) > 0 { + sort.Sort(ByGroupPair(ip.UserIDGroupPairs)) + for _, pair := range ip.UserIDGroupPairs { + if pair.GroupID != nil { + buf.WriteString(fmt.Sprintf("%s-", *pair.GroupID)) + } else { + buf.WriteString("-") + } + if pair.GroupName != nil { + buf.WriteString(fmt.Sprintf("%s-", *pair.GroupName)) + } else { + buf.WriteString("-") + } + } + } + return fmt.Sprintf("sg-%d", hashcode.String(buf.String())) } diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate.go new file mode 100644 index 000000000..30eb8c09f --- /dev/null +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate.go @@ -0,0 +1,101 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/terraform" +) + +func resourceAwsSecurityGroupRuleMigrateState( + v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + switch v { + case 0: + log.Println("[INFO] Found AWS Security Group State v0; migrating to v1") + return migrateSGRuleStateV0toV1(is) + default: + return is, fmt.Errorf("Unexpected schema version: %d", v) + } + + return is, nil +} + +func migrateSGRuleStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { + if is.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return is, nil + } + + perm, err := migrateExpandIPPerm(is.Attributes) + + if err != nil { + return nil, fmt.Errorf("[WARN] Error making new IP Permission in Security Group migration") + } + + log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes) + newID := ipPermissionIDHash(is.Attributes["type"], perm) + is.Attributes["id"] = newID + is.ID = newID + log.Printf("[DEBUG] Attributes after migration: %#v, new id: %s", is.Attributes, newID) + return is, nil +} + +func migrateExpandIPPerm(attrs map[string]string) (*ec2.IPPermission, error) { + var perm ec2.IPPermission + tp, err := strconv.Atoi(attrs["to_port"]) + if err != nil { + return nil, fmt.Errorf("Error converting to_port in Security Group migration") + } + + fp, err := strconv.Atoi(attrs["from_port"]) + if err != nil { + return nil, fmt.Errorf("Error converting from_port in Security Group migration") + } + + perm.ToPort = aws.Long(int64(tp)) + perm.FromPort = aws.Long(int64(fp)) + perm.IPProtocol = aws.String(attrs["protocol"]) + + groups := make(map[string]bool) + if attrs["self"] == "true" { + groups[attrs["security_group_id"]] = true + } + + if attrs["source_security_group_id"] != "" { + groups[attrs["source_security_group_id"]] = true + } + + if len(groups) > 0 { + perm.UserIDGroupPairs = make([]*ec2.UserIDGroupPair, len(groups)) + // build string list of group name/ids + var gl []string + for k, _ := range groups { + gl = append(gl, k) + } + + for i, name := range gl { + perm.UserIDGroupPairs[i] = &ec2.UserIDGroupPair{ + GroupID: aws.String(name), + } + } + } + + var cb []string + for k, v := range attrs { + if k != "cidr_blocks.#" && strings.HasPrefix(k, "cidr_blocks") { + cb = append(cb, v) + } + } + if len(cb) > 0 { + perm.IPRanges = make([]*ec2.IPRange, len(cb)) + for i, v := range cb { + perm.IPRanges[i] = &ec2.IPRange{CIDRIP: aws.String(v)} + } + } + + return &perm, nil +} diff --git a/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go new file mode 100644 index 000000000..664f05039 --- /dev/null +++ b/builtin/providers/aws/resource_aws_security_group_rule_migrate_test.go @@ -0,0 +1,67 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestAWSSecurityGroupRuleMigrateState(t *testing.T) { + cases := map[string]struct { + StateVersion int + ID string + Attributes map[string]string + Expected string + Meta interface{} + }{ + "v0_1": { + StateVersion: 0, + ID: "sg-4235098228", + Attributes: map[string]string{ + "self": "false", + "to_port": "0", + "security_group_id": "sg-13877277", + "cidr_blocks.#": "0", + "type": "ingress", + "protocol": "-1", + "from_port": "0", + "source_security_group_id": "sg-11877275", + }, + Expected: "sg-3766347571", + }, + "v0_2": { + StateVersion: 0, + ID: "sg-1021609891", + Attributes: map[string]string{ + "security_group_id": "sg-0981746d", + "from_port": "0", + "to_port": "0", + "type": "ingress", + "self": "false", + "protocol": "-1", + "cidr_blocks.0": "172.16.1.0/24", + "cidr_blocks.1": "172.16.2.0/24", + "cidr_blocks.2": "172.16.3.0/24", + "cidr_blocks.3": "172.16.4.0/24", + "cidr_blocks.#": "4"}, + Expected: "sg-4100229787", + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: tc.ID, + Attributes: tc.Attributes, + } + is, err := resourceAwsSecurityGroupRuleMigrateState( + tc.StateVersion, is, tc.Meta) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + if is.ID != tc.Expected { + t.Fatalf("bad sg rule id: %s\n\n expected: %s", is.ID, tc.Expected) + } + } +} diff --git a/builtin/providers/aws/resource_aws_security_group_rule_test.go b/builtin/providers/aws/resource_aws_security_group_rule_test.go index f322ba44e..595886e18 100644 --- a/builtin/providers/aws/resource_aws_security_group_rule_test.go +++ b/builtin/providers/aws/resource_aws_security_group_rule_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -44,6 +45,46 @@ func TestIpPermissionIDHash(t *testing.T) { }, } + vpc_security_group_source := &ec2.IPPermission{ + IPProtocol: aws.String("tcp"), + FromPort: aws.Long(int64(80)), + ToPort: aws.Long(int64(8000)), + UserIDGroupPairs: []*ec2.UserIDGroupPair{ + &ec2.UserIDGroupPair{ + UserID: aws.String("987654321"), + GroupID: aws.String("sg-12345678"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupID: aws.String("sg-987654321"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupID: aws.String("sg-12345678"), + }, + }, + } + + security_group_source := &ec2.IPPermission{ + IPProtocol: aws.String("tcp"), + FromPort: aws.Long(int64(80)), + ToPort: aws.Long(int64(8000)), + UserIDGroupPairs: []*ec2.UserIDGroupPair{ + &ec2.UserIDGroupPair{ + UserID: aws.String("987654321"), + GroupName: aws.String("my-security-group"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupName: aws.String("my-security-group"), + }, + &ec2.UserIDGroupPair{ + UserID: aws.String("123456789"), + GroupName: aws.String("my-other-security-group"), + }, + }, + } + // hardcoded hashes, to detect future change cases := []struct { Input *ec2.IPPermission @@ -52,13 +93,15 @@ func TestIpPermissionIDHash(t *testing.T) { }{ {simple, "ingress", "sg-82613597"}, {egress, "egress", "sg-363054720"}, - {egress_all, "egress", "sg-857124156"}, + {egress_all, "egress", "sg-2766285362"}, + {vpc_security_group_source, "egress", "sg-2661404947"}, + {security_group_source, "egress", "sg-1841245863"}, } for _, tc := range cases { actual := ipPermissionIDHash(tc.Type, tc.Input) if actual != tc.Output { - t.Fatalf("input: %s - %#v\noutput: %s", tc.Type, tc.Input, actual) + t.Errorf("input: %s - %s\noutput: %s", tc.Type, awsutil.StringValue(tc.Input), actual) } } } @@ -141,9 +184,9 @@ func TestAccAWSSecurityGroupRule_MultiIngress(t *testing.T) { var group ec2.SecurityGroup testMultiRuleCount := func(*terraform.State) error { - if len(group.IPPermissions) != 3 { + if len(group.IPPermissions) != 2 { return fmt.Errorf("Wrong Security Group rule count, expected %d, got %d", - 3, len(group.IPPermissions)) + 2, len(group.IPPermissions)) } var rule *ec2.IPPermission @@ -395,7 +438,6 @@ resource "aws_security_group_rule" "ingress_1" { cidr_blocks = ["10.0.0.0/8"] security_group_id = "${aws_security_group.web.id}" - source_security_group_id = "${aws_security_group.worker.id}" } resource "aws_security_group_rule" "ingress_2" { diff --git a/builtin/providers/azure/config.go b/builtin/providers/azure/config.go index 52c69d40c..fff31853d 100644 --- a/builtin/providers/azure/config.go +++ b/builtin/providers/azure/config.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/affinitygroup" "github.com/Azure/azure-sdk-for-go/management/hostedservice" "github.com/Azure/azure-sdk-for-go/management/networksecuritygroup" "github.com/Azure/azure-sdk-for-go/management/osimage" @@ -31,13 +32,15 @@ type Config struct { type Client struct { mgmtClient management.Client + affinityGroupClient affinitygroup.AffinityGroupClient + hostedServiceClient hostedservice.HostedServiceClient secGroupClient networksecuritygroup.SecurityGroupClient osImageClient osimage.OSImageClient - sqlClient sql.SqlDatabaseClient + sqlClient sql.SQLDatabaseClient storageServiceClient storageservice.StorageServiceClient @@ -107,6 +110,7 @@ func (c *Config) NewClientFromSettingsFile() (*Client, error) { return &Client{ mgmtClient: mc, + affinityGroupClient: affinitygroup.NewClient(mc), hostedServiceClient: hostedservice.NewClient(mc), secGroupClient: networksecuritygroup.NewClient(mc), osImageClient: osimage.NewClient(mc), @@ -130,6 +134,7 @@ func (c *Config) NewClient() (*Client, error) { return &Client{ mgmtClient: mc, + affinityGroupClient: affinitygroup.NewClient(mc), hostedServiceClient: hostedservice.NewClient(mc), secGroupClient: networksecuritygroup.NewClient(mc), osImageClient: osimage.NewClient(mc), diff --git a/builtin/providers/azure/provider.go b/builtin/providers/azure/provider.go index a6be93f5c..365488415 100644 --- a/builtin/providers/azure/provider.go +++ b/builtin/providers/azure/provider.go @@ -33,6 +33,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "azure_instance": resourceAzureInstance(), + "azure_affinity_group": resourceAzureAffinityGroup(), "azure_data_disk": resourceAzureDataDisk(), "azure_sql_database_server": resourceAzureSqlDatabaseServer(), "azure_sql_database_service": resourceAzureSqlDatabaseService(), diff --git a/builtin/providers/azure/resource_azure_affinity_group.go b/builtin/providers/azure/resource_azure_affinity_group.go new file mode 100644 index 000000000..5dd5bced4 --- /dev/null +++ b/builtin/providers/azure/resource_azure_affinity_group.go @@ -0,0 +1,168 @@ +package azure + +import ( + "fmt" + "log" + + "github.com/Azure/azure-sdk-for-go/management" + "github.com/Azure/azure-sdk-for-go/management/affinitygroup" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceAzureAffinityGroup returns the *schema.Resource associated to a +// resource affinity group on Azure. +func resourceAzureAffinityGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAzureAffinityGroupCreate, + Read: resourceAzureAffinityGroupRead, + Update: resourceAzureAffinityGroupUpdate, + Exists: resourceAzureAffinityGroupExists, + Delete: resourceAzureAffinityGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "label": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +// resourceAzureAffinityGroupCreate does all the necessary API calls to +// create an affinity group on Azure. +func resourceAzureAffinityGroupCreate(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Begun creating Azure Affinity Group creation request.") + name := d.Get("name").(string) + params := affinitygroup.CreateAffinityGroupParams{ + Name: name, + Label: d.Get("label").(string), + Location: d.Get("location").(string), + } + + if desc, ok := d.GetOk("description"); ok { + params.Description = desc.(string) + } + + log.Println("[INFO] Sending Affinity Group creation request to Azure.") + err := affinityGroupClient.CreateAffinityGroup(params) + if err != nil { + return fmt.Errorf("Error issuing Azure Affinity Group creation: %s", err) + } + + d.SetId(name) + return nil +} + +// resourceAzureAffinityGroupRead does all the necessary API calls to +// read the state of the affinity group off Azure. +func resourceAzureAffinityGroupRead(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Issuing Azure Affinity Group list request.") + affinityGroups, err := affinityGroupClient.ListAffinityGroups() + if err != nil { + return fmt.Errorf("Error obtaining Affinity Group list off Azure: %s", err) + } + + var found bool + name := d.Get("name").(string) + for _, group := range affinityGroups.AffinityGroups { + if group.Name == name { + found = true + d.Set("location", group.Location) + d.Set("label", group.Label) + d.Set("description", group.Description) + break + } + } + + if !found { + // it means the affinity group has been deleted in the meantime, so we + // must stop tracking it: + d.SetId("") + } + + return nil +} + +// resourceAzureAffinityGroupUpdate does all the necessary API calls to +// update the state of the affinity group on Azure. +func resourceAzureAffinityGroupUpdate(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + name := d.Get("name").(string) + clabel := d.HasChange("label") + cdesc := d.HasChange("description") + if clabel || cdesc { + log.Println("[INFO] Beginning Affinity Group update process.") + params := affinitygroup.UpdateAffinityGroupParams{} + + if clabel { + params.Label = d.Get("label").(string) + } + if cdesc { + params.Description = d.Get("description").(string) + } + + log.Println("[INFO] Sending Affinity Group update request to Azure.") + err := affinityGroupClient.UpdateAffinityGroup(name, params) + if err != nil { + return fmt.Errorf("Error updating Azure Affinity Group parameters: %s", err) + } + } + + return nil +} + +// resourceAzureAffinityGroupExists does all the necessary API calls to +// check for the existence of the affinity group on Azure. +func resourceAzureAffinityGroupExists(d *schema.ResourceData, meta interface{}) (bool, error) { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Issuing Azure Affinity Group get request.") + name := d.Get("name").(string) + _, err := affinityGroupClient.GetAffinityGroup(name) + if err != nil { + if management.IsResourceNotFoundError(err) { + // it means that the affinity group has been deleted in the + // meantime, so we must untrack it from the schema: + d.SetId("") + return false, nil + } else { + return false, fmt.Errorf("Error getting Affinity Group off Azure: %s", err) + } + } + + return true, nil +} + +// resourceAzureAffinityGroupDelete does all the necessary API calls to +// delete the affinity group off Azure. +func resourceAzureAffinityGroupDelete(d *schema.ResourceData, meta interface{}) error { + affinityGroupClient := meta.(*Client).affinityGroupClient + + log.Println("[INFO] Sending Affinity Group deletion request to Azure.") + name := d.Get("name").(string) + err := affinityGroupClient.DeleteAffinityGroup(name) + if err != nil { + return fmt.Errorf("Error deleting Azure Affinity Group: %s", err) + } + + return nil +} diff --git a/builtin/providers/azure/resource_azure_affinity_group_test.go b/builtin/providers/azure/resource_azure_affinity_group_test.go new file mode 100644 index 000000000..1cc5211c4 --- /dev/null +++ b/builtin/providers/azure/resource_azure_affinity_group_test.go @@ -0,0 +1,121 @@ +package azure + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/management" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureAffinityGroupBasic(t *testing.T) { + name := "azure_affinity_group.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureAffinityGroupDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureAffinityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureAffinityGroupExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-group"), + resource.TestCheckResourceAttr(name, "location", "West US"), + resource.TestCheckResourceAttr(name, "label", "A nice label."), + resource.TestCheckResourceAttr(name, "description", "A nice description."), + ), + }, + }, + }) +} + +func TestAccAzureAffinityGroupUpdate(t *testing.T) { + name := "azure_affinity_group.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAzureAffinityGroupDestroyed, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureAffinityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureAffinityGroupExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-group"), + resource.TestCheckResourceAttr(name, "location", "West US"), + resource.TestCheckResourceAttr(name, "label", "A nice label."), + resource.TestCheckResourceAttr(name, "description", "A nice description."), + ), + }, + resource.TestStep{ + Config: testAccAzureAffinityGroupUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureAffinityGroupExists(name), + resource.TestCheckResourceAttr(name, "name", "terraform-testing-group"), + resource.TestCheckResourceAttr(name, "location", "West US"), + resource.TestCheckResourceAttr(name, "label", "An even nicer label."), + resource.TestCheckResourceAttr(name, "description", "An even nicer description."), + ), + }, + }, + }) +} + +func testAccCheckAzureAffinityGroupExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Affinity Group resource %q doesn't exist.", name) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Affinity Group resource %q ID not set.", name) + } + + affinityGroupClient := testAccProvider.Meta().(*Client).affinityGroupClient + _, err := affinityGroupClient.GetAffinityGroup(resource.Primary.ID) + return err + } +} + +func testAccCheckAzureAffinityGroupDestroyed(s *terraform.State) error { + var err error + affinityGroupClient := testAccProvider.Meta().(*Client).affinityGroupClient + + for _, resource := range s.RootModule().Resources { + if resource.Type != "azure_affinity_group" { + continue + } + + if resource.Primary.ID == "" { + return fmt.Errorf("Affinity Group resource ID not set.") + } + + _, err = affinityGroupClient.GetAffinityGroup(resource.Primary.ID) + if !management.IsResourceNotFoundError(err) { + return err + } + } + + return nil +} + +const testAccAzureAffinityGroupConfig = ` +resource "azure_affinity_group" "foo" { + name = "terraform-testing-group" + location = "West US" + label = "A nice label." + description = "A nice description." +} +` + +const testAccAzureAffinityGroupUpdateConfig = ` +resource "azure_affinity_group" "foo" { + name = "terraform-testing-group" + location = "West US" + label = "An even nicer label." + description = "An even nicer description." +} +` diff --git a/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown new file mode 100644 index 000000000..2543c0220 --- /dev/null +++ b/website/source/docs/providers/aws/r/autoscaling_policy.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "aws" +page_title: "AWS: aws_autoscaling_policy" +sidebar_current: "docs-aws-resource-autoscaling-policy" +description: |- + Provides an AutoScaling Scaling Group resource. +--- + +# aws\_autoscaling\_policy + +Provides an AutoScaling Scaling Policy resource. + +~> **NOTE:** You may want to omit `desired_capacity` attribute from attached `aws_autoscaling_group` +when using autoscaling policies. It's good practice to pick either +[manual](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-manual-scaling.html) +or [dynamic](http://docs.aws.amazon.com/AutoScaling/latest/DeveloperGuide/as-scale-based-on-demand.html) +(policy-based) scaling. + +## Example Usage +``` +resource "aws_autoscaling_policy" "bat" { + name = "foobar3-terraform-test" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.bar.name}" +} + +resource "aws_autoscaling_group" "bar" { + availability_zones = ["us-east-1a"] + name = "foobar3-terraform-test" + max_size = 5 + min_size = 2 + health_check_grace_period = 300 + health_check_type = "ELB" + force_delete = true + launch_configuration = "${aws_launch_configuration.foo.name}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the policy. +* `autoscaling_group_name` - (Required) The name or ARN of the group. +* `adjustment_type` - (Required) Specifies whether the `scaling_adjustment` is an absolute number or a percentage of the current capacity. Valid values are `ChangeInCapacity`, `ExactCapacity`, and `PercentChangeInCapacity`. +* `scaling_adjustment` - (Required) The number of instances by which to scale. `adjustment_type` determines the interpretation of this number (e.g., as an absolute number or as a percentage of the existing Auto Scaling group size). A positive increment adds to the current capacity and a negative value removes from the current capacity. +* `cooldown` - (Optional) The amount of time, in seconds, after a scaling activity completes and before the next scaling activity can start. +* `min_adjustment_step` - (Optional) Used with `adjustment_type` with the value `PercentChangeInCapacity`, the scaling policy changes the `desired_capacity` of the Auto Scaling group by at least the number of instances specified in the value. + +## Attribute Reference +* `arn` - The ARN assigned by AWS to the scaling policy. diff --git a/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown b/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown new file mode 100644 index 000000000..e6ede269f --- /dev/null +++ b/website/source/docs/providers/aws/r/cloudwatch_metric_alarm.html.markdown @@ -0,0 +1,75 @@ +--- +layout: "aws" +page_title: "AWS: cloudwatch_metric_alarm" +sidebar_current: "docs-aws-resource-cloudwatch-metric-alarm" +description: |- + Provides an AutoScaling Scaling Group resource. +--- + +# aws\_cloudwatch\_metric\_alarm + +Provides a CloudWatch Metric Alarm resource. + +## Example Usage +``` +resource "aws_cloudwatch_metric_alarm" "foobar" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + insufficient_data_actions = [] +} +``` + +## Example in Conjuction with Scaling Policies +``` +resource "aws_autoscaling_policy" "bat" { + name = "foobar3-terraform-test" + scaling_adjustment = 4 + adjustment_type = "ChangeInCapacity" + cooldown = 300 + autoscaling_group_name = "${aws_autoscaling_group.bar.name}" +} + +resource "aws_cloudwatch_metric_alarm" "bat" { + alarm_name = "terraform-test-foobar5" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "CPUUtilization" + namespace = "AWS/EC2" + period = "120" + statistic = "Average" + threshold = "80" + alarm_description = "This metric monitor ec2 cpu utilization" + alarm_actions = ["${aws_autoscaling_policy.bat.arn}"] +} +``` +## Argument Reference + +See [related part of AWS Docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html) +for details about valid values. + +The following arguments are supported: + +* `alarm_name` - (Required) The descriptive name for the alarm. This name must be unique within the user's AWS account +* `comparison_operator` - (Required) The arithmetic operation to use when comparing the specified Statistic and Threshold. The specified Statistic value is used as the first operand. Either of the following is supported: `GreaterThanOrEqualToThreshold`, `GreaterThanThreshold`, `LessThanThreshold`, `LessThanOrEqualToThreshold`. +* `evaluation_periods` - (Required) The number of periods over which data is compared to the specified threshold. +* `metric_name` - (Required) The name for the alarm's associated metric. + See docs for [supported metrics]([valid metrics](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html)). +* `namespace` - (Required) The namespace for the alarm's associated metric. +* `period` - (Required) The period in seconds over which the specified `statistic` is applied. +* `statistic` - (Required) The statistic to apply to the alarm's associated metric. + Either of the following is supported: `SampleCount`, `Average`, `Sum`, `Minimum`, `Maximum` +* `threshold` - (Required) The value against which the specified statistic is compared. +* `actions_enabled` - (Optional) Indicates whether or not actions should be executed during any changes to the alarm's state. Defaults to `true`. +* `alarm_actions` - (Optional) The list of actions to execute when this alarm transitions into an ALARM state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `alarm_description` - (Optional) The description for the alarm. +* `dimensions` - (Optional) The dimensions for the alarm's associated metric. +* `insufficient_data_actions` - (Optional) The list of actions to execute when this alarm transitions into an INSUFFICIENT_DATA state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `ok_actions` - (Optional) The list of actions to execute when this alarm transitions into an OK state from any other state. Each action is specified as an Amazon Resource Number (ARN). +* `unit` - (Optional) The unit for the alarm's associated metric. diff --git a/website/source/docs/providers/azure/r/affinity_group.html.markdown b/website/source/docs/providers/azure/r/affinity_group.html.markdown new file mode 100644 index 000000000..906f73070 --- /dev/null +++ b/website/source/docs/providers/azure/r/affinity_group.html.markdown @@ -0,0 +1,42 @@ +--- +layout: "azure" +page_title: "Azure: azure_affinity_group" +sidebar_current: "docs-azure-affinity-group" +description: |- + Creates a new affinity group on Azure. +--- + +# azure\_affinity\_group + +Creates a new affinity group on Azure. + +## Example Usage + +``` +resource "azure_affinity_group" "terraform-main-group" { + name = "terraform-group" + location = "North Europe" + label = "tf-group-01" + description = "Affinity group created by Terraform." +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the affinity group. Must be unique on your + Azure subscription. + +* `location` - (Required) The location where the affinity group should be created. + For a list of all Azure locations, please consult [this link](http://azure.microsoft.com/en-us/regions/). + +* `label` - (Required) A label to be used for tracking purposes. + +* `description` - (Optional) A description for the affinity group. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The affinity group ID. Coincides with the given `name`. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 3e17133e1..5ca03ee54 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -21,6 +21,14 @@ aws_autoscaling_notification + > + aws_autoscaling_policy + + + > + aws_cloudwatch_metric_alarm + + > aws_customer_gateway diff --git a/website/source/layouts/azure.erb b/website/source/layouts/azure.erb index 4fc096f0e..9525c45c9 100644 --- a/website/source/layouts/azure.erb +++ b/website/source/layouts/azure.erb @@ -13,6 +13,10 @@ > Resources