From 7e0a0780c60df6b971155bf45223ccdb556318a2 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 22 Jun 2020 10:29:59 -0400 Subject: [PATCH 1/2] internal/keyvaluetags: Implement support for additional tag information, fix nil value panics, implement autoscaling service tags Reference: https://github.com/terraform-providers/terraform-provider-aws/issues/12368 Reference: https://github.com/terraform-providers/terraform-provider-aws/issues/13808 --- .../generators/createtags/main.go | 19 +- .../keyvaluetags/generators/gettag/main.go | 19 +- .../keyvaluetags/generators/listtags/main.go | 16 +- .../generators/servicetags/main.go | 217 ++++++- .../generators/updatetags/main.go | 39 +- aws/internal/keyvaluetags/get_tag_gen.go | 31 + aws/internal/keyvaluetags/key_value_tags.go | 260 +++++++- .../keyvaluetags/key_value_tags_test.go | 582 ++++++++++++++++++ aws/internal/keyvaluetags/list_tags_gen.go | 23 + .../service_generation_customizations.go | 76 ++- aws/internal/keyvaluetags/service_tags_gen.go | 164 ++++- aws/internal/keyvaluetags/update_tags_gen.go | 35 ++ 12 files changed, 1403 insertions(+), 78 deletions(-) diff --git a/aws/internal/keyvaluetags/generators/createtags/main.go b/aws/internal/keyvaluetags/generators/createtags/main.go index 54c505be465..44672a3ded4 100644 --- a/aws/internal/keyvaluetags/generators/createtags/main.go +++ b/aws/internal/keyvaluetags/generators/createtags/main.go @@ -41,9 +41,10 @@ func main() { "TagInputCustomValue": keyvaluetags.ServiceTagInputCustomValue, "TagInputIdentifierField": keyvaluetags.ServiceTagInputIdentifierField, "TagInputIdentifierRequiresSlice": keyvaluetags.ServiceTagInputIdentifierRequiresSlice, - "TagInputResourceTypeField": keyvaluetags.ServiceTagInputResourceTypeField, "TagInputTagsField": keyvaluetags.ServiceTagInputTagsField, "TagPackage": keyvaluetags.ServiceTagPackage, + "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, + "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, "Title": strings.Title, } @@ -133,25 +134,27 @@ func isResourceTimeoutError(err error) bool { // {{ . | Title }}CreateTags creates {{ . }} service tags for new resources. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func {{ . | Title }}CreateTags(conn {{ . | ClientType }}, identifier string{{ if . | TagInputResourceTypeField }}, resourceType string{{ end }}, tagsMap interface{}) error { +func {{ . | Title }}CreateTags(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, tagsMap interface{}) error { tags := New(tagsMap) {{- if . | TagFunctionBatchSize }} for _, tags := range tags.Chunks({{ . | TagFunctionBatchSize }}) { {{- end }} input := &{{ . | TagPackage }}.{{ . | TagFunction }}Input{ + {{- if not ( . | TagTypeIdentifierField ) }} {{- if . | TagInputIdentifierRequiresSlice }} - {{ . | TagInputIdentifierField }}: aws.StringSlice([]string{identifier}), + {{ . | TagInputIdentifierField }}: aws.StringSlice([]string{identifier}), {{- else }} - {{ . | TagInputIdentifierField }}: aws.String(identifier), + {{ . | TagInputIdentifierField }}: aws.String(identifier), + {{- end }} + {{- if . | TagResourceTypeField }} + {{ . | TagResourceTypeField }}: aws.String(resourceType), {{- end }} - {{- if . | TagInputResourceTypeField }} - {{ . | TagInputResourceTypeField }}: aws.String(resourceType), {{- end }} {{- if . | TagInputCustomValue }} - {{ . | TagInputTagsField }}: {{ . | TagInputCustomValue }}, + {{ . | TagInputTagsField }}: {{ . | TagInputCustomValue }}, {{- else }} - {{ . | TagInputTagsField }}: tags.IgnoreAws().{{ . | Title }}Tags(), + {{ . | TagInputTagsField }}: tags.IgnoreAws().{{ . | Title }}Tags(), {{- end }} } diff --git a/aws/internal/keyvaluetags/generators/gettag/main.go b/aws/internal/keyvaluetags/generators/gettag/main.go index b01858bcf50..4c9b176abca 100644 --- a/aws/internal/keyvaluetags/generators/gettag/main.go +++ b/aws/internal/keyvaluetags/generators/gettag/main.go @@ -17,6 +17,7 @@ import ( const filename = `get_tag_gen.go` var serviceNames = []string{ + "autoscaling", "dynamodb", "ec2", "ecs", @@ -38,9 +39,11 @@ func main() { "ClientType": keyvaluetags.ServiceClientType, "ListTagsFunction": keyvaluetags.ServiceListTagsFunction, "ListTagsInputFilterIdentifierName": keyvaluetags.ServiceListTagsInputFilterIdentifierName, - "ListTagsInputResourceTypeField": keyvaluetags.ServiceListTagsInputResourceTypeField, "ListTagsOutputTagsField": keyvaluetags.ServiceListTagsOutputTagsField, "TagPackage": keyvaluetags.ServiceTagPackage, + "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, + "TagTypeAdditionalBoolFields": keyvaluetags.ServiceTagTypeAdditionalBoolFields, + "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, "Title": strings.Title, } @@ -97,7 +100,11 @@ import ( // This function will optimise the handling over {{ . | Title }}ListTags, if possible. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | ListTagsInputResourceTypeField }}, resourceType string{{ end }}, key string) (bool, *string, error) { +{{- if or ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields) }} +func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, key string) (bool, *TagData, error) { +{{- else }} +func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, key string) (bool, *string, error) { +{{- end }} {{- if . | ListTagsInputFilterIdentifierName }} input := &{{ . | TagPackage }}.{{ . | ListTagsFunction }}Input{ Filters: []*{{ . | TagPackage }}.Filter{ @@ -118,16 +125,20 @@ func {{ . | Title }}GetTag(conn {{ . | ClientType }}, identifier string{{ if . | return false, nil, err } - listTags := {{ . | Title }}KeyValueTags(output.{{ . | ListTagsOutputTagsField }}) + listTags := {{ . | Title }}KeyValueTags(output.{{ . | ListTagsOutputTagsField }}{{ if . | TagTypeIdentifierField }}, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}{{ end }}) {{- else }} - listTags, err := {{ . | Title }}ListTags(conn, identifier{{ if . | ListTagsInputResourceTypeField }}, resourceType{{ end }}) + listTags, err := {{ . | Title }}ListTags(conn, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}) if err != nil { return false, nil, err } {{- end }} + {{ if or ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields) }} + return listTags.KeyExists(key), listTags.KeyTagData(key), nil + {{- else }} return listTags.KeyExists(key), listTags.KeyValue(key), nil + {{- end }} } {{- end }} ` diff --git a/aws/internal/keyvaluetags/generators/listtags/main.go b/aws/internal/keyvaluetags/generators/listtags/main.go index bee18a8f225..aa1b14ba1a5 100644 --- a/aws/internal/keyvaluetags/generators/listtags/main.go +++ b/aws/internal/keyvaluetags/generators/listtags/main.go @@ -26,6 +26,7 @@ var serviceNames = []string{ "appstream", "appsync", "athena", + "autoscaling", "backup", "cloud9", "cloudfront", @@ -134,9 +135,10 @@ func main() { "ListTagsInputFilterIdentifierName": keyvaluetags.ServiceListTagsInputFilterIdentifierName, "ListTagsInputIdentifierField": keyvaluetags.ServiceListTagsInputIdentifierField, "ListTagsInputIdentifierRequiresSlice": keyvaluetags.ServiceListTagsInputIdentifierRequiresSlice, - "ListTagsInputResourceTypeField": keyvaluetags.ServiceListTagsInputResourceTypeField, "ListTagsOutputTagsField": keyvaluetags.ServiceListTagsOutputTagsField, "TagPackage": keyvaluetags.ServiceTagPackage, + "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, + "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, "Title": strings.Title, } @@ -190,7 +192,7 @@ import ( // {{ . | Title }}ListTags lists {{ . }} service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func {{ . | Title }}ListTags(conn {{ . | ClientType }}, identifier string{{ if . | ListTagsInputResourceTypeField }}, resourceType string{{ end }}) (KeyValueTags, error) { +func {{ . | Title }}ListTags(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}) (KeyValueTags, error) { input := &{{ . | TagPackage }}.{{ . | ListTagsFunction }}Input{ {{- if . | ListTagsInputFilterIdentifierName }} Filters: []*{{ . | TagPackage }}.Filter{ @@ -201,12 +203,12 @@ func {{ . | Title }}ListTags(conn {{ . | ClientType }}, identifier string{{ if . }, {{- else }} {{- if . | ListTagsInputIdentifierRequiresSlice }} - {{ . | ListTagsInputIdentifierField }}: aws.StringSlice([]string{identifier}), + {{ . | ListTagsInputIdentifierField }}: aws.StringSlice([]string{identifier}), {{- else }} - {{ . | ListTagsInputIdentifierField }}: aws.String(identifier), + {{ . | ListTagsInputIdentifierField }}: aws.String(identifier), {{- end }} - {{- if . | ListTagsInputResourceTypeField }} - {{ . | ListTagsInputResourceTypeField }}: aws.String(resourceType), + {{- if . | TagResourceTypeField }} + {{ . | TagResourceTypeField }}: aws.String(resourceType), {{- end }} {{- end }} } @@ -217,7 +219,7 @@ func {{ . | Title }}ListTags(conn {{ . | ClientType }}, identifier string{{ if . return New(nil), err } - return {{ . | Title }}KeyValueTags(output.{{ . | ListTagsOutputTagsField }}), nil + return {{ . | Title }}KeyValueTags(output.{{ . | ListTagsOutputTagsField }}{{ if . | TagTypeIdentifierField }}, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}{{ end }}), nil } {{- end }} ` diff --git a/aws/internal/keyvaluetags/generators/servicetags/main.go b/aws/internal/keyvaluetags/generators/servicetags/main.go index 85e35e44f4d..e618ab37777 100644 --- a/aws/internal/keyvaluetags/generators/servicetags/main.go +++ b/aws/internal/keyvaluetags/generators/servicetags/main.go @@ -22,7 +22,7 @@ var sliceServiceNames = []string{ "acmpca", "appmesh", "athena", - /* "autoscaling", // includes extra PropagateAtLaunch, skip for now */ + "autoscaling", "cloud9", "cloudformation", "cloudfront", @@ -154,13 +154,17 @@ func main() { SliceServiceNames: sliceServiceNames, } templateFuncMap := template.FuncMap{ - "TagKeyType": keyvaluetags.ServiceTagKeyType, - "TagPackage": keyvaluetags.ServiceTagPackage, - "TagType": keyvaluetags.ServiceTagType, - "TagType2": keyvaluetags.ServiceTagType2, - "TagTypeKeyField": keyvaluetags.ServiceTagTypeKeyField, - "TagTypeValueField": keyvaluetags.ServiceTagTypeValueField, - "Title": strings.Title, + "TagKeyType": keyvaluetags.ServiceTagKeyType, + "TagPackage": keyvaluetags.ServiceTagPackage, + "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, + "TagType": keyvaluetags.ServiceTagType, + "TagType2": keyvaluetags.ServiceTagType2, + "TagTypeAdditionalBoolFields": keyvaluetags.ServiceTagTypeAdditionalBoolFields, + "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, + "TagTypeKeyField": keyvaluetags.ServiceTagTypeKeyField, + "TagTypeValueField": keyvaluetags.ServiceTagTypeValueField, + "Title": strings.Title, + "ToSnakeCase": keyvaluetags.ToSnakeCase, } tmpl, err := template.New("servicetags").Funcs(templateFuncMap).Parse(templateBody) @@ -203,12 +207,15 @@ var templateBody = ` package keyvaluetags import ( + "strconv" + "github.com/aws/aws-sdk-go/aws" {{- range .SliceServiceNames }} {{- if eq . (. | TagPackage) }} "github.com/aws/aws-sdk-go/service/{{ . }}" {{- end }} {{- end }} + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) // map[string]*string handling @@ -228,6 +235,57 @@ func {{ . | Title }}KeyValueTags(tags map[string]*string) KeyValueTags { // []*SERVICE.Tag handling {{- range .SliceServiceNames }} +{{ if and ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields ) }} +// {{ . | Title }}ListOfMap returns a list of {{ . }} in flattened map. +// +// Compatible with setting Terraform state for strongly typed configuration blocks. +// +// This function strips tag resource identifier and type. Generally, this is +// the desired behavior so the tag schema does not require those attributes. +// Use (keyvaluetags.KeyValueTags).ListOfMap() for full tag information. +func (tags KeyValueTags) {{ . | Title }}ListOfMap() []interface{} { + var result []interface{} + + for _, key := range tags.Keys() { + m := map[string]interface{}{ + "key": key, + "value": aws.StringValue(tags.KeyValue(key)), + {{- range . | TagTypeAdditionalBoolFields }} + "{{ . | ToSnakeCase }}": aws.BoolValue(tags.KeyAdditionalBoolValue(key, "{{ . }}")), + {{- end }} + } + + result = append(result, m) + } + + return result +} +{{- end }} + +{{ if eq . "autoscaling" }} +// {{ . | Title }}ListOfStringMap returns a list of {{ . }} tags in flattened map of only string values. +// +// Compatible with setting Terraform state for legacy []map[string]string schema. +// Deprecated: Will be removed in a future major version without replacement. +func (tags KeyValueTags) {{ . | Title }}ListOfStringMap() []interface{} { + var result []interface{} + + for _, key := range tags.Keys() { + m := map[string]string{ + "key": key, + "value": aws.StringValue(tags.KeyValue(key)), + {{- range . | TagTypeAdditionalBoolFields }} + "{{ . | ToSnakeCase }}": strconv.FormatBool(aws.BoolValue(tags.KeyAdditionalBoolValue(key, "{{ . }}"))), + {{- end }} + } + + result = append(result, m) + } + + return result +} +{{- end }} + {{- if . | TagKeyType }} // {{ . | Title }}TagKeys returns {{ . }} service tag keys. func (tags KeyValueTags) {{ . | Title }}TagKeys() []*{{ . | TagPackage }}.{{ . | TagKeyType }} { @@ -247,6 +305,27 @@ func (tags KeyValueTags) {{ . | Title }}TagKeys() []*{{ . | TagPackage }}.{{ . | // {{ . | Title }}Tags returns {{ . }} service tags. func (tags KeyValueTags) {{ . | Title }}Tags() []*{{ . | TagPackage }}.{{ . | TagType }} { + {{- if or ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields) }} + var result []*{{ . | TagPackage }}.{{ . | TagType }} + + for _, key := range tags.Keys() { + tag := &{{ . | TagPackage }}.{{ . | TagType }}{ + {{ . | TagTypeKeyField }}: aws.String(key), + {{ . | TagTypeValueField }}: tags.KeyValue(key), + {{- if ( . | TagTypeIdentifierField ) }} + {{ . | TagTypeIdentifierField }}: tags.KeyAdditionalStringValue(key, "{{ . | TagTypeIdentifierField }}"), + {{- if ( . | TagResourceTypeField ) }} + {{ . | TagResourceTypeField }}: tags.KeyAdditionalStringValue(key, "{{ . | TagResourceTypeField }}"), + {{- end }} + {{- end }} + {{- range . | TagTypeAdditionalBoolFields }} + {{ . }}: tags.KeyAdditionalBoolValue(key, "{{ . }}"), + {{- end }} + } + + result = append(result, tag) + } + {{- else }} result := make([]*{{ . | TagPackage }}.{{ . | TagType }}, 0, len(tags)) for k, v := range tags.Map() { @@ -257,31 +336,149 @@ func (tags KeyValueTags) {{ . | Title }}Tags() []*{{ . | TagPackage }}.{{ . | Ta result = append(result, tag) } + {{- end }} return result } // {{ . | Title }}KeyValueTags creates KeyValueTags from {{ . }} service tags. +{{- if or ( . | TagType2 ) ( . | TagTypeAdditionalBoolFields ) }} +// +// Accepts the following types: +// - []*{{ . | TagPackage }}.{{ . | TagType }} {{- if . | TagType2 }} -// Accepts []*{{ . | TagPackage }}.{{ . | TagType }} and []*{{ . | TagPackage }}.{{ . | TagType2 }}. -func {{ . | Title }}KeyValueTags(tags interface{}) KeyValueTags { +// - []*{{ . | TagPackage }}.{{ . | TagType2 }} +{{- end }} +{{- if . | TagTypeAdditionalBoolFields }} +// - []interface{} (Terraform TypeList configuration block compatible) +// - *schema.Set (Terraform TypeSet configuration block compatible) +{{- end }} +func {{ . | Title }}KeyValueTags(tags interface{}{{ if . | TagTypeIdentifierField }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}{{ end }}) KeyValueTags { switch tags := tags.(type) { case []*{{ . | TagPackage }}.{{ . | TagType }}: + {{- if or ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields) }} + m := make(map[string]*TagData, len(tags)) + + for _, tag := range tags { + tagData := &TagData{ + Value: tag.{{ . | TagTypeValueField }}, + } + + tagData.AdditionalBoolFields = make(map[string]*bool) + {{- range . | TagTypeAdditionalBoolFields }} + tagData.AdditionalBoolFields["{{ . }}"] = tag.{{ . }} + {{- end }} + + {{- if . | TagTypeIdentifierField }} + tagData.AdditionalStringFields = make(map[string]*string) + tagData.AdditionalStringFields["{{ . | TagTypeIdentifierField }}"] = &identifier + {{- if . | TagResourceTypeField }} + tagData.AdditionalStringFields["{{ . | TagResourceTypeField }}"] = &resourceType + {{- end }} + {{- end }} + + m[aws.StringValue(tag.{{ . | TagTypeKeyField }})] = tagData + } + {{- else }} m := make(map[string]*string, len(tags)) for _, tag := range tags { m[aws.StringValue(tag.{{ . | TagTypeKeyField }})] = tag.{{ . | TagTypeValueField }} } + {{- end }} return New(m) case []*{{ . | TagPackage }}.{{ . | TagType2 }}: + {{- if or ( . | TagTypeIdentifierField ) ( . | TagTypeAdditionalBoolFields) }} + m := make(map[string]*TagData, len(tags)) + + for _, tag := range tags { + tagData := &TagData{ + Value: tag.{{ . | TagTypeValueField }}, + } + + {{ if . | TagTypeAdditionalBoolFields }} + tagData.AdditionalBoolFields = make(map[string]*bool) + {{- range . | TagTypeAdditionalBoolFields }} + tagData.AdditionalBoolFields["{{ . }}"] = tag.{{ . }} + {{- end }} + {{- end }} + + {{- if . | TagTypeIdentifierField }} + tagData.AdditionalStringFields = make(map[string]*string) + tagData.AdditionalStringFields["{{ . | TagTypeIdentifierField }}"] = &identifier + {{- if . | TagResourceTypeField }} + tagData.AdditionalStringFields["{{ . | TagResourceTypeField }}"] = &resourceType + {{- end }} + {{- end }} + + m[aws.StringValue(tag.{{ . | TagTypeKeyField }})] = tagData + } + {{- else }} m := make(map[string]*string, len(tags)) for _, tag := range tags { m[aws.StringValue(tag.{{ . | TagTypeKeyField }})] = tag.{{ . | TagTypeValueField }} } + {{- end }} return New(m) + {{- if . | TagTypeAdditionalBoolFields }} + case *schema.Set: + return {{ . | Title }}KeyValueTags(tags.List(){{ if . | TagTypeIdentifierField }}, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}{{ end }}) + case []interface{}: + result := make(map[string]*TagData) + + for _, tfMapRaw := range tags { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + key, ok := tfMap["key"].(string) + + if !ok { + continue + } + + tagData := &TagData{} + + if v, ok := tfMap["value"].(string); ok { + tagData.Value = &v + } + + {{ if . | TagTypeAdditionalBoolFields }} + tagData.AdditionalBoolFields = make(map[string]*bool) + {{- range . | TagTypeAdditionalBoolFields }} + if v, ok := tfMap["{{ . | ToSnakeCase }}"].(bool); ok { + tagData.AdditionalBoolFields["{{ . }}"] = &v + } + {{- end }} + {{ if eq . "autoscaling" }} + // Deprecated: Legacy map handling + {{- range . | TagTypeAdditionalBoolFields }} + if v, ok := tfMap["{{ . | ToSnakeCase }}"].(string); ok { + b, _ := strconv.ParseBool(v) + tagData.AdditionalBoolFields["{{ . }}"] = &b + } + {{- end }} + {{- end }} + {{- end }} + + {{ if . | TagTypeIdentifierField }} + tagData.AdditionalStringFields = make(map[string]*string) + tagData.AdditionalStringFields["{{ . | TagTypeIdentifierField }}"] = &identifier + {{- if . | TagResourceTypeField }} + tagData.AdditionalStringFields["{{ . | TagResourceTypeField }}"] = &resourceType + {{- end }} + {{- end }} + + result[key] = tagData + } + + return New(result) + {{- end }} default: return New(nil) } diff --git a/aws/internal/keyvaluetags/generators/updatetags/main.go b/aws/internal/keyvaluetags/generators/updatetags/main.go index 555e67ba04f..0c55e064462 100644 --- a/aws/internal/keyvaluetags/generators/updatetags/main.go +++ b/aws/internal/keyvaluetags/generators/updatetags/main.go @@ -27,6 +27,7 @@ var serviceNames = []string{ "appstream", "appsync", "athena", + "autoscaling", "backup", "cloud9", "cloudfront", @@ -142,9 +143,11 @@ func main() { "TagInputCustomValue": keyvaluetags.ServiceTagInputCustomValue, "TagInputIdentifierField": keyvaluetags.ServiceTagInputIdentifierField, "TagInputIdentifierRequiresSlice": keyvaluetags.ServiceTagInputIdentifierRequiresSlice, - "TagInputResourceTypeField": keyvaluetags.ServiceTagInputResourceTypeField, "TagInputTagsField": keyvaluetags.ServiceTagInputTagsField, "TagPackage": keyvaluetags.ServiceTagPackage, + "TagResourceTypeField": keyvaluetags.ServiceTagResourceTypeField, + "TagTypeAdditionalBoolFields": keyvaluetags.ServiceTagTypeAdditionalBoolFields, + "TagTypeIdentifierField": keyvaluetags.ServiceTagTypeIdentifierField, "Title": strings.Title, "UntagFunction": keyvaluetags.ServiceUntagFunction, "UntagInputCustomValue": keyvaluetags.ServiceUntagInputCustomValue, @@ -205,9 +208,15 @@ import ( // {{ . | Title }}UpdateTags updates {{ . }} service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. -func {{ . | Title }}UpdateTags(conn {{ . | ClientType }}, identifier string{{ if . | TagInputResourceTypeField }}, resourceType string{{ end }}, oldTagsMap interface{}, newTagsMap interface{}) error { +{{- if . | TagTypeAdditionalBoolFields }} +func {{ . | Title }}UpdateTags(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, oldTagsSet interface{}, newTagsSet interface{}) error { + oldTags := {{ . | Title }}KeyValueTags(oldTagsSet, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}) + newTags := {{ . | Title }}KeyValueTags(newTagsSet, identifier{{ if . | TagResourceTypeField }}, resourceType{{ end }}) +{{- else }} +func {{ . | Title }}UpdateTags(conn {{ . | ClientType }}, identifier string{{ if . | TagResourceTypeField }}, resourceType string{{ end }}, oldTagsMap interface{}, newTagsMap interface{}) error { oldTags := New(oldTagsMap) newTags := New(newTagsMap) +{{- end }} {{- if eq (. | TagFunction) (. | UntagFunction) }} removedTags := oldTags.Removed(newTags) updatedTags := oldTags.Updated(newTags) @@ -218,13 +227,15 @@ func {{ . | Title }}UpdateTags(conn {{ . | ClientType }}, identifier string{{ if } input := &{{ . | TagPackage }}.{{ . | TagFunction }}Input{ + {{- if not ( . | TagTypeIdentifierField ) }} {{- if . | TagInputIdentifierRequiresSlice }} {{ . | TagInputIdentifierField }}: aws.StringSlice([]string{identifier}), {{- else }} {{ . | TagInputIdentifierField }}: aws.String(identifier), {{- end }} - {{- if . | TagInputResourceTypeField }} - {{ . | TagInputResourceTypeField }}: aws.String(resourceType), + {{- if . | TagResourceTypeField }} + {{ . | TagResourceTypeField }}: aws.String(resourceType), + {{- end }} {{- end }} } @@ -257,13 +268,15 @@ func {{ . | Title }}UpdateTags(conn {{ . | ClientType }}, identifier string{{ if for _, removedTags := range removedTags.Chunks({{ . | TagFunctionBatchSize }}) { {{- end }} input := &{{ . | TagPackage }}.{{ . | UntagFunction }}Input{ + {{- if not ( . | TagTypeIdentifierField ) }} {{- if . | TagInputIdentifierRequiresSlice }} {{ . | TagInputIdentifierField }}: aws.StringSlice([]string{identifier}), {{- else }} {{ . | TagInputIdentifierField }}: aws.String(identifier), {{- end }} - {{- if . | TagInputResourceTypeField }} - {{ . | TagInputResourceTypeField }}: aws.String(resourceType), + {{- if . | TagResourceTypeField }} + {{ . | TagResourceTypeField }}: aws.String(resourceType), + {{- end }} {{- end }} {{- if . | UntagInputRequiresTagType }} {{ . | UntagInputTagsField }}: removedTags.IgnoreAws().{{ . | Title }}Tags(), @@ -291,18 +304,20 @@ func {{ . | Title }}UpdateTags(conn {{ . | ClientType }}, identifier string{{ if for _, updatedTags := range updatedTags.Chunks({{ . | TagFunctionBatchSize }}) { {{- end }} input := &{{ . | TagPackage }}.{{ . | TagFunction }}Input{ + {{- if not ( . | TagTypeIdentifierField ) }} {{- if . | TagInputIdentifierRequiresSlice }} - {{ . | TagInputIdentifierField }}: aws.StringSlice([]string{identifier}), + {{ . | TagInputIdentifierField }}: aws.StringSlice([]string{identifier}), {{- else }} - {{ . | TagInputIdentifierField }}: aws.String(identifier), + {{ . | TagInputIdentifierField }}: aws.String(identifier), + {{- end }} + {{- if . | TagResourceTypeField }} + {{ . | TagResourceTypeField }}: aws.String(resourceType), {{- end }} - {{- if . | TagInputResourceTypeField }} - {{ . | TagInputResourceTypeField }}: aws.String(resourceType), {{- end }} {{- if . | TagInputCustomValue }} - {{ . | TagInputTagsField }}: {{ . | TagInputCustomValue }}, + {{ . | TagInputTagsField }}: {{ . | TagInputCustomValue }}, {{- else }} - {{ . | TagInputTagsField }}: updatedTags.IgnoreAws().{{ . | Title }}Tags(), + {{ . | TagInputTagsField }}: updatedTags.IgnoreAws().{{ . | Title }}Tags(), {{- end }} } diff --git a/aws/internal/keyvaluetags/get_tag_gen.go b/aws/internal/keyvaluetags/get_tag_gen.go index f2bf57afd23..455e383045c 100644 --- a/aws/internal/keyvaluetags/get_tag_gen.go +++ b/aws/internal/keyvaluetags/get_tag_gen.go @@ -4,12 +4,43 @@ package keyvaluetags import ( "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/route53resolver" ) +// AutoscalingGetTag fetches an individual autoscaling service tag for a resource. +// Returns whether the key exists, the key value, and any errors. +// This function will optimise the handling over AutoscalingListTags, if possible. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func AutoscalingGetTag(conn *autoscaling.AutoScaling, identifier string, resourceType string, key string) (bool, *TagData, error) { + input := &autoscaling.DescribeTagsInput{ + Filters: []*autoscaling.Filter{ + { + Name: aws.String("auto-scaling-group"), + Values: []*string{aws.String(identifier)}, + }, + { + Name: aws.String("key"), + Values: []*string{aws.String(key)}, + }, + }, + } + + output, err := conn.DescribeTags(input) + + if err != nil { + return false, nil, err + } + + listTags := AutoscalingKeyValueTags(output.Tags, identifier, resourceType) + + return listTags.KeyExists(key), listTags.KeyTagData(key), nil +} + // DynamodbGetTag fetches an individual dynamodb service tag for a resource. // Returns whether the key exists, the key value, and any errors. // This function will optimise the handling over DynamodbListTags, if possible. diff --git a/aws/internal/keyvaluetags/key_value_tags.go b/aws/internal/keyvaluetags/key_value_tags.go index 1b8f57f6933..2822feec9f7 100644 --- a/aws/internal/keyvaluetags/key_value_tags.go +++ b/aws/internal/keyvaluetags/key_value_tags.go @@ -9,6 +9,8 @@ package keyvaluetags import ( "fmt" "net/url" + "reflect" + "regexp" "strings" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" @@ -31,7 +33,7 @@ type IgnoreConfig struct { // The AWS Go SDK is split into multiple service packages, each service with // its own Go struct type representing a resource tag. To standardize logic // across all these Go types, we convert them into this Go type. -type KeyValueTags map[string]*string +type KeyValueTags map[string]*TagData // IgnoreAws returns non-AWS tag keys. func (tags KeyValueTags) IgnoreAws() KeyValueTags { @@ -139,9 +141,40 @@ func (tags KeyValueTags) Ignore(ignoreTags KeyValueTags) KeyValueTags { return result } +// KeyAdditionalBoolValue returns the boolean value of an additional tag field. +// If the key or additional field is not found, returns nil. +func (tags KeyValueTags) KeyAdditionalBoolValue(key string, fieldName string) *bool { + tag, ok := tags[key] + + if !ok || tag == nil || tag.AdditionalBoolFields == nil { + return nil + } + + if v, ok := tag.AdditionalBoolFields[fieldName]; ok { + return v + } + + return nil +} + +// KeyAdditionalStringValue returns the string value of an additional tag field. +// If the key or additional field is not found, returns nil. +func (tags KeyValueTags) KeyAdditionalStringValue(key string, fieldName string) *string { + tag, ok := tags[key] + + if !ok || tag == nil || tag.AdditionalStringFields == nil { + return nil + } + + if v, ok := tag.AdditionalStringFields[fieldName]; ok { + return v + } + + return nil +} + // KeyExists returns true if a tag key exists. // If the key is not found, returns nil. -// Use KeyExists to determine if key is present. func (tags KeyValueTags) KeyExists(key string) bool { if _, ok := tags[key]; ok { return true @@ -150,10 +183,10 @@ func (tags KeyValueTags) KeyExists(key string) bool { return false } -// KeyValue returns a tag key value. +// KeyTagData returns all tag key data. // If the key is not found, returns nil. // Use KeyExists to determine if key is present. -func (tags KeyValueTags) KeyValue(key string) *string { +func (tags KeyValueTags) KeyTagData(key string) *TagData { if v, ok := tags[key]; ok { return v } @@ -161,6 +194,19 @@ func (tags KeyValueTags) KeyValue(key string) *string { return nil } +// KeyValue returns a tag key value. +// If the key is not found, returns nil. +// Use KeyExists to determine if key is present. +func (tags KeyValueTags) KeyValue(key string) *string { + v, ok := tags[key] + + if !ok || v == nil { + return nil + } + + return v.Value +} + // Keys returns tag keys. func (tags KeyValueTags) Keys() []string { result := make([]string, 0, len(tags)) @@ -172,12 +218,59 @@ func (tags KeyValueTags) Keys() []string { return result } +// ListofMap returns a list of flattened tags. +// Compatible with setting Terraform state for strongly typed configuration blocks. +func (tags KeyValueTags) ListofMap() []map[string]interface{} { + result := make([]map[string]interface{}, len(tags)) + + for k, v := range tags { + m := map[string]interface{}{ + "key": k, + "value": "", + } + + if v == nil { + result = append(result, m) + continue + } + + if v.Value != nil { + m["value"] = *v.Value + } + + for k, v := range v.AdditionalBoolFields { + m[ToSnakeCase(k)] = false + + if v != nil { + m[ToSnakeCase(k)] = *v + } + } + + for k, v := range v.AdditionalStringFields { + m[ToSnakeCase(k)] = "" + + if v != nil { + m[ToSnakeCase(k)] = *v + } + } + + result = append(result, m) + } + + return result +} + // Map returns tag keys mapped to their values. func (tags KeyValueTags) Map() map[string]string { result := make(map[string]string, len(tags)) for k, v := range tags { - result[k] = *v + if v == nil || v.Value == nil { + result[k] = "" + continue + } + + result[k] = *v.Value } return result @@ -198,6 +291,21 @@ func (tags KeyValueTags) Merge(mergeTags KeyValueTags) KeyValueTags { return result } +// Only returns matching tag keys. +func (tags KeyValueTags) Only(onlyTags KeyValueTags) KeyValueTags { + result := make(KeyValueTags) + + for k, v := range tags { + if _, ok := onlyTags[k]; !ok { + continue + } + + result[k] = v + } + + return result +} + // Removed returns tags removed. func (tags KeyValueTags) Removed(newTags KeyValueTags) KeyValueTags { result := make(KeyValueTags) @@ -216,7 +324,7 @@ func (tags KeyValueTags) Updated(newTags KeyValueTags) KeyValueTags { result := make(KeyValueTags) for k, newV := range newTags { - if oldV, ok := tags[k]; !ok || *oldV != *newV { + if oldV, ok := tags[k]; !ok || !oldV.Equal(newV) { result[k] = newV } } @@ -247,7 +355,7 @@ func (tags KeyValueTags) Chunks(size int) []KeyValueTags { // ContainsAll returns whether or not all the target tags are contained. func (tags KeyValueTags) ContainsAll(target KeyValueTags) bool { for key, value := range target { - if v, ok := tags[key]; !ok || *v != *value { + if v, ok := tags[key]; !ok || !v.Equal(value) { return false } } @@ -261,7 +369,12 @@ func (tags KeyValueTags) Hash() int { hash := 0 for k, v := range tags { - hash = hash ^ hashcode.String(fmt.Sprintf("%s-%s", k, *v)) + if v == nil || v.Value == nil { + hash = hash ^ hashcode.String(k) + continue + } + + hash = hash ^ hashcode.String(fmt.Sprintf("%s-%s", k, *v.Value)) } return hash @@ -272,7 +385,11 @@ func (tags KeyValueTags) UrlEncode() string { values := url.Values{} for k, v := range tags { - values.Add(k, *v) + if v == nil || v.Value == nil { + continue + } + + values.Add(k, *v.Value) } return values.Encode() @@ -283,23 +400,45 @@ func (tags KeyValueTags) UrlEncode() string { // When passed []interface{}, all elements are treated as keys and assigned nil values. func New(i interface{}) KeyValueTags { switch value := i.(type) { + case map[string]*TagData: + kvtm := make(KeyValueTags, len(value)) + + for k, v := range value { + tagData := v + kvtm[k] = tagData + } + + return kvtm case map[string]string: kvtm := make(KeyValueTags, len(value)) for k, v := range value { str := v // Prevent referencing issues - kvtm[k] = &str + kvtm[k] = &TagData{Value: &str} } return kvtm case map[string]*string: - return KeyValueTags(value) + kvtm := make(KeyValueTags, len(value)) + + for k, v := range value { + strPtr := v + + if strPtr == nil { + kvtm[k] = nil + continue + } + + kvtm[k] = &TagData{Value: strPtr} + } + + return kvtm case map[string]interface{}: kvtm := make(KeyValueTags, len(value)) for k, v := range value { str := v.(string) - kvtm[k] = &str + kvtm[k] = &TagData{Value: &str} } return kvtm @@ -323,3 +462,100 @@ func New(i interface{}) KeyValueTags { return make(KeyValueTags) } } + +// TagData represents the data associated with a resource tag key. +// Almost exclusively for AWS services, this is just a tag value, +// however there are services that attach additional data to tags. +// An example is autoscaling with the PropagateAtLaunch field. +type TagData struct { + // Additional boolean field names and values associated with this tag. + // Each service is responsible for properly handling this data. + AdditionalBoolFields map[string]*bool + + // Additional string field names and values associated with this tag. + // Each service is responsible for properly handling this data. + AdditionalStringFields map[string]*string + + // Tag value. + Value *string +} + +func (td *TagData) Equal(other *TagData) bool { + if td == nil && other == nil { + return true + } + + if td == nil || other == nil { + return false + } + + if !reflect.DeepEqual(td.AdditionalBoolFields, other.AdditionalBoolFields) { + return false + } + + if !reflect.DeepEqual(td.AdditionalStringFields, other.AdditionalStringFields) { + return false + } + + if !reflect.DeepEqual(td.Value, other.Value) { + return false + } + + return true +} + +func (td *TagData) String() string { + if td == nil { + return "" + } + + var fields []string + + if len(td.AdditionalBoolFields) > 0 { + var additionalBoolFields []string + + for k, v := range td.AdditionalBoolFields { + additionalBoolField := fmt.Sprintf("%s:", k) + + if v != nil { + additionalBoolField += fmt.Sprintf("%t", *v) + } + + additionalBoolFields = append(additionalBoolFields, additionalBoolField) + } + + fields = append(fields, fmt.Sprintf("AdditionalBoolFields: map[%s]", strings.Join(additionalBoolFields, " "))) + } + + if len(td.AdditionalStringFields) > 0 { + var additionalStringFields []string + + for k, v := range td.AdditionalStringFields { + additionalStringField := fmt.Sprintf("%s:", k) + + if v != nil { + additionalStringField += *v + } + + additionalStringFields = append(additionalStringFields, additionalStringField) + } + + fields = append(fields, fmt.Sprintf("AdditionalStringFields: map[%s]", strings.Join(additionalStringFields, " "))) + } + + if td.Value != nil { + fields = append(fields, fmt.Sprintf("Value: %s", *td.Value)) + } + + return fmt.Sprintf("TagData{%s}", strings.Join(fields, ", ")) +} + +// ToSnakeCase converts a string to snake case. +// +// For example, AWS Go SDK field names are in PascalCase, +// while Terraform schema attribute names are in snake_case. +func ToSnakeCase(str string) string { + result := regexp.MustCompile("(.)([A-Z][a-z]+)").ReplaceAllString(str, "${1}_${2}") + result = regexp.MustCompile("([a-z0-9])([A-Z])").ReplaceAllString(result, "${1}_${2}") + return strings.ToLower(result) +} diff --git a/aws/internal/keyvaluetags/key_value_tags_test.go b/aws/internal/keyvaluetags/key_value_tags_test.go index 789829f8a68..8d4d0939331 100644 --- a/aws/internal/keyvaluetags/key_value_tags_test.go +++ b/aws/internal/keyvaluetags/key_value_tags_test.go @@ -533,6 +533,182 @@ func TestKeyValueTagsIgnore(t *testing.T) { } } +func TestKeyValueTagsKeyAdditionalBoolValue(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + key string + field string + want *bool + }{ + { + name: "empty", + tags: New(map[string]*string{}), + key: "key1", + field: "field1", + want: nil, + }, + { + name: "non-existent key", + tags: New(map[string]*string{"key1": testStringPtr("value1")}), + key: "key2", + field: "field2", + want: nil, + }, + { + name: "non-existent TagData", + tags: New(map[string]*string{"key1": testStringPtr("value1")}), + key: "key1", + field: "field1", + want: nil, + }, + { + name: "non-existent field", + tags: New(map[string]*TagData{ + "key1": { + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + }), + key: "key1", + field: "field2", + want: nil, + }, + { + name: "matching value", + tags: New(map[string]*TagData{ + "key1": { + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + }), + key: "key1", + field: "field1", + want: testBoolPtr(true), + }, + { + name: "matching nil", + tags: New(map[string]*TagData{ + "key1": { + AdditionalBoolFields: map[string]*bool{"field1": nil}, + Value: testStringPtr("value1"), + }, + }), + key: "key1", + field: "field1", + want: nil, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.KeyAdditionalBoolValue(testCase.key, testCase.field) + + if testCase.want == nil && got != nil { + t.Fatalf("expected: nil, got: %#v", got) + } + + if testCase.want != nil && got == nil { + t.Fatalf("expected: %#v, got: nil", testCase.want) + } + + if testCase.want != nil && got != nil && *testCase.want != *got { + t.Fatalf("expected: %#v, got: %#v", testCase.want, got) + } + }) + } +} + +func TestKeyValueTagsKeyAdditionalStringValue(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + key string + field string + want *string + }{ + { + name: "empty", + tags: New(map[string]*string{}), + key: "key1", + field: "field1", + want: nil, + }, + { + name: "non-existent key", + tags: New(map[string]*string{"key1": testStringPtr("value1")}), + key: "key2", + field: "field2", + want: nil, + }, + { + name: "non-existent TagData", + tags: New(map[string]*string{"key1": testStringPtr("value1")}), + key: "key1", + field: "field1", + want: nil, + }, + { + name: "non-existent field", + tags: New(map[string]*TagData{ + "key1": { + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + }), + key: "key1", + field: "field2", + want: nil, + }, + { + name: "matching value", + tags: New(map[string]*TagData{ + "key1": { + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + }), + key: "key1", + field: "field1", + want: testStringPtr("field1value"), + }, + { + name: "matching nil", + tags: New(map[string]*TagData{ + "key1": { + AdditionalStringFields: map[string]*string{"field1": nil}, + Value: testStringPtr("value1"), + }, + }), + key: "key1", + field: "field1", + want: nil, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.KeyAdditionalStringValue(testCase.key, testCase.field) + + if testCase.want == nil && got != nil { + t.Fatalf("expected: nil, got: %#v", got) + } + + if testCase.want != nil && got == nil { + t.Fatalf("expected: %#v, got: nil", testCase.want) + } + + if testCase.want != nil && got != nil && *testCase.want != *got { + t.Fatalf("expected: %#v, got: %#v", testCase.want, got) + } + }) + } +} + func TestKeyValueTagsKeyExists(t *testing.T) { testCases := []struct { name string @@ -577,6 +753,76 @@ func TestKeyValueTagsKeyExists(t *testing.T) { } } +func TestKeyValueTagsKeyTagData(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + key string + want *TagData + }{ + { + name: "empty", + tags: New(map[string]*string{}), + key: "key1", + want: nil, + }, + { + name: "non-existent", + tags: New(map[string]*string{"key1": testStringPtr("value1")}), + key: "key2", + want: nil, + }, + { + name: "matching with additional boolean fields", + tags: New(map[string]*TagData{ + "key1": { + AdditionalBoolFields: map[string]*bool{"boolfield": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + }), + key: "key1", + want: &TagData{ + AdditionalBoolFields: map[string]*bool{"boolfield": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + }, + { + name: "matching with string value", + tags: New(map[string]*string{"key1": testStringPtr("value1")}), + key: "key1", + want: &TagData{ + Value: testStringPtr("value1"), + }, + }, + { + name: "matching with nil value", + tags: New(map[string]*string{"key1": nil}), + key: "key1", + want: nil, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.KeyTagData(testCase.key) + + if testCase.want == nil && got != nil { + t.Fatalf("expected: nil, got: %#v", *got) + } + + if testCase.want != nil && got == nil { + t.Fatalf("expected: %#v, got: nil", *testCase.want) + } + + if testCase.want != nil && got != nil && !testCase.want.Equal(got) { + t.Fatalf("expected: %#v, got: %#v", testCase.want, got) + } + }) + } +} + func TestKeyValueTagsKeyValues(t *testing.T) { testCases := []struct { name string @@ -796,6 +1042,15 @@ func TestKeyValueTagsMap(t *testing.T) { "key3": "value3", }, }, + { + name: "nil_value", + tags: New(map[string]*string{ + "key1": nil, + }), + want: map[string]string{ + "key1": "", + }, + }, } for _, testCase := range testCases { @@ -888,6 +1143,80 @@ func TestKeyValueTagsMerge(t *testing.T) { } } +func TestKeyValueTagsOnly(t *testing.T) { + testCases := []struct { + name string + tags KeyValueTags + onlyTags KeyValueTags + want map[string]string + }{ + { + name: "empty", + tags: New(map[string]string{}), + onlyTags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + want: map[string]string{}, + }, + { + name: "all", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + onlyTags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + want: map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + }, + { + name: "mixed", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + onlyTags: New(map[string]string{ + "key1": "value1", + }), + want: map[string]string{ + "key1": "value1", + }, + }, + { + name: "none", + tags: New(map[string]string{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + }), + onlyTags: New(map[string]string{ + "key4": "value4", + "key5": "value5", + "key6": "value6", + }), + want: map[string]string{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tags.Only(testCase.onlyTags) + + testKeyValueTagsVerifyMap(t, got.Map(), testCase.want) + }) + } +} + func TestKeyValueTagsRemoved(t *testing.T) { testCases := []struct { name string @@ -1147,6 +1476,16 @@ func TestKeyValueTagsContainsAll(t *testing.T) { target: New(map[string]string{}), want: true, }, + { + name: "nil value matches", + source: New(map[string]*string{ + "key1": nil, + }), + target: New(map[string]*string{ + "key1": nil, + }), + want: true, + }, { name: "exact_match", source: New(map[string]string{ @@ -1209,6 +1548,13 @@ func TestKeyValueTagsHash(t *testing.T) { tags: New(map[string]string{}), zero: true, }, + { + name: "nil value", + tags: New(map[string]*string{ + "key1": nil, + }), + zero: false, + }, { name: "not_empty", tags: New(map[string]string{ @@ -1243,6 +1589,13 @@ func TestKeyValueTagsUrlEncode(t *testing.T) { tags: New(map[string]string{}), want: "", }, + { + name: "nil value", + tags: New(map[string]*string{ + "key1": nil, + }), + want: "", + }, { name: "single", tags: New(map[string]string{ @@ -1281,6 +1634,231 @@ func TestKeyValueTagsUrlEncode(t *testing.T) { } } +func TestTagDataEqual(t *testing.T) { + testCases := []struct { + name string + tagData1 *TagData + tagData2 *TagData + want bool + }{ + { + name: "both nil", + tagData1: nil, + tagData2: nil, + want: true, + }, + { + name: "first nil", + tagData1: nil, + tagData2: &TagData{ + Value: testStringPtr("value1"), + }, + want: false, + }, + { + name: "second nil", + tagData1: &TagData{ + Value: testStringPtr("value1"), + }, + tagData2: nil, + want: false, + }, + { + name: "differing value", + tagData1: &TagData{ + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + Value: testStringPtr("value2"), + }, + want: false, + }, + { + name: "differing additional bool fields", + tagData1: &TagData{ + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + AdditionalBoolFields: map[string]*bool{"field2": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + want: false, + }, + { + name: "differing additional bool field values", + tagData1: &TagData{ + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(false)}, + Value: testStringPtr("value1"), + }, + want: false, + }, + { + name: "differing additional string fields", + tagData1: &TagData{ + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + AdditionalStringFields: map[string]*string{"field2": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + want: false, + }, + { + name: "differing additional string field values", + tagData1: &TagData{ + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field2value")}, + Value: testStringPtr("value1"), + }, + want: false, + }, + { + name: "same value", + tagData1: &TagData{ + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + Value: testStringPtr("value1"), + }, + want: true, + }, + { + name: "same additional bool fields", + tagData1: &TagData{ + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + want: true, + }, + { + name: "same additional string fields", + tagData1: &TagData{ + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + tagData2: &TagData{ + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + want: true, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tagData1.Equal(testCase.tagData2) + + if testCase.want != got { + t.Fatalf("expected: %t, got: %t", testCase.want, got) + } + }) + } +} + +func TestTagDataString(t *testing.T) { + testCases := []struct { + name string + tagData *TagData + want string + }{ + { + name: "nil", + tagData: nil, + want: "", + }, + { + name: "value", + tagData: &TagData{ + Value: testStringPtr("value1"), + }, + want: "TagData{Value: value1}", + }, + { + name: "additional bool fields", + tagData: &TagData{ + AdditionalBoolFields: map[string]*bool{"field1": testBoolPtr(true)}, + Value: testStringPtr("value1"), + }, + want: "TagData{AdditionalBoolFields: map[field1:true], Value: value1}", + }, + { + name: "additional string fields", + tagData: &TagData{ + AdditionalStringFields: map[string]*string{"field1": testStringPtr("field1value")}, + Value: testStringPtr("value1"), + }, + want: "TagData{AdditionalStringFields: map[field1:field1value], Value: value1}", + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + got := testCase.tagData.String() + + if testCase.want != got { + t.Fatalf("expected: %s, got: %s", testCase.want, got) + } + }) + } +} + +func TestToSnakeCase(t *testing.T) { + testCases := []struct { + Input string + Expected string + }{ + { + Input: "ARN", + Expected: "arn", + }, + { + Input: "PropagateAtLaunch", + Expected: "propagate_at_launch", + }, + { + Input: "ResourceId", + Expected: "resource_id", + }, + { + Input: "ResourceArn", + Expected: "resource_arn", + }, + { + Input: "ResourceARN", + Expected: "resource_arn", + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.Input, func(t *testing.T) { + got := ToSnakeCase(testCase.Input) + + if got != testCase.Expected { + t.Errorf("got %s, expected %s", got, testCase.Expected) + } + }) + } +} + func testKeyValueTagsVerifyKeys(t *testing.T, got []string, want []string) { for _, g := range got { found := false @@ -1334,6 +1912,10 @@ func testKeyValueTagsVerifyMap(t *testing.T, got map[string]string, want map[str } } +func testBoolPtr(b bool) *bool { + return &b +} + func testStringPtr(str string) *string { return &str } diff --git a/aws/internal/keyvaluetags/list_tags_gen.go b/aws/internal/keyvaluetags/list_tags_gen.go index 1ed937c97c5..df8f6d6a786 100644 --- a/aws/internal/keyvaluetags/list_tags_gen.go +++ b/aws/internal/keyvaluetags/list_tags_gen.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go/service/appstream" "github.com/aws/aws-sdk-go/service/appsync" "github.com/aws/aws-sdk-go/service/athena" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/backup" "github.com/aws/aws-sdk-go/service/cloud9" "github.com/aws/aws-sdk-go/service/cloudfront" @@ -257,6 +258,28 @@ func AthenaListTags(conn *athena.Athena, identifier string) (KeyValueTags, error return AthenaKeyValueTags(output.Tags), nil } +// AutoscalingListTags lists autoscaling service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func AutoscalingListTags(conn *autoscaling.AutoScaling, identifier string, resourceType string) (KeyValueTags, error) { + input := &autoscaling.DescribeTagsInput{ + Filters: []*autoscaling.Filter{ + { + Name: aws.String("auto-scaling-group"), + Values: []*string{aws.String(identifier)}, + }, + }, + } + + output, err := conn.DescribeTags(input) + + if err != nil { + return New(nil), err + } + + return AutoscalingKeyValueTags(output.Tags, identifier, resourceType), nil +} + // BackupListTags lists backup service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. diff --git a/aws/internal/keyvaluetags/service_generation_customizations.go b/aws/internal/keyvaluetags/service_generation_customizations.go index 86c45b1ece9..9e44696bdd3 100644 --- a/aws/internal/keyvaluetags/service_generation_customizations.go +++ b/aws/internal/keyvaluetags/service_generation_customizations.go @@ -16,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go/service/appstream" "github.com/aws/aws-sdk-go/service/appsync" "github.com/aws/aws-sdk-go/service/athena" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/backup" "github.com/aws/aws-sdk-go/service/cloud9" "github.com/aws/aws-sdk-go/service/cloudfront" @@ -142,6 +143,8 @@ func ServiceClientType(serviceName string) string { funcType = reflect.TypeOf(appsync.New) case "athena": funcType = reflect.TypeOf(athena.New) + case "autoscaling": + funcType = reflect.TypeOf(autoscaling.New) case "backup": funcType = reflect.TypeOf(backup.New) case "cloud9": @@ -352,6 +355,8 @@ func ServiceListTagsFunction(serviceName string) string { return "ListTags" case "apigatewayv2": return "GetTags" + case "autoscaling": + return "DescribeTags" case "backup": return "ListTags" case "cloudhsmv2": @@ -413,6 +418,8 @@ func ServiceListTagsFunction(serviceName string) string { // This causes the implementation to use the Filters field with the Input struct. func ServiceListTagsInputFilterIdentifierName(serviceName string) string { switch serviceName { + case "autoscaling": + return "auto-scaling-group" case "ec2": return "resource-id" default: @@ -452,18 +459,6 @@ func ServiceListTagsInputIdentifierRequiresSlice(serviceName string) string { } } -// ServiceListTagsInputResourceTypeField determines the service list tagging resource type field. -func ServiceListTagsInputResourceTypeField(serviceName string) string { - switch serviceName { - case "route53": - return "ResourceType" - case "ssm": - return "ResourceType" - default: - return "" - } -} - // ServiceListTagsOutputTagsField determines the service list tag field. func ServiceListTagsOutputTagsField(serviceName string) string { switch serviceName { @@ -553,6 +548,8 @@ func ServiceTagFunction(serviceName string) string { return "AddTagsToCertificate" case "acmpca": return "TagCertificateAuthority" + case "autoscaling": + return "CreateOrUpdateTags" case "cloudtrail": return "AddTags" case "cloudwatchlogs": @@ -792,18 +789,6 @@ func ServiceTagInputCustomValue(serviceName string) string { } } -// ServiceTagInputResourceTypeField determines the service tagging resource type field. -func ServiceTagInputResourceTypeField(serviceName string) string { - switch serviceName { - case "route53": - return "ResourceType" - case "ssm": - return "ResourceType" - default: - return "" - } -} - func ServiceTagPackage(serviceName string) string { switch serviceName { case "wafregional": @@ -823,6 +808,20 @@ func ServiceTagKeyType(serviceName string) string { } } +// ServiceTagResourceTypeField determines the service tagging resource type field. +func ServiceTagResourceTypeField(serviceName string) string { + switch serviceName { + case "autoscaling": + return "ResourceType" + case "route53": + return "ResourceType" + case "ssm": + return "ResourceType" + default: + return "" + } +} + // ServiceTagType determines the service tagging tag type. func ServiceTagType(serviceName string) string { switch serviceName { @@ -843,6 +842,8 @@ func ServiceTagType(serviceName string) string { // The two types must be equivalent. func ServiceTagType2(serviceName string) string { switch serviceName { + case "autoscaling": + return "TagDescription" case "ec2": return "TagDescription" default: @@ -850,6 +851,27 @@ func ServiceTagType2(serviceName string) string { } } +// ServiceTagTypeAdditionalBoolFields returns the names of additional boolean fields in the type. +func ServiceTagTypeAdditionalBoolFields(serviceName string) []string { + switch serviceName { + case "autoscaling": + return []string{"PropagateAtLaunch"} + default: + return nil + } +} + +// ServiceTagTypeIdentifierField determines the type self-contained identifier field. +// Use ServiceTagResourceTypeField if the type also self-contains resource type. +func ServiceTagTypeIdentifierField(serviceName string) string { + switch serviceName { + case "autoscaling": + return "ResourceId" + default: + return "" + } +} + // ServiceTagTypeKeyField determines the service tagging tag type key field. func ServiceTagTypeKeyField(serviceName string) string { switch serviceName { @@ -877,6 +899,8 @@ func ServiceUntagFunction(serviceName string) string { return "RemoveTagsFromCertificate" case "acmpca": return "UntagCertificateAuthority" + case "autoscaling": + return "DeleteTags" case "cloudtrail": return "RemoveTags" case "cloudwatchlogs": @@ -947,6 +971,8 @@ func ServiceUntagInputRequiresTagType(serviceName string) string { return "yes" case "acmpca": return "yes" + case "autoscaling": + return "yes" case "cloudtrail": return "yes" case "ec2": @@ -973,6 +999,8 @@ func ServiceUntagInputTagsField(serviceName string) string { return "Tags" case "acmpca": return "Tags" + case "autoscaling": + return "Tags" case "backup": return "TagKeyList" case "cloudhsmv2": diff --git a/aws/internal/keyvaluetags/service_tags_gen.go b/aws/internal/keyvaluetags/service_tags_gen.go index 8ed15980412..11613e10db7 100644 --- a/aws/internal/keyvaluetags/service_tags_gen.go +++ b/aws/internal/keyvaluetags/service_tags_gen.go @@ -3,11 +3,14 @@ package keyvaluetags import ( + "strconv" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acm" "github.com/aws/aws-sdk-go/service/acmpca" "github.com/aws/aws-sdk-go/service/appmesh" "github.com/aws/aws-sdk-go/service/athena" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/cloud9" "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/cloudfront" @@ -81,6 +84,7 @@ import ( "github.com/aws/aws-sdk-go/service/wafv2" "github.com/aws/aws-sdk-go/service/workspaces" "github.com/aws/aws-sdk-go/service/xray" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) // map[string]*string handling @@ -565,6 +569,161 @@ func AthenaKeyValueTags(tags []*athena.Tag) KeyValueTags { return New(m) } +// AutoscalingListOfMap returns a list of autoscaling in flattened map. +// +// Compatible with setting Terraform state for strongly typed configuration blocks. +// +// This function strips tag resource identifier and type. Generally, this is +// the desired behavior so the tag schema does not require those attributes. +// Use (keyvaluetags.KeyValueTags).ListOfMap() for full tag information. +func (tags KeyValueTags) AutoscalingListOfMap() []interface{} { + var result []interface{} + + for _, key := range tags.Keys() { + m := map[string]interface{}{ + "key": key, + "value": aws.StringValue(tags.KeyValue(key)), + "propagate_at_launch": aws.BoolValue(tags.KeyAdditionalBoolValue(key, "PropagateAtLaunch")), + } + + result = append(result, m) + } + + return result +} + +// AutoscalingListOfStringMap returns a list of autoscaling tags in flattened map of only string values. +// +// Compatible with setting Terraform state for legacy []map[string]string schema. +// Deprecated: Will be removed in a future major version without replacement. +func (tags KeyValueTags) AutoscalingListOfStringMap() []interface{} { + var result []interface{} + + for _, key := range tags.Keys() { + m := map[string]string{ + "key": key, + "value": aws.StringValue(tags.KeyValue(key)), + "propagate_at_launch": strconv.FormatBool(aws.BoolValue(tags.KeyAdditionalBoolValue(key, "PropagateAtLaunch"))), + } + + result = append(result, m) + } + + return result +} + +// AutoscalingTags returns autoscaling service tags. +func (tags KeyValueTags) AutoscalingTags() []*autoscaling.Tag { + var result []*autoscaling.Tag + + for _, key := range tags.Keys() { + tag := &autoscaling.Tag{ + Key: aws.String(key), + Value: tags.KeyValue(key), + ResourceId: tags.KeyAdditionalStringValue(key, "ResourceId"), + ResourceType: tags.KeyAdditionalStringValue(key, "ResourceType"), + PropagateAtLaunch: tags.KeyAdditionalBoolValue(key, "PropagateAtLaunch"), + } + + result = append(result, tag) + } + + return result +} + +// AutoscalingKeyValueTags creates KeyValueTags from autoscaling service tags. +// +// Accepts the following types: +// - []*autoscaling.Tag +// - []*autoscaling.TagDescription +// - []interface{} (Terraform TypeList configuration block compatible) +// - *schema.Set (Terraform TypeSet configuration block compatible) +func AutoscalingKeyValueTags(tags interface{}, identifier string, resourceType string) KeyValueTags { + switch tags := tags.(type) { + case []*autoscaling.Tag: + m := make(map[string]*TagData, len(tags)) + + for _, tag := range tags { + tagData := &TagData{ + Value: tag.Value, + } + + tagData.AdditionalBoolFields = make(map[string]*bool) + tagData.AdditionalBoolFields["PropagateAtLaunch"] = tag.PropagateAtLaunch + tagData.AdditionalStringFields = make(map[string]*string) + tagData.AdditionalStringFields["ResourceId"] = &identifier + tagData.AdditionalStringFields["ResourceType"] = &resourceType + + m[aws.StringValue(tag.Key)] = tagData + } + + return New(m) + case []*autoscaling.TagDescription: + m := make(map[string]*TagData, len(tags)) + + for _, tag := range tags { + tagData := &TagData{ + Value: tag.Value, + } + + tagData.AdditionalBoolFields = make(map[string]*bool) + tagData.AdditionalBoolFields["PropagateAtLaunch"] = tag.PropagateAtLaunch + tagData.AdditionalStringFields = make(map[string]*string) + tagData.AdditionalStringFields["ResourceId"] = &identifier + tagData.AdditionalStringFields["ResourceType"] = &resourceType + + m[aws.StringValue(tag.Key)] = tagData + } + + return New(m) + case *schema.Set: + return AutoscalingKeyValueTags(tags.List(), identifier, resourceType) + case []interface{}: + result := make(map[string]*TagData) + + for _, tfMapRaw := range tags { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + key, ok := tfMap["key"].(string) + + if !ok { + continue + } + + tagData := &TagData{} + + if v, ok := tfMap["value"].(string); ok { + tagData.Value = &v + } + + tagData.AdditionalBoolFields = make(map[string]*bool) + if v, ok := tfMap["propagate_at_launch"].(bool); ok { + tagData.AdditionalBoolFields["PropagateAtLaunch"] = &v + } + + // Deprecated: Legacy map handling + if v, ok := tfMap["propagate_at_launch"].(string); ok { + b, _ := strconv.ParseBool(v) + tagData.AdditionalBoolFields["PropagateAtLaunch"] = &b + } + + tagData.AdditionalStringFields = make(map[string]*string) + tagData.AdditionalStringFields["ResourceId"] = &identifier + tagData.AdditionalStringFields["ResourceType"] = &resourceType + + result[key] = tagData + } + + return New(result) + default: + return New(nil) + } +} + // Cloud9Tags returns cloud9 service tags. func (tags KeyValueTags) Cloud9Tags() []*cloud9.Tag { result := make([]*cloud9.Tag, 0, len(tags)) @@ -1122,7 +1281,10 @@ func (tags KeyValueTags) Ec2Tags() []*ec2.Tag { } // Ec2KeyValueTags creates KeyValueTags from ec2 service tags. -// Accepts []*ec2.Tag and []*ec2.TagDescription. +// +// Accepts the following types: +// - []*ec2.Tag +// - []*ec2.TagDescription func Ec2KeyValueTags(tags interface{}) KeyValueTags { switch tags := tags.(type) { case []*ec2.Tag: diff --git a/aws/internal/keyvaluetags/update_tags_gen.go b/aws/internal/keyvaluetags/update_tags_gen.go index f2d5cd53b8f..e26ee9ad5c6 100644 --- a/aws/internal/keyvaluetags/update_tags_gen.go +++ b/aws/internal/keyvaluetags/update_tags_gen.go @@ -16,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go/service/appstream" "github.com/aws/aws-sdk-go/service/appsync" "github.com/aws/aws-sdk-go/service/athena" + "github.com/aws/aws-sdk-go/service/autoscaling" "github.com/aws/aws-sdk-go/service/backup" "github.com/aws/aws-sdk-go/service/cloud9" "github.com/aws/aws-sdk-go/service/cloudfront" @@ -473,6 +474,40 @@ func AthenaUpdateTags(conn *athena.Athena, identifier string, oldTagsMap interfa return nil } +// AutoscalingUpdateTags updates autoscaling service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func AutoscalingUpdateTags(conn *autoscaling.AutoScaling, identifier string, resourceType string, oldTagsSet interface{}, newTagsSet interface{}) error { + oldTags := AutoscalingKeyValueTags(oldTagsSet, identifier, resourceType) + newTags := AutoscalingKeyValueTags(newTagsSet, identifier, resourceType) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &autoscaling.DeleteTagsInput{ + Tags: removedTags.IgnoreAws().AutoscalingTags(), + } + + _, err := conn.DeleteTags(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &autoscaling.CreateOrUpdateTagsInput{ + Tags: updatedTags.IgnoreAws().AutoscalingTags(), + } + + _, err := conn.CreateOrUpdateTags(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + // BackupUpdateTags updates backup service tags. // The identifier is typically the Amazon Resource Name (ARN), although // it may also be a different identifier depending on the service. From d51ee6657d717fba61ff1cfb54c91bf9c3e8664a Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 22 Jun 2020 10:31:36 -0400 Subject: [PATCH 2/2] resource/aws_autoscaling_group: Implement keyvaluetags and support provider ignore_tags Reference: https://github.com/terraform-providers/terraform-provider-aws/issues/13808 Output from acceptance testing: ``` --- PASS: TestAccAWSAutoScalingGroup_ALB_TargetGroups (179.86s) --- PASS: TestAccAWSAutoScalingGroup_ALB_TargetGroups_ELBCapacity (333.20s) --- PASS: TestAccAWSAutoScalingGroup_autoGeneratedName (51.77s) --- PASS: TestAccAWSAutoScalingGroup_basic (240.41s) --- PASS: TestAccAWSAutoScalingGroup_classicVpcZoneIdentifier (134.91s) --- PASS: TestAccAWSAutoScalingGroup_enablingMetrics (177.09s) --- PASS: TestAccAWSAutoScalingGroup_initialLifecycleHook (243.57s) --- PASS: TestAccAWSAutoScalingGroup_launchTemplate (45.88s) --- PASS: TestAccAWSAutoScalingGroup_LaunchTemplate_IAMInstanceProfile (58.72s) --- PASS: TestAccAWSAutoScalingGroup_launchTemplate_update (106.61s) --- PASS: TestAccAWSAutoScalingGroup_launchTempPartitionNum (51.25s) --- PASS: TestAccAWSAutoScalingGroup_LoadBalancers (618.13s) --- PASS: TestAccAWSAutoScalingGroup_MaxInstanceLifetime (165.12s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy (44.59s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDemandAllocationStrategy (42.10s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDemandBaseCapacity (76.16s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_OnDemandPercentageAboveBaseCapacity (47.32s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotAllocationStrategy (43.62s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotInstancePools (82.44s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_SpotMaxPrice (150.94s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_InstancesDistribution_UpdateToZeroOnDemandBaseCapacity (78.70s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_LaunchTemplateSpecification_LaunchTemplateName (45.47s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_LaunchTemplateSpecification_Version (84.44s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_InstanceType (76.53s) --- PASS: TestAccAWSAutoScalingGroup_MixedInstancesPolicy_LaunchTemplate_Override_WeightedCapacity (207.67s) --- PASS: TestAccAWSAutoScalingGroup_namePrefix (50.25s) --- PASS: TestAccAWSAutoScalingGroup_serviceLinkedRoleARN (54.73s) --- PASS: TestAccAWSAutoScalingGroup_suspendingProcesses (198.11s) --- PASS: TestAccAWSAutoScalingGroup_tags (232.54s) --- PASS: TestAccAWSAutoScalingGroup_TargetGroupArns (247.63s) --- PASS: TestAccAWSAutoScalingGroup_terminationPolicies (105.39s) --- PASS: TestAccAWSAutoScalingGroup_VpcUpdates (175.27s) --- PASS: TestAccAWSAutoScalingGroup_WithLoadBalancer (251.24s) --- PASS: TestAccAWSAutoScalingGroup_WithLoadBalancer_ToTargetGroup (392.43s) --- PASS: TestAccAWSAutoScalingGroup_withMetrics (81.00s) --- PASS: TestAccAWSAutoScalingGroup_withPlacementGroup (146.95s) ``` --- aws/autoscaling_tags.go | 295 --------------------- aws/autoscaling_tags_test.go | 188 ------------- aws/resource_aws_autoscaling_group.go | 120 +++++---- aws/resource_aws_autoscaling_group_test.go | 45 ++++ website/docs/index.html.markdown | 2 +- 5 files changed, 118 insertions(+), 532 deletions(-) delete mode 100644 aws/autoscaling_tags.go delete mode 100644 aws/autoscaling_tags_test.go diff --git a/aws/autoscaling_tags.go b/aws/autoscaling_tags.go deleted file mode 100644 index 3ed28397055..00000000000 --- a/aws/autoscaling_tags.go +++ /dev/null @@ -1,295 +0,0 @@ -package aws - -import ( - "bytes" - "fmt" - "log" - "regexp" - "strconv" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/autoscaling" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" -) - -// autoscalingTagSchema returns the schema to use for the tag element. -func autoscalingTagSchema() *schema.Schema { - return &schema.Schema{ - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Required: true, - }, - - "value": { - Type: schema.TypeString, - Required: true, - }, - - "propagate_at_launch": { - Type: schema.TypeBool, - Required: true, - }, - }, - }, - Set: autoscalingTagToHash, - } -} - -func autoscalingTagToHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["key"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) - buf.WriteString(fmt.Sprintf("%t-", m["propagate_at_launch"].(bool))) - - return hashcode.String(buf.String()) -} - -// setTags is a helper to set the tags for a resource. It expects the -// tags field to be named "tag" -func setAutoscalingTags(conn *autoscaling.AutoScaling, d *schema.ResourceData) error { - resourceID := d.Get("name").(string) - var createTags, removeTags []*autoscaling.Tag - - if d.HasChanges("tag", "tags") { - oraw, nraw := d.GetChange("tag") - o := setToMapByKey(oraw.(*schema.Set)) - n := setToMapByKey(nraw.(*schema.Set)) - - old, err := autoscalingTagsFromMap(o, resourceID) - if err != nil { - return err - } - - new, err := autoscalingTagsFromMap(n, resourceID) - if err != nil { - return err - } - - c, r, err := diffAutoscalingTags(old, new, resourceID) - if err != nil { - return err - } - - createTags = append(createTags, c...) - removeTags = append(removeTags, r...) - - oraw, nraw = d.GetChange("tags") - old, err = autoscalingTagsFromList(oraw.(*schema.Set).List(), resourceID) - if err != nil { - return err - } - - new, err = autoscalingTagsFromList(nraw.(*schema.Set).List(), resourceID) - if err != nil { - return err - } - - c, r, err = diffAutoscalingTags(old, new, resourceID) - if err != nil { - return err - } - - createTags = append(createTags, c...) - removeTags = append(removeTags, r...) - } - - // Set tags - if len(removeTags) > 0 { - log.Printf("[DEBUG] Removing autoscaling tags: %#v", removeTags) - - remove := autoscaling.DeleteTagsInput{ - Tags: removeTags, - } - - if _, err := conn.DeleteTags(&remove); err != nil { - return err - } - } - - if len(createTags) > 0 { - log.Printf("[DEBUG] Creating autoscaling tags: %#v", createTags) - - create := autoscaling.CreateOrUpdateTagsInput{ - Tags: createTags, - } - - if _, err := conn.CreateOrUpdateTags(&create); err != nil { - return err - } - } - - return nil -} - -// diffTags takes our tags locally and the ones remotely and returns -// the set of tags that must be created, and the set of tags that must -// be destroyed. -func diffAutoscalingTags(oldTags, newTags []*autoscaling.Tag, resourceID string) ([]*autoscaling.Tag, []*autoscaling.Tag, error) { - // First, we're creating everything we have - create := make(map[string]interface{}) - for _, t := range newTags { - tag := map[string]interface{}{ - "key": *t.Key, - "value": *t.Value, - "propagate_at_launch": *t.PropagateAtLaunch, - } - create[*t.Key] = tag - } - - // Build the list of what to remove - var remove []*autoscaling.Tag - for _, t := range oldTags { - old, ok := create[*t.Key].(map[string]interface{}) - - if !ok || old["value"] != *t.Value || old["propagate_at_launch"] != *t.PropagateAtLaunch { - // Delete it! - remove = append(remove, t) - } - } - - createTags, err := autoscalingTagsFromMap(create, resourceID) - if err != nil { - return nil, nil, err - } - - return createTags, remove, nil -} - -func autoscalingTagsFromList(vs []interface{}, resourceID string) ([]*autoscaling.Tag, error) { - result := make([]*autoscaling.Tag, 0, len(vs)) - for _, tag := range vs { - attr, ok := tag.(map[string]interface{}) - if !ok || len(attr) == 0 { - continue - } - - t, err := autoscalingTagFromMap(attr, resourceID) - if err != nil { - return nil, err - } - - if t != nil { - result = append(result, t) - } - } - return result, nil -} - -// tagsFromMap returns the tags for the given map of data. -func autoscalingTagsFromMap(m map[string]interface{}, resourceID string) ([]*autoscaling.Tag, error) { - result := make([]*autoscaling.Tag, 0, len(m)) - for _, v := range m { - attr, ok := v.(map[string]interface{}) - if !ok { - continue - } - - t, err := autoscalingTagFromMap(attr, resourceID) - if err != nil { - return nil, err - } - - if t != nil { - result = append(result, t) - } - } - - return result, nil -} - -func autoscalingTagFromMap(attr map[string]interface{}, resourceID string) (*autoscaling.Tag, error) { - if _, ok := attr["key"]; !ok { - return nil, fmt.Errorf("%s: invalid tag attributes: key missing", resourceID) - } - - if _, ok := attr["value"]; !ok { - return nil, fmt.Errorf("%s: invalid tag attributes: value missing", resourceID) - } - - if _, ok := attr["propagate_at_launch"]; !ok { - return nil, fmt.Errorf("%s: invalid tag attributes: propagate_at_launch missing", resourceID) - } - - var propagateAtLaunch bool - var err error - - if v, ok := attr["propagate_at_launch"].(bool); ok { - propagateAtLaunch = v - } - - if v, ok := attr["propagate_at_launch"].(string); ok { - if propagateAtLaunch, err = strconv.ParseBool(v); err != nil { - return nil, fmt.Errorf( - "%s: invalid tag attribute: invalid value for propagate_at_launch: %s", - resourceID, - v, - ) - } - } - - t := &autoscaling.Tag{ - Key: aws.String(attr["key"].(string)), - Value: aws.String(attr["value"].(string)), - PropagateAtLaunch: aws.Bool(propagateAtLaunch), - ResourceId: aws.String(resourceID), - ResourceType: aws.String("auto-scaling-group"), - } - - if tagIgnoredAutoscaling(t) { - return nil, nil - } - - return t, nil -} - -// autoscalingTagDescriptionsToSlice turns the list of tags into a slice. If -// forceStrings is true, all values are converted to strings -func autoscalingTagDescriptionsToSlice(ts []*autoscaling.TagDescription, forceStrings bool) []map[string]interface{} { - tags := make([]map[string]interface{}, 0, len(ts)) - for _, t := range ts { - var propagateAtLaunch interface{} - if forceStrings { - propagateAtLaunch = strconv.FormatBool(aws.BoolValue(t.PropagateAtLaunch)) - } else { - propagateAtLaunch = aws.BoolValue(t.PropagateAtLaunch) - } - tags = append(tags, map[string]interface{}{ - "key": aws.StringValue(t.Key), - "value": aws.StringValue(t.Value), - "propagate_at_launch": propagateAtLaunch, - }) - } - - return tags -} - -func setToMapByKey(s *schema.Set) map[string]interface{} { - result := make(map[string]interface{}) - for _, rawData := range s.List() { - data := rawData.(map[string]interface{}) - result[data["key"].(string)] = data - } - - return result -} - -// compare a tag against a list of strings and checks if it should -// be ignored or not -func tagIgnoredAutoscaling(t *autoscaling.Tag) bool { - filter := []string{"^aws:"} - for _, v := range filter { - log.Printf("[DEBUG] Matching %v with %v\n", v, *t.Key) - r, _ := regexp.MatchString(v, *t.Key) - if r { - log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", *t.Key, *t.Value) - return true - } - } - return false -} diff --git a/aws/autoscaling_tags_test.go b/aws/autoscaling_tags_test.go deleted file mode 100644 index 48570c25e4a..00000000000 --- a/aws/autoscaling_tags_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package aws - -import ( - "fmt" - "reflect" - "testing" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/autoscaling" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestDiffAutoscalingTags(t *testing.T) { - cases := []struct { - Old, New map[string]interface{} - Create, Remove map[string]interface{} - }{ - // Basic add/remove - { - Old: map[string]interface{}{ - "Name": map[string]interface{}{ - "key": "Name", - "value": "bar", - "propagate_at_launch": true, - }, - }, - New: map[string]interface{}{ - "DifferentTag": map[string]interface{}{ - "key": "DifferentTag", - "value": "baz", - "propagate_at_launch": true, - }, - }, - Create: map[string]interface{}{ - "DifferentTag": map[string]interface{}{ - "key": "DifferentTag", - "value": "baz", - "propagate_at_launch": true, - }, - }, - Remove: map[string]interface{}{ - "Name": map[string]interface{}{ - "key": "Name", - "value": "bar", - "propagate_at_launch": true, - }, - }, - }, - - // Modify - { - Old: map[string]interface{}{ - "Name": map[string]interface{}{ - "key": "Name", - "value": "bar", - "propagate_at_launch": true, - }, - }, - New: map[string]interface{}{ - "Name": map[string]interface{}{ - "key": "Name", - "value": "baz", - "propagate_at_launch": false, - }, - }, - Create: map[string]interface{}{ - "Name": map[string]interface{}{ - "key": "Name", - "value": "baz", - "propagate_at_launch": false, - }, - }, - Remove: map[string]interface{}{ - "Name": map[string]interface{}{ - "key": "Name", - "value": "bar", - "propagate_at_launch": true, - }, - }, - }, - } - - var resourceID = "sample" - - for i, tc := range cases { - awsTagsOld, err := autoscalingTagsFromMap(tc.Old, resourceID) - if err != nil { - t.Fatalf("%d: unexpected error convertig old tags: %v", i, err) - } - - awsTagsNew, err := autoscalingTagsFromMap(tc.New, resourceID) - if err != nil { - t.Fatalf("%d: unexpected error convertig new tags: %v", i, err) - } - - c, r, err := diffAutoscalingTags(awsTagsOld, awsTagsNew, resourceID) - if err != nil { - t.Fatalf("%d: unexpected error diff'ing tags: %v", i, err) - } - - cm := autoscalingTagsToMap(c) - rm := autoscalingTagsToMap(r) - if !reflect.DeepEqual(cm, tc.Create) { - t.Fatalf("%d: bad create: \n%#v\n%#v", i, cm, tc.Create) - } - if !reflect.DeepEqual(rm, tc.Remove) { - t.Fatalf("%d: bad remove: \n%#v\n%#v", i, rm, tc.Remove) - } - } -} - -// testAccCheckTags can be used to check the tags on a resource. -func testAccCheckAutoscalingTags( - ts *[]*autoscaling.TagDescription, key string, expected map[string]interface{}) resource.TestCheckFunc { - return func(s *terraform.State) error { - m := autoscalingTagDescriptionsToMap(ts) - v, ok := m[key] - if !ok { - return fmt.Errorf("Missing tag: %s", key) - } - - if v["value"] != expected["value"].(string) || - v["propagate_at_launch"] != expected["propagate_at_launch"].(bool) { - return fmt.Errorf("%s: bad value: %s", key, v) - } - - return nil - } -} - -func testAccCheckAutoscalingTagNotExists(ts *[]*autoscaling.TagDescription, key string) resource.TestCheckFunc { - return func(s *terraform.State) error { - m := autoscalingTagDescriptionsToMap(ts) - if _, ok := m[key]; ok { - return fmt.Errorf("Tag exists when it should not: %s", key) - } - - return nil - } -} - -func TestIgnoringTagsAutoscaling(t *testing.T) { - var ignoredTags []*autoscaling.Tag - ignoredTags = append(ignoredTags, &autoscaling.Tag{ - Key: aws.String("aws:cloudformation:logical-id"), - Value: aws.String("foo"), - }) - ignoredTags = append(ignoredTags, &autoscaling.Tag{ - Key: aws.String("aws:foo:bar"), - Value: aws.String("baz"), - }) - for _, tag := range ignoredTags { - if !tagIgnoredAutoscaling(tag) { - t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value) - } - } -} - -// autoscalingTagsToMap turns the list of tags into a map. -func autoscalingTagsToMap(ts []*autoscaling.Tag) map[string]interface{} { - tags := make(map[string]interface{}) - for _, t := range ts { - tag := map[string]interface{}{ - "key": *t.Key, - "value": *t.Value, - "propagate_at_launch": *t.PropagateAtLaunch, - } - tags[*t.Key] = tag - } - - return tags -} - -// autoscalingTagDescriptionsToMap turns the list of tags into a map. -func autoscalingTagDescriptionsToMap(ts *[]*autoscaling.TagDescription) map[string]map[string]interface{} { - tags := make(map[string]map[string]interface{}) - for _, t := range *ts { - tag := map[string]interface{}{ - "key": *t.Key, - "value": *t.Value, - "propagate_at_launch": *t.PropagateAtLaunch, - } - tags[*t.Key] = tag - } - - return tags -} diff --git a/aws/resource_aws_autoscaling_group.go b/aws/resource_aws_autoscaling_group.go index b181340b85b..3990d981f73 100644 --- a/aws/resource_aws_autoscaling_group.go +++ b/aws/resource_aws_autoscaling_group.go @@ -10,17 +10,21 @@ import ( "strings" "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/autoscaling" + "github.com/aws/aws-sdk-go/service/elb" + "github.com/aws/aws-sdk-go/service/elbv2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" +) - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/autoscaling" - "github.com/aws/aws-sdk-go/service/elb" - "github.com/aws/aws-sdk-go/service/elbv2" +const ( + autoscalingTagResourceTypeAutoScalingGroup = `auto-scaling-group` ) func resourceAwsAutoscalingGroup() *schema.Resource { @@ -385,7 +389,38 @@ func resourceAwsAutoscalingGroup() *schema.Resource { }, }, - "tag": autoscalingTagSchema(), + "tag": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + + "value": { + Type: schema.TypeString, + Required: true, + }, + + "propagate_at_launch": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + // This should be removable, but wait until other tags work is being done. + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["key"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["value"].(string))) + buf.WriteString(fmt.Sprintf("%t-", m["propagate_at_launch"].(bool))) + + return hashcode.String(buf.String()) + }, + }, "tags": { Type: schema.TypeSet, @@ -565,22 +600,13 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) } resourceID := d.Get("name").(string) + if v, ok := d.GetOk("tag"); ok { - var err error - createOpts.Tags, err = autoscalingTagsFromMap( - setToMapByKey(v.(*schema.Set)), resourceID) - if err != nil { - return err - } + createOpts.Tags = keyvaluetags.AutoscalingKeyValueTags(v, resourceID, autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().AutoscalingTags() } if v, ok := d.GetOk("tags"); ok { - tags, err := autoscalingTagsFromList(v.(*schema.Set).List(), resourceID) - if err != nil { - return err - } - - createOpts.Tags = append(createOpts.Tags, tags...) + createOpts.Tags = keyvaluetags.AutoscalingKeyValueTags(v, resourceID, autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().AutoscalingTags() } if v, ok := d.GetOk("default_cooldown"); ok { @@ -687,6 +713,7 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).autoscalingconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig g, err := getAwsAutoscalingGroup(d.Id(), conn) if err != nil { @@ -745,47 +772,31 @@ func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) e return fmt.Errorf("error setting suspended_processes: %s", err) } - var tagList, tagsList []*autoscaling.TagDescription var tagOk, tagsOk bool var v interface{} + // Deprecated: In a future major version, this should always set all tags except those ignored. + // Remove d.GetOk() and Only() handling. if v, tagOk = d.GetOk("tag"); tagOk { - tags := setToMapByKey(v.(*schema.Set)) - for _, t := range g.Tags { - if _, ok := tags[*t.Key]; ok { - tagList = append(tagList, t) - } + proposedStateTags := keyvaluetags.AutoscalingKeyValueTags(v, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + + if err := d.Set("tag", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Only(proposedStateTags).AutoscalingListOfMap()); err != nil { + return fmt.Errorf("error setting tag: %w", err) } - d.Set("tag", autoscalingTagDescriptionsToSlice(tagList, false)) } if v, tagsOk = d.GetOk("tags"); tagsOk { - tags := map[string]struct{}{} - for _, tag := range v.(*schema.Set).List() { - attr, ok := tag.(map[string]interface{}) - if !ok { - continue - } - - key, ok := attr["key"].(string) - if !ok { - continue - } - - tags[key] = struct{}{} - } + proposedStateTags := keyvaluetags.AutoscalingKeyValueTags(v, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) - for _, t := range g.Tags { - if _, ok := tags[*t.Key]; ok { - tagsList = append(tagsList, t) - } + if err := d.Set("tags", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Only(proposedStateTags).AutoscalingListOfStringMap()); err != nil { + return fmt.Errorf("error setting tags: %w", err) } - //lintignore:AWSR002 - d.Set("tags", autoscalingTagDescriptionsToSlice(tagsList, true)) } if !tagOk && !tagsOk { - d.Set("tag", autoscalingTagDescriptionsToSlice(g.Tags, false)) + if err := d.Set("tag", keyvaluetags.AutoscalingKeyValueTags(g.Tags, d.Id(), autoscalingTagResourceTypeAutoScalingGroup).IgnoreAws().IgnoreConfig(ignoreTagsConfig).AutoscalingListOfMap()); err != nil { + return fmt.Errorf("error setting tag: %w", err) + } } if err := d.Set("target_group_arns", flattenStringList(g.TargetGroupARNs)); err != nil { @@ -972,8 +983,21 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) opts.ServiceLinkedRoleARN = aws.String(d.Get("service_linked_role_arn").(string)) } - if err := setAutoscalingTags(conn, d); err != nil { - return err + if d.HasChanges("tag", "tags") { + oTagRaw, nTagRaw := d.GetChange("tag") + oTagsRaw, nTagsRaw := d.GetChange("tags") + + oTag := keyvaluetags.AutoscalingKeyValueTags(oTagRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + oTags := keyvaluetags.AutoscalingKeyValueTags(oTagsRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + oldTags := oTag.Merge(oTags).AutoscalingTags() + + nTag := keyvaluetags.AutoscalingKeyValueTags(nTagRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + nTags := keyvaluetags.AutoscalingKeyValueTags(nTagsRaw, d.Id(), autoscalingTagResourceTypeAutoScalingGroup) + newTags := nTag.Merge(nTags).AutoscalingTags() + + if err := keyvaluetags.AutoscalingUpdateTags(conn, d.Id(), autoscalingTagResourceTypeAutoScalingGroup, oldTags, newTags); err != nil { + return fmt.Errorf("error updating tags for Auto Scaling Group (%s): %w", d.Id(), err) + } } log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts) diff --git a/aws/resource_aws_autoscaling_group_test.go b/aws/resource_aws_autoscaling_group_test.go index 9a65113251a..d5028d9235d 100644 --- a/aws/resource_aws_autoscaling_group_test.go +++ b/aws/resource_aws_autoscaling_group_test.go @@ -1188,6 +1188,51 @@ func testAccCheckAWSAutoScalingGroupAttributesVPCZoneIdentifier(group *autoscali } } +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckAutoscalingTags( + ts *[]*autoscaling.TagDescription, key string, expected map[string]interface{}) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := autoscalingTagDescriptionsToMap(ts) + v, ok := m[key] + if !ok { + return fmt.Errorf("Missing tag: %s", key) + } + + if v["value"] != expected["value"].(string) || + v["propagate_at_launch"] != expected["propagate_at_launch"].(bool) { + return fmt.Errorf("%s: bad value: %s", key, v) + } + + return nil + } +} + +func testAccCheckAutoscalingTagNotExists(ts *[]*autoscaling.TagDescription, key string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := autoscalingTagDescriptionsToMap(ts) + if _, ok := m[key]; ok { + return fmt.Errorf("Tag exists when it should not: %s", key) + } + + return nil + } +} + +// autoscalingTagDescriptionsToMap turns the list of tags into a map. +func autoscalingTagDescriptionsToMap(ts *[]*autoscaling.TagDescription) map[string]map[string]interface{} { + tags := make(map[string]map[string]interface{}) + for _, t := range *ts { + tag := map[string]interface{}{ + "key": aws.StringValue(t.Key), + "value": aws.StringValue(t.Value), + "propagate_at_launch": aws.BoolValue(t.PropagateAtLaunch), + } + tags[aws.StringValue(t.Key)] = tag + } + + return tags +} + // testAccCheckAWSALBTargetGroupHealthy checks an *elbv2.TargetGroup to make // sure that all instances in it are healthy. func testAccCheckAWSALBTargetGroupHealthy(res *elbv2.TargetGroup) resource.TestCheckFunc { diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 2942f93fc20..bca68b20529 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -191,7 +191,7 @@ for more information about connecting to alternate AWS endpoints or AWS compatib potentially end up destroying a live environment). Conflicts with `allowed_account_ids`. -* `ignore_tags` - (Optional) Configuration block with resource tag settings to ignore across all resources handled by this provider (except `aws_autoscaling_group` and any individual service tag resources such as `aws_ec2_tag`) for situations where external systems are managing certain resource tags. Arguments to the configuration block are described below in the `ignore_tags` Configuration Block section. See the [Terraform multiple provider instances documentation](/docs/configuration/providers.html#alias-multiple-provider-instances) for more information about additional provider configurations. +* `ignore_tags` - (Optional) Configuration block with resource tag settings to ignore across all resources handled by this provider (except any individual service tag resources such as `aws_ec2_tag`) for situations where external systems are managing certain resource tags. Arguments to the configuration block are described below in the `ignore_tags` Configuration Block section. See the [Terraform multiple provider instances documentation](/docs/configuration/providers.html#alias-multiple-provider-instances) for more information about additional provider configurations. * `insecure` - (Optional) Explicitly allow the provider to perform "insecure" SSL requests. If omitted, default value is `false`.