diff --git a/README.md b/README.md index c7dbd11..d318c2b 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ Activity * Retrieving RDS instance counts...................OK (7) * Retrieving Lightsail instance counts................OK (0) * Retrieving S3 bucket counts...OK (13) + * Retrieving EKS Node counts....................OK (2) * Writing to file...OK Success. @@ -127,9 +128,9 @@ As you can see above, no command line arguments were necessary: it used my defau Here is what the CSV file looks like. It is important to mention that this tool was run TWICE to collect the results of two different accounts/profiles. ```csv -Account ID,Timestamp,Region,# of EC2 Instances,# of Spot Instances,# of EBS Volumes,# of Unique Containers,# of Lambda Functions,# of RDS Instances,# of Lightsail Instances,# of S3 Buckets -896149672290,2020-10-20T16:29:39-04:00,ALL_REGIONS,2,3,7,3,2,3,2,2 -240520192079,2020-10-21T16:24:06-04:00,ALL_REGIONS,5,4,9,3,12,7,0,13 +Account ID,Timestamp,Region,# of EC2 Instances,# of Spot Instances,# of EBS Volumes,# of Unique Containers,# of Lambda Functions,# of RDS Instances,# of Lightsail Instances,# of S3 Buckets,# of EKS Nodes +896149672290,2020-10-20T16:29:39-04:00,ALL_REGIONS,2,3,7,3,2,3,2,2,2 +240520192079,2020-10-21T16:24:06-04:00,ALL_REGIONS,5,4,9,3,12,7,0,13,0 ``` Here are some notes on specific columns: @@ -221,7 +222,10 @@ To use this utility, this minimal IAM Profile can be associated with a bare user "lightsail:GetInstances", "lightsail:GetRegions", "rds:DescribeDBInstances", - "s3:ListAllMyBuckets" + "s3:ListAllMyBuckets", + "eks:DescribeNodegroup", + "eks:ListNodegroups", + "eks:ListClusters" ], "Resource": "*" } @@ -278,6 +282,11 @@ The `aws-resource-counter` examines the following resources: * *NOTE:* We cannot currently count S3 buckets on a per-region basis (due to limitations with the AWS SDK). * This is stored in the generated CSV file under the "# of S3 Buckets" column. +1. **EKS Nodes.** We count the number of nodes across all clusters in all regions. + + * We do not qualify the type of EKS node. + * This is stored in the generated CSV file under the "# of EKS Nodes" column. + ## Alternative Means of Resource Counting If you do not wish to use the `aws-resource-counter` utility, you can use the AWS CLI to collect these same counts. For some of these counts, it will be easy to do. For others, the command line is a bit more complex. @@ -568,3 +577,29 @@ $ aws s3api list-buckets $aws_p --query 'length(Buckets)' ``` Note that it is not possible through the AWS CLI to get S3 buckets on a per-region basis. + + +### EKS Nodes + +To get a list of EKS nodes in a given region, we use the AWS CLI `eks` command, as in: + +```bash +$ region=us-east-2 +$ clusters=$(aws eks list-clusters $aws_p --no-paginate --region $region --output text --query='clusters') +$ for cluster in $clusters; do \ + for node_pool in $(aws eks list-nodegroups $aws_p --no-paginate --region $region --cluster-name $cluster --query=nodegroups --output text); do \ + aws eks describe-nodegroup $aws_p --no-paginate --region $region --cluster $cluster --nodegroup-name $node_pool --query="nodegroup.scalingConfig.desiredSize"; \ + done; done | paste -s -d+ - | bc +1 +``` + +To get a list of all EKS nodes across all regions use: + +```bash +$ for reg in $ec2_r; do \ + for cluster in $(aws eks list-clusters $aws_p --no-paginate --region $reg --output text --query='clusters'); do \ + for node_pool in $(aws eks list-nodegroups $aws_p --no-paginate --cluster-name $cluster --region $reg --query=nodegroups --output text); do \ + aws eks describe-nodegroup $aws_p --no-paginate --region $reg --cluster $cluster --nodegroup-name $node_pool --query="nodegroup.scalingConfig.desiredSize"; \ + done; done; done | paste -s -d+ - | bc +5 +``` \ No newline at end of file diff --git a/activityMonitor.go b/activityMonitor.go index 26111d5..e64091c 100644 --- a/activityMonitor.go +++ b/activityMonitor.go @@ -24,6 +24,7 @@ type ActivityMonitor interface { StartAction(string, ...interface{}) CheckError(error) bool ActionError(string, ...interface{}) + SubResourceError(string, ...interface{}) EndAction(string, ...interface{}) Exit(int) } @@ -38,7 +39,7 @@ type TerminalActivityMonitor struct { // Message constructs a simple message from the format string and arguments // and sends it to the associated io.Writer. func (tam *TerminalActivityMonitor) Message(format string, v ...interface{}) { - fmt.Fprint(tam.Writer, fmt.Sprintf(format, v...)) + fmt.Fprintf(tam.Writer, format, v...) } // StartAction constructs a structured message to the associated Writer @@ -66,7 +67,6 @@ func (tam *TerminalActivityMonitor) CheckError(err error) bool { case "NoCredentialProviders": // TODO Can we establish this failure earlier? When the session is created? tam.ActionError("Either the profile does not exist, is misspelled or credentials are not stored there.") - break case "AccessDeniedException": // Construct a message by taking the first part of the string up to a newline character tam.ActionError(parts[0]) @@ -94,6 +94,13 @@ func (tam *TerminalActivityMonitor) ActionError(format string, v ...interface{}) tam.Exit(1) } +// SubResourceError formats the supplied format string (and associated parameters) in +// RED. +func (tam *TerminalActivityMonitor) SubResourceError(format string, v ...interface{}) { + // Display an error message (and newline) + fmt.Fprintln(tam.Writer, color.Red(fmt.Sprintf(fmt.Sprintf(" - [ERROR] %s", format), v...))) +} + // EndAction receives a format string (and arguments) and sends to the supplied // Writer. func (tam *TerminalActivityMonitor) EndAction(format string, v ...interface{}) { diff --git a/api.go b/api.go index be1898e..904915b 100644 --- a/api.go +++ b/api.go @@ -18,6 +18,8 @@ import ( "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/ecs/ecsiface" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/aws/aws-sdk-go/service/eks/eksiface" "github.com/aws/aws-sdk-go/service/lambda" "github.com/aws/aws-sdk-go/service/lambda/lambdaiface" "github.com/aws/aws-sdk-go/service/lightsail" @@ -168,6 +170,33 @@ func (lss *LightsailService) InspectInstances(input *lightsail.GetInstancesInput return lss.Client.GetInstances(input) } +// EKSService is a struct that knows how to get a list of all EKS clusters and +// describes the clusters +type EKSService struct { + Client eksiface.EKSAPI +} + +// ListClusters takes an input filter specification and a function +// to evaluate a ListClustersOutput struct. The supplied function +// can determine when to stop iterating through EKS clusters. +func (eksi *EKSService) ListClusters(input *eks.ListClustersInput, + fn func(*eks.ListClustersOutput, bool) bool) error { + return eksi.Client.ListClustersPages(input, fn) +} + +// ListNodeGroups takes an input filter specification and a function +// to evaluate a ListNodeGroupsOutput struct. The supplied function +// can determine when to stop iterating through Nodegroups. +func (eksi *EKSService) ListNodeGroups(input *eks.ListNodegroupsInput, + fn func(*eks.ListNodegroupsOutput, bool) bool) error { + return eksi.Client.ListNodegroupsPages(input, fn) +} + +// DescribeNodegroups returns a full description of a Nodegroup +func (eksi *EKSService) DescribeNodegroups(input *eks.DescribeNodegroupInput) (*eks.DescribeNodegroupOutput, error) { + return eksi.Client.DescribeNodegroup(input) +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Abstract Service Factory (provides access to all Abstract Services) // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -178,6 +207,7 @@ type ServiceFactory interface { GetCurrentRegion() string GetAccountIDService() *AccountIDService GetEC2InstanceService(string) *EC2InstanceService + GetEKSService(string) *EKSService GetRDSInstanceService(string) *RDSInstanceService GetS3Service() *S3Service GetLambdaService(string) *LambdaService @@ -362,3 +392,20 @@ func (awssf *AWSServiceFactory) GetLightsailService(regionName string) *Lightsai Client: client, } } + +// GetEKSService returns an instance of an EKSService associated +// with our session. The caller can supply an optional region name to contruct +// an instance associated with that region. +func (awssf *AWSServiceFactory) GetEKSService(regionName string) *EKSService { + // Construct our service client + var client eksiface.EKSAPI + if regionName == "" { + client = eks.New(awssf.Session) + } else { + client = eks.New(awssf.Session, aws.NewConfig().WithRegion(regionName)) + } + + return &EKSService{ + Client: client, + } +} diff --git a/api_test.go b/api_test.go index 8f4975e..8a6c42b 100644 --- a/api_test.go +++ b/api_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "strings" "testing" @@ -9,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "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/lambda" "github.com/aws/aws-sdk-go/service/lightsail" "github.com/aws/aws-sdk-go/service/rds" @@ -386,3 +388,53 @@ func TestAwsServiceFactoryGetLightsailService(t *testing.T) { } } } + +func TestAwsServiceFactoryGetEKSService(t *testing.T) { + // Create our test cases + cases := []struct { + RegionName string + }{ + {}, + { + RegionName: "us-west-1", + }, + } + + // Loop through the test cases + for _, c := range cases { + // Create a config for the region? + var config = &aws.Config{} + if c.RegionName != "" { + config = config.WithRegion(c.RegionName) + } + + // Create our test + session, err := session.NewSession(config) + if err != nil { + t.Errorf("Unexpected error while creating a new session: %v", err) + } + + // Create an AWS Service Factory + sf := &AWSServiceFactory{ + Session: session, + } + + t.Run(fmt.Sprintf("testing with region name: %s", c.RegionName), func(t *testing.T) { + // Get the desired service + service := sf.GetEKSService(c.RegionName) + + // Is the service nil? + if service == nil { + t.Errorf("No service returned for %s", "GetLightsailService") + } else if service.Client != nil { + // Convert to implementation type + implType, ok := service.Client.(*eks.EKS) + if !ok { + t.Errorf("Unexpected Client type: expected %v, actual %v", "*eks.EKS", implType) + } else if *implType.Config.Region != c.RegionName { + t.Errorf("Unexpected value for Client.Config.Region: expected %s, actual %s", c.RegionName, *implType.Config.Region) + } + } + }) + } +} diff --git a/containers_test.go b/containers_test.go index 4446945..2c2ecca 100644 --- a/containers_test.go +++ b/containers_test.go @@ -268,6 +268,11 @@ func (fsf fakeCntrServiceFactory) GetLightsailService(string) *LightsailService return nil } +// Don't need to implement +func (fsf fakeCntrServiceFactory) GetEKSService(regionName string) *EKSService { + return nil +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Unit Test for UniqueContainerImages // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/ebs_test.go b/ebs_test.go index a40c970..ea4e240 100644 --- a/ebs_test.go +++ b/ebs_test.go @@ -244,6 +244,11 @@ func (fsf fakeEBSServiceFactory) GetLightsailService(string) *LightsailService { return nil } +// Don't need to implement +func (fsf fakeEBSServiceFactory) GetEKSService(regionName string) *EKSService { + return nil +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Unit Test for EBSVolumes // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/ec2_test.go b/ec2_test.go index 76f7015..970b6a1 100644 --- a/ec2_test.go +++ b/ec2_test.go @@ -436,6 +436,11 @@ func (fsf fakeEC2ServiceFactory) GetLightsailService(string) *LightsailService { return nil } +// Don't need to implement +func (fsf fakeEC2ServiceFactory) GetEKSService(regionName string) *EKSService { + return nil +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Unit Test for EC2Counts // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/eks.go b/eks.go new file mode 100644 index 0000000..eeca9de --- /dev/null +++ b/eks.go @@ -0,0 +1,114 @@ +/****************************************************************************** +Cloud Resource Counter +File: eks.go + +Summary: Provides a count of all EKS nodes. +******************************************************************************/ + +package main + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + color "github.com/logrusorgru/aurora" +) + +// EKSNodes retrieves the count of all EKS Nodes either for all +// regions (allRegions is true) or the region associated with the +// session. This method gives status back to the user via the supplied +// ActivityMonitor instance. +func EKSNodes(sf ServiceFactory, am ActivityMonitor, allRegions bool) int { + nodeCount := 0 + + errs := make([]error, 0) + + // Indicate activity + am.StartAction("Retrieving EKS Node counts") + + // Create a new instance of the EKS service + regionsSlice := []string{""} + if allRegions { + regionsSlice = GetEC2Regions(sf.GetEC2InstanceService(""), am) + } + + for _, regionName := range regionsSlice { + count, eksErrs := eksCountForSingleRegion(regionName, sf, am) + errs = append(errs, eksErrs...) + nodeCount += count + } + + // Indicate end of activity + am.EndAction("OK (%d)", color.Bold(nodeCount)) + + // Print list of errors that happened while retrieving node counts + for _, err := range errs { + am.SubResourceError(err.Error()) + } + + return nodeCount +} + +func eksCountForSingleRegion(region string, sf ServiceFactory, am ActivityMonitor) (int, []error) { + errs := make([]error, 0) + + // Indicate activity + am.Message(".") + + // Retrieve an EKS service + eksSvc := sf.GetEKSService(region) + + // Construct our input to find all Clusters + clusterInput := &eks.ListClustersInput{} + + nodeCount := 0 + err := eksSvc.ListClusters(clusterInput, func(clusterList *eks.ListClustersOutput, _ bool) bool { + // Loop through each cluster list + for _, cluster := range clusterList.Clusters { + count, err := countNodes(eksSvc, cluster) + errs = append(errs, err...) + nodeCount += count + } + return true + }) + + if err != nil { + errs = append(errs, fmt.Errorf("unable to list clusters for region %s (%s)", region, err)) + } + + return nodeCount, errs +} + +func countNodes(eksSvc *EKSService, cluster *string) (int, []error) { + nodeCount := 0 + errs := make([]error, 0) + nodeGroupsInput := &eks.ListNodegroupsInput{ClusterName: aws.String(*cluster)} + + err := eksSvc.ListNodeGroups(nodeGroupsInput, func(nodeGroupList *eks.ListNodegroupsOutput, _ bool) bool { + // Loop through each nodegroup + for _, nodeGroup := range nodeGroupList.Nodegroups { + describeNodeGroupInput := &eks.DescribeNodegroupInput{ + ClusterName: aws.String(*cluster), + NodegroupName: aws.String(*nodeGroup), + } + + // Retrieve nodegroup info + nodeGroupInfo, err := eksSvc.DescribeNodegroups(describeNodeGroupInput) + if err != nil { + errs = append(errs, fmt.Errorf("unable to describe %s nodegroup (%s)", *nodeGroup, err)) + return true + } + + // Add the node count for the nodepool + nodeCount += int(*nodeGroupInfo.Nodegroup.ScalingConfig.DesiredSize) + } + return true + }) + + if err != nil { + errs = append(errs, fmt.Errorf("unable to list nodegroups for %s cluster (%s)", *cluster, err)) + } + + return nodeCount, errs +} diff --git a/eks_test.go b/eks_test.go new file mode 100644 index 0000000..1411a41 --- /dev/null +++ b/eks_test.go @@ -0,0 +1,252 @@ +/****************************************************************************** +Cloud Resource Counter +File: s3_test.go + +Summary: The Unit Test for s3. +******************************************************************************/ + +package main + +import ( + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/eks" + "github.com/aws/aws-sdk-go/service/eks/eksiface" + "github.com/expel-io/aws-resource-counter/mock" +) + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Fake EKS Clusters +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//This simulates the minimal response from an AWS call +var fakeEKSClustersSlice = []*eks.ListClustersOutput{ + { + Clusters: []*string{ + aws.String("cluster1"), + aws.String("cluster2"), + aws.String("cluster3"), + }, + }, +} +var size = int64(2) +var fakeEKSDescribeNodeGroup = &eks.DescribeNodegroupOutput{ + Nodegroup: &eks.Nodegroup{ + ScalingConfig: &eks.NodegroupScalingConfig{ + DesiredSize: &size, + }, + }, +} + +var fakeEKSNodeGroupSlice = []*eks.ListNodegroupsOutput{ + { + Nodegroups: []*string{ + aws.String("nodegroup-1"), + aws.String("nodegroup-2"), + }, + }, +} + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Fake EKS Service +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// To use this struct, the caller must supply a ListClustersOutput and DescribeClusterOutput +// struct. If it is missing, it will trigger the mock function to simulate an error from +// the corresponding function. +type fakeEKService struct { + eksiface.EKSAPI + LCResponse []*eks.ListClustersOutput + DNGResponse *eks.DescribeNodegroupOutput + LNGResponse []*eks.ListNodegroupsOutput +} + +func (feks *fakeEKService) DescribeNodegroup(input *eks.DescribeNodegroupInput) (*eks.DescribeNodegroupOutput, error) { + // If there was no supplied response, then simulate a possible error + if feks.DNGResponse == nil { + return nil, errors.New("ListClusters returns an unexpected error: 2345") + } + + return feks.DNGResponse, nil +} + +// Simulate the ListClustersPages function +func (feks *fakeEKService) ListClustersPages(input *eks.ListClustersInput, + fn func(*eks.ListClustersOutput, bool) bool) error { + // If the supplied response is nil, then simulate an error + if feks.LCResponse == nil { + return errors.New("ListClustersPages encountered an unexpected error: 1234") + } + + // Loop through the slice, invoking the supplied function + for index, output := range feks.LCResponse { + // Are we looking at the last "page" of our output? + lastPage := index == len(feks.LCResponse)-1 + + // Shall we exit our loop? + if cont := fn(output, lastPage); !cont { + break + } + } + + return nil +} + +// Simulate the ListNodegroupsPages function +func (feks *fakeEKService) ListNodegroupsPages(input *eks.ListNodegroupsInput, + fn func(*eks.ListNodegroupsOutput, bool) bool) error { + // If the supplied response is nil, then simulate an error + if feks.LNGResponse == nil { + return errors.New("ListNodeGroups encountered an unexpected error: 1234") + } + + // Loop through the slice, invoking the supplied function + for index, output := range feks.LNGResponse { + // Are we looking at the last "page" of our output? + lastPage := index == len(feks.LNGResponse)-1 + + // Shall we exit our loop? + if cont := fn(output, lastPage); !cont { + break + } + } + + return nil +} + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Fake Service Factory +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +// This structure simulates the AWS Service Factory by storing some pregenerated +// responses (that would come from AWS). +type fakeEKSServiceFactory struct { + LCResponse []*eks.ListClustersOutput + DNGResponse *eks.DescribeNodegroupOutput + LNGResponse []*eks.ListNodegroupsOutput +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) Init() {} + +// Don't implement +func (fsf fakeEKSServiceFactory) GetCurrentRegion() string { + return "" +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) GetAccountIDService() *AccountIDService { + return nil +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) GetEC2InstanceService(string) *EC2InstanceService { + return nil +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) GetRDSInstanceService(regionName string) *RDSInstanceService { + return nil +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) GetS3Service() *S3Service { + return nil +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) GetLambdaService(string) *LambdaService { + return nil +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) GetContainerService(string) *ContainerService { + return nil +} + +// Don't need to implement +func (fsf fakeEKSServiceFactory) GetLightsailService(string) *LightsailService { + return nil +} + +func (fsf fakeEKSServiceFactory) GetEKSService(regionName string) *EKSService { + return &EKSService{ + Client: &fakeEKService{ + LCResponse: fsf.LCResponse, + DNGResponse: fsf.DNGResponse, + LNGResponse: fsf.LNGResponse, + }, + } +} + +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Unit Test for S3Buckets +// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +func TestEKSNodes(t *testing.T) { + // Describe all of our test cases: 1 failure and 1 success + cases := []struct { + ExpectedCount int + ExpectErrorClusterList bool + ExpectErrorDescribeNodegroup bool + ExpectErrorNodegroupList bool + name string + }{ + // Expected count is 12 because there are 2 nodes defined for each node pool + // each cluster has 2 node pools. so: 2 nodes * 2 node pools * 3 clusters + {name: "the expected count is returned", ExpectedCount: 12}, + {name: "an error is logged for cluster list", ExpectErrorClusterList: true}, + {name: "an error is logged for describe nodegroup", ExpectErrorDescribeNodegroup: true}, + {name: "an error is logged for nodegroup list", ExpectErrorNodegroupList: true}, + } + + // Loop through each test case + for _, c := range cases { + // Construct a ListBucketsOutput object based on whether + // we expect an error or not + lcResponse := fakeEKSClustersSlice + ldngRsponse := fakeEKSDescribeNodeGroup + lngResponse := fakeEKSNodeGroupSlice + + switch { + case c.ExpectErrorClusterList: + lcResponse = nil + case c.ExpectErrorDescribeNodegroup: + ldngRsponse = nil + case c.ExpectErrorNodegroupList: + lngResponse = nil + } + + // Create our fake service factory + sf := fakeEKSServiceFactory{ + LCResponse: lcResponse, + DNGResponse: ldngRsponse, + LNGResponse: lngResponse, + } + + // Create a mock activity monitor + mon := &mock.ActivityMonitorImpl{} + + t.Run(fmt.Sprintf("testing %s", c.name), func(t *testing.T) { + // Invoke our EKS Function + actualCount := EKSNodes(sf, mon, false) + + // Did we expect an error? + if c.ExpectErrorNodegroupList || c.ExpectErrorClusterList || c.ExpectErrorDescribeNodegroup { + // Did it fail to arrive? + if !mon.ErrorOccured { + t.Error("Expected an error to occur, but it did not... :^(") + } + } else if mon.ErrorOccured { + t.Errorf("Unexpected error occurred: %s", mon.ErrorMessage) + } else if actualCount != c.ExpectedCount { + t.Errorf("Error: Nodes returned %d; expected %d", actualCount, c.ExpectedCount) + } else if mon.ProgramExited { + t.Errorf("Unexpected Exit: The program unexpected exited with status code=%d", mon.ExitCode) + } + }) + } +} diff --git a/go.mod b/go.mod index 6fa1bf8..09a136f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,13 @@ module github.com/expel-io/aws-resource-counter go 1.18 require ( - github.com/aws/aws-sdk-go v1.43.0 + github.com/aws/aws-sdk-go v1.44.213 github.com/logrusorgru/aurora v2.0.3+incompatible - github.com/jmespath/go-jmespath v0.4.0 // indirect +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + golang.org/x/net v0.7.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e0f5043..46e2a6a 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,8 @@ -github.com/aws/aws-sdk-go v1.43.0 h1:y4UrPbxU/mIL08qksVPE/nwH9IXuC1udjOaNyhEe+pI= -github.com/aws/aws-sdk-go v1.43.0/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/aws/aws-sdk-go v1.44.213 h1:WahquyWs7cQdz0vpDVWyWETEemgSoORx0PbWL9oz2WA= +github.com/aws/aws-sdk-go v1.44.213/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -12,14 +13,37 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/lambda_test.go b/lambda_test.go index 4d70e61..e53b5b2 100644 --- a/lambda_test.go +++ b/lambda_test.go @@ -187,6 +187,11 @@ func (fsf fakeLambdaServiceFactory) GetLightsailService(string) *LightsailServic return nil } +// Don't need to implement +func (fsf fakeLambdaServiceFactory) GetEKSService(regionName string) *EKSService { + return nil +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Unit Test for LambdaFunctions // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/lightsail_test.go b/lightsail_test.go index 5236874..a28a771 100644 --- a/lightsail_test.go +++ b/lightsail_test.go @@ -181,6 +181,11 @@ func (fsf fakeLightsailServiceFactory) GetLightsailService(regionName string) *L } } +// Don't need to implement +func (fsf fakeLightsailServiceFactory) GetEKSService(regionName string) *EKSService { + return nil +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Unit Test for LightsailInstances // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/main.go b/main.go index 23c43c2..ecd790c 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,7 @@ func main() { results.Append("# of RDS Instances", RDSInstances(serviceFactory, monitor, settings.allRegions)) results.Append("# of Lightsail Instances", LightsailInstances(serviceFactory, monitor, settings.allRegions)) results.Append("# of S3 Buckets", S3Buckets(serviceFactory, monitor, settings.allRegions)) + results.Append("# of EKS Nodes", EKSNodes(serviceFactory, monitor, settings.allRegions)) /* =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= * Construct CSV Output diff --git a/mock/mock.go b/mock/mock.go index f9d09b4..a720299 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -16,13 +16,13 @@ import ( // errored, ended). // type ActivityMonitorImpl struct { - ActionStarted bool - ErrorOccured bool - ActionEnded bool - ErrorMessage string + ActionStarted bool + ErrorOccured bool + ActionEnded bool + ErrorMessage string ProgramExited bool - ExitCode int - Messages []string + ExitCode int + Messages []string } // Message does nothing @@ -58,6 +58,12 @@ func (m *ActivityMonitorImpl) ActionError(format string, v ...interface{}) { m.ErrorOccured = true } +// SubResourceError is called if we encounter an error in EKS. +func (m *ActivityMonitorImpl) SubResourceError(format string, v ...interface{}) { + m.Messages = append(m.Messages, fmt.Sprintf(format, v...)) + m.ErrorOccured = true +} + // EndAction records that an action was ended. func (m *ActivityMonitorImpl) EndAction(format string, v ...interface{}) { m.Messages = append(m.Messages, fmt.Sprintf(format, v...)) diff --git a/rds_test.go b/rds_test.go index 2171538..226f2ef 100644 --- a/rds_test.go +++ b/rds_test.go @@ -205,6 +205,11 @@ func (fsf fakeRDSServiceFactory) GetLightsailService(string) *LightsailService { return nil } +// Don't need to implement +func (fsf fakeRDSServiceFactory) GetEKSService(regionName string) *EKSService { + return nil +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Unit Test for RDSInstances // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/s3_test.go b/s3_test.go index 6fa129a..26ba4c6 100644 --- a/s3_test.go +++ b/s3_test.go @@ -129,6 +129,11 @@ func (fsf fakeS3ServiceFactory) GetLightsailService(string) *LightsailService { return nil } +// Don't need to implement +func (fsf fakeS3ServiceFactory) GetEKSService(regionName string) *EKSService { + return nil +} + // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // Unit Test for S3Buckets // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=