Skip to content

Commit

Permalink
Merge pull request #16678 from hashicorp/f-aws_autoscaling_group-inst…
Browse files Browse the repository at this point in the history
…ance_refresh

resource/aws_autoscaling_group: Add Instance Refresh configuration
  • Loading branch information
gdavison authored Dec 18, 2020
2 parents 37e4818 + aa79125 commit bfec53d
Show file tree
Hide file tree
Showing 10 changed files with 1,186 additions and 111 deletions.
78 changes: 78 additions & 0 deletions aws/internal/experimental/nullable/int.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package nullable

import (
"fmt"
"strconv"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

const (
TypeNullableInt = schema.TypeString
)

type Int string

func (i Int) IsNull() bool {
return i == ""
}

func (i Int) Value() (int64, bool, error) {
if i.IsNull() {
return 0, true, nil
}

value, err := strconv.ParseInt(string(i), 10, 64)
if err != nil {
return 0, false, err
}
return value, false, nil
}

// ValidateTypeStringNullableInt provides custom error messaging for TypeString ints
// Some arguments require an int value or unspecified, empty field.
func ValidateTypeStringNullableInt(v interface{}, k string) (ws []string, es []error) {
value, ok := v.(string)
if !ok {
es = append(es, fmt.Errorf("expected type of %s to be string", k))
return
}

if value == "" {
return
}

if _, err := strconv.ParseInt(value, 10, 64); err != nil {
es = append(es, fmt.Errorf("%s: cannot parse '%s' as int: %w", k, value, err))
}

return
}

// ValidateTypeStringNullableIntAtLeast provides custom error messaging for TypeString ints
// Some arguments require an int value or unspecified, empty field.
func ValidateTypeStringNullableIntAtLeast(min int) schema.SchemaValidateFunc {
return func(i interface{}, k string) (ws []string, es []error) {
value, ok := i.(string)
if !ok {
es = append(es, fmt.Errorf("expected type of %s to be string", k))
return
}

if value == "" {
return
}

v, err := strconv.ParseInt(value, 10, 64)
if err != nil {
es = append(es, fmt.Errorf("%s: cannot parse '%s' as int: %w", k, value, err))
return
}

if v < int64(min) {
es = append(es, fmt.Errorf("expected %s to be at least (%d), got %d", k, min, v))
}

return
}
}
100 changes: 100 additions & 0 deletions aws/internal/experimental/nullable/int_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package nullable

import (
"errors"
"regexp"
"strconv"
"testing"
)

func TestNullableInt(t *testing.T) {
cases := []struct {
val string
expectedNull bool
expectedValue int64
expectedErr error
}{
{
val: "1",
expectedNull: false,
expectedValue: 1,
},
{
val: "",
expectedNull: true,
expectedValue: 0,
},
{
val: "A",
expectedNull: false,
expectedValue: 0,
expectedErr: strconv.ErrSyntax,
},
}

for i, tc := range cases {
v := Int(tc.val)

if null := v.IsNull(); null != tc.expectedNull {
t.Fatalf("expected test case %d IsNull to return %t, got %t", i, null, tc.expectedNull)
}

value, null, err := v.Value()
if value != tc.expectedValue {
t.Fatalf("expected test case %d Value to be %d, got %d", i, tc.expectedValue, value)
}
if null != tc.expectedNull {
t.Fatalf("expected test case %d Value null flag to be %t, got %t", i, tc.expectedNull, null)
}
if tc.expectedErr == nil && err != nil {
t.Fatalf("expected test case %d to succeed, got error %s", i, err)
}
if tc.expectedErr != nil {
if !errors.Is(err, tc.expectedErr) {
t.Fatalf("expected test case %d to have error matching \"%s\", got %s", i, tc.expectedErr, err)
}
}
}
}

func TestValidationInt(t *testing.T) {
runTestCases(t, []testCase{
{
val: "1",
f: ValidateTypeStringNullableInt,
},
{
val: "A",
f: ValidateTypeStringNullableInt,
expectedErr: regexp.MustCompile(`[\w]+: cannot parse 'A' as int: .*`),
},
{
val: 1,
f: ValidateTypeStringNullableInt,
expectedErr: regexp.MustCompile(`expected type of [\w]+ to be string`),
},
})
}

func TestValidationIntAtLeast(t *testing.T) {
runTestCases(t, []testCase{
{
val: "1",
f: ValidateTypeStringNullableIntAtLeast(1),
},
{
val: "1",
f: ValidateTypeStringNullableIntAtLeast(0),
},
{
val: "1",
f: ValidateTypeStringNullableIntAtLeast(2),
expectedErr: regexp.MustCompile(`expected [\w]+ to be at least \(2\), got 1`),
},
{
val: 1,
f: ValidateTypeStringNullableIntAtLeast(2),
expectedErr: regexp.MustCompile(`expected type of [\w]+ to be string`),
},
})
}
45 changes: 45 additions & 0 deletions aws/internal/experimental/nullable/testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package nullable

import (
"regexp"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
testing "github.com/mitchellh/go-testing-interface"
)

type testCase struct {
val interface{}
f schema.SchemaValidateFunc
expectedErr *regexp.Regexp
}

func runTestCases(t testing.T, cases []testCase) {
t.Helper()

matchErr := func(errs []error, r *regexp.Regexp) bool {
// err must match one provided
for _, err := range errs {
if r.MatchString(err.Error()) {
return true
}
}

return false
}

for i, tc := range cases {
_, errs := tc.f(tc.val, "test_property")

if len(errs) == 0 && tc.expectedErr == nil {
continue
}

if len(errs) != 0 && tc.expectedErr == nil {
t.Fatalf("expected test case %d to produce no errors, got %v", i, errs)
}

if !matchErr(errs, tc.expectedErr) {
t.Fatalf("expected test case %d to produce error matching \"%s\", got %v", i, tc.expectedErr, errs)
}
}
}
28 changes: 28 additions & 0 deletions aws/internal/service/autoscaling/waiter/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package waiter

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func InstanceRefreshStatus(conn *autoscaling.AutoScaling, asgName, instanceRefreshId string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
input := autoscaling.DescribeInstanceRefreshesInput{
AutoScalingGroupName: aws.String(asgName),
InstanceRefreshIds: []*string{aws.String(instanceRefreshId)},
}
output, err := conn.DescribeInstanceRefreshes(&input)
if err != nil {
return nil, "", err
}

if output == nil || len(output.InstanceRefreshes) == 0 || output.InstanceRefreshes[0] == nil {
return nil, "", nil
}

instanceRefresh := output.InstanceRefreshes[0]

return instanceRefresh, aws.StringValue(instanceRefresh.Status), nil
}
}
44 changes: 44 additions & 0 deletions aws/internal/service/autoscaling/waiter/waiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package waiter

