From 25ee1609ede8c0651cec6fe3f6210c7c2017f41b Mon Sep 17 00:00:00 2001 From: James Kwon Date: Mon, 31 Jul 2023 19:49:31 -0400 Subject: [PATCH] refactor IAM roles and IAM service linked roles --- aws/aws.go | 4 +- aws/iam_role.go | 88 ++++------ aws/iam_role_test.go | 253 +++++++++++++-------------- aws/iam_role_types.go | 12 +- aws/iam_service_linked_role.go | 57 +++--- aws/iam_service_linked_role_test.go | 194 +++++++++----------- aws/iam_service_linked_role_types.go | 12 +- 7 files changed, 278 insertions(+), 342 deletions(-) diff --git a/aws/aws.go b/aws/aws.go index 6f2f9836..9ac62245 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -2100,7 +2100,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp } if IsNukeable(iamRoles.ResourceName(), resourceTypes) { start := time.Now() - roleNames, err := getAllIamRoles(session, excludeAfter, configObj) + roleNames, err := iamRoles.getALl(configObj) if err != nil { ge := report.GeneralError{ Error: err, @@ -2129,7 +2129,7 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp } if IsNukeable(iamServiceLinkedRoles.ResourceName(), resourceTypes) { start := time.Now() - roleNames, err := getAllIamServiceLinkedRoles(session, excludeAfter, configObj) + roleNames, err := iamServiceLinkedRoles.getAll(configObj) if err != nil { ge := report.GeneralError{ Error: err, diff --git a/aws/iam_role.go b/aws/iam_role.go index ddc2c0e9..3f5be1b2 100644 --- a/aws/iam_role.go +++ b/aws/iam_role.go @@ -1,35 +1,31 @@ package aws import ( - "github.com/gruntwork-io/cloud-nuke/telemetry" - commonTelemetry "github.com/gruntwork-io/go-commons/telemetry" - "strings" - "sync" - "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" "github.com/gruntwork-io/cloud-nuke/config" "github.com/gruntwork-io/cloud-nuke/logging" "github.com/gruntwork-io/cloud-nuke/report" + "github.com/gruntwork-io/cloud-nuke/telemetry" "github.com/gruntwork-io/go-commons/errors" + commonTelemetry "github.com/gruntwork-io/go-commons/telemetry" "github.com/hashicorp/go-multierror" + "strings" + "sync" ) // List all IAM Roles in the AWS account -func getAllIamRoles(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { - svc := iam.New(session) - +func (ir IAMRoles) getALl(configObj config.Config) ([]*string, error) { allIAMRoles := []*string{} - err := svc.ListRolesPages( + err := ir.Client.ListRolesPages( &iam.ListRolesInput{}, func(page *iam.ListRolesOutput, lastPage bool) bool { for _, iamRole := range page.Roles { - if shouldIncludeIAMRole(iamRole, excludeAfter, configObj) { + if ir.shouldInclude(iamRole, configObj) { allIAMRoles = append(allIAMRoles, iamRole.RoleName) } } + return !lastPage }, ) @@ -39,8 +35,8 @@ func getAllIamRoles(session *session.Session, excludeAfter time.Time, configObj return allIAMRoles, nil } -func deleteManagedRolePolicies(svc *iam.IAM, roleName *string) error { - policiesOutput, err := svc.ListAttachedRolePolicies(&iam.ListAttachedRolePoliciesInput{ +func (ir IAMRoles) deleteManagedRolePolicies(roleName *string) error { + policiesOutput, err := ir.Client.ListAttachedRolePolicies(&iam.ListAttachedRolePoliciesInput{ RoleName: roleName, }) if err != nil { @@ -49,7 +45,7 @@ func deleteManagedRolePolicies(svc *iam.IAM, roleName *string) error { for _, attachedPolicy := range policiesOutput.AttachedPolicies { arn := attachedPolicy.PolicyArn - _, err = svc.DetachRolePolicy(&iam.DetachRolePolicyInput{ + _, err = ir.Client.DetachRolePolicy(&iam.DetachRolePolicyInput{ PolicyArn: arn, RoleName: roleName, }) @@ -63,8 +59,8 @@ func deleteManagedRolePolicies(svc *iam.IAM, roleName *string) error { return nil } -func deleteInlineRolePolicies(svc *iam.IAM, roleName *string) error { - policyOutput, err := svc.ListRolePolicies(&iam.ListRolePoliciesInput{ +func (ir IAMRoles) deleteInlineRolePolicies(roleName *string) error { + policyOutput, err := ir.Client.ListRolePolicies(&iam.ListRolePoliciesInput{ RoleName: roleName, }) if err != nil { @@ -73,7 +69,7 @@ func deleteInlineRolePolicies(svc *iam.IAM, roleName *string) error { } for _, policyName := range policyOutput.PolicyNames { - _, err := svc.DeleteRolePolicy(&iam.DeleteRolePolicyInput{ + _, err := ir.Client.DeleteRolePolicy(&iam.DeleteRolePolicyInput{ PolicyName: policyName, RoleName: roleName, }) @@ -87,8 +83,8 @@ func deleteInlineRolePolicies(svc *iam.IAM, roleName *string) error { return nil } -func deleteInstanceProfilesFromRole(svc *iam.IAM, roleName *string) error { - profilesOutput, err := svc.ListInstanceProfilesForRole(&iam.ListInstanceProfilesForRoleInput{ +func (ir IAMRoles) deleteInstanceProfilesFromRole(roleName *string) error { + profilesOutput, err := ir.Client.ListInstanceProfilesForRole(&iam.ListInstanceProfilesForRoleInput{ RoleName: roleName, }) if err != nil { @@ -98,7 +94,7 @@ func deleteInstanceProfilesFromRole(svc *iam.IAM, roleName *string) error { for _, profile := range profilesOutput.InstanceProfiles { // Role needs to be removed from instance profile before it can be deleted - _, err := svc.RemoveRoleFromInstanceProfile(&iam.RemoveRoleFromInstanceProfileInput{ + _, err := ir.Client.RemoveRoleFromInstanceProfile(&iam.RemoveRoleFromInstanceProfileInput{ InstanceProfileName: profile.InstanceProfileName, RoleName: roleName, }) @@ -106,7 +102,7 @@ func deleteInstanceProfilesFromRole(svc *iam.IAM, roleName *string) error { logging.Logger.Debugf("[Failed] %s", err) return errors.WithStackTrace(err) } else { - _, err := svc.DeleteInstanceProfile(&iam.DeleteInstanceProfileInput{ + _, err := ir.Client.DeleteInstanceProfile(&iam.DeleteInstanceProfileInput{ InstanceProfileName: profile.InstanceProfileName, }) if err != nil { @@ -119,8 +115,8 @@ func deleteInstanceProfilesFromRole(svc *iam.IAM, roleName *string) error { return nil } -func deleteIamRole(svc *iam.IAM, roleName *string) error { - _, err := svc.DeleteRole(&iam.DeleteRoleInput{ +func (ir IAMRoles) deleteIamRole(roleName *string) error { + _, err := ir.Client.DeleteRole(&iam.DeleteRoleInput{ RoleName: roleName, }) if err != nil { @@ -131,10 +127,7 @@ func deleteIamRole(svc *iam.IAM, roleName *string) error { } // Delete all IAM Roles -func nukeAllIamRoles(session *session.Session, roleNames []*string) error { - region := aws.StringValue(session.Config.Region) - svc := iam.New(session) - +func (ir IAMRoles) nukeAll(roleNames []*string) error { if len(roleNames) == 0 { logging.Logger.Debug("No IAM Roles to nuke") return nil @@ -150,13 +143,13 @@ func nukeAllIamRoles(session *session.Session, roleNames []*string) error { } // There is no bulk delete IAM Roles API, so we delete the batch of IAM roles concurrently using go routines - logging.Logger.Debugf("Deleting all IAM Roles in region %s", region) + logging.Logger.Debugf("Deleting all IAM Roles") wg := new(sync.WaitGroup) wg.Add(len(roleNames)) errChans := make([]chan error, len(roleNames)) for i, roleName := range roleNames { errChans[i] = make(chan error, 1) - go deleteIamRoleAsync(wg, errChans[i], svc, roleName) + go ir.deleteIamRoleAsync(wg, errChans[i], roleName) } wg.Wait() @@ -168,9 +161,7 @@ func nukeAllIamRoles(session *session.Session, roleNames []*string) error { logging.Logger.Debugf("[Failed] %s", err) telemetry.TrackEvent(commonTelemetry.EventContext{ EventName: "Error Nuking IAM Role", - }, map[string]interface{}{ - "region": *session.Config.Region, - }) + }, map[string]interface{}{}) } } finalErr := allErrs.ErrorOrNil() @@ -179,12 +170,12 @@ func nukeAllIamRoles(session *session.Session, roleNames []*string) error { } for _, roleName := range roleNames { - logging.Logger.Debugf("[OK] IAM Role %s was deleted in %s", aws.StringValue(roleName), region) + logging.Logger.Debugf("[OK] IAM Role %s was deleted", aws.StringValue(roleName)) } return nil } -func shouldIncludeIAMRole(iamRole *iam.Role, excludeAfter time.Time, configObj config.Config) bool { +func (ir IAMRoles) shouldInclude(iamRole *iam.Role, configObj config.Config) bool { if iamRole == nil { return false } @@ -202,18 +193,13 @@ func shouldIncludeIAMRole(iamRole *iam.Role, excludeAfter time.Time, configObj c return false } - if excludeAfter.Before(*iamRole.CreateDate) { - return false - } - - return config.ShouldInclude( - aws.StringValue(iamRole.RoleName), - configObj.IAMRoles.IncludeRule.NamesRegExp, - configObj.IAMRoles.ExcludeRule.NamesRegExp, - ) + return configObj.IAMRoles.ShouldInclude(config.ResourceValue{ + Name: iamRole.RoleName, + Time: iamRole.CreateDate, + }) } -func deleteIamRoleAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, roleName *string) { +func (ir IAMRoles) deleteIamRoleAsync(wg *sync.WaitGroup, errChan chan error, roleName *string) { defer wg.Done() var result *multierror.Error @@ -222,15 +208,15 @@ func deleteIamRoleAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, ro // items we need delete/detach them before actually deleting it. // NOTE: The actual role deletion should always be the last one. This way we // can guarantee that it will fail if we forgot to delete/detach an item. - functions := []func(svc *iam.IAM, roleName *string) error{ - deleteInstanceProfilesFromRole, - deleteInlineRolePolicies, - deleteManagedRolePolicies, - deleteIamRole, + functions := []func(roleName *string) error{ + ir.deleteInstanceProfilesFromRole, + ir.deleteInlineRolePolicies, + ir.deleteManagedRolePolicies, + ir.deleteIamRole, } for _, fn := range functions { - if err := fn(svc, roleName); err != nil { + if err := fn(roleName); err != nil { result = multierror.Append(result, err) } } diff --git a/aws/iam_role_test.go b/aws/iam_role_test.go index 2e3ce9fd..b27b40c3 100644 --- a/aws/iam_role_test.go +++ b/aws/iam_role_test.go @@ -1,166 +1,161 @@ package aws import ( - "github.com/gruntwork-io/cloud-nuke/telemetry" - "testing" - "time" - "github.com/aws/aws-sdk-go/aws" - awsgo "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/iam/iamiface" "github.com/gruntwork-io/cloud-nuke/config" - "github.com/gruntwork-io/cloud-nuke/util" - "github.com/stretchr/testify/assert" + "github.com/gruntwork-io/cloud-nuke/telemetry" "github.com/stretchr/testify/require" + "regexp" + "testing" + "time" ) -func TestListIamRoles(t *testing.T) { - telemetry.InitTelemetry("cloud-nuke", "") - t.Parallel() - - region, err := getRandomRegion() - require.NoError(t, err) - - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) - - roleNames, err := getAllIamRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - - assert.NotEmpty(t, roleNames) +type mockedIAMRoles struct { + iamiface.IAMAPI + ListRolesPagesOutput iam.ListRolesOutput + ListInstanceProfilesForRoleOutput iam.ListInstanceProfilesForRoleOutput + RemoveRoleFromInstanceProfileOutput iam.RemoveRoleFromInstanceProfileOutput + DeleteInstanceProfileOutput iam.DeleteInstanceProfileOutput + ListRolePoliciesOutput iam.ListRolePoliciesOutput + DeleteRolePolicyOutput iam.DeleteRolePolicyOutput + ListAttachedRolePoliciesOutput iam.ListAttachedRolePoliciesOutput + DetachRolePolicyOutput iam.DetachRolePolicyOutput + DeleteRoleOutput iam.DeleteRoleOutput } -func createTestRole(t *testing.T, session *session.Session, name string) error { - svc := iam.New(session) - - input := &iam.CreateRoleInput{ - RoleName: aws.String(name), - AssumeRolePolicyDocument: aws.String(`{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "ec2.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] - }`), - } - - _, err := svc.CreateRole(input) - require.NoError(t, err) - +func (m mockedIAMRoles) ListRolesPages(input *iam.ListRolesInput, f func(*iam.ListRolesOutput, bool) bool) error { + f(&m.ListRolesPagesOutput, true) return nil } -func createAndAttachInstanceProfile(t *testing.T, session *session.Session, name string) error { - svc := iam.New(session) - - instanceProfile := &iam.CreateInstanceProfileInput{ - InstanceProfileName: aws.String(name), - } - - instanceProfileLink := &iam.AddRoleToInstanceProfileInput{ - InstanceProfileName: aws.String(name), - RoleName: aws.String(name), - } - - _, err := svc.CreateInstanceProfile(instanceProfile) - require.NoError(t, err) - - _, err = svc.AddRoleToInstanceProfile(instanceProfileLink) - require.NoError(t, err) - - return nil +func (m mockedIAMRoles) ListInstanceProfilesForRole(input *iam.ListInstanceProfilesForRoleInput) (*iam.ListInstanceProfilesForRoleOutput, error) { + return &m.ListInstanceProfilesForRoleOutput, nil } -func TestCreateIamRole(t *testing.T) { - telemetry.InitTelemetry("cloud-nuke", "") - t.Parallel() +func (m mockedIAMRoles) RemoveRoleFromInstanceProfile(input *iam.RemoveRoleFromInstanceProfileInput) (*iam.RemoveRoleFromInstanceProfileOutput, error) { + return &m.RemoveRoleFromInstanceProfileOutput, nil +} - region, err := getRandomRegion() - require.NoError(t, err) +func (m mockedIAMRoles) DeleteInstanceProfile(input *iam.DeleteInstanceProfileInput) (*iam.DeleteInstanceProfileOutput, error) { + return &m.DeleteInstanceProfileOutput, nil +} - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) +func (m mockedIAMRoles) ListRolePolicies(input *iam.ListRolePoliciesInput) (*iam.ListRolePoliciesOutput, error) { + return &m.ListRolePoliciesOutput, nil +} - name := "cloud-nuke-test-" + util.UniqueID() - roleNames, err := getAllIamRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - assert.NotContains(t, awsgo.StringValueSlice(roleNames), name) +func (m mockedIAMRoles) DeleteRolePolicy(input *iam.DeleteRolePolicyInput) (*iam.DeleteRolePolicyOutput, error) { + return &m.DeleteRolePolicyOutput, nil +} - err = createTestRole(t, session, name) - require.NoError(t, err) +func (m mockedIAMRoles) ListAttachedRolePolicies(input *iam.ListAttachedRolePoliciesInput) (*iam.ListAttachedRolePoliciesOutput, error) { + return &m.ListAttachedRolePoliciesOutput, nil +} - err = createAndAttachInstanceProfile(t, session, name) - defer nukeAllIamRoles(session, []*string{&name}) - require.NoError(t, err) +func (m mockedIAMRoles) DetachRolePolicy(input *iam.DetachRolePolicyInput) (*iam.DetachRolePolicyOutput, error) { + return &m.DetachRolePolicyOutput, nil +} - roleNames, err = getAllIamRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - assert.Contains(t, awsgo.StringValueSlice(roleNames), name) +func (m mockedIAMRoles) DeleteRole(input *iam.DeleteRoleInput) (*iam.DeleteRoleOutput, error) { + return &m.DeleteRoleOutput, nil } -func TestNukeIamRoles(t *testing.T) { +func TestIAMRoles_GetAll(t *testing.T) { telemetry.InitTelemetry("cloud-nuke", "") t.Parallel() - region, err := getRandomRegion() - require.NoError(t, err) - - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) - - name := "cloud-nuke-test-" + util.UniqueID() - err = createTestRole(t, session, name) - require.NoError(t, err) + testName1 := "test-role1" + testName2 := "test-role2" + now := time.Now() + ir := IAMRoles{ + Client: mockedIAMRoles{ + ListRolesPagesOutput: iam.ListRolesOutput{ + Roles: []*iam.Role{ + { + RoleName: aws.String(testName1), + CreateDate: aws.Time(now), + }, + { + RoleName: aws.String(testName2), + CreateDate: aws.Time(now.Add(1)), + }, + }, + }, + }, + } - err = createAndAttachInstanceProfile(t, session, name) - require.NoError(t, err) + tests := map[string]struct { + configObj config.ResourceType + expected []string + }{ + "emptyFilter": { + configObj: config.ResourceType{}, + expected: []string{testName1, testName2}, + }, + "nameExclusionFilter": { + configObj: config.ResourceType{ + ExcludeRule: config.FilterRule{ + NamesRegExp: []config.Expression{{ + RE: *regexp.MustCompile(testName1), + }}}, + }, + expected: []string{testName2}, + }, + "timeAfterExclusionFilter": { + configObj: config.ResourceType{ + ExcludeRule: config.FilterRule{ + TimeAfter: aws.Time(now), + }}, + expected: []string{testName1}, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + names, err := ir.getALl(config.Config{ + IAMRoles: tc.configObj, + }) + require.NoError(t, err) + require.Equal(t, tc.expected, aws.StringValueSlice(names)) + }) + } - err = nukeAllIamRoles(session, []*string{&name}) - require.NoError(t, err) } -func TestTimeFilterExclusionNewlyCreatedIamRole(t *testing.T) { +func TestIAMRoles_NukeAll(t *testing.T) { telemetry.InitTelemetry("cloud-nuke", "") t.Parallel() - region, err := getRandomRegion() - require.NoError(t, err) - - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) - - // Assert role didn't exist - name := "cloud-nuke-test-" + util.UniqueID() - roleNames, err := getAllIamRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - assert.NotContains(t, awsgo.StringValueSlice(roleNames), name) - - // Creates a role - err = createTestRole(t, session, name) - defer nukeAllIamRoles(session, []*string{&name}) - - // Assert role is created - roleNames, err = getAllIamRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - assert.Contains(t, awsgo.StringValueSlice(roleNames), name) + ir := IAMRoles{ + Client: mockedIAMRoles{ + ListInstanceProfilesForRoleOutput: iam.ListInstanceProfilesForRoleOutput{ + InstanceProfiles: []*iam.InstanceProfile{ + { + InstanceProfileName: aws.String("test-instance-profile"), + }, + }, + }, + RemoveRoleFromInstanceProfileOutput: iam.RemoveRoleFromInstanceProfileOutput{}, + DeleteInstanceProfileOutput: iam.DeleteInstanceProfileOutput{}, + ListRolePoliciesOutput: iam.ListRolePoliciesOutput{ + PolicyNames: []*string{ + aws.String("test-policy"), + }, + }, + DeleteRolePolicyOutput: iam.DeleteRolePolicyOutput{}, + ListAttachedRolePoliciesOutput: iam.ListAttachedRolePoliciesOutput{ + AttachedPolicies: []*iam.AttachedPolicy{ + { + PolicyArn: aws.String("test-policy-arn"), + }, + }, + }, + DetachRolePolicyOutput: iam.DetachRolePolicyOutput{}, + DeleteRoleOutput: iam.DeleteRoleOutput{}, + }, + } - // Assert role doesn't appear when we look at roles older than 1 Hour - olderThan := time.Now().Add(-1 * time.Hour) - roleNames, err = getAllIamRoles(session, olderThan, config.Config{}) + err := ir.nukeAll([]*string{aws.String("test-role")}) require.NoError(t, err) - assert.NotContains(t, awsgo.StringValueSlice(roleNames), name) } diff --git a/aws/iam_role_types.go b/aws/iam_role_types.go index 813d1063..682c4067 100644 --- a/aws/iam_role_types.go +++ b/aws/iam_role_types.go @@ -14,23 +14,23 @@ type IAMRoles struct { } // ResourceName - the simple name of the aws resource -func (r IAMRoles) ResourceName() string { +func (ir IAMRoles) ResourceName() string { return "iam-role" } // ResourceIdentifiers - The IAM UserNames -func (r IAMRoles) ResourceIdentifiers() []string { - return r.RoleNames +func (ir IAMRoles) ResourceIdentifiers() []string { + return ir.RoleNames } // Tentative batch size to ensure AWS doesn't throttle -func (r IAMRoles) MaxBatchSize() int { +func (ir IAMRoles) MaxBatchSize() int { return 20 } // Nuke - nuke 'em all!!! -func (r IAMRoles) Nuke(session *session.Session, identifiers []string) error { - if err := nukeAllIamRoles(session, awsgo.StringSlice(identifiers)); err != nil { +func (ir IAMRoles) Nuke(session *session.Session, identifiers []string) error { + if err := ir.nukeAll(awsgo.StringSlice(identifiers)); err != nil { return errors.WithStackTrace(err) } diff --git a/aws/iam_service_linked_role.go b/aws/iam_service_linked_role.go index 7f4eb4c8..a29b9bfd 100644 --- a/aws/iam_service_linked_role.go +++ b/aws/iam_service_linked_role.go @@ -10,7 +10,6 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" "github.com/gruntwork-io/cloud-nuke/config" "github.com/gruntwork-io/cloud-nuke/logging" @@ -20,15 +19,13 @@ import ( ) // List all IAM Roles in the AWS account -func getAllIamServiceLinkedRoles(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { - svc := iam.New(session) - +func (islr IAMServiceLinkedRoles) getAll(configObj config.Config) ([]*string, error) { allIAMServiceLinkedRoles := []*string{} - err := svc.ListRolesPages( + err := islr.Client.ListRolesPages( &iam.ListRolesInput{}, func(page *iam.ListRolesOutput, lastPage bool) bool { for _, iamServiceLinkedRole := range page.Roles { - if shouldIncludeIAMServiceLinkedRole(iamServiceLinkedRole, excludeAfter, configObj) { + if islr.shouldInclude(iamServiceLinkedRole, configObj) { allIAMServiceLinkedRoles = append(allIAMServiceLinkedRoles, iamServiceLinkedRole.RoleName) } } @@ -41,12 +38,12 @@ func getAllIamServiceLinkedRoles(session *session.Session, excludeAfter time.Tim return allIAMServiceLinkedRoles, nil } -func deleteIamServiceLinkedRole(svc *iam.IAM, roleName *string) error { +func (islr IAMServiceLinkedRoles) deleteIamServiceLinkedRole(roleName *string) error { // Deletion ID looks like this: " //{ // DeletionTaskId: "task/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling_2/d3c4c9fc-7fd3-4a36-974a-afb0eb78f102" //} - deletionData, err := svc.DeleteServiceLinkedRole(&iam.DeleteServiceLinkedRoleInput{ + deletionData, err := islr.Client.DeleteServiceLinkedRole(&iam.DeleteServiceLinkedRoleInput{ RoleName: roleName, }) if err != nil { @@ -62,7 +59,7 @@ func deleteIamServiceLinkedRole(svc *iam.IAM, roleName *string) error { for !done { done = true // Check if the deletion is complete - deletionStatus, err = svc.GetServiceLinkedRoleDeletionStatus(&iam.GetServiceLinkedRoleDeletionStatusInput{ + deletionStatus, err = islr.Client.GetServiceLinkedRoleDeletionStatus(&iam.GetServiceLinkedRoleDeletionStatusInput{ DeletionTaskId: deletionData.DeletionTaskId, }) if err != nil { @@ -85,10 +82,7 @@ func deleteIamServiceLinkedRole(svc *iam.IAM, roleName *string) error { } // Delete all IAM Roles -func nukeAllIamServiceLinkedRoles(session *session.Session, roleNames []*string) error { - region := aws.StringValue(session.Config.Region) - svc := iam.New(session) - +func (islr IAMServiceLinkedRoles) nukeAll(roleNames []*string) error { if len(roleNames) == 0 { logging.Logger.Debug("No IAM Service Linked Roles to nuke") return nil @@ -104,13 +98,13 @@ func nukeAllIamServiceLinkedRoles(session *session.Session, roleNames []*string) } // There is no bulk delete IAM Roles API, so we delete the batch of IAM roles concurrently using go routines - logging.Logger.Debugf("Deleting all IAM Service Linked Roles in region %s", region) + logging.Logger.Debugf("Deleting all IAM Service Linked Roles") wg := new(sync.WaitGroup) wg.Add(len(roleNames)) errChans := make([]chan error, len(roleNames)) for i, roleName := range roleNames { errChans[i] = make(chan error, 1) - go deleteIamServiceLinkedRoleAsync(wg, errChans[i], svc, roleName) + go islr.deleteIamServiceLinkedRoleAsync(wg, errChans[i], roleName) } wg.Wait() @@ -122,9 +116,7 @@ func nukeAllIamServiceLinkedRoles(session *session.Session, roleNames []*string) logging.Logger.Debugf("[Failed] %s", err) telemetry.TrackEvent(commonTelemetry.EventContext{ EventName: "Error Nuking IAM Service Linked Role", - }, map[string]interface{}{ - "region": *session.Config.Region, - }) + }, map[string]interface{}{}) } } finalErr := allErrs.ErrorOrNil() @@ -133,32 +125,23 @@ func nukeAllIamServiceLinkedRoles(session *session.Session, roleNames []*string) } for _, roleName := range roleNames { - logging.Logger.Debugf("[OK] IAM Service Linked Role %s was deleted in %s", aws.StringValue(roleName), region) + logging.Logger.Debugf("[OK] IAM Service Linked Role %s was deleted.", aws.StringValue(roleName)) } return nil } -func shouldIncludeIAMServiceLinkedRole(iamServiceLinkedRole *iam.Role, excludeAfter time.Time, configObj config.Config) bool { - if iamServiceLinkedRole == nil { - return false - } - +func (islr IAMServiceLinkedRoles) shouldInclude(iamServiceLinkedRole *iam.Role, configObj config.Config) bool { if !strings.Contains(aws.StringValue(iamServiceLinkedRole.Arn), "aws-service-role") { return false } - if excludeAfter.Before(*iamServiceLinkedRole.CreateDate) { - return false - } - - return config.ShouldInclude( - aws.StringValue(iamServiceLinkedRole.RoleName), - configObj.IAMServiceLinkedRoles.IncludeRule.NamesRegExp, - configObj.IAMServiceLinkedRoles.ExcludeRule.NamesRegExp, - ) + return configObj.IAMServiceLinkedRoles.ShouldInclude(config.ResourceValue{ + Time: iamServiceLinkedRole.CreateDate, + Name: iamServiceLinkedRole.RoleName, + }) } -func deleteIamServiceLinkedRoleAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, roleName *string) { +func (islr IAMServiceLinkedRoles) deleteIamServiceLinkedRoleAsync(wg *sync.WaitGroup, errChan chan error, roleName *string) { defer wg.Done() var result *multierror.Error @@ -167,12 +150,12 @@ func deleteIamServiceLinkedRoleAsync(wg *sync.WaitGroup, errChan chan error, svc // items we need delete/detach them before actually deleting it. // NOTE: The actual role deletion should always be the last one. This way we // can guarantee that it will fail if we forgot to delete/detach an item. - functions := []func(svc *iam.IAM, roleName *string) error{ - deleteIamServiceLinkedRole, + functions := []func(roleName *string) error{ + islr.deleteIamServiceLinkedRole, } for _, fn := range functions { - if err := fn(svc, roleName); err != nil { + if err := fn(roleName); err != nil { result = multierror.Append(result, err) } } diff --git a/aws/iam_service_linked_role_test.go b/aws/iam_service_linked_role_test.go index 2f0fb81c..7ae91b79 100644 --- a/aws/iam_service_linked_role_test.go +++ b/aws/iam_service_linked_role_test.go @@ -1,142 +1,114 @@ package aws import ( - "github.com/gruntwork-io/cloud-nuke/telemetry" - "testing" - "time" - "github.com/aws/aws-sdk-go/aws" - awsgo "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/iam/iamiface" "github.com/gruntwork-io/cloud-nuke/config" - "github.com/gruntwork-io/cloud-nuke/util" - "github.com/stretchr/testify/assert" + "github.com/gruntwork-io/cloud-nuke/telemetry" "github.com/stretchr/testify/require" + "regexp" + "testing" + "time" ) -func TestListIamServiceLinkedRoles(t *testing.T) { - telemetry.InitTelemetry("cloud-nuke", "") - t.Parallel() - - region, err := getRandomRegion() - require.NoError(t, err) - - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) - - roleNames, err := getAllIamServiceLinkedRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - - assert.NotEmpty(t, roleNames) +type mockedIAMServiceLinkedRoles struct { + iamiface.IAMAPI + ListRolesPagesOutput iam.ListRolesOutput + DeleteServiceLinkedRoleOutput iam.DeleteServiceLinkedRoleOutput + GetServiceLinkedRoleDeletionStatusOutput iam.GetServiceLinkedRoleDeletionStatusOutput } -func createTestServiceLinkedRole(t *testing.T, session *session.Session, name, awsServiceName string) error { - svc := iam.New(session) - - input := &iam.CreateServiceLinkedRoleInput{ - AWSServiceName: aws.String(awsServiceName), - Description: aws.String("cloud-nuke-test"), - CustomSuffix: aws.String(name), - } - - _, err := svc.CreateServiceLinkedRole(input) - require.NoError(t, err) - +func (m mockedIAMServiceLinkedRoles) ListRolesPages(input *iam.ListRolesInput, fn func(*iam.ListRolesOutput, bool) bool) error { + fn(&m.ListRolesPagesOutput, true) return nil } -func TestCreateIamServiceLinkedRole(t *testing.T) { - telemetry.InitTelemetry("cloud-nuke", "") - t.Parallel() - - region, err := getRandomRegion() - require.NoError(t, err) - - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) - - name := "cloud-nuke-test-" + util.UniqueID() - awsServiceName := "autoscaling.amazonaws.com" - iamServiceLinkedRoleName := "AWSServiceRoleForAutoScaling_" + name - roleNames, err := getAllIamServiceLinkedRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - assert.NotContains(t, awsgo.StringValueSlice(roleNames), name) - - err = createTestServiceLinkedRole(t, session, name, awsServiceName) - require.NoError(t, err) +func (m mockedIAMServiceLinkedRoles) DeleteServiceLinkedRole(input *iam.DeleteServiceLinkedRoleInput) (*iam.DeleteServiceLinkedRoleOutput, error) { + return &m.DeleteServiceLinkedRoleOutput, nil +} - roleNames, err = getAllIamServiceLinkedRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - //AWSServiceRoleForAutoScaling_cloud-nuke-test - assert.Contains(t, awsgo.StringValueSlice(roleNames), iamServiceLinkedRoleName) +func (m mockedIAMServiceLinkedRoles) GetServiceLinkedRoleDeletionStatus(input *iam.GetServiceLinkedRoleDeletionStatusInput) (*iam.GetServiceLinkedRoleDeletionStatusOutput, error) { + return &m.GetServiceLinkedRoleDeletionStatusOutput, nil } -func TestNukeIamServiceLinkedRoles(t *testing.T) { +func TestIAMServiceLinkedRoles_GetAll(t *testing.T) { telemetry.InitTelemetry("cloud-nuke", "") t.Parallel() - region, err := getRandomRegion() - require.NoError(t, err) - - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) - - name := "cloud-nuke-test-" + util.UniqueID() - awsServiceName := "autoscaling.amazonaws.com" - iamServiceLinkedRoleName := "AWSServiceRoleForAutoScaling_" + name - - err = createTestServiceLinkedRole(t, session, name, awsServiceName) - require.NoError(t, err) - - err = nukeAllIamServiceLinkedRoles(session, []*string{&iamServiceLinkedRoleName}) - require.NoError(t, err) + now := time.Now() + testName1 := "test-role1" + testName2 := "test-role2" + islr := IAMServiceLinkedRoles{ + Client: &mockedIAMServiceLinkedRoles{ + ListRolesPagesOutput: iam.ListRolesOutput{ + Roles: []*iam.Role{ + { + RoleName: aws.String(testName1), + CreateDate: aws.Time(now), + Arn: aws.String("aws-service-role"), + }, + { + RoleName: aws.String(testName2), + CreateDate: aws.Time(now.Add(1)), + Arn: aws.String("aws-service-role"), + }, + }, + }, + }, + } - roleNames, err := getAllIamServiceLinkedRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) + tests := map[string]struct { + configObj config.ResourceType + expected []string + }{ + "emptyFilter": { + configObj: config.ResourceType{}, + expected: []string{testName1, testName2}, + }, + "nameExclusionFilter": { + configObj: config.ResourceType{ + ExcludeRule: config.FilterRule{ + NamesRegExp: []config.Expression{{ + RE: *regexp.MustCompile(testName1), + }}}, + }, + expected: []string{testName2}, + }, + "timeAfterExclusionFilter": { + configObj: config.ResourceType{ + ExcludeRule: config.FilterRule{ + TimeAfter: aws.Time(now), + }}, + expected: []string{testName1}, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + names, err := islr.getAll(config.Config{ + IAMServiceLinkedRoles: tc.configObj, + }) + require.NoError(t, err) + require.Equal(t, tc.expected, aws.StringValueSlice(names)) + }) + } - assert.NotContains(t, awsgo.StringValueSlice(roleNames), iamServiceLinkedRoleName) } -func TestTimeFilterExclusionNewlyCreatedIamServiceLinkedRole(t *testing.T) { +func TestIAMServiceLinkedRoles_NukeAll(t *testing.T) { telemetry.InitTelemetry("cloud-nuke", "") t.Parallel() - region, err := getRandomRegion() - require.NoError(t, err) - - session, err := session.NewSession(&awsgo.Config{ - Region: awsgo.String(region)}, - ) - require.NoError(t, err) + islr := IAMServiceLinkedRoles{ - // Assert role didn't exist - name := "cloud-nuke-test-" + util.UniqueID() - awsServiceName := "autoscaling.amazonaws.com" - iamServiceLinkedRoleName := "AWSServiceRoleForAutoScaling_" + name - - roleNames, err := getAllIamServiceLinkedRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - assert.NotContains(t, awsgo.StringValueSlice(roleNames), name) - - // Creates a role - err = createTestServiceLinkedRole(t, session, name, awsServiceName) - defer nukeAllIamRoles(session, []*string{&name}) - - // Assert role is created - roleNames, err = getAllIamServiceLinkedRoles(session, time.Now(), config.Config{}) - require.NoError(t, err) - assert.Contains(t, awsgo.StringValueSlice(roleNames), iamServiceLinkedRoleName) + Client: &mockedIAMServiceLinkedRoles{ + DeleteServiceLinkedRoleOutput: iam.DeleteServiceLinkedRoleOutput{}, + GetServiceLinkedRoleDeletionStatusOutput: iam.GetServiceLinkedRoleDeletionStatusOutput{ + Status: aws.String("SUCCEEDED"), + }, + }, + } - // Assert role doesn't appear when we look at roles older than 1 Hour - olderThan := time.Now().Add(-1 * time.Hour) - roleNames, err = getAllIamServiceLinkedRoles(session, olderThan, config.Config{}) + err := islr.nukeAll([]*string{aws.String("test-role1")}) require.NoError(t, err) - assert.NotContains(t, awsgo.StringValueSlice(roleNames), iamServiceLinkedRoleName) } diff --git a/aws/iam_service_linked_role_types.go b/aws/iam_service_linked_role_types.go index 84c3a266..c08a2d3d 100644 --- a/aws/iam_service_linked_role_types.go +++ b/aws/iam_service_linked_role_types.go @@ -14,23 +14,23 @@ type IAMServiceLinkedRoles struct { } // ResourceName - the simple name of the aws resource -func (r IAMServiceLinkedRoles) ResourceName() string { +func (islr IAMServiceLinkedRoles) ResourceName() string { return "iam-service-linked-role" } // ResourceIdentifiers - The IAM UserNames -func (r IAMServiceLinkedRoles) ResourceIdentifiers() []string { - return r.RoleNames +func (islr IAMServiceLinkedRoles) ResourceIdentifiers() []string { + return islr.RoleNames } // Tentative batch size to ensure AWS doesn't throttle -func (r IAMServiceLinkedRoles) MaxBatchSize() int { +func (islr IAMServiceLinkedRoles) MaxBatchSize() int { return 49 } // Nuke - nuke 'em all!!! -func (r IAMServiceLinkedRoles) Nuke(session *session.Session, identifiers []string) error { - if err := nukeAllIamServiceLinkedRoles(session, awsgo.StringSlice(identifiers)); err != nil { +func (islr IAMServiceLinkedRoles) Nuke(session *session.Session, identifiers []string) error { + if err := islr.nukeAll(awsgo.StringSlice(identifiers)); err != nil { return errors.WithStackTrace(err) }