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

Tags: Add custom Map type with semantic equality for tags and tags_all attributes #38869

Merged
merged 55 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
2b4ac70
Supports `null` valued tags in Framework. Implements in `batch`
gdavison Aug 12, 2024
6c638e2
Centralizes common import body
gdavison Aug 12, 2024
5d81c01
Uses `tags.MapValue` for `tags_all`
gdavison Aug 12, 2024
b401ff7
Adds Semgrep check for type of Tags and TagsAll fields
gdavison Aug 12, 2024
f74747c
Renames `tags.MapValue` to `tags.Map`
gdavison Aug 12, 2024
b4c5924
Ignore Semgrep rule for schema in migrator
gdavison Aug 12, 2024
1b19102
`appconfig`
gdavison Aug 13, 2024
df39646
Tweaks generator
gdavison Aug 13, 2024
c80fb16
`amp`
gdavison Aug 13, 2024
fffc649
`appfabric`
gdavison Aug 13, 2024
c56d28a
`auditmanager`
gdavison Aug 15, 2024
4b4193f
`bcmdataexport`
gdavison Aug 16, 2024
642f707
`bedrockagent`
gdavison Aug 16, 2024
14dae51
`chatbot`
gdavison Aug 16, 2024
5dba853
`codeguruprofiler`
gdavison Aug 16, 2024
b06e1a6
`datazone`
gdavison Aug 16, 2024
6538fda
`bedrock`
gdavison Aug 17, 2024
667efd8
`docdbelastic`
gdavison Aug 27, 2024
bfb9b6e
`drs`
gdavison Aug 27, 2024
bc5d212
`ec2`
gdavison Aug 27, 2024
ed6a09c
`elasticache`
gdavison Aug 28, 2024
cf8ee56
`eks`
gdavison Aug 28, 2024
300766f
`globalaccelerator`
gdavison Aug 28, 2024
fd16764
`guardduty`
gdavison Aug 28, 2024
595e094
`lexv2models`
gdavison Aug 28, 2024
30fb053
`m2`
gdavison Aug 28, 2024
969dbb8
`medialive`
gdavison Aug 28, 2024
3e18576
`meta`
gdavison Aug 28, 2024
0c01f87
`networkfirewall`
gdavison Aug 28, 2024
9737bd1
`networkmonitor`
gdavison Aug 28, 2024
bd4d470
`opensearchserverless`
gdavison Aug 28, 2024
0ef2a09
`osis`
gdavison Aug 28, 2024
33305eb
`paymentcryptography`
gdavison Aug 28, 2024
40302eb
`pinpoint`
gdavison Aug 28, 2024
3d71e19
`quicksight`
gdavison Aug 28, 2024
950c6bc
`rds`
gdavison Aug 28, 2024
6b1f629
`rekognition`
gdavison Aug 29, 2024
d100572
`resourceexplorer2`
gdavison Aug 29, 2024
5fc3b39
`s3control`
gdavison Aug 29, 2024
ed65df3
`securityhub`
gdavison Aug 29, 2024
45f7406
`securitylake`
gdavison Aug 29, 2024
1119071
`ssmcontacts`
gdavison Aug 29, 2024
7790dfa
`ssoadmin`
gdavison Aug 29, 2024
8a0259a
`transfer`
gdavison Aug 29, 2024
6456499
`workspaces`
gdavison Aug 29, 2024
18a28f7
`timestreaminfluxdb`
gdavison Aug 29, 2024
cf6dda1
`fms`
gdavison Aug 29, 2024
b34782d
Updates `skaff` templates
gdavison Aug 29, 2024
4143f9d
`make gen`
gdavison Aug 30, 2024
e41dedb
Semgrep fixes
gdavison Aug 30, 2024
33c36dd
Linting fixes
gdavison Aug 30, 2024
bcf2759
`nolint` directive formatting fix
gdavison Aug 30, 2024
253301f
Corrects map type name
gdavison Sep 4, 2024
2cf5e3a
Merge branch 'main' into td-framwork-tags-custom-type
gdavison Sep 4, 2024
c286b47
Adds CHANGELOG entry
gdavison Sep 4, 2024
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/38869.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
many resources: Fixes perpetual diff when tag has a `null` value.
```
2 changes: 1 addition & 1 deletion .ci/.golangci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
issues:
max-per-linter: 0
max-issues-per-linter: 0
max-same-issues: 0

linters:
Expand Down
8 changes: 8 additions & 0 deletions .ci/semgrep/framework/tags.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rules:
- id: model-tag-types
languages: [go]
message: Use type `tags.Map` for $1
patterns:
- pattern-regex: (\w+)\s+types\.Map\s+`tfsdk:"tags(_all)?"`
- pattern-inside: type $TYPE struct { ... }
severity: ERROR
2 changes: 1 addition & 1 deletion .teamcity/components/generated/services_all.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ val services = mapOf(
"autoscalingplans" to ServiceSpec("Auto Scaling Plans"),
"backup" to ServiceSpec("Backup"),
"batch" to ServiceSpec("Batch", vpcLock = true),
"bcmdataexports" to ServiceSpec("BCM Data Exports"),
"bcmdataexports" to ServiceSpec("BCM Data Exports", parallelismOverride = 5),
"bedrock" to ServiceSpec("Bedrock"),
"bedrockagent" to ServiceSpec("Bedrock Agents"),
"budgets" to ServiceSpec("Web Services Budgets"),
Expand Down
1 change: 1 addition & 0 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ yamllint: ## [CI] YAML Linting / yamllint
go-misspell \
golangci-lint1 \
golangci-lint2 \
golangci-lint3 \
golangci-lint \
help \
import-lint \
Expand Down
3 changes: 3 additions & 0 deletions docs/resource-tagging.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,9 @@ For example, 3 minutes and 30 seconds is `3m30s`.
Some services do not support tags with an empty string value.
In that case, use the annotation `@Testing(skipEmptyTags=true)`.

Some services do not support tags with an null string value.
In that case, use the annotation `@Testing(skipNullTags=true)`.

Some resource types use the no-op `CheckDestroy` function `acctest.CheckDestroyNoop`.
Use the annotation `@Testing(checkDestroyNoop=true)`.

Expand Down
10 changes: 7 additions & 3 deletions internal/framework/resource_with_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ package framework
import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
Expand Down Expand Up @@ -38,7 +38,7 @@ func (r *ResourceWithConfigure) SetTagsAll(ctx context.Context, request resource
defaultTagsConfig := r.Meta().DefaultTagsConfig
ignoreTagsConfig := r.Meta().IgnoreTagsConfig

var planTags types.Map
var planTags tftags.Map

response.Diagnostics.Append(request.Plan.GetAttribute(ctx, path.Root(names.AttrTags), &planTags)...)

Expand All @@ -60,7 +60,11 @@ func (r *ResourceWithConfigure) SetTagsAll(ctx context.Context, request resource
}
}

func mapHasUnknownElements(m types.Map) bool {
type mapValueElementsable interface {
Elements() map[string]attr.Value
}

func mapHasUnknownElements(m mapValueElementsable) bool {
for _, v := range m.Elements() {
if v.IsUnknown() {
return true
Expand Down
9 changes: 9 additions & 0 deletions internal/generate/tagstests/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ type ResourceDatum struct {
SerializeDelay bool
PreCheck bool
SkipEmptyTags bool // TODO: Remove when we have a strategy for resources that have a minimum tag value length of 1
SkipNullTags bool
NoRemoveTags bool
GoImports []goImport
GenerateConfig bool
Expand Down Expand Up @@ -646,6 +647,14 @@ func (v *visitor) processFuncDecl(funcDecl *ast.FuncDecl) {
d.SkipEmptyTags = b
}
}
if attr, ok := args.Keyword["skipNullTags"]; ok {
if b, err := strconv.ParseBool(attr); err != nil {
v.errs = append(v.errs, fmt.Errorf("invalid skipNullTags value: %q at %s. Should be boolean value.", attr, fmt.Sprintf("%s.%s", v.packageName, v.functionName)))
continue
} else {
d.SkipNullTags = b
}
}
if attr, ok := args.Keyword["noRemoveTags"]; ok {
if b, err := strconv.ParseBool(attr); err != nil {
v.errs = append(v.errs, fmt.Errorf("invalid noRemoveTags value: %q at %s. Should be boolean value.", attr, fmt.Sprintf("%s.%s", v.packageName, v.functionName)))
Expand Down
116 changes: 103 additions & 13 deletions internal/generate/tagstests/resource_test.go.gtpl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), know
{{- end }}
{{- end }}

{{ define "ImportBody" }}
{{ define "CommonImportBody" -}}
ResourceName: resourceName,
ImportState: true,
{{ if gt (len .ImportStateID) 0 -}}
Expand All @@ -56,13 +56,49 @@ plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), know
ImportStateIdFunc: {{ .ImportStateIDFunc }}(resourceName),
{{ end -}}
ImportStateVerify: true,
{{- end }}

{{ define "ImportBody" }}
{{ template "CommonImportBody" . }}
{{ if gt (len .ImportIgnore) 0 -}}
ImportStateVerifyIgnore: []string{
{{ range $i, $v := .ImportIgnore }}{{ $v }},{{ end }}
},
{{- end }}
{{ end }}

{{ define "ImportBodyIgnoreKey1" }}
{{ template "CommonImportBody" . }}
{{ if eq .Implementation "framework" -}}
ImportStateVerifyIgnore: []string{
acctest.CtTagsKey1, // The canonical value returned by the AWS API is ""
{{ if gt (len .ImportIgnore) 0 -}}
{{ range $i, $v := .ImportIgnore }}{{ $v }},{{ end }}
{{ end -}}
},
{{- else if gt (len .ImportIgnore) 0 -}}
ImportStateVerifyIgnore: []string{
{{ range $i, $v := .ImportIgnore }}{{ $v }},{{ end }}
},
{{- end }}
{{ end }}

{{ define "ImportBodyIgnoreResourceKey1" }}
{{ template "CommonImportBody" . }}
{{ if eq .Implementation "framework" -}}
ImportStateVerifyIgnore: []string{
"tags.resourcekey1", // The canonical value returned by the AWS API is ""
{{ if gt (len .ImportIgnore) 0 -}}
{{ range $i, $v := .ImportIgnore }}{{ $v }},{{ end }}
{{ end -}}
},
{{ else if gt (len .ImportIgnore) 0 -}}
ImportStateVerifyIgnore: []string{
{{ range $i, $v := .ImportIgnore }}{{ $v }},{{ end }}
},
{{- end }}
{{ end }}

{{ define "testname" -}}
{{ if .Serialize }}testAcc{{ else }}TestAcc{{ end }}{{ .ResourceProviderNameUpper }}{{ .Name }}
{{- end }}
Expand Down Expand Up @@ -347,8 +383,8 @@ func {{ template "testname" . }}_tags(t *testing.T) {
}

func {{ template "testname" . }}_tags_null(t *testing.T) {
{{- if eq .Implementation "framework" }}
t.Skip("Tags with null values are not correctly handled with the Plugin Framework")
{{- if .SkipNullTags }}
t.Skip("Resource {{ .Name }} does not support null tags")
{{ end }}
{{- template "Init" . }}

Expand All @@ -371,16 +407,30 @@ func {{ template "testname" . }}_tags_null(t *testing.T) {
{{- template "ExistsCheck" . -}}
),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
{{ if eq .Implementation "framework" -}}
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.Null(),
})),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.StringExact(""),
})),
{{- else -}}
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{})),
{{- end }}
},
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
{{ if eq .Implementation "framework" -}}
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{})),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.Null(),
})),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.StringExact(""),
})),
{{- else -}}
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
// TODO: Should be known
plancheck.ExpectUnknownValue(resourceName, tfjsonpath.New(names.AttrTagsAll)),
{{- end }}
Expand All @@ -400,9 +450,10 @@ func {{ template "testname" . }}_tags_null(t *testing.T) {
}),
{{ template "AdditionalTfVars" . }}
},
{{- template "ImportBody" . -}}
{{- template "ImportBodyIgnoreKey1" . -}}
},
{{- end }}
{{ if eq .Implementation "sdk" -}}
{
{{ if .AlternateRegionProvider -}}
ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t),
Expand All @@ -416,6 +467,7 @@ func {{ template "testname" . }}_tags_null(t *testing.T) {
PlanOnly: true,
ExpectNonEmptyPlan: false,
},
{{- end }}
},
})
}
Expand Down Expand Up @@ -1854,8 +1906,8 @@ func {{ template "testname" . }}_tags_DefaultTags_emptyProviderOnlyTag(t *testin
}

func {{ template "testname" . }}_tags_DefaultTags_nullOverlappingResourceTag(t *testing.T) {
{{- if eq .Implementation "framework" }}
t.Skip("Tags with null values are not correctly handled with the Plugin Framework")
{{- if .SkipNullTags }}
t.Skip("Resource {{ .Name }} does not support null tags")
{{ end }}
{{- template "Init" . }}

Expand Down Expand Up @@ -1883,18 +1935,36 @@ func {{ template "testname" . }}_tags_DefaultTags_nullOverlappingResourceTag(t *
{{- template "ExistsCheck" . -}}
),
ConfigStateChecks: []statecheck.StateCheck{
{{ if eq .Implementation "framework" -}}
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.Null(),
})),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.StringExact(""),
})),
{{- else -}}
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.StringExact(acctest.CtProviderValue1),
})),
{{- end }}
},
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate),
{{ if eq .Implementation "framework" -}}
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.Null(),
})),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.StringExact(""),
})),
{{- else -}}
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtKey1: knownvalue.StringExact(acctest.CtProviderValue1),
})),
{{- end }}
},
},
},
Expand All @@ -1916,16 +1986,16 @@ func {{ template "testname" . }}_tags_DefaultTags_nullOverlappingResourceTag(t *
}),
{{ template "AdditionalTfVars" . }}
},
{{- template "ImportBody" . -}}
{{- template "ImportBodyIgnoreKey1" . -}}
},
{{- end }}
},
})
}

func {{ template "testname" . }}_tags_DefaultTags_nullNonOverlappingResourceTag(t *testing.T) {
{{- if eq .Implementation "framework" }}
t.Skip("Tags with null values are not correctly handled with the Plugin Framework")
{{- if .SkipNullTags }}
t.Skip("Resource {{ .Name }} does not support null tags")
{{ end }}
{{- template "Init" . }}

Expand Down Expand Up @@ -1953,18 +2023,38 @@ func {{ template "testname" . }}_tags_DefaultTags_nullNonOverlappingResourceTag(
{{- template "ExistsCheck" . -}}
),
ConfigStateChecks: []statecheck.StateCheck{
{{ if eq .Implementation "framework" -}}
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtResourceKey1: knownvalue.Null(),
})),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtResourceKey1: knownvalue.StringExact(""),
acctest.CtProviderKey1: knownvalue.StringExact(acctest.CtProviderValue1),
})),
{{- else -}}
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtProviderKey1: knownvalue.StringExact(acctest.CtProviderValue1),
})),
{{- end }}
},
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate),
{{ if eq .Implementation "framework" -}}
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtResourceKey1: knownvalue.Null(),
})),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtResourceKey1: knownvalue.StringExact(""),
acctest.CtProviderKey1: knownvalue.StringExact(acctest.CtProviderValue1),
})),
{{- else -}}
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTags), knownvalue.Null()),
plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrTagsAll), knownvalue.MapExact(map[string]knownvalue.Check{
acctest.CtProviderKey1: knownvalue.StringExact(acctest.CtProviderValue1),
})),
{{- end }}
},
},
},
Expand All @@ -1986,7 +2076,7 @@ func {{ template "testname" . }}_tags_DefaultTags_nullNonOverlappingResourceTag(
}),
{{ template "AdditionalTfVars" . }}
},
{{- template "ImportBody" . -}}
{{- template "ImportBodyIgnoreResourceKey1" . -}}
},
{{- end }}
},
Expand Down
4 changes: 4 additions & 0 deletions internal/generate/teamcity/acctest_services.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ service "batch" {
vpc_lock = true
}

service "bcmdataexports" {
parallelism = 5
}

service "cloudformation" {
vpc_lock = true
}
Expand Down
Loading
Loading