//go:build e2e package harness import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" "github.com/stretchr/testify/require" ) // IAMRoleARN constructs arn:aws:iam:::role/. func IAMRoleARN(account, name string) string { return "arn:aws:iam::" + account + ":role/" + name } // CreateEKSClusterRole creates a role EKS can assume as a cluster role and // deletes it at cleanup. CreateCluster refuses a role that does not exist. func CreateEKSClusterRole(t *testing.T, c *AWSClient, name string) string { t.Helper() const eksTrustPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"eks.amazonaws.com"},"Action":"sts:AssumeRole"}]}` out, err := c.IAM.CreateRole(&iam.CreateRoleInput{ RoleName: aws.String(name), AssumeRolePolicyDocument: aws.String(eksTrustPolicy), Description: aws.String("E2E EKS cluster role"), }) require.NoError(t, err, "create-role (cluster) %s", name) t.Cleanup(func() { IAMDeleteRoleAndProfilesBestEffort(c, name, nil) }) return aws.StringValue(out.Role.Arn) } // IAMPolicyARN builds the canonical policy ARN for a given account + policy // "key" (a bare name, or "path/name" without the leading slash). func IAMPolicyARN(account, key string) string { return "arn:aws:iam::" + account + ":policy/" + key } // IAMAccountID returns the active account ID via STS GetCallerIdentity. Unlike // deriving it from a created IAM user's ARN, this is side-effect-free, so suites // sharing one account can call it concurrently without minting colliding users. func IAMAccountID(t *testing.T, c *AWSClient) string { t.Helper() out, err := c.STS.GetCallerIdentity(&sts.GetCallerIdentityInput{}) require.NoError(t, err, "sts:GetCallerIdentity") require.NotEmpty(t, aws.StringValue(out.Account), "empty account in GetCallerIdentity response") return aws.StringValue(out.Account) } // IAMDeleteRoleAndProfilesBestEffort tears down every fragment of a role + // instance-profile graph a suite might have left behind. Each step swallows // errors so a missing fragment never cascades; usable both as a pre-test sweep // and as a fixture-teardown cleanup. Order matters: unbind role-from-profile // before deleting the profile, detach policies before deleting the role. func IAMDeleteRoleAndProfilesBestEffort(c *AWSClient, roleName string, profileNames []string, policyARNs ...string) { for _, p := range profileNames { _, _ = c.IAM.RemoveRoleFromInstanceProfile(&iam.RemoveRoleFromInstanceProfileInput{ InstanceProfileName: aws.String(p), RoleName: aws.String(roleName), }) _, _ = c.IAM.DeleteInstanceProfile(&iam.DeleteInstanceProfileInput{ InstanceProfileName: aws.String(p), }) } for _, arn := range policyARNs { _, _ = c.IAM.DetachRolePolicy(&iam.DetachRolePolicyInput{ RoleName: aws.String(roleName), PolicyArn: aws.String(arn), }) } // Defensive: pull any other attached policies so the final DeleteRole isn't // blocked by a stray attach from a partial run. if attached, err := c.IAM.ListAttachedRolePolicies(&iam.ListAttachedRolePoliciesInput{ RoleName: aws.String(roleName), }); err == nil { for _, p := range attached.AttachedPolicies { _, _ = c.IAM.DetachRolePolicy(&iam.DetachRolePolicyInput{ RoleName: aws.String(roleName), PolicyArn: p.PolicyArn, }) } } _, _ = c.IAM.DeleteRole(&iam.DeleteRoleInput{RoleName: aws.String(roleName)}) } // IAMDeleteGroupBestEffort tears down a group: removes any members, detaches all // attached policies, deletes any inline policies, then deletes the group. Each // step swallows errors so a missing fragment never cascades; usable both as a // pre-test sweep and as a fixture-teardown cleanup. Order matters: a group cannot // be deleted while it still has members, attached policies, or inline policies. func IAMDeleteGroupBestEffort(c *AWSClient, groupName string, memberUsers []string, policyARNs ...string) { for _, u := range memberUsers { _, _ = c.IAM.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{ GroupName: aws.String(groupName), UserName: aws.String(u), }) } for _, arn := range policyARNs { _, _ = c.IAM.DetachGroupPolicy(&iam.DetachGroupPolicyInput{ GroupName: aws.String(groupName), PolicyArn: aws.String(arn), }) } // Defensive: pull any other attached policies so the final DeleteGroup isn't // blocked by a stray attach from a partial run. if attached, err := c.IAM.ListAttachedGroupPolicies(&iam.ListAttachedGroupPoliciesInput{ GroupName: aws.String(groupName), }); err == nil { for _, p := range attached.AttachedPolicies { _, _ = c.IAM.DetachGroupPolicy(&iam.DetachGroupPolicyInput{ GroupName: aws.String(groupName), PolicyArn: p.PolicyArn, }) } } // Defensive: delete any inline policies so the inline-policy guard can't block // the final DeleteGroup after a partial run. if inline, err := c.IAM.ListGroupPolicies(&iam.ListGroupPoliciesInput{ GroupName: aws.String(groupName), }); err == nil { for _, name := range inline.PolicyNames { _, _ = c.IAM.DeleteGroupPolicy(&iam.DeleteGroupPolicyInput{ GroupName: aws.String(groupName), PolicyName: name, }) } } // Defensive: drop any members the caller didn't pass in (partial run could // have added one), so the non-empty guard can't block the delete. if got, err := c.IAM.GetGroup(&iam.GetGroupInput{GroupName: aws.String(groupName)}); err == nil { for _, u := range got.Users { _, _ = c.IAM.RemoveUserFromGroup(&iam.RemoveUserFromGroupInput{ GroupName: aws.String(groupName), UserName: u.UserName, }) } } _, _ = c.IAM.DeleteGroup(&iam.DeleteGroupInput{GroupName: aws.String(groupName)}) }