Skip to content

Commit

Permalink
Add support for ECS capacity provider update
Browse files Browse the repository at this point in the history
  • Loading branch information
shuheiktgw authored and ewbankkit committed Jun 23, 2021
1 parent fae0277 commit 13d6a44
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 44 deletions.
22 changes: 22 additions & 0 deletions aws/internal/service/ecs/waiter/status.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package waiter

import (
"fmt"
"log"

"github.com/aws/aws-sdk-go/aws"
Expand Down Expand Up @@ -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
}
}
21 changes: 21 additions & 0 deletions aws/internal/service/ecs/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
123 changes: 92 additions & 31 deletions aws/resource_aws_ecs_capacity_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -99,7 +99,6 @@ func resourceAwsEcsCapacityProvider() *schema.Resource {
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validation.IntBetween(1, 100),
},
},
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down
38 changes: 25 additions & 13 deletions aws/resource_aws_ecs_capacity_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
),
},
},
})
}
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 13d6a44

Please sign in to comment.