Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource/aws_autoscaling_group: Add Instance Refresh configuration #16678

Merged
merged 14 commits into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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