Repository: gruntwork-io/cloud-nuke (go)

Issue:
Feature request: support tag filtering for iam-role
Cloud-nuke should support filtering for tags in the `iam-role` resource.

<sub>Jan Roehrich <jan.roehrich@mercedes-benz.com>, Mercedes-Benz Tech Innovation GmbH, [legal info/Impressum](https://github.com/mercedes-benz/foss/blob/master/LEGAL_IMPRINT.md)</sub> 

Pull request description:
feat(iam-role): add tag filtering support
## Description

Fixes #889.


## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [x] Update the docs.
- [x] Run the relevant tests successfully, including pre-commit checks.
- [x] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.
- [x] Include release notes. If this PR is backward incompatible, include a migration guide.
- [x] Attention Grunts - if this PR adds support for a new resource, ensure the `nuke_sandbox` and `nuke_phxdevops` jobs in `.circleci/config.yml` have been updated with appropriate exclusions (either directly in the job or via the `.circleci/nuke_config.yml` file) to prevent nuking IAM roles, groups, resources, etc that are important for the test accounts.


## Release Notes (draft)

Added tag filtering support for IAM roles

<sub>Jan Roehrich <jan.roehrich@mercedes-benz.com>, Mercedes-Benz Tech Innovation GmbH, [legal info/Impressum](https://github.com/mercedes-benz/foss/blob/master/LEGAL_IMPRINT.md)</sub> 

Patch:
diff --git a/README.md b/README.md
index b8bb62b..6a29a7b 100644
--- a/README.md
+++ b/README.md
@@ -652,7 +652,7 @@ of the file that are supported are listed here.
 | guardduty                        | GuardDuty                     | ❌                                     | ✅ (Created Time)                    | ❌    | ✅       |
 | iam-group                        | IAMGroups                     | ✅ (Group Name)                        | ✅ (Creation Time)                   | ❌    | ✅       |
 | iam-policy                       | IAMPolicies                   | ✅ (Policy Name)                       | ✅ (Creation Time)                   | ❌    | ✅       |
-| iam-role                         | IAMRoles                      | ✅ (Role Name)                         | ✅ (Creation Time)                   | ❌    | ✅       |
+| iam-role                         | IAMRoles                      | ✅ (Role Name)                         | ✅ (Creation Time)                   | ✅    | ✅       |
 | iam-service-linked-role          | IAMServiceLinkedRoles         | ✅ (Service Linked Role Name)          | ✅ (Creation Time)                   | ❌    | ✅       |
 | iam                              | IAMUsers                      | ✅ (User Name)                         | ✅ (Creation Time)                   | ✅    | ✅       |
 | internet-gateway                 | InternetGateway               | ✅ (Gateway Name)                      | ✅ (Creation Time)                   | ✅    | ✅       |
diff --git a/aws/resources/iam_role.go b/aws/resources/iam_role.go
index 02f72d2..82b00db 100644
--- a/aws/resources/iam_role.go
+++ b/aws/resources/iam_role.go
@@ -2,6 +2,7 @@ package resources
 
 import (
 	"context"
+	"github.com/gruntwork-io/cloud-nuke/util"
 	"strings"
 	"sync"
 
@@ -26,7 +27,16 @@ func (ir *IAMRoles) getAll(c context.Context, configObj config.Config) ([]*strin
 		}
 
 		for _, iamRole := range page.Roles {
-			if ir.shouldInclude(&iamRole, configObj) {
+			var tags []types.Tag
+			if len(configObj.IAMRoles.IncludeRule.Tags) > 0 || len(configObj.IAMRoles.ExcludeRule.Tags) > 0 {
+				tagsOut, err := ir.Client.ListRoleTags(c, &iam.ListRoleTagsInput{RoleName: iamRole.RoleName})
+				if err != nil {
+					return nil, errors.WithStackTrace(err)
+				}
+				tags = tagsOut.Tags
+			}
+
+			if ir.shouldInclude(&iamRole, configObj, tags) {
 				allIAMRoles = append(allIAMRoles, iamRole.RoleName)
 			}
 		}
@@ -172,7 +182,7 @@ func (ir *IAMRoles) nukeAll(roleNames []*string) error {
 	return nil
 }
 
-func (ir *IAMRoles) shouldInclude(iamRole *types.Role, configObj config.Config) bool {
+func (ir *IAMRoles) shouldInclude(iamRole *types.Role, configObj config.Config, tags []types.Tag) bool {
 	if iamRole == nil {
 		return false
 	}
@@ -201,6 +211,7 @@ func (ir *IAMRoles) shouldInclude(iamRole *types.Role, configObj config.Config)
 	return configObj.IAMRoles.ShouldInclude(config.ResourceValue{
 		Name: iamRole.RoleName,
 		Time: iamRole.CreateDate,
+		Tags: util.ConvertIAMTagsToMap(tags),
 	})
 }
 
diff --git a/aws/resources/iam_role_types.go b/aws/resources/iam_role_types.go
index d709d93..0024579 100644
--- a/aws/resources/iam_role_types.go
+++ b/aws/resources/iam_role_types.go
@@ -14,6 +14,7 @@ type IAMRolesAPI interface {
 	ListInstanceProfilesForRole(ctx context.Context, params *iam.ListInstanceProfilesForRoleInput, optFns ...func(*iam.Options)) (*iam.ListInstanceProfilesForRoleOutput, error)
 	ListRolePolicies(ctx context.Context, params *iam.ListRolePoliciesInput, optFns ...func(*iam.Options)) (*iam.ListRolePoliciesOutput, error)
 	ListRoles(ctx context.Context, params *iam.ListRolesInput, optFns ...func(*iam.Options)) (*iam.ListRolesOutput, error)
+	ListRoleTags(ctx context.Context, params *iam.ListRoleTagsInput, optFns ...func(*iam.Options)) (*iam.ListRoleTagsOutput, error)
 	DeleteInstanceProfile(ctx context.Context, params *iam.DeleteInstanceProfileInput, optFns ...func(*iam.Options)) (*iam.DeleteInstanceProfileOutput, error)
 	DetachRolePolicy(ctx context.Context, params *iam.DetachRolePolicyInput, optFns ...func(*iam.Options)) (*iam.DetachRolePolicyOutput, error)
 	DeleteRolePolicy(ctx context.Context, params *iam.DeleteRolePolicyInput, optFns ...func(*iam.Options)) (*iam.DeleteRolePolicyOutput, error)


Test patch:
diff --git a/aws/resources/iam_role_test.go b/aws/resources/iam_role_test.go
index 6c8fd6d..7259661 100644
--- a/aws/resources/iam_role_test.go
+++ b/aws/resources/iam_role_test.go
@@ -19,6 +19,7 @@ type mockedIAMRoles struct {
 	ListInstanceProfilesForRoleOutput   iam.ListInstanceProfilesForRoleOutput
 	ListRolePoliciesOutput              iam.ListRolePoliciesOutput
 	ListRolesOutput                     iam.ListRolesOutput
+	ListRoleTagsOutputByName            map[string]*iam.ListRoleTagsOutput
 	DeleteInstanceProfileOutput         iam.DeleteInstanceProfileOutput
 	DetachRolePolicyOutput              iam.DetachRolePolicyOutput
 	DeleteRolePolicyOutput              iam.DeleteRolePolicyOutput
@@ -42,6 +43,10 @@ func (m mockedIAMRoles) ListRoles(ctx context.Context, params *iam.ListRolesInpu
 	return &m.ListRolesOutput, nil
 }
 
+func (m mockedIAMRoles) ListRoleTags(ctx context.Context, params *iam.ListRoleTagsInput, optFns ...func(*iam.Options)) (*iam.ListRoleTagsOutput, error) {
+	return m.ListRoleTagsOutputByName[*params.RoleName], nil
+}
+
 func (m mockedIAMRoles) DeleteInstanceProfile(ctx context.Context, params *iam.DeleteInstanceProfileInput, optFns ...func(*iam.Options)) (*iam.DeleteInstanceProfileOutput, error) {
 	return &m.DeleteInstanceProfileOutput, nil
 }
@@ -81,6 +86,10 @@ func TestIAMRoles_GetAll(t *testing.T) {
 					},
 				},
 			},
+			ListRoleTagsOutputByName: map[string]*iam.ListRoleTagsOutput{
+				testName1: {Tags: []types.Tag{{Key: aws.String("foo"), Value: aws.String("bar")}}},
+				testName2: {Tags: []types.Tag{{Key: aws.String("faz"), Value: aws.String("baz")}}},
+			},
 		},
 	}
 
@@ -108,6 +117,20 @@ func TestIAMRoles_GetAll(t *testing.T) {
 				}},
 			expected: []string{testName1},
 		},
+		"tagExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					Tags: map[string]config.Expression{"foo": {RE: *regexp.MustCompile("bar")}},
+				}},
+			expected: []string{testName2},
+		},
+		"tagInclusionFilter": {
+			configObj: config.ResourceType{
+				IncludeRule: config.FilterRule{
+					Tags: map[string]config.Expression{"foo": {RE: *regexp.MustCompile("bar")}},
+				}},
+			expected: []string{testName1},
+		},
 	}
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
@@ -234,7 +257,7 @@ func TestIAMRoles_ServiceLinkedRoles(t *testing.T) {
 			if tt.role.RoleName != nil || tt.role.Arn != nil {
 				rolePtr = &tt.role
 			}
-			result := ir.shouldInclude(rolePtr, configObj)
+			result := ir.shouldInclude(rolePtr, configObj, []types.Tag{})
 			require.Equal(t, tt.expected, result)
 		})
 	}