diff --git a/aws/internal/service/ecs/waiter/status.go b/aws/internal/service/ecs/waiter/status.go index 14d54c83e191..d4e569815854 100644 --- a/aws/internal/service/ecs/waiter/status.go +++ b/aws/internal/service/ecs/waiter/status.go @@ -1,6 +1,7 @@ package waiter import ( + "fmt" "log" "github.com/aws/aws-sdk-go/aws" @@ -95,3 +96,24 @@ func ClusterStatus(conn *ecs.ECS, arn string) resource.StateRefreshFunc { return output, aws.StringValue(output.Clusters[0].Status), err } } + +// CapacityProviderUpdateStatus fetches the Capacity Provider and its Update Status +func CapacityProviderUpdateStatus(conn *ecs.ECS, capacityProvider string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := conn.DescribeCapacityProviders(&ecs.DescribeCapacityProvidersInput{ + CapacityProviders: []*string{aws.String(capacityProvider)}, + }) + + if err != nil { + return nil, "", err + } + + if len(output.CapacityProviders) == 0 { + return nil, "", fmt.Errorf("ECS Capacity Provider %q missing", capacityProvider) + } + + c := output.CapacityProviders[0] + + return c, aws.StringValue(c.UpdateStatus), nil + } +} diff --git a/aws/internal/service/ecs/waiter/waiter.go b/aws/internal/service/ecs/waiter/waiter.go index a7135e7b3faf..6048ed4d2ff3 100644 --- a/aws/internal/service/ecs/waiter/waiter.go +++ b/aws/internal/service/ecs/waiter/waiter.go @@ -12,6 +12,9 @@ const ( // Maximum amount of time to wait for a Capacity Provider to return INACTIVE CapacityProviderInactiveTimeout = 20 * time.Minute + // Maximum amount of time to wait for a Capacity Provider to return UPDATE_COMPLETE or UPDATE_FAILED + CapacityProviderUpdateTimeout = 10 * time.Minute + ServiceCreateTimeout = 2 * time.Minute ServiceInactiveTimeout = 10 * time.Minute ServiceInactiveTimeoutMin = 1 * time.Second @@ -137,3 +140,21 @@ func ClusterDeleted(conn *ecs.ECS, arn string) (*ecs.Cluster, error) { return nil, err } + +// CapacityProviderUpdate waits for a Capacity Provider to return UPDATE_COMPLETE or UPDATE_FAILED +func CapacityProviderUpdate(conn *ecs.ECS, capacityProvider string) (*ecs.CapacityProvider, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ecs.CapacityProviderUpdateStatusUpdateInProgress}, + Target: []string{ecs.CapacityProviderUpdateStatusUpdateComplete}, + Refresh: CapacityProviderUpdateStatus(conn, capacityProvider), + Timeout: CapacityProviderUpdateTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*ecs.CapacityProvider); ok { + return v, err + } + + return nil, err +} diff --git a/aws/resource_aws_ecs_capacity_provider.go b/aws/resource_aws_ecs_capacity_provider.go index 0e63f6aeb3b9..8bd6f84f3652 100644 --- a/aws/resource_aws_ecs_capacity_provider.go +++ b/aws/resource_aws_ecs_capacity_provider.go @@ -3,16 +3,22 @@ package aws import ( "fmt" "log" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ecs" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ecs/waiter" ) +const ( + ecsCapacityProviderTimeoutUpdate = 10 * time.Minute +) + func resourceAwsEcsCapacityProvider() *schema.Resource { return &schema.Resource{ Create: resourceAwsEcsCapacityProviderCreate, @@ -51,7 +57,6 @@ func resourceAwsEcsCapacityProvider() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, ValidateFunc: validation.StringInSlice([]string{ ecs.ManagedTerminationProtectionEnabled, ecs.ManagedTerminationProtectionDisabled, @@ -62,35 +67,30 @@ func resourceAwsEcsCapacityProvider() *schema.Resource { MaxItems: 1, Optional: true, Computed: true, - ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "instance_warmup_period": { Type: schema.TypeInt, Optional: true, Computed: true, - ForceNew: true, ValidateFunc: validation.IntBetween(1, 10000), }, "maximum_scaling_step_size": { Type: schema.TypeInt, Optional: true, Computed: true, - ForceNew: true, ValidateFunc: validation.IntBetween(1, 10000), }, "minimum_scaling_step_size": { Type: schema.TypeInt, Optional: true, Computed: true, - ForceNew: true, ValidateFunc: validation.IntBetween(1, 10000), }, "status": { Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, ValidateFunc: validation.StringInSlice([]string{ ecs.ManagedScalingStatusEnabled, ecs.ManagedScalingStatusDisabled, @@ -99,7 +99,6 @@ func resourceAwsEcsCapacityProvider() *schema.Resource { Type: schema.TypeInt, Optional: true, Computed: true, - ForceNew: true, ValidateFunc: validation.IntBetween(1, 100), }, }, @@ -121,7 +120,7 @@ func resourceAwsEcsCapacityProviderCreate(d *schema.ResourceData, meta interface input := ecs.CreateCapacityProviderInput{ Name: aws.String(d.Get("name").(string)), - AutoScalingGroupProvider: expandAutoScalingGroupProvider(d.Get("auto_scaling_group_provider")), + AutoScalingGroupProvider: expandAutoScalingGroupProviderCreate(d.Get("auto_scaling_group_provider")), } // `CreateCapacityProviderInput` does not accept an empty array of tags @@ -132,7 +131,7 @@ func resourceAwsEcsCapacityProviderCreate(d *schema.ResourceData, meta interface out, err := conn.CreateCapacityProvider(&input) if err != nil { - return fmt.Errorf("error creating capacity provider: %s", err) + return fmt.Errorf("error creating ECS Capacity Provider: %s", err) } provider := *out.CapacityProvider @@ -203,15 +202,44 @@ func resourceAwsEcsCapacityProviderRead(d *schema.ResourceData, meta interface{} func resourceAwsEcsCapacityProviderUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ecsconn + input := &ecs.UpdateCapacityProviderInput{ + Name: aws.String(d.Get("name").(string)), + } + + if d.HasChange("auto_scaling_group_provider") { + input.AutoScalingGroupProvider = expandAutoScalingGroupProviderUpdate(d.Get("auto_scaling_group_provider")) + + err := resource.Retry(ecsCapacityProviderTimeoutUpdate, func() *resource.RetryError { + _, err := conn.UpdateCapacityProvider(input) + if err != nil { + if isAWSErr(err, ecs.ErrCodeUpdateInProgressException, "") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + if isResourceTimeoutError(err) { + _, err = conn.UpdateCapacityProvider(input) + } + if err != nil { + return fmt.Errorf("error updating ECS Capacity Provider (%s): %s", d.Id(), err) + } + + if _, err = waiter.CapacityProviderUpdate(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for ECS Capacity Provider (%s) update: %s", d.Id(), err) + } + } + if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") if err := keyvaluetags.EcsUpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating ECS Cluster (%s) tags: %s", d.Id(), err) + return fmt.Errorf("error updating ECS Capacity Provider (%s) tags: %s", d.Id(), err) } } - return nil + return resourceAwsEcsCapacityProviderRead(d, meta) } func resourceAwsEcsCapacityProviderDelete(d *schema.ResourceData, meta interface{}) error { @@ -246,7 +274,7 @@ func resourceAwsEcsCapacityProviderImport(d *schema.ResourceData, meta interface return []*schema.ResourceData{d}, nil } -func expandAutoScalingGroupProvider(configured interface{}) *ecs.AutoScalingGroupProvider { +func expandAutoScalingGroupProviderCreate(configured interface{}) *ecs.AutoScalingGroupProvider { if configured == nil { return nil } @@ -264,31 +292,64 @@ func expandAutoScalingGroupProvider(configured interface{}) *ecs.AutoScalingGrou prov.ManagedTerminationProtection = aws.String(mtp) } - if v := p["managed_scaling"].([]interface{}); len(v) > 0 && v[0].(map[string]interface{}) != nil { - ms := v[0].(map[string]interface{}) - managedScaling := ecs.ManagedScaling{} + prov.ManagedScaling = expandManagedScaling(p["managed_scaling"]) - if val, ok := ms["instance_warmup_period"].(int); ok && val != 0 { - managedScaling.InstanceWarmupPeriod = aws.Int64(int64(val)) - } - if val, ok := ms["maximum_scaling_step_size"].(int); ok && val != 0 { - managedScaling.MaximumScalingStepSize = aws.Int64(int64(val)) - } - if val, ok := ms["minimum_scaling_step_size"].(int); ok && val != 0 { - managedScaling.MinimumScalingStepSize = aws.Int64(int64(val)) - } - if val, ok := ms["status"].(string); ok && len(val) > 0 { - managedScaling.Status = aws.String(val) - } - if val, ok := ms["target_capacity"].(int); ok && val != 0 { - managedScaling.TargetCapacity = aws.Int64(int64(val)) - } - prov.ManagedScaling = &managedScaling + return &prov +} + +func expandAutoScalingGroupProviderUpdate(configured interface{}) *ecs.AutoScalingGroupProviderUpdate { + if configured == nil { + return nil + } + + if configured.([]interface{}) == nil || len(configured.([]interface{})) == 0 { + return nil } + prov := ecs.AutoScalingGroupProviderUpdate{} + p := configured.([]interface{})[0].(map[string]interface{}) + + if mtp := p["managed_termination_protection"].(string); len(mtp) > 0 { + prov.ManagedTerminationProtection = aws.String(mtp) + } + + prov.ManagedScaling = expandManagedScaling(p["managed_scaling"]) + return &prov } +func expandManagedScaling(configured interface{}) *ecs.ManagedScaling { + if configured == nil { + return nil + } + + if configured.([]interface{}) == nil || len(configured.([]interface{})) == 0 { + return nil + } + + p := configured.([]interface{})[0].(map[string]interface{}) + + managedScaling := ecs.ManagedScaling{} + + if val, ok := p["instance_warmup_period"].(int); ok && val != 0 { + managedScaling.InstanceWarmupPeriod = aws.Int64(int64(val)) + } + if val, ok := p["maximum_scaling_step_size"].(int); ok && val != 0 { + managedScaling.MaximumScalingStepSize = aws.Int64(int64(val)) + } + if val, ok := p["minimum_scaling_step_size"].(int); ok && val != 0 { + managedScaling.MinimumScalingStepSize = aws.Int64(int64(val)) + } + if val, ok := p["status"].(string); ok && len(val) > 0 { + managedScaling.Status = aws.String(val) + } + if val, ok := p["target_capacity"].(int); ok && val != 0 { + managedScaling.TargetCapacity = aws.Int64(int64(val)) + } + + return &managedScaling +} + func flattenAutoScalingGroupProvider(provider *ecs.AutoScalingGroupProvider) []map[string]interface{} { if provider == nil { return nil diff --git a/aws/resource_aws_ecs_capacity_provider_test.go b/aws/resource_aws_ecs_capacity_provider_test.go index baea10c40277..bf740f330a72 100644 --- a/aws/resource_aws_ecs_capacity_provider_test.go +++ b/aws/resource_aws_ecs_capacity_provider_test.go @@ -170,14 +170,14 @@ func TestAccAWSEcsCapacityProvider_ManagedScaling(t *testing.T) { CheckDestroy: testAccCheckAWSEcsCapacityProviderDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSEcsCapacityProviderConfigManagedScaling(rName), + Config: testAccAWSEcsCapacityProviderConfigManagedScaling(rName, ecs.ManagedScalingStatusEnabled, 300, 10, 1, 50), Check: resource.ComposeTestCheckFunc( testAccCheckAWSEcsCapacityProviderExists(resourceName, &provider), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttrPair(resourceName, "auto_scaling_group_provider.0.auto_scaling_group_arn", "aws_autoscaling_group.test", "arn"), resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_termination_protection", "DISABLED"), - resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.instance_warmup_period", "400"), - resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.minimum_scaling_step_size", "2"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.instance_warmup_period", "300"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.minimum_scaling_step_size", "1"), resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.maximum_scaling_step_size", "10"), resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.status", "ENABLED"), resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.target_capacity", "50"), @@ -189,6 +189,20 @@ func TestAccAWSEcsCapacityProvider_ManagedScaling(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + Config: testAccAWSEcsCapacityProviderConfigManagedScaling(rName, ecs.ManagedScalingStatusDisabled, 400, 100, 10, 100), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSEcsCapacityProviderExists(resourceName, &provider), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "auto_scaling_group_provider.0.auto_scaling_group_arn", "aws_autoscaling_group.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_termination_protection", "DISABLED"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.instance_warmup_period", "400"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.minimum_scaling_step_size", "10"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.maximum_scaling_step_size", "100"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.status", "DISABLED"), + resource.TestCheckResourceAttr(resourceName, "auto_scaling_group_provider.0.managed_scaling.0.target_capacity", "100"), + ), + }, }, }) } @@ -274,8 +288,6 @@ func TestAccAWSEcsCapacityProvider_Tags(t *testing.T) { }) } -// TODO add an update test config - Reference: https://github.com/aws/containers-roadmap/issues/633 - func testAccCheckAWSEcsCapacityProviderDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).ecsconn @@ -396,24 +408,24 @@ resource "aws_ecs_capacity_provider" "test" { `, rName) } -func testAccAWSEcsCapacityProviderConfigManagedScaling(rName string) string { +func testAccAWSEcsCapacityProviderConfigManagedScaling(rName, status string, warmup, max, min, cap int) string { return testAccAWSEcsCapacityProviderConfigBase(rName) + fmt.Sprintf(` resource "aws_ecs_capacity_provider" "test" { name = %q auto_scaling_group_provider { - auto_scaling_group_arn = aws_autoscaling_group.test.arn + auto_scaling_group_arn = aws_autoscaling_group.test.arn managed_scaling { - instance_warmup_period = 400 - maximum_scaling_step_size = 10 - minimum_scaling_step_size = 2 - status = "ENABLED" - target_capacity = 50 + instance_warmup_period = %v + maximum_scaling_step_size = %v + minimum_scaling_step_size = %v + status = %q + target_capacity = %v } } } -`, rName) +`, rName, warmup, max, min, status, cap) } func testAccAWSEcsCapacityProviderConfigManagedScalingPartial(rName string) string {