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

r/aws_connect_instance: Fix crash when CreatedTime is null #31689

Merged
merged 7 commits into from
May 31, 2023
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
3 changes: 3 additions & 0 deletions .changelog/31689.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_connect_instance: Fix crash when reading instances with `CREATION_FAILED` status
```
210 changes: 143 additions & 67 deletions internal/service/connect/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package connect

import (
"context"
"errors"
"fmt"
"log"
"regexp"
Expand All @@ -13,9 +14,11 @@ import (
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
)

// @SDKResource("aws_connect_instance")
Expand All @@ -25,13 +28,16 @@ func ResourceInstance() *schema.Resource {
ReadWithoutTimeout: resourceInstanceRead,
UpdateWithoutTimeout: resourceInstanceUpdate,
DeleteWithoutTimeout: resourceInstanceDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(instanceCreatedTimeout),
Delete: schema.DefaultTimeout(instanceDeletedTimeout),
Create: schema.DefaultTimeout(5 * time.Minute),
Delete: schema.DefaultTimeout(5 * time.Minute),
},

Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Expand Down Expand Up @@ -129,146 +135,216 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, meta in
if v, ok := d.GetOk("directory_id"); ok {
input.DirectoryId = aws.String(v.(string))
}

if v, ok := d.GetOk("instance_alias"); ok {
input.InstanceAlias = aws.String(v.(string))
}

log.Printf("[DEBUG] Creating Connect Instance %s", input)
output, err := conn.CreateInstanceWithContext(ctx, input)

if err != nil {
return diag.FromErr(fmt.Errorf("error creating Connect Instance (%s): %w", d.Id(), err))
return diag.Errorf("creating Connect Instance: %s", err)
}

d.SetId(aws.StringValue(output.Id))

if _, err := waitInstanceCreated(ctx, conn, d.Timeout(schema.TimeoutCreate), d.Id()); err != nil {
return diag.FromErr(fmt.Errorf("error waiting for Connect instance creation (%s): %w", d.Id(), err))
if _, err := waitInstanceCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
return diag.Errorf("waiting for Connect Instance (%s) create: %s", d.Id(), err)
}

for att := range InstanceAttributeMapping() {
rKey := InstanceAttributeMapping()[att]
err := resourceInstanceUpdateAttribute(ctx, conn, d.Id(), att, strconv.FormatBool(d.Get(rKey).(bool)))
//Pre-release attribute, user/account/instance now allow-listed
if err != nil && tfawserr.ErrCodeEquals(err, ErrCodeAccessDeniedException) || tfawserr.ErrMessageContains(err, ErrCodeAccessDeniedException, "not authorized to update") {
log.Printf("[WARN] error setting Connect instance (%s) attribute (%s): %s", d.Id(), att, err)
} else if err != nil {
return diag.FromErr(fmt.Errorf("error setting Connect instance (%s) attribute (%s): %w", d.Id(), att, err))
for attributeType, key := range InstanceAttributeMapping() {
if err := updateInstanceAttribute(ctx, conn, d.Id(), attributeType, strconv.FormatBool(d.Get(key).(bool))); err != nil {
return diag.FromErr(err)
}
}

return resourceInstanceRead(ctx, d, meta)
}

func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).ConnectConn()

for att := range InstanceAttributeMapping() {
rKey := InstanceAttributeMapping()[att]
if d.HasChange(rKey) {
_, n := d.GetChange(rKey)
err := resourceInstanceUpdateAttribute(ctx, conn, d.Id(), att, strconv.FormatBool(n.(bool)))
//Pre-release attribute, user/account/instance now allow-listed
if err != nil && tfawserr.ErrCodeEquals(err, ErrCodeAccessDeniedException) || tfawserr.ErrMessageContains(err, ErrCodeAccessDeniedException, "not authorized to update") {
log.Printf("[WARN] error setting Connect instance (%s) attribute (%s): %s", d.Id(), att, err)
} else if err != nil {
return diag.FromErr(fmt.Errorf("error setting Connect instance (%s) attribute (%s): %s", d.Id(), att, err))
}
}
}

return nil
}
func resourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).ConnectConn()

input := connect.DescribeInstanceInput{
InstanceId: aws.String(d.Id()),
}
instance, err := FindInstanceByID(ctx, conn, d.Id())

log.Printf("[DEBUG] Reading Connect Instance %s", d.Id())
output, err := conn.DescribeInstanceWithContext(ctx, &input)

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, connect.ErrCodeResourceNotFoundException) {
if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] Connect Instance (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return diag.FromErr(fmt.Errorf("error reading Connect Instance (%s): %s", d.Id(), err))
return diag.Errorf("reading Connect Instance (%s): %s", d.Id(), err)
}

instance := output.Instance

d.SetId(aws.StringValue(instance.Id))
d.Set("arn", instance.Arn)
d.Set("created_time", instance.CreatedTime.Format(time.RFC3339))
if instance.CreatedTime != nil {
d.Set("created_time", instance.CreatedTime.Format(time.RFC3339))
}
d.Set("identity_management_type", instance.IdentityManagementType)
d.Set("inbound_calls_enabled", instance.InboundCallsEnabled)
d.Set("instance_alias", instance.InstanceAlias)
d.Set("outbound_calls_enabled", instance.OutboundCallsEnabled)
d.Set("service_role", instance.ServiceRole)
d.Set("status", instance.InstanceStatus)

for att := range InstanceAttributeMapping() {
value, err := resourceInstanceReadAttribute(ctx, conn, d.Id(), att)
for attributeType, key := range InstanceAttributeMapping() {
input := &connect.DescribeInstanceAttributeInput{
AttributeType: aws.String(attributeType),
InstanceId: aws.String(d.Id()),
}

output, err := conn.DescribeInstanceAttributeWithContext(ctx, input)

if err != nil {
return diag.FromErr(fmt.Errorf("error reading Connect instance (%s) attribute (%s): %s", d.Id(), att, err))
return diag.Errorf("reading Connect Instance (%s) attribute (%s): %s", d.Id(), attributeType, err)
}
d.Set(InstanceAttributeMapping()[att], value)

v, err := strconv.ParseBool(aws.StringValue(output.Attribute.Value))

if err != nil {
return diag.FromErr(err)
}

d.Set(key, v)
}

return nil
}

func resourceInstanceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).ConnectConn()

input := &connect.DeleteInstanceInput{
InstanceId: aws.String(d.Id()),
for attributeType, key := range InstanceAttributeMapping() {
if !d.HasChange(key) {
continue
}

if err := updateInstanceAttribute(ctx, conn, d.Id(), attributeType, strconv.FormatBool(d.Get(key).(bool))); err != nil {
return diag.FromErr(err)
}
}

log.Printf("[DEBUG] Deleting Connect Instance %s", d.Id())
return nil
}

func resourceInstanceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).ConnectConn()

_, err := conn.DeleteInstanceWithContext(ctx, input)
log.Printf("[DEBUG] Deleting Connect Instance: %s", d.Id())
_, err := conn.DeleteInstanceWithContext(ctx, &connect.DeleteInstanceInput{
InstanceId: aws.String(d.Id()),
})

if tfawserr.ErrCodeEquals(err, connect.ErrCodeResourceNotFoundException) {
return nil
}

if err != nil {
return diag.FromErr(fmt.Errorf("error deleting Connect Instance (%s): %s", d.Id(), err))
return diag.Errorf("deleting Connect Instance (%s): %s", d.Id(), err)
}

if _, err := waitInstanceDeleted(ctx, conn, d.Timeout(schema.TimeoutCreate), d.Id()); err != nil {
return diag.FromErr(fmt.Errorf("error waiting for Connect Instance deletion (%s): %s", d.Id(), err))
if _, err := waitInstanceDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
return diag.Errorf("waiting for Connect Instance (%s) delete: %s", d.Id(), err)
}

return nil
}

func resourceInstanceUpdateAttribute(ctx context.Context, conn *connect.Connect, instanceID string, attributeType string, value string) error {
func updateInstanceAttribute(ctx context.Context, conn *connect.Connect, instanceID, attributeType, value string) error {
input := &connect.UpdateInstanceAttributeInput{
InstanceId: aws.String(instanceID),
AttributeType: aws.String(attributeType),
InstanceId: aws.String(instanceID),
Value: aws.String(value),
}

_, err := conn.UpdateInstanceAttributeWithContext(ctx, input)

return err
if tfawserr.ErrCodeEquals(err, ErrCodeAccessDeniedException) || tfawserr.ErrMessageContains(err, ErrCodeAccessDeniedException, "not authorized to update") {
return nil
}

if err != nil {
return fmt.Errorf("updating Connect Instance (%s) attribute (%s): %w", instanceID, attributeType, err)
}

return nil
}

func resourceInstanceReadAttribute(ctx context.Context, conn *connect.Connect, instanceID string, attributeType string) (bool, error) {
input := &connect.DescribeInstanceAttributeInput{
InstanceId: aws.String(instanceID),
AttributeType: aws.String(attributeType),
func FindInstanceByID(ctx context.Context, conn *connect.Connect, id string) (*connect.Instance, error) {
input := &connect.DescribeInstanceInput{
InstanceId: aws.String(id),
}

output, err := conn.DescribeInstanceWithContext(ctx, input)

if tfawserr.ErrCodeEquals(err, connect.ErrCodeResourceNotFoundException) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

output, err := conn.DescribeInstanceAttributeWithContext(ctx, input)
if err != nil {
return false, err
return nil, err
}

if output == nil || output.Instance == nil {
return nil, tfresource.NewEmptyResultError(input)
}

return output.Instance, nil
}

func statusInstance(ctx context.Context, conn *connect.Connect, id string) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := FindInstanceByID(ctx, conn, id)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

return output, aws.StringValue(output.InstanceStatus), nil
}
}

func waitInstanceCreated(ctx context.Context, conn *connect.Connect, id string, timeout time.Duration) (*connect.Instance, error) {
stateConf := &retry.StateChangeConf{
Pending: []string{connect.InstanceStatusCreationInProgress},
Target: []string{connect.InstanceStatusActive},
Refresh: statusInstance(ctx, conn, id),
Timeout: timeout,
}

outputRaw, err := stateConf.WaitForStateContext(ctx)

if output, ok := outputRaw.(*connect.Instance); ok {
if output.StatusReason != nil {
tfresource.SetLastError(err, errors.New(aws.StringValue(output.StatusReason.Message)))
}
return output, err
}

return nil, err
}

func waitInstanceDeleted(ctx context.Context, conn *connect.Connect, id string, timeout time.Duration) (*connect.Instance, error) {
stateConf := &retry.StateChangeConf{
Pending: []string{connect.InstanceStatusActive},
Target: []string{},
Refresh: statusInstance(ctx, conn, id),
Timeout: timeout,
}
result, parseerr := strconv.ParseBool(*output.Attribute.Value)
return result, parseerr

outputRaw, err := stateConf.WaitForStateContext(ctx)

if output, ok := outputRaw.(*connect.Instance); ok {
return output, err
}

return nil, err
}
25 changes: 7 additions & 18 deletions internal/service/connect/instance_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package connect
import (
"context"
"fmt"
"log"
"strconv"
"time"

Expand Down Expand Up @@ -93,25 +92,14 @@ func dataSourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta in
var matchedInstance *connect.Instance

if v, ok := d.GetOk("instance_id"); ok {
instanceId := v.(string)

input := connect.DescribeInstanceInput{
InstanceId: aws.String(instanceId),
}

log.Printf("[DEBUG] Reading Connect Instance by instance_id: %s", input)

output, err := conn.DescribeInstanceWithContext(ctx, &input)
instanceID := v.(string)
instance, err := FindInstanceByID(ctx, conn, instanceID)

if err != nil {
return diag.FromErr(fmt.Errorf("error getting Connect Instance by instance_id (%s): %w", instanceId, err))
return diag.Errorf("reading Connect Instance (%s): %s", instanceID, err)
}

if output == nil {
return diag.FromErr(fmt.Errorf("error getting Connect Instance by instance_id (%s): empty output", instanceId))
}

matchedInstance = output.Instance
matchedInstance = instance
} else if v, ok := d.GetOk("instance_alias"); ok {
instanceAlias := v.(string)

Expand Down Expand Up @@ -143,9 +131,10 @@ func dataSourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta in
}

d.SetId(aws.StringValue(matchedInstance.Id))

d.Set("arn", matchedInstance.Arn)
d.Set("created_time", matchedInstance.CreatedTime.Format(time.RFC3339))
if matchedInstance.CreatedTime != nil {
d.Set("created_time", matchedInstance.CreatedTime.Format(time.RFC3339))
}
d.Set("identity_management_type", matchedInstance.IdentityManagementType)
d.Set("inbound_calls_enabled", matchedInstance.InboundCallsEnabled)
d.Set("instance_alias", matchedInstance.InstanceAlias)
Expand Down
Loading