From 305a4502398029733aff23177bcc993f7dd6ec7d Mon Sep 17 00:00:00 2001 From: Christopher Tiwald Date: Fri, 11 Dec 2015 18:52:02 -0500 Subject: [PATCH] aws_autoscaling_policy: Add support for StepScaling policies. Unlike SimpleScaling policies, StepScaling policies require one or more "steps", which are interval ranges in which a tracked metric can lie. Policies can then execute scaling adjustments wedded to these steps. This commit also adds a slew of additional policy attributes which are only applicable to step policies. --- .../aws/resource_aws_autoscaling_policy.go | 131 ++++++++++++++++-- builtin/providers/aws/structure.go | 71 ++++++++++ 2 files changed, 194 insertions(+), 8 deletions(-) diff --git a/builtin/providers/aws/resource_aws_autoscaling_policy.go b/builtin/providers/aws/resource_aws_autoscaling_policy.go index 07e94feb87..5ec4136bd7 100644 --- a/builtin/providers/aws/resource_aws_autoscaling_policy.go +++ b/builtin/providers/aws/resource_aws_autoscaling_policy.go @@ -1,11 +1,13 @@ package aws import ( + "bytes" "fmt" "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/schema" ) @@ -35,17 +37,59 @@ func resourceAwsAutoscalingPolicy() *schema.Resource { Required: true, ForceNew: true, }, + "policy_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "SimpleScaling", // preserve AWS's default to make validation easier. + }, "cooldown": &schema.Schema{ Type: schema.TypeInt, Optional: true, }, - "min_adjustment_step": &schema.Schema{ + "estimated_instance_warmup": &schema.Schema{ Type: schema.TypeInt, Optional: true, }, - "scaling_adjustment": &schema.Schema{ + "metric_aggregation_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "min_adjustment_magnitude": &schema.Schema{ Type: schema.TypeInt, - Required: true, + Optional: true, + }, + "min_adjustment_step": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Deprecated: "Use min_adjustment_magnitude instead.", + ConflictsWith: []string{"min_adjustment_magnitude"}, + }, + "scaling_adjustment": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"step_adjustment"}, + }, + "step_adjustment": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"scaling_adjustment"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "metric_interval_lower_bound": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "metric_interval_upper_bound": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "scaling_adjustment": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + }, + Set: resourceAwsAutoscalingScalingAdjustmentHash, }, }, } @@ -54,7 +98,10 @@ func resourceAwsAutoscalingPolicy() *schema.Resource { func resourceAwsAutoscalingPolicyCreate(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - params := getAwsAutoscalingPutScalingPolicyInput(d) + params, err := getAwsAutoscalingPutScalingPolicyInput(d) + if err != nil { + return err + } log.Printf("[DEBUG] AutoScaling PutScalingPolicy: %#v", params) resp, err := autoscalingconn.PutScalingPolicy(¶ms) @@ -84,10 +131,15 @@ func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) d.Set("adjustment_type", p.AdjustmentType) d.Set("autoscaling_group_name", p.AutoScalingGroupName) d.Set("cooldown", p.Cooldown) + d.Set("estimated_instance_warmup", p.EstimatedInstanceWarmup) + d.Set("metric_aggregation_type", p.MetricAggregationType) + d.Set("policy_type", p.PolicyType) + d.Set("min_adjustment_magnitude", p.MinAdjustmentMagnitude) d.Set("min_adjustment_step", p.MinAdjustmentStep) d.Set("arn", p.PolicyARN) d.Set("name", p.PolicyName) d.Set("scaling_adjustment", p.ScalingAdjustment) + d.Set("step_adjustment", flattenStepAdjustments(p.StepAdjustments)) return nil } @@ -95,7 +147,10 @@ func resourceAwsAutoscalingPolicyRead(d *schema.ResourceData, meta interface{}) func resourceAwsAutoscalingPolicyUpdate(d *schema.ResourceData, meta interface{}) error { autoscalingconn := meta.(*AWSClient).autoscalingconn - params := getAwsAutoscalingPutScalingPolicyInput(d) + params, inputErr := getAwsAutoscalingPutScalingPolicyInput(d) + if inputErr != nil { + return inputErr + } log.Printf("[DEBUG] Autoscaling Update Scaling Policy: %#v", params) _, err := autoscalingconn.PutScalingPolicy(¶ms) @@ -128,8 +183,10 @@ func resourceAwsAutoscalingPolicyDelete(d *schema.ResourceData, meta interface{} 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 { +// PutScalingPolicy can safely resend all parameters without destroying the +// resource, so create and update can share this common function. It will error +// if certain mutually exclusive values are set. +func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) (autoscaling.PutScalingPolicyInput, error) { var params = autoscaling.PutScalingPolicyInput{ AutoScalingGroupName: aws.String(d.Get("autoscaling_group_name").(string)), PolicyName: aws.String(d.Get("name").(string)), @@ -143,15 +200,59 @@ func getAwsAutoscalingPutScalingPolicyInput(d *schema.ResourceData) autoscaling. params.Cooldown = aws.Int64(int64(v.(int))) } + if v, ok := d.GetOk("estimated_instance_warmup"); ok { + params.EstimatedInstanceWarmup = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("metric_aggregation_type"); ok { + params.MetricAggregationType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("policy_type"); ok { + params.PolicyType = aws.String(v.(string)) + } + if v, ok := d.GetOk("scaling_adjustment"); ok { params.ScalingAdjustment = aws.Int64(int64(v.(int))) } + if v, ok := d.GetOk("step_adjustment"); ok { + steps, err := expandStepAdjustments(v.(*schema.Set).List()) + if err != nil { + return params, fmt.Errorf("metric_interval_lower_bound and metric_interval_upper_bound must be strings!") + } + params.StepAdjustments = steps + } + + if v, ok := d.GetOk("min_adjustment_magnitude"); ok { + params.MinAdjustmentMagnitude = aws.Int64(int64(v.(int))) + } + if v, ok := d.GetOk("min_adjustment_step"); ok { params.MinAdjustmentStep = aws.Int64(int64(v.(int))) } - return params + // Validate our final input to confirm it won't error when sent to AWS. + // First, SimpleScaling policy types... + if *params.PolicyType == "SimpleScaling" && params.StepAdjustments != nil { + return params, fmt.Errorf("SimpleScaling policy types cannot use step_adjustments!") + } + if *params.PolicyType == "SimpleScaling" && params.MetricAggregationType != nil { + return params, fmt.Errorf("SimpleScaling policy types cannot use metric_aggregation_type!") + } + if *params.PolicyType == "SimpleScaling" && params.EstimatedInstanceWarmup != nil { + return params, fmt.Errorf("SimpleScaling policy types cannot use estimated_instance_warmup!") + } + + // Second, StepScaling policy types... + if *params.PolicyType == "StepScaling" && params.ScalingAdjustment != nil { + return params, fmt.Errorf("StepScaling policy types cannot use scaling_adjustment!") + } + if *params.PolicyType == "StepScaling" && params.Cooldown != nil { + return params, fmt.Errorf("StepScaling policy types cannot use cooldown!") + } + + return params, nil } func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autoscaling.ScalingPolicy, error) { @@ -179,3 +280,17 @@ func getAwsAutoscalingPolicy(d *schema.ResourceData, meta interface{}) (*autosca // policy not found return nil, nil } + +func resourceAwsAutoscalingScalingAdjustmentHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + if v, ok := m["metric_interval_lower_bound"]; ok { + buf.WriteString(fmt.Sprintf("%f-", v)) + } + if v, ok := m["metric_interval_upper_bound"]; ok { + buf.WriteString(fmt.Sprintf("%f-", v)) + } + buf.WriteString(fmt.Sprintf("%d-", m["scaling_adjustment"].(int))) + + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/aws/structure.go b/builtin/providers/aws/structure.go index d122909921..72766c0269 100644 --- a/builtin/providers/aws/structure.go +++ b/builtin/providers/aws/structure.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "sort" + "strconv" "strings" "github.com/aws/aws-sdk-go/aws" @@ -305,6 +306,58 @@ func flattenAccessLog(l *elb.AccessLog) []map[string]interface{} { return result } +// Takes the result of flatmap.Expand for an array of step adjustments and +// returns a []*autoscaling.StepAdjustment. +func expandStepAdjustments(configured []interface{}) ([]*autoscaling.StepAdjustment, error) { + var adjustments []*autoscaling.StepAdjustment + + // Loop over our configured step adjustments and create an array + // of aws-sdk-go compatible objects. We're forced to convert strings + // to floats here because there's no way to detect whether or not + // an uninitialized, optional schema element is "0.0" deliberately. + // With strings, we can test for "", which is definitely an empty + // struct value. + for _, raw := range configured { + data := raw.(map[string]interface{}) + a := &autoscaling.StepAdjustment{ + ScalingAdjustment: aws.Int64(int64(data["scaling_adjustment"].(int))), + } + if data["metric_interval_lower_bound"] != "" { + bound := data["metric_interval_lower_bound"] + switch bound := bound.(type) { + case string: + f, err := strconv.ParseFloat(bound, 64) + if err != nil { + return nil, fmt.Errorf( + "metric_interval_lower_bound must be a float value represented as a string") + } + a.MetricIntervalLowerBound = aws.Float64(f) + default: + return nil, fmt.Errorf( + "metric_interval_lower_bound isn't a string. This is a bug. Please file an issue.") + } + } + if data["metric_interval_upper_bound"] != "" { + bound := data["metric_interval_upper_bound"] + switch bound := bound.(type) { + case string: + f, err := strconv.ParseFloat(bound, 64) + if err != nil { + return nil, fmt.Errorf( + "metric_interval_upper_bound must be a float value represented as a string") + } + a.MetricIntervalUpperBound = aws.Float64(f) + default: + return nil, fmt.Errorf( + "metric_interval_upper_bound isn't a string. This is a bug. Please file an issue.") + } + } + adjustments = append(adjustments, a) + } + + return adjustments, nil +} + // Flattens a health check into something that flatmap.Flatten() // can handle func flattenHealthCheck(check *elb.HealthCheck) []map[string]interface{} { @@ -564,6 +617,24 @@ func flattenAttachment(a *ec2.NetworkInterfaceAttachment) map[string]interface{} return att } +// Flattens step adjustments into a list of map[string]interface. +func flattenStepAdjustments(adjustments []*autoscaling.StepAdjustment) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(adjustments)) + for _, raw := range adjustments { + a := map[string]interface{}{ + "scaling_adjustment": *raw.ScalingAdjustment, + } + if raw.MetricIntervalUpperBound != nil { + a["metric_interval_upper_bound"] = *raw.MetricIntervalUpperBound + } + if raw.MetricIntervalLowerBound != nil { + a["metric_interval_lower_bound"] = *raw.MetricIntervalLowerBound + } + result = append(result, a) + } + return result +} + func flattenResourceRecords(recs []*route53.ResourceRecord) []string { strs := make([]string, 0, len(recs)) for _, r := range recs {