Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/44739.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_iam_role_policy_attachment: Adds List support
```
94 changes: 94 additions & 0 deletions internal/acctest/querycheck/expect_identity_func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package querycheck

import (
"context"
"errors"
"fmt"
"slices"
"strings"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/querycheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
)

var _ querycheck.QueryResultCheck = expectIdentityFunc{}

type expectIdentityFunc struct {
listResourceAddress string
identityFunc func() map[string]knownvalue.Check
}

// Adapted (with updates) from github.com/hashicorp/terraform-plugin-testing/statecheck.ExpectIdentity
func (e expectIdentityFunc) CheckQuery(_ context.Context, req querycheck.CheckQueryRequest, resp *querycheck.CheckQueryResponse) {
checks := e.identityFunc()

for _, res := range req.Query {
var errCollection []error

if e.listResourceAddress != strings.TrimPrefix(res.Address, "list.") {
continue
}

if len(res.Identity) != len(checks) {
var deltaMsg string
if len(res.Identity) > len(checks) {
deltaMsg = statecheck.CreateDeltaString(res.Identity, checks, "actual identity has extra attribute(s): ")
} else {
deltaMsg = statecheck.CreateDeltaString(checks, res.Identity, "actual identity is missing attribute(s): ")
}

resp.Error = fmt.Errorf("%s - Expected %d attribute(s) in the actual identity object, got %d attribute(s): %s", e.listResourceAddress, len(checks), len(res.Identity), deltaMsg)
return
}

var keys []string

for k := range checks {
keys = append(keys, k)
}

slices.Sort(keys)

for _, k := range keys {
actualIdentityVal, ok := res.Identity[k]

if !ok {
resp.Error = fmt.Errorf("%s - missing attribute %q in actual identity object", e.listResourceAddress, k)
return
}

if err := checks[k].CheckValue(actualIdentityVal); err != nil {
errCollection = append(errCollection, fmt.Errorf("%s - %q identity attribute: %w", e.listResourceAddress, k, err))
}
}

if errCollection == nil {
return
}
}

var errCollection []error
errCollection = append(errCollection, fmt.Errorf("an identity with the following attributes was not found"))

// wrap errors for each check
for attr, check := range checks {
errCollection = append(errCollection, fmt.Errorf("attribute %q: %s", attr, check))
}
errCollection = append(errCollection, fmt.Errorf("address: %s\n", e.listResourceAddress))
resp.Error = errors.Join(errCollection...)
}

// ExpectIdentityFunc returns a query check that asserts that the given list resource contains a resource with an identity matching
// the identity checks returned by the identityFunc.
//
// This query check can only be used with managed resources that support resource identity and query. Query is only supported in Terraform v1.14+
func ExpectIdentityFunc(resourceAddress string, identityFunc func() map[string]knownvalue.Check) querycheck.QueryResultCheck {
return expectIdentityFunc{
listResourceAddress: resourceAddress,
identityFunc: identityFunc,
}
}
71 changes: 71 additions & 0 deletions internal/acctest/statecheck/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package statecheck

import (
"context"
"fmt"
"maps"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
)

type identity struct {
resourceAddress string
values map[string]any
}

func Identity() identity {
return identity{}
}

// GetIdentity sets the resource address to check and stores the identity values.
// Calls to GetIdentity occur before any TestStep is run.
func (v *identity) GetIdentity(resourceAddress string) statecheck.StateCheck {
v.resourceAddress = resourceAddress

return newIdentityStateChecker(v)
}

type identityStateChecker struct {
base Base
identity *identity
}

func newIdentityStateChecker(identity *identity) identityStateChecker {
return identityStateChecker{
base: NewBase(identity.resourceAddress),
identity: identity,
}
}

func (vc identityStateChecker) CheckState(ctx context.Context, request statecheck.CheckStateRequest, response *statecheck.CheckStateResponse) {
resource, ok := vc.base.ResourceFromState(request, response)
if !ok {
return
}

if resource.IdentitySchemaVersion == nil || len(resource.IdentityValues) == 0 {
response.Error = fmt.Errorf("%s - Identity not found in state. Either the resource does not support identity or the Terraform version running the test does not support identity. (must be v1.12+)", vc.base.resourceAddress)

return
}

vc.identity.values = maps.Collect(maps.All(resource.IdentityValues))
}

// Checks returns a function that provides the identity values as knownvalue.Checks.
// Calls to Checks occur before any TestStep is run.
func (v *identity) Checks() func() map[string]knownvalue.Check {
return func() map[string]knownvalue.Check {
checks := make(map[string]knownvalue.Check, len(v.values))

for k, val := range v.values {
checks[k] = knownvalue.StringExact(val.(string))
}

return checks
}
}
70 changes: 54 additions & 16 deletions internal/service/iam/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/hashicorp/terraform-provider-aws/internal/flex"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
"github.com/hashicorp/terraform-provider-aws/internal/logging"
"github.com/hashicorp/terraform-provider-aws/internal/provider/sdkv2/importer"
"github.com/hashicorp/terraform-provider-aws/internal/retry"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
Expand Down Expand Up @@ -731,6 +732,32 @@ func listRoles(ctx context.Context, conn *iam.Client, input *iam.ListRolesInput)
}
}

func listNonServiceLinkedRoles(ctx context.Context, conn *iam.Client, input *iam.ListRolesInput) iter.Seq2[awstypes.Role, error] {
return func(yield func(awstypes.Role, error) bool) {
roles := listRoles(ctx, conn, input)
for role, err := range roles {
if err != nil {
yield(awstypes.Role{}, err)
return
}

// Exclude Service-Linked Roles
if strings.HasPrefix(aws.ToString(role.Path), "/aws-service-role/") {
tflog.Debug(ctx, "Skipping resource", map[string]any{
"skip_reason": "Service-Linked Role",
logging.ResourceAttributeKey("role_name"): aws.ToString(role.RoleName),
logging.ResourceAttributeKey(names.AttrPath): aws.ToString(role.Path),
})
continue
}

if !yield(role, nil) {
return
}
}
}
}

func resourceRoleFlatten(ctx context.Context, role *awstypes.Role, d *schema.ResourceData) diag.Diagnostics {
var diags diag.Diagnostics

Expand Down Expand Up @@ -1132,29 +1159,25 @@ func (l *roleListResource) List(ctx context.Context, request list.ListRequest, s
return
}

stream.Results = func(yield func(list.ListResult) bool) {
result := request.NewListResult(ctx)
tflog.Info(ctx, "Listing resources")

for output, err := range listRoles(ctx, conn, &input) {
stream.Results = func(yield func(list.ListResult) bool) {
for role, err := range listNonServiceLinkedRoles(ctx, conn, &input) {
if err != nil {
result = fwdiag.NewListResultErrorDiagnostic(err)
result := fwdiag.NewListResultErrorDiagnostic(err)
yield(result)
return
}

// Exclude Service-Linked Roles
if strings.HasPrefix(aws.ToString(output.Path), "/aws-service-role/") {
tflog.Debug(ctx, "Skipping resource", map[string]any{
"skip_reason": "Service-Linked Role",
"role_name": aws.ToString(output.RoleName),
names.AttrPath: aws.ToString(output.Path),
})
continue
}
ctx := resourceRoleListItemLoggingContext(ctx, role)

result := request.NewListResult(ctx)

rd := l.ResourceData()
rd.SetId(aws.ToString(output.RoleName))
result.Diagnostics.Append(translateDiags(resourceRoleFlatten(ctx, &output, rd))...)
rd.SetId(aws.ToString(role.RoleName))

tflog.Info(ctx, "Reading resource")
result.Diagnostics.Append(translateDiags(resourceRoleFlatten(ctx, &role, rd))...)
if result.Diagnostics.HasError() {
yield(result)
return
Expand All @@ -1168,7 +1191,7 @@ func (l *roleListResource) List(ctx context.Context, request list.ListRequest, s
return
}

result.DisplayName = aws.ToString(output.RoleName)
result.DisplayName = resourceRoleDisplayName(role)

l.SetResult(ctx, awsClient, request.IncludeResource, &result, rd)
if result.Diagnostics.HasError() {
Expand All @@ -1183,6 +1206,17 @@ func (l *roleListResource) List(ctx context.Context, request list.ListRequest, s
}
}

func resourceRoleDisplayName(role awstypes.Role) string {
var buf strings.Builder

path := aws.ToString(role.Path)
buf.WriteString(strings.TrimPrefix(path, "/"))

buf.WriteString(aws.ToString(role.RoleName))

return buf.String()
}

func translateDiags(in diag.Diagnostics) frameworkdiag.Diagnostics {
out := make(frameworkdiag.Diagnostics, len(in))
for i, diagIn := range in {
Expand Down Expand Up @@ -1237,3 +1271,7 @@ func translatePath(in cty.Path) path.Path {

return out
}

func resourceRoleListItemLoggingContext(ctx context.Context, role awstypes.Role) context.Context {
return tflog.SetField(ctx, logging.ResourceAttributeKey(names.AttrName), aws.ToString(role.RoleName))
}
Loading
Loading