diff --git a/Gopkg.lock b/Gopkg.lock index d7c0c66a..57c3bdfa 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,103 +2,200 @@ [[projects]] + digest = "1:abc0a25693a2ed04f40c011d2d185b55df46e78781256dae83823a7cb6fc3520" name = "github.com/aws/aws-sdk-go" - packages = ["aws","aws/awserr","aws/awsutil","aws/client","aws/client/metadata","aws/corehandlers","aws/credentials","aws/credentials/ec2rolecreds","aws/credentials/endpointcreds","aws/credentials/stscreds","aws/defaults","aws/ec2metadata","aws/endpoints","aws/request","aws/session","aws/signer/v4","internal/shareddefaults","private/protocol","private/protocol/ec2query","private/protocol/query","private/protocol/query/queryutil","private/protocol/rest","private/protocol/xml/xmlutil","service/ec2","service/sts"] - revision = "c13a879e75646fe750d21bcb05bf2cabea9791c1" - version = "v1.12.65" - -[[projects]] + packages = [ + "aws", + "aws/awserr", + "aws/awsutil", + "aws/client", + "aws/client/metadata", + "aws/corehandlers", + "aws/credentials", + "aws/credentials/ec2rolecreds", + "aws/credentials/endpointcreds", + "aws/credentials/stscreds", + "aws/csm", + "aws/defaults", + "aws/ec2metadata", + "aws/endpoints", + "aws/request", + "aws/session", + "aws/signer/v4", + "internal/ini", + "internal/sdkio", + "internal/sdkrand", + "internal/sdkuri", + "internal/shareddefaults", + "private/protocol", + "private/protocol/ec2query", + "private/protocol/json/jsonutil", + "private/protocol/jsonrpc", + "private/protocol/query", + "private/protocol/query/queryutil", + "private/protocol/rest", + "private/protocol/restjson", + "private/protocol/xml/xmlutil", + "service/autoscaling", + "service/ec2", + "service/ecs", + "service/eks", + "service/elb", + "service/elbv2", + "service/iam", + "service/sts", + ] + pruneopts = "" + revision = "180cc10e5ff368b86dee226b034af7d1672baec6" + version = "v1.15.87" + +[[projects]] + digest = "1:15ceb8ca7a71db4c426d8aef1909ea074f6840efa163490bb2798f475624e4ae" name = "github.com/bgentry/speakeasy" packages = ["."] + pruneopts = "" revision = "4aabc24848ce5fd31929f7d1e4ea74d3709c14cd" version = "v0.1.0" [[projects]] + digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "" revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" [[projects]] + digest = "1:c9bebdae4ac52d0c3bbe5876de3d72f3bb05b4986865cdb3f15e305e1dc4fbca" name = "github.com/fatih/color" packages = ["."] + pruneopts = "" revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260" version = "v1.5.0" [[projects]] + branch = "master" + digest = "1:26317724ed32bcf2ef15454613d2a8fe9d670b12f073cfd20db3bcec54e069ab" name = "github.com/go-errors/errors" packages = ["."] - revision = "3afebba5a48dbc89b574d890b6b34d9ee10b4785" - version = "v1.0.0" - -[[projects]] - name = "github.com/go-ini/ini" - packages = ["."] - revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a" - version = "v1.32.0" + pruneopts = "" + revision = "d98b870cc4e05f1545532a80e9909be8216095b6" [[projects]] + digest = "1:945bd89ef3393fc20dc43aeec26104306f76923b171efb0f3456ffc7f5164314" name = "github.com/gruntwork-io/gruntwork-cli" - packages = ["entrypoint","errors","logging","shell"] + packages = [ + "collections", + "entrypoint", + "errors", + "logging", + "shell", + ] + pruneopts = "" revision = "94044eeeb0a48b5e8dd52190fa0d0daba53e157f" version = "v0.1.2" [[projects]] + digest = "1:6f49eae0c1e5dab1dafafee34b207aeb7a42303105960944828c2079b92fc88e" name = "github.com/jmespath/go-jmespath" packages = ["."] + pruneopts = "" revision = "0b12d6b5" [[projects]] + digest = "1:9ea83adf8e96d6304f394d40436f2eb44c1dc3250d223b74088cc253a6cd0a1c" name = "github.com/mattn/go-colorable" packages = ["."] + pruneopts = "" revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" version = "v0.0.9" [[projects]] + digest = "1:78229b46ddb7434f881390029bd1af7661294af31f6802e0e1bedaad4ab0af3c" name = "github.com/mattn/go-isatty" packages = ["."] + pruneopts = "" revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" [[projects]] + digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" name = "github.com/pmezard/go-difflib" packages = ["difflib"] + pruneopts = "" revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" [[projects]] + digest = "1:42a42c4bc67bed17f40fddf0f24d4403e25e7b96488456cf4248e6d16659d370" name = "github.com/sirupsen/logrus" packages = ["."] + pruneopts = "" revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" version = "v1.0.4" [[projects]] + digest = "1:2d0dc026c4aef5e2f3a0e06a4dabe268b840d8f63190cf6894e02134a03f52c5" name = "github.com/stretchr/testify" - packages = ["assert"] + packages = [ + "assert", + "require", + ] + pruneopts = "" revision = "b91bfb9ebec76498946beb6af7c0230c7cc7ba6c" version = "v1.2.0" [[projects]] + digest = "1:e85837cb04b78f61688c6eba93ea9d14f60d611e2aaf8319999b1a60d2dafbfa" name = "github.com/urfave/cli" packages = ["."] + pruneopts = "" revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" version = "v1.20.0" [[projects]] branch = "master" + digest = "1:43adf91783cc814f60c0dd21c9aadf0b5284721e13542e124536638e0b43a6b3" name = "golang.org/x/crypto" packages = ["ssh/terminal"] + pruneopts = "" revision = "13931e22f9e72ea58bb73048bc752b48c6d4d4ac" [[projects]] branch = "master" + digest = "1:aba25123b0b02601b134eaf08bd84afe8e12cab70e43770e7b38d8eef8f1d93e" name = "golang.org/x/sys" - packages = ["unix","windows"] + packages = [ + "unix", + "windows", + ] + pruneopts = "" revision = "2c42eef0765b9837fbdab12011af7830f55f88f0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f565d90c6d10da0317d3cf32baa42bce0d5872351b81fc979f36c4b51ccb1be4" + input-imports = [ + "github.com/aws/aws-sdk-go/aws", + "github.com/aws/aws-sdk-go/aws/awserr", + "github.com/aws/aws-sdk-go/aws/endpoints", + "github.com/aws/aws-sdk-go/aws/session", + "github.com/aws/aws-sdk-go/service/autoscaling", + "github.com/aws/aws-sdk-go/service/ec2", + "github.com/aws/aws-sdk-go/service/ecs", + "github.com/aws/aws-sdk-go/service/eks", + "github.com/aws/aws-sdk-go/service/elb", + "github.com/aws/aws-sdk-go/service/elbv2", + "github.com/aws/aws-sdk-go/service/iam", + "github.com/fatih/color", + "github.com/gruntwork-io/gruntwork-cli/collections", + "github.com/gruntwork-io/gruntwork-cli/entrypoint", + "github.com/gruntwork-io/gruntwork-cli/errors", + "github.com/gruntwork-io/gruntwork-cli/logging", + "github.com/gruntwork-io/gruntwork-cli/shell", + "github.com/stretchr/testify/assert", + "github.com/stretchr/testify/require", + "github.com/urfave/cli", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 91b71823..ccddf277 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -31,7 +31,7 @@ [[constraint]] name = "github.com/aws/aws-sdk-go" - version = "1.12.65" + version = "1.14.26" [[constraint]] name = "github.com/go-ini/ini" diff --git a/README.md b/README.md index aa728df7..c3a206b2 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The currently supported functionality includes: * Deleting all Elastic IPs in an AWS account * Deleting all Launch Configurations in an AWS account * Deleting all ECS services in an AWS account +* Deleting all EKS clusters in an AWS account ### Caveats diff --git a/aws/aws.go b/aws/aws.go index 78fe0e00..953eded2 100644 --- a/aws/aws.go +++ b/aws/aws.go @@ -222,6 +222,18 @@ func GetAllResources(regions []string, excludedRegions []string, excludeAfter ti resourcesInRegion.Resources = append(resourcesInRegion.Resources, ecsServices) // End ECS resources + // EKS resources + eksClusterNames, err := getAllEksClusters(session, excludeAfter) + if err != nil { + return nil, errors.WithStackTrace(err) + } + + eksClusters := EKSClusters{ + Clusters: awsgo.StringValueSlice(eksClusterNames), + } + resourcesInRegion.Resources = append(resourcesInRegion.Resources, eksClusters) + // End EKS resources + account.Resources[region] = resourcesInRegion } diff --git a/aws/eks.go b/aws/eks.go new file mode 100644 index 00000000..31d59742 --- /dev/null +++ b/aws/eks.go @@ -0,0 +1,95 @@ +package aws + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/gruntwork-io/cloud-nuke/logging" + "github.com/gruntwork-io/gruntwork-cli/errors" +) + +// getAllEksClusters returns a list of strings of EKS Cluster Names that uniquely identify each cluster. +func getAllEksClusters(awsSession *session.Session, excludeAfter time.Time) ([]*string, error) { + svc := eks.New(awsSession) + result, err := svc.ListClusters(&eks.ListClustersInput{}) + if err != nil { + return nil, errors.WithStackTrace(err) + } + filteredClusters, err := filterOutRecentEksClusters(svc, result.Clusters, excludeAfter) + if err != nil { + return nil, errors.WithStackTrace(err) + } + return filteredClusters, nil +} + +// filterOutRecentEksClusters will take in the list of clusters and filter out any clusters that were created after +// `excludeAfter`. +func filterOutRecentEksClusters(svc *eks.EKS, clusterNames []*string, excludeAfter time.Time) ([]*string, error) { + var filteredEksClusterNames []*string + for _, clusterName := range clusterNames { + describeResult, err := svc.DescribeCluster(&eks.DescribeClusterInput{ + Name: clusterName, + }) + if err != nil { + return nil, errors.WithStackTrace(err) + } + cluster := describeResult.Cluster + if excludeAfter.After(*cluster.CreatedAt) { + filteredEksClusterNames = append(filteredEksClusterNames, cluster.Name) + } + } + return filteredEksClusterNames, nil +} + +// deleteEksClusters deletes all clusters requested. Returns a list of cluster names that have been accepted by AWS +// for deletion. +func deleteEksClusters(svc *eks.EKS, eksClusterNames []*string) []*string { + var requestedDeletes []*string + for _, eksClusterName := range eksClusterNames { + _, err := svc.DeleteCluster(&eks.DeleteClusterInput{Name: eksClusterName}) + if err != nil { + logging.Logger.Errorf("[Failed] Failed deleting EKS cluster %s: %s", *eksClusterName, err) + } else { + requestedDeletes = append(requestedDeletes, eksClusterName) + } + } + return requestedDeletes +} + +// waitUntilEksClustersDeleted waits until the EKS cluster has been actually deleted from AWS. Returns a list of EKS +// cluster names that have been successfully deleted. +func waitUntilEksClustersDeleted(svc *eks.EKS, eksClusterNames []*string) []*string { + var successfullyDeleted []*string + for _, eksClusterName := range eksClusterNames { + err := svc.WaitUntilClusterDeleted(&eks.DescribeClusterInput{Name: eksClusterName}) + if err != nil { + logging.Logger.Errorf("[Failed] Failed waiting for EKS cluster to be deleted %s: %s", *eksClusterName, err) + } else { + logging.Logger.Infof("Deleted EKS cluster: %s", *eksClusterName) + successfullyDeleted = append(successfullyDeleted, eksClusterName) + } + } + return successfullyDeleted +} + +// nukeAllEksClusters deletes all provided EKS clusters, waiting for them to be deleted before returning. +func nukeAllEksClusters(awsSession *session.Session, eksClusterNames []*string) error { + numNuking := len(eksClusterNames) + svc := eks.New(awsSession) + + if numNuking == 0 { + logging.Logger.Infof("No EKS clusters to nuke in region %s", *awsSession.Config.Region) + return nil + } + + logging.Logger.Infof("Deleting %d EKS clusters in region %s", numNuking, *awsSession.Config.Region) + + requestedDeletes := deleteEksClusters(svc, eksClusterNames) + successfullyDeleted := waitUntilEksClustersDeleted(svc, requestedDeletes) + + numNuked := len(successfullyDeleted) + logging.Logger.Infof("[OK] %d of %d EKS cluster(s) deleted in %s", numNuked, numNuking, *awsSession.Config.Region) + return nil + +} diff --git a/aws/eks_test.go b/aws/eks_test.go new file mode 100644 index 00000000..cb8cc28c --- /dev/null +++ b/aws/eks_test.go @@ -0,0 +1,69 @@ +package aws + +import ( + "testing" + "time" + + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/cloud-nuke/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test that we can successfully list clusters by manually creating a cluster, and then using the list function to find +// it. +func TestListEksClusters(t *testing.T) { + t.Parallel() + + region := getRandomEksSupportedRegion(t) + awsSession, err := session.NewSession(&awsgo.Config{ + Region: awsgo.String(region), + }) + require.NoError(t, err) + + uniqueID := util.UniqueID() + + role := createEksClusterRole(t, awsSession, uniqueID) + defer deleteRole(awsSession, role) + + cluster := createEksCluster(t, awsSession, uniqueID, *role.Arn) + defer nukeAllEksClusters(awsSession, []*string{cluster.Name}) + + eksClusterNames, err := getAllEksClusters(awsSession, time.Now().Add(1*time.Hour*-1)) + if err != nil { + assert.Failf(t, "Unable to fetch list of clusters: %s", err.Error()) + } + assert.NotContains(t, awsgo.StringValueSlice(eksClusterNames), *cluster.Name) + + eksClusterNames, err = getAllEksClusters(awsSession, time.Now().Add(1*time.Hour)) + if err != nil { + assert.Failf(t, "Unable to fetch list of clusters: %s", err.Error()) + } + assert.Contains(t, awsgo.StringValueSlice(eksClusterNames), *cluster.Name) +} + +// Test that we can successfully nuke EKS clusters by manually creating a cluster, and then using the nuke function to +// delete it. +func TestNukeEksClusters(t *testing.T) { + t.Parallel() + + region := getRandomEksSupportedRegion(t) + awsSession, err := session.NewSession(&awsgo.Config{ + Region: awsgo.String(region), + }) + require.NoError(t, err) + + uniqueID := util.UniqueID() + + role := createEksClusterRole(t, awsSession, uniqueID) + defer deleteRole(awsSession, role) + + cluster := createEksCluster(t, awsSession, uniqueID, *role.Arn) + err = nukeAllEksClusters(awsSession, []*string{cluster.Name}) + require.NoError(t, err) + + eksClusterNames, err := getAllEksClusters(awsSession, time.Now().Add(1*time.Hour)) + require.NoError(t, err) + assert.NotContains(t, awsgo.StringValueSlice(eksClusterNames), *cluster.Name) +} diff --git a/aws/eks_types.go b/aws/eks_types.go new file mode 100644 index 00000000..80441ecd --- /dev/null +++ b/aws/eks_types.go @@ -0,0 +1,34 @@ +package aws + +import ( + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/gruntwork-io/gruntwork-cli/errors" +) + +// EKSClusters - Represents all EKS clusters found in a region +type EKSClusters struct { + Clusters []string +} + +// ResourceName - The simple name of the aws resource +func (clusters EKSClusters) ResourceName() string { + return "ekscluster" +} + +// ResourceIdentifiers - The Name of the collected EKS clusters +func (clusters EKSClusters) ResourceIdentifiers() []string { + return clusters.Clusters +} + +func (clusters EKSClusters) MaxBatchSize() int { + return 200 +} + +// Nuke - nuke all EKS Cluster resources +func (clusters EKSClusters) Nuke(awsSession *session.Session, identifiers []string) error { + if err := nukeAllEksClusters(awsSession, awsgo.StringSlice(identifiers)); err != nil { + return errors.WithStackTrace(err) + } + return nil +} diff --git a/aws/eks_utils_for_test.go b/aws/eks_utils_for_test.go new file mode 100644 index 00000000..20603899 --- /dev/null +++ b/aws/eks_utils_for_test.go @@ -0,0 +1,100 @@ +package aws + +import ( + "fmt" + "testing" + "time" + + awsgo "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/aws/aws-sdk-go/service/iam" + gruntworkerrors "github.com/gruntwork-io/gruntwork-cli/errors" + terraAws "github.com/gruntwork-io/terratest/modules/aws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getRandomEksSupportedRegion - Returns a random AWS region that supports EKS. +// Refer to https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/ +func getRandomEksSupportedRegion(t *testing.T) string { + // Approve only regions where EKS and the EKS optimized Linux AMI are available + approvedRegions := []string{"us-west-2", "us-east-1", "us-east-2", "eu-west-1"} + return terraAws.GetRandomRegion(t, approvedRegions, []string{}) +} + +func createEksCluster( + t *testing.T, + awsSession *session.Session, + randomId string, + roleArn string, +) eks.Cluster { + clusterName := fmt.Sprintf("cloud-nuke-%s-%s", t.Name(), randomId) + subnet1, subnet2 := getSubnetsInDifferentAZs(t, awsSession) + + svc := eks.New(awsSession) + result, err := svc.CreateCluster(&eks.CreateClusterInput{ + Name: awsgo.String(clusterName), + RoleArn: awsgo.String(roleArn), + ResourcesVpcConfig: &eks.VpcConfigRequest{ + SubnetIds: []*string{subnet1.SubnetId, subnet2.SubnetId}, + }, + }) + if err != nil { + require.Fail(t, gruntworkerrors.WithStackTrace(err).Error()) + } + err = svc.WaitUntilClusterActive(&eks.DescribeClusterInput{Name: result.Cluster.Name}) + if err != nil { + require.Fail(t, gruntworkerrors.WithStackTrace(err).Error()) + } + return *result.Cluster +} + +func createEksClusterRole( + t *testing.T, + awsSession *session.Session, + randomId string, +) iam.Role { + roleName := fmt.Sprintf("cloud-nuke-%s-%s", t.Name(), randomId) + svc := iam.New(awsSession) + createRoleParams := &iam.CreateRoleInput{ + AssumeRolePolicyDocument: awsgo.String(EKS_ASSUME_ROLE_POLICY), + RoleName: awsgo.String(roleName), + } + result, err := svc.CreateRole(createRoleParams) + if err != nil { + assert.Fail(t, gruntworkerrors.WithStackTrace(err).Error()) + } + attachRolePolicy(t, svc, roleName, "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy") + attachRolePolicy(t, svc, roleName, "arn:aws:iam::aws:policy/AmazonEKSServicePolicy") + + // IAM resources are slow to propagate, so give it some + // time + time.Sleep(15 * time.Second) + + return *result.Role +} + +func attachRolePolicy(t *testing.T, svc *iam.IAM, roleName string, policyArn string) { + attachRolePolicyParams := &iam.AttachRolePolicyInput{ + RoleName: awsgo.String(roleName), + PolicyArn: awsgo.String(policyArn), + } + _, err := svc.AttachRolePolicy(attachRolePolicyParams) + if err != nil { + assert.Fail(t, gruntworkerrors.WithStackTrace(err).Error()) + } +} + +const EKS_ASSUME_ROLE_POLICY = `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +}`