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

elasticache: Allow for tagging retries #36310

Merged
merged 7 commits into from
Mar 12, 2024
Merged
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/36310.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_elasticache_replication_group: Fix bugs causing errors like `InvalidReplicationGroupState: Cluster not in available state to perform tagging operations.`
```
27 changes: 24 additions & 3 deletions internal/generate/tags/main.go
Original file line number Diff line number Diff line change
@@ -54,6 +54,10 @@ var (
listTagsOp = flag.String("ListTagsOp", "ListTagsForResource", "listTagsOp")
listTagsOpPaginated = flag.Bool("ListTagsOpPaginated", false, "whether ListTagsOp is paginated")
listTagsOutTagsElem = flag.String("ListTagsOutTagsElem", "Tags", "listTagsOutTagsElem")
retryTagsListTagsType = flag.String("RetryTagsListTagsType", "", "type of the first ListTagsOp return value such as TagListMessage")
retryTagsErrorCodes = flag.String("RetryTagsErrorCodes", "", "comma-separated list of error codes to retry, must be used with RetryTagsListTagsType and same length as RetryTagsErrorMessages")
retryTagsErrorMessages = flag.String("RetryTagsErrorMessages", "", "comma-separated list of error messages to retry, must be used with RetryTagsListTagsType and same length as RetryTagsErrorCodes")
retryTagsTimeout = flag.Duration("RetryTagsTimeout", 1*time.Minute, "Timeout for retrying tag operations")
setTagsOutFunc = flag.String("SetTagsOutFunc", "setTagsOut", "setTagsOutFunc")
tagInCustomVal = flag.String("TagInCustomVal", "", "tagInCustomVal")
tagInIDElem = flag.String("TagInIDElem", "ResourceArn", "tagInIDElem")
@@ -171,7 +175,11 @@ type TemplateData struct {
ListTagsOutTagsElem string
ParentNotFoundErrCode string
ParentNotFoundErrMsg string
RetryCreateOnNotFound string
RetryCreateOnNotFound string // is this used?
RetryTagsListTagsType string
RetryTagsErrorCodes []string
RetryTagsErrorMessages []string
RetryTagsTimeout string
ServiceTagsMap bool
SetTagsOutFunc string
TagInCustomVal string
@@ -295,6 +303,15 @@ func main() {
}
}

var cleanRetryErrorCodes []string
for _, c := range strings.Split(*retryTagsErrorCodes, ",") {
if strings.HasPrefix(c, fmt.Sprintf("%s.", servicePackage)) || strings.HasPrefix(c, "types.") {
cleanRetryErrorCodes = append(cleanRetryErrorCodes, c)
} else {
cleanRetryErrorCodes = append(cleanRetryErrorCodes, fmt.Sprintf(`"%s"`, c))
}
}

templateData := TemplateData{
AWSService: awsPkg,
AWSServiceIfacePackage: awsIntfPkg,
@@ -312,8 +329,8 @@ func main() {
SkipServiceImp: *skipServiceImp,
SkipTypesImp: *skipTypesImp,
TfLogPkg: *updateTags,
TfResourcePkg: (*getTag || *waitForPropagation),
TimePkg: *waitForPropagation,
TfResourcePkg: *getTag || *waitForPropagation || *retryTagsListTagsType != "",
TimePkg: *waitForPropagation || *retryTagsListTagsType != "",

CreateTagsFunc: createTagsFunc,
GetTagFunc: *getTagFunc,
@@ -329,6 +346,10 @@ func main() {
ListTagsOutTagsElem: *listTagsOutTagsElem,
ParentNotFoundErrCode: *parentNotFoundErrCode,
ParentNotFoundErrMsg: *parentNotFoundErrMsg,
RetryTagsListTagsType: *retryTagsListTagsType,
RetryTagsErrorCodes: cleanRetryErrorCodes,
RetryTagsErrorMessages: strings.Split(*retryTagsErrorMessages, ","),
RetryTagsTimeout: formatDuration(*retryTagsTimeout),
ServiceTagsMap: *serviceTagsMap,
SetTagsOutFunc: *setTagsOutFunc,
TagInCustomVal: *tagInCustomVal,
18 changes: 18 additions & 0 deletions internal/generate/tags/templates/v1/get_tag_body.tmpl
Original file line number Diff line number Diff line change
@@ -23,7 +23,25 @@ func {{ .GetTagFunc }}(ctx context.Context, conn {{ .ClientType }}, identifier{{
},
}

{{ if .RetryTagsListTagsType }}
output, err := tfresource.RetryGWhenMessageContains(ctx, {{ .RetryTagsTimeout }},
func() (*{{ .TagPackage }}.{{ .RetryTagsListTagsType }}, error) {
return conn.{{ .ListTagsOp }}WithContext(ctx, input)
},
[]string{
{{- range .RetryTagsErrorCodes }}
{{ . }},
{{- end }}
},
[]string{
{{- range .RetryTagsErrorMessages }}
"{{ . }}",
{{- end }}
},
)
{{ else }}
output, err := conn.{{ .ListTagsOp }}WithContext(ctx, input)
{{- end }}

if err != nil {
return nil, err
53 changes: 53 additions & 0 deletions internal/generate/tags/templates/v1/list_tags_body.tmpl
Original file line number Diff line number Diff line change
@@ -28,6 +28,39 @@ func {{ .ListTagsFunc }}(ctx context.Context, conn {{ .ClientType }}, identifier
var output []*{{ .TagPackage }}.{{ or .TagType2 .TagType }}
{{- end }}

{{ if .RetryTagsListTagsType }}
_, err := tfresource.RetryWhenMessageContains(ctx, {{ .RetryTagsTimeout }},
func() (string, error) {
return "", conn.{{ .ListTagsOp }}PagesWithContext(ctx, input, func(page *{{ .TagPackage }}.{{ .ListTagsOp }}Output, lastPage bool) bool {
if page == nil {
return !lastPage
}

{{ if .ServiceTagsMap }}
maps.Copy(output, page.{{ .ListTagsOutTagsElem }})
{{- else }}
for _, v := range page.{{ .ListTagsOutTagsElem }} {
if v != nil {
output = append(output, v)
}
}
{{- end }}

return !lastPage
})
},
[]string{
{{- range .RetryTagsErrorCodes }}
{{ . }},
{{- end }}
},
[]string{
{{- range .RetryTagsErrorMessages }}
"{{ . }}",
{{- end }}
},
)
{{ else }}
err := conn.{{ .ListTagsOp }}PagesWithContext(ctx, input, func(page *{{ .TagPackage }}.{{ .ListTagsOp }}Output, lastPage bool) bool {
if page == nil {
return !lastPage
@@ -45,9 +78,29 @@ func {{ .ListTagsFunc }}(ctx context.Context, conn {{ .ClientType }}, identifier

return !lastPage
})
{{- end }}
{{ else }}
{{- if .RetryTagsListTagsType }}

output, err := tfresource.RetryGWhenMessageContains(ctx, {{ .RetryTagsTimeout }},
func() (*{{ .TagPackage }}.{{ .RetryTagsListTagsType }}, error) {
return conn.{{ .ListTagsOp }}WithContext(ctx, input)
},
[]string{
{{- range .RetryTagsErrorCodes }}
{{ . }},
{{- end }}
},
[]string{
{{- range .RetryTagsErrorMessages }}
"{{ . }}",
{{- end }}
},
)
{{- else }}

output, err := conn.{{ .ListTagsOp }}WithContext(ctx, input)
{{- end }}
{{- end }}

{{ if and ( .ParentNotFoundErrCode ) ( .ParentNotFoundErrMsg ) }}
54 changes: 54 additions & 0 deletions internal/generate/tags/templates/v1/update_tags_body.tmpl
Original file line number Diff line number Diff line change
@@ -60,7 +60,25 @@ func {{ .UpdateTagsFunc }}(ctx context.Context, conn {{ .ClientType }}, identifi
{{- end }}
}

{{ if .RetryTagsListTagsType }}
_, err := tfresource.RetryWhenMessageContains(ctx, {{ .RetryTagsTimeout }},
func() (any, error) {
return conn.{{ .TagOp }}WithContext(ctx, input)
},
[]string{
{{- range .RetryTagsErrorCodes }}
{{ . }},
{{- end }}
},
[]string{
{{- range .RetryTagsErrorMessages }}
"{{ . }}",
{{- end }}
},
)
{{ else }}
_, err := conn.{{ .TagOp }}WithContext(ctx, input)
{{- end }}

if err != nil {
return fmt.Errorf("tagging resource (%s): %w", identifier, err)
@@ -100,7 +118,25 @@ func {{ .UpdateTagsFunc }}(ctx context.Context, conn {{ .ClientType }}, identifi
{{- end }}
}

{{ if .RetryTagsListTagsType }}
_, err := tfresource.RetryWhenMessageContains(ctx, {{ .RetryTagsTimeout }},
func() (any, error) {
return conn.{{ .UntagOp }}WithContext(ctx, input)
},
[]string{
{{- range .RetryTagsErrorCodes }}
{{ . }},
{{- end }}
},
[]string{
{{- range .RetryTagsErrorMessages }}
"{{ . }}",
{{- end }}
},
)
{{ else }}
_, err := conn.{{ .UntagOp }}WithContext(ctx, input)
{{- end }}

if err != nil {
return fmt.Errorf("untagging resource (%s): %w", identifier, err)
@@ -138,7 +174,25 @@ func {{ .UpdateTagsFunc }}(ctx context.Context, conn {{ .ClientType }}, identifi
{{- end }}
}

{{ if .RetryTagsListTagsType }}
_, err := tfresource.RetryWhenMessageContains(ctx, {{ .RetryTagsTimeout }},
func() (any, error) {
return conn.{{ .TagOp }}WithContext(ctx, input)
},
[]string{
{{- range .RetryTagsErrorCodes }}
{{ . }},
{{- end }}
},
[]string{
{{- range .RetryTagsErrorMessages }}
"{{ . }}",
{{- end }}
},
)
{{ else }}
_, err := conn.{{ .TagOp }}WithContext(ctx, input)
{{- end }}

if err != nil {
return fmt.Errorf("tagging resource (%s): %w", identifier, err)
2 changes: 1 addition & 1 deletion internal/service/elasticache/generate.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

//go:generate go run ../../generate/tags/main.go -ListTags -ListTagsInIDElem=ResourceName -ListTagsOutTagsElem=TagList -ServiceTagsSlice -TagOp=AddTagsToResource -TagInIDElem=ResourceName -UntagOp=RemoveTagsFromResource -UpdateTags -CreateTags
//go:generate go run ../../generate/tags/main.go -ListTags -ListTagsInIDElem=ResourceName -ListTagsOutTagsElem=TagList -ServiceTagsSlice -TagOp=AddTagsToResource -TagInIDElem=ResourceName -UntagOp=RemoveTagsFromResource -UpdateTags -CreateTags -RetryTagsListTagsType=TagListMessage -RetryTagsErrorCodes=elasticache.ErrCodeInvalidReplicationGroupStateFault "-RetryTagsErrorMessages=not in available state" -RetryTagsTimeout=15m
//go:generate go run ../../generate/tags/main.go -AWSSDKVersion=2 -TagsFunc=TagsV2 -KeyValueTagsFunc=keyValueTagsV2 -GetTagsInFunc=getTagsInV2 -SetTagsOutFunc=setTagsOutV2 -SkipAWSServiceImp -KVTValues -ServiceTagsSlice -- tagsv2_gen.go
//go:generate go run ../../generate/servicepackage/main.go
// ONLY generate directives and package declaration! Do not add anything else to this file.
16 changes: 14 additions & 2 deletions internal/service/elasticache/replication_group.go
Original file line number Diff line number Diff line change
@@ -862,7 +862,13 @@ func resourceReplicationGroupUpdate(ctx context.Context, d *schema.ResourceData,
}

if requestUpdate {
_, err := conn.ModifyReplicationGroupWithContext(ctx, input)
// tagging may cause this resource to not yet be available, so wait for it to be available
_, err := WaitReplicationGroupAvailable(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate))
if err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for ElastiCache Replication Group (%s) to update: %s", d.Id(), err)
}

_, err = conn.ModifyReplicationGroupWithContext(ctx, input)
if err != nil {
return sdkdiag.AppendErrorf(diags, "updating ElastiCache Replication Group (%s): %s", d.Id(), err)
}
@@ -881,7 +887,13 @@ func resourceReplicationGroupUpdate(ctx context.Context, d *schema.ResourceData,
AuthToken: aws.String(d.Get("auth_token").(string)),
}

_, err := conn.ModifyReplicationGroupWithContext(ctx, params)
// tagging may cause this resource to not yet be available, so wait for it to be available
_, err := WaitReplicationGroupAvailable(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate))
if err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for ElastiCache Replication Group (%s) to update: %s", d.Id(), err)
}

_, err = conn.ModifyReplicationGroupWithContext(ctx, params)
if err != nil {
return sdkdiag.AppendErrorf(diags, "changing auth_token for ElastiCache Replication Group (%s): %s", d.Id(), err)
}
38 changes: 35 additions & 3 deletions internal/service/elasticache/tags_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions internal/tfresource/retry.go
Original file line number Diff line number Diff line change
@@ -57,6 +57,41 @@ func RetryWhen(ctx context.Context, timeout time.Duration, f func() (interface{}
return output, nil
}

// RetryGWhen is the generic version of RetryWhen which obviates the need for a type
// assertion after the call. It retries the function `f` when the error it returns
// satisfies `retryable`. `f` is retried until `timeout` expires.
func RetryGWhen[T any](ctx context.Context, timeout time.Duration, f func() (*T, error), retryable Retryable) (*T, error) {
var output *T

err := Retry(ctx, timeout, func() *retry.RetryError {
var err error
var again bool

output, err = f()
again, err = retryable(err)

if again {
return retry.RetryableError(err)
}

if err != nil {
return retry.NonRetryableError(err)
}

return nil
})

if TimedOut(err) {
output, err = f()
}

if err != nil {
return nil, err
}

return output, nil
}

// RetryWhenAWSErrCodeEquals retries the specified function when it returns one of the specified AWS error codes.
func RetryWhenAWSErrCodeEquals(ctx context.Context, timeout time.Duration, f func() (interface{}, error), codes ...string) (interface{}, error) { // nosemgrep:ci.aws-in-func-name
return RetryWhen(ctx, timeout, f, func(err error) (bool, error) {
@@ -90,6 +125,32 @@ func RetryWhenAWSErrMessageContains(ctx context.Context, timeout time.Duration,
})
}

// RetryWhenMessageContains retries the specified function when it returns an error containing any of the specified messages.
func RetryWhenMessageContains(ctx context.Context, timeout time.Duration, f func() (interface{}, error), codes []string, messages []string) (interface{}, error) {
return RetryWhen(ctx, timeout, f, func(err error) (bool, error) {
for i, message := range messages {
if tfawserr.ErrMessageContains(err, codes[i], message) || tfawserr_sdkv2.ErrMessageContains(err, codes[i], message) {
return true, err
}
}

return false, err
})
}

// RetryGWhenMessageContains retries the specified function when it returns an error containing any of the specified messages.
func RetryGWhenMessageContains[T any](ctx context.Context, timeout time.Duration, f func() (*T, error), codes []string, messages []string) (*T, error) {
return RetryGWhen(ctx, timeout, f, func(err error) (bool, error) {
for i, message := range messages {
if tfawserr.ErrMessageContains(err, codes[i], message) || tfawserr_sdkv2.ErrMessageContains(err, codes[i], message) {
return true, err
}
}

return false, err
})
}

func RetryWhenIsA[T error](ctx context.Context, timeout time.Duration, f func() (interface{}, error)) (interface{}, error) {
return RetryWhen(ctx, timeout, f, func(err error) (bool, error) {
if errs.IsA[T](err) {