import (
"time"

"github.com/aws/aws-sdk-go/service/autoscaling"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

const (
// Maximum amount of time to wait for an InstanceRefresh to be started
// Must be at least as long as InstanceRefreshCancelledTimeout, since we try to cancel any
// existing Instance Refreshes when starting.
InstanceRefreshStartedTimeout = InstanceRefreshCancelledTimeout

// Maximum amount of time to wait for an Instance Refresh to be Cancelled
InstanceRefreshCancelledTimeout = 15 * time.Minute
)

func InstanceRefreshCancelled(conn *autoscaling.AutoScaling, asgName, instanceRefreshId string) (*autoscaling.InstanceRefresh, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{
autoscaling.InstanceRefreshStatusPending,
autoscaling.InstanceRefreshStatusInProgress,
autoscaling.InstanceRefreshStatusCancelling,
},
Target: []string{
autoscaling.InstanceRefreshStatusCancelled,
// Failed and Successful are also acceptable end-states
autoscaling.InstanceRefreshStatusFailed,
autoscaling.InstanceRefreshStatusSuccessful,
},
Refresh: InstanceRefreshStatus(conn, asgName, instanceRefreshId),
Timeout: InstanceRefreshCancelledTimeout,
}

outputRaw, err := stateConf.WaitForState()

if v, ok := outputRaw.(*autoscaling.InstanceRefresh); ok {
return v, err
}

return nil, err
}
Loading

0 comments on commit bfec53d

Please sign in to comment.