Skip to content

Commit 790f80b

Browse files
authored
Merge pull request #47568 from hashicorp/b-ecs-express-gateway-service-current-deployment
resource/aws_ecs_express_gateway_service: Various fixes and test fixes
2 parents 98ab15b + 28e9fe6 commit 790f80b

10 files changed

Lines changed: 324 additions & 57 deletions

File tree

.changelog/47568.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
```release-note:bug
2+
resource/aws_ecs_express_gateway_service: Waits until the service is `INACTIVE` instead of `DRAINING`.
3+
```
4+
5+
```release-note:bug
6+
resource/aws_ecs_express_gateway_service: Handles more transient API errors during creation and deletion.
7+
```
8+
9+
```release-note:bug
10+
resource/aws_ecs_express_gateway_service: Marks resource for re-creation if it fails while waiting for creation.
11+
```
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright IBM Corp. 2014, 2026
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue
5+
6+
import (
7+
"fmt"
8+
"regexp"
9+
10+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
11+
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
12+
)
13+
14+
var _ knownvalue.Check = regionalHostnameOnDotAWSRegexp{}
15+
16+
type regionalHostnameOnDotAWSRegexp struct {
17+
check string
18+
region string
19+
service string
20+
hostnameRegexp *regexp.Regexp
21+
}
22+
23+
// CheckValue determines whether the passed value is of type string, and
24+
// contains a matching sequence of bytes.
25+
func (v regionalHostnameOnDotAWSRegexp) CheckValue(other any) error {
26+
otherVal, ok := other.(string)
27+
28+
if !ok {
29+
return fmt.Errorf("expected string value for %s check, got: %T", v.check, other)
30+
}
31+
32+
re, err := regexp.Compile(v.buildHostnameString())
33+
if err != nil {
34+
return fmt.Errorf("unable to compile hostname regexp (%s): %w", v.buildHostnameString(), err)
35+
}
36+
37+
if !re.MatchString(otherVal) {
38+
return fmt.Errorf("expected regex match %s for %s check, got: %s", re.String(), v.check, otherVal)
39+
}
40+
41+
return nil
42+
}
43+
44+
// String returns the string representation of the value.
45+
func (v regionalHostnameOnDotAWSRegexp) String() string {
46+
return v.buildHostnameString()
47+
}
48+
49+
func (v regionalHostnameOnDotAWSRegexp) buildHostnameString() string {
50+
return fmt.Sprintf(`^%s\.%s\.%s\.%s$`, v.hostnameRegexp.String(), v.service, v.region, "on.aws")
51+
}
52+
53+
func RegionalHostnameOnDotAWSRegexp(service string, hostname *regexp.Regexp) knownvalue.Check { // nosemgrep:ci.aws-in-func-name
54+
return regionalHostnameOnDotAWSRegexp{
55+
check: "RegionalHostnameOnDotAWSRegexp",
56+
region: acctest.Region(),
57+
service: service,
58+
hostnameRegexp: hostname,
59+
}
60+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright IBM Corp. 2014, 2026
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue
5+
6+
import (
7+
"fmt"
8+
"regexp"
9+
10+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
11+
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
12+
"github.com/hashicorp/terraform-provider-aws/names"
13+
)
14+
15+
var _ knownvalue.Check = regionalHostnameRegexp{}
16+
17+
type regionalHostnameRegexp struct {
18+
check string
19+
region string
20+
service string
21+
hostnameRegexp *regexp.Regexp
22+
}
23+
24+
// CheckValue determines whether the passed value is of type string, and
25+
// contains a matching sequence of bytes.
26+
func (v regionalHostnameRegexp) CheckValue(other any) error {
27+
otherVal, ok := other.(string)
28+
29+
if !ok {
30+
return fmt.Errorf("expected string value for %s check, got: %T", v.check, other)
31+
}
32+
33+
re, err := regexp.Compile(v.buildHostnameString())
34+
if err != nil {
35+
return fmt.Errorf("unable to compile hostname regexp (%s): %w", v.buildHostnameString(), err)
36+
}
37+
38+
if !re.MatchString(otherVal) {
39+
return fmt.Errorf("expected regex match %s for %s check, got: %s", re.String(), v.check, otherVal)
40+
}
41+
42+
return nil
43+
}
44+
45+
// String returns the string representation of the value.
46+
func (v regionalHostnameRegexp) String() string {
47+
return v.buildHostnameString()
48+
}
49+
50+
func (v regionalHostnameRegexp) buildHostnameString() string {
51+
return fmt.Sprintf(`^%s\.%s\.%s\.%s$`, v.hostnameRegexp.String(), v.service, v.region, names.PartitionForRegion(v.region).DNSSuffix())
52+
}
53+
54+
func RegionalHostnameRegexp(service string, hostname *regexp.Regexp) knownvalue.Check {
55+
return regionalHostnameRegexp{
56+
check: "RegionalHostnameRegexp",
57+
region: acctest.Region(),
58+
service: service,
59+
hostnameRegexp: hostname,
60+
}
61+
}

internal/service/ecs/express_gateway_service.go

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ func (r *expressGatewayServiceResource) Create(ctx context.Context, req resource
248248
return
249249
}
250250

251+
// Set a state value to trigger replacement on error
252+
resp.State.SetAttribute(ctx, path.Root("service_arn"), out.Service.ServiceArn)
253+
251254
serviceARN := aws.ToString(out.Service.ServiceArn)
252255
createTimeout := r.CreateTimeout(ctx, plan.Timeouts)
253256
var waitOut *awstypes.ECSExpressGatewayService
@@ -376,8 +379,7 @@ func (r *expressGatewayServiceResource) Update(ctx context.Context, req resource
376379
conn := r.Meta().ECSClient(ctx)
377380

378381
diff, d := fwflex.Diff(ctx, plan, state, fwflex.WithIgnoredField("active_configurations"), fwflex.WithIgnoredField("current_deployment"),
379-
fwflex.WithIgnoredField("scaling_target"), fwflex.WithIgnoredField(names.AttrTags), fwflex.WithIgnoredField(names.AttrTags),
380-
fwflex.WithIgnoredField(names.AttrTagsAll))
382+
fwflex.WithIgnoredField("scaling_target"), fwflex.WithIgnoredField(names.AttrTags), fwflex.WithIgnoredField(names.AttrTagsAll))
381383
smerr.AddEnrich(ctx, &resp.Diagnostics, d)
382384
if resp.Diagnostics.HasError() {
383385
return
@@ -487,12 +489,12 @@ func (r *expressGatewayServiceResource) Delete(ctx context.Context, req resource
487489
_, err := conn.DeleteExpressGatewayService(ctx, &input)
488490
if err != nil {
489491
if errs.IsAErrorMessageContains[*awstypes.InvalidParameterException](err, "Resource not found") ||
490-
errs.IsAErrorMessageContains[*awstypes.ServiceNotActiveException](err, "Cannot perform this operation on a service in INACTIVE status") ||
491-
errs.IsAErrorMessageContains[*awstypes.ServiceNotActiveException](err, "Service is in DRAINING status") {
492+
errs.IsAErrorMessageContains[*awstypes.ServiceNotActiveException](err, "Cannot perform this operation on a service in INACTIVE status") {
492493
// Service was already deleted/inactive/draining - deletion is already in progress or complete
493494
return
494-
} else {
495-
// Real error occurred
495+
} else if !errs.IsAErrorMessageContains[*awstypes.ServiceNotActiveException](err, "Service is in DRAINING status") {
496+
// Real error occurred.
497+
// If service is in DRAINING status, fall-through to wait for it to become INACTIVE
496498
smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, serviceARN)
497499
return
498500
}
@@ -553,8 +555,8 @@ func waitExpressGatewayServiceStable(ctx context.Context, conn *ecs.Client, gate
553555

554556
func waitExpressGatewayServiceInactive(ctx context.Context, conn *ecs.Client, id string, timeout time.Duration) (*awstypes.ECSExpressGatewayService, error) {
555557
stateConf := &sdkretry.StateChangeConf{
556-
Pending: []string{gatewayServiceStatusActive},
557-
Target: []string{gatewayServiceStatusInactive, gatewayServiceStatusDraining},
558+
Pending: []string{gatewayServiceStatusActive, gatewayServiceStatusDraining},
559+
Target: []string{gatewayServiceStatusInactive},
558560
Refresh: statusExpressGatewayServiceForDeletion(ctx, conn, id),
559561
Timeout: timeout,
560562
MinTimeout: 1 * time.Second,
@@ -587,15 +589,8 @@ func statusExpressGatewayServiceForDeletion(ctx context.Context, conn *ecs.Clien
587589
return func() (any, string, error) {
588590
output, err := findExpressGatewayServiceNoTagsByARN(ctx, conn, gatewayServiceARN)
589591
if err != nil {
590-
if retry.NotFound(err) || errs.IsAErrorMessageContains[*awstypes.InvalidParameterException](err, "Resource not found") ||
591-
errs.IsAErrorMessageContains[*awstypes.ServiceNotActiveException](err, "Cannot perform this operation on a service in INACTIVE status") {
592-
mockService := &awstypes.ECSExpressGatewayService{
593-
ServiceArn: aws.String(gatewayServiceARN),
594-
Status: &awstypes.ExpressGatewayServiceStatus{
595-
StatusCode: awstypes.ExpressGatewayServiceStatusCodeInactive,
596-
},
597-
}
598-
return mockService, gatewayServiceStatusInactive, nil
592+
if retry.NotFound(err) || errs.IsAErrorMessageContains[*awstypes.InvalidParameterException](err, "Resource not found") {
593+
return nil, "", nil
599594
}
600595
return nil, "", smarterr.NewError(err)
601596
}
@@ -950,14 +945,7 @@ func retryExpressGatewayServiceCreate(ctx context.Context, conn *ecs.Client, inp
950945
func(ctx context.Context) (any, error) {
951946
return conn.CreateExpressGatewayService(ctx, input)
952947
},
953-
func(err error) (bool, error) {
954-
if errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "Cannot assume role") ||
955-
errs.IsAErrorMessageContains[*awstypes.ClientException](err, "AWS was not able to validate the provided access credentials") ||
956-
errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "is not authorized to perform: sts:AssumeRole") {
957-
return true, err
958-
}
959-
return false, err
960-
},
948+
expressGatewayRetryable,
961949
)
962950
if err != nil {
963951
return nil, err
@@ -974,17 +962,29 @@ func retryExpressGatewayServiceUpdate(ctx context.Context, conn *ecs.Client, inp
974962
func(ctx context.Context) (any, error) {
975963
return conn.UpdateExpressGatewayService(ctx, input)
976964
},
977-
func(err error) (bool, error) {
978-
if errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "Cannot assume role") ||
979-
errs.IsAErrorMessageContains[*awstypes.ClientException](err, "AWS was not able to validate the provided access credentials") ||
980-
errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "is not authorized to perform: sts:AssumeRole") {
981-
return true, err
982-
}
983-
return false, err
984-
},
965+
expressGatewayRetryable,
985966
)
986967
if err != nil {
987968
return nil, err
988969
}
989970
return outputRaw.(*ecs.UpdateExpressGatewayServiceOutput), nil
990971
}
972+
973+
func expressGatewayRetryable(err error) (bool, error) {
974+
if errs.Contains(err, "is not authorized to perform") || // This message can occur with at least AccessDeniedException, ClientException, and InvalidParameterException
975+
errs.Contains(err, "AWS was not able to validate the provided access credentials") || // This message can occur with at least ClientException and InvalidParameterException
976+
errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "Cannot assume role") ||
977+
errs.IsAErrorMessageContains[*awstypes.ClientException](err, "The security token included in the request is invalid") {
978+
return true, err
979+
}
980+
return false, err
981+
}
982+
983+
// newListExpressGatewayServicesPaginator returns a new paginator for ListServices that only returns Customer-managed Services.
984+
func newListExpressGatewayServicesPaginator(conn *ecs.Client, input *ecs.ListServicesInput) *ecs.ListServicesPaginator {
985+
return ecs.NewListServicesPaginator(conn, &ecs.ListServicesInput{
986+
Cluster: input.Cluster,
987+
LaunchType: input.LaunchType,
988+
ResourceManagementType: awstypes.ResourceManagementTypeEcs,
989+
})
990+
}

0 commit comments

Comments
 (0)