Repository: gruntwork-io/cloud-nuke
Pull request #616 (2023-10-24 23:39:18)

Use time utility function to parse & format timestamp for all AWS res…
…ources

<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Fixes https://github.com/gruntwork-io/cloud-nuke/issues/137.

<!-- Description of the changes introduced by this PR. -->

## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [ ] Update the docs.
- [ ] Run the relevant tests successfully, including pre-commit checks.
- [ ] 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.
- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.
- [ ] 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)

<!-- One-line description of the PR that can be included in the final release notes. -->
Added / Removed / Updated [X].

### Migration Guide

<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->




Patch:
diff --git a/aws/resources/ami.go b/aws/resources/ami.go
index 87300ad..9af14d5 100644
--- a/aws/resources/ami.go
+++ b/aws/resources/ami.go
@@ -2,11 +2,10 @@ package resources
 
 import (
 	"context"
-	"strings"
-	"time"
-
 	awsgo "github.com/aws/aws-sdk-go/aws"
+	"github.com/gruntwork-io/cloud-nuke/util"
 	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
+	"strings"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/ec2"
@@ -30,8 +29,7 @@ func (ami *AMIs) getAll(c context.Context, configObj config.Config) ([]*string,
 
 	var imageIds []*string
 	for _, image := range output.Images {
-		layout := "2006-01-02T15:04:05.000Z"
-		createdTime, err := time.Parse(layout, *image.CreationDate)
+		createdTime, err := util.ParseTimestamp(image.CreationDate)
 		if err != nil {
 			return nil, err
 		}
@@ -52,7 +50,7 @@ func (ami *AMIs) getAll(c context.Context, configObj config.Config) ([]*string,
 
 		if configObj.AMI.ShouldInclude(config.ResourceValue{
 			Name: image.Name,
-			Time: &createdTime,
+			Time: createdTime,
 		}) {
 			imageIds = append(imageIds, image.ImageId)
 		}
diff --git a/aws/resources/ec2_vpc.go b/aws/resources/ec2_vpc.go
index d2e26a6..330f70b 100644
--- a/aws/resources/ec2_vpc.go
+++ b/aws/resources/ec2_vpc.go
@@ -29,7 +29,7 @@ func (v *EC2VPCs) setFirstSeenTag(vpc ec2.Vpc, value time.Time) error {
 		Tags: []*ec2.Tag{
 			{
 				Key:   awsgo.String(util.FirstSeenTagKey),
-				Value: awsgo.String(util.FormatTimestampTag(value)),
+				Value: awsgo.String(util.FormatTimestamp(value)),
 			},
 		},
 	})
@@ -44,7 +44,7 @@ func (v *EC2VPCs) getFirstSeenTag(vpc ec2.Vpc) (*time.Time, error) {
 	tags := vpc.Tags
 	for _, tag := range tags {
 		if util.IsFirstSeenTag(tag.Key) {
-			firstSeenTime, err := util.ParseTimestampTag(tag.Value)
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
 			if err != nil {
 				return nil, errors.WithStackTrace(err)
 			}
diff --git a/aws/resources/ecs_cluster.go b/aws/resources/ecs_cluster.go
index f6f6511..1f5c0cf 100644
--- a/aws/resources/ecs_cluster.go
+++ b/aws/resources/ecs_cluster.go
@@ -171,7 +171,7 @@ func (clusters *ECSClusters) nukeAll(ecsClusterArns []*string) error {
 
 // Tag an ECS cluster identified by the given cluster ARN when it's first seen by cloud-nuke
 func (clusters *ECSClusters) setFirstSeenTag(clusterArn *string, timestamp time.Time) error {
-	firstSeenTime := util.FormatTimestampTag(timestamp)
+	firstSeenTime := util.FormatTimestamp(timestamp)
 
 	input := &ecs.TagResourceInput{
 		ResourceArn: clusterArn,
@@ -208,7 +208,7 @@ func (clusters *ECSClusters) getFirstSeenTag(clusterArn *string) (*time.Time, er
 	for _, tag := range clusterTags.Tags {
 		if util.IsFirstSeenTag(tag.Key) {
 
-			firstSeenTime, err := util.ParseTimestampTag(tag.Value)
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
 			if err != nil {
 				logging.Debugf("Error parsing the `cloud-nuke-first-seen` tag for ECS cluster with ARN %s", aws.StringValue(clusterArn))
 				return firstSeenTime, errors.WithStackTrace(err)
diff --git a/aws/resources/eip.go b/aws/resources/eip.go
index 14bd7b9..7a8b679 100644
--- a/aws/resources/eip.go
+++ b/aws/resources/eip.go
@@ -24,7 +24,7 @@ func (ea *EIPAddresses) setFirstSeenTag(address ec2.Address, value time.Time) er
 		Tags: []*ec2.Tag{
 			{
 				Key:   awsgo.String(util.FirstSeenTagKey),
-				Value: awsgo.String(util.FormatTimestampTag(value)),
+				Value: awsgo.String(util.FormatTimestamp(value)),
 			},
 		},
 	})
@@ -39,7 +39,7 @@ func (ea *EIPAddresses) getFirstSeenTag(address ec2.Address) (*time.Time, error)
 	tags := address.Tags
 	for _, tag := range tags {
 		if util.IsFirstSeenTag(tag.Key) {
-			firstSeenTime, err := util.ParseTimestampTag(tag.Value)
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
 			if err != nil {
 				return nil, errors.WithStackTrace(err)
 			}
diff --git a/aws/resources/guardduty.go b/aws/resources/guardduty.go
index d391440..e17643b 100644
--- a/aws/resources/guardduty.go
+++ b/aws/resources/guardduty.go
@@ -2,16 +2,15 @@ package resources
 
 import (
 	"context"
-	"github.com/gruntwork-io/cloud-nuke/telemetry"
-	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
-	"time"
-
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/guardduty"
 	"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/cloud-nuke/util"
 	"github.com/gruntwork-io/go-commons/errors"
+	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
 )
 
 type DetectorOutputWithID struct {
@@ -50,13 +49,12 @@ func (gd *GuardDuty) getAll(c context.Context, configObj config.Config) ([]*stri
 }
 
 func (gd *GuardDuty) shouldInclude(detector *guardduty.GetDetectorOutput, detectorId *string, configObj config.Config) bool {
-	detectorCreatedAt := aws.StringValue(detector.CreatedAt)
-	createdAtDateTime, err := time.Parse(time.RFC3339, detectorCreatedAt)
+	createdAtDateTime, err := util.ParseTimestamp(detector.CreatedAt)
 	if err != nil {
-		logging.Debugf("Could not parse createdAt timestamp (%s) of GuardDuty detector %s. Excluding from delete.", detectorCreatedAt, *detectorId)
+		logging.Debugf("Could not parse createdAt timestamp (%s) of GuardDuty detector %s. Excluding from delete.", *createdAtDateTime, *detectorId)
 	}
 
-	return configObj.GuardDuty.ShouldInclude(config.ResourceValue{Time: &createdAtDateTime})
+	return configObj.GuardDuty.ShouldInclude(config.ResourceValue{Time: createdAtDateTime})
 }
 
 func (gd *GuardDuty) nukeAll(detectorIds []string) error {
diff --git a/aws/resources/opensearch.go b/aws/resources/opensearch.go
index 43f9e5e..64cc431 100644
--- a/aws/resources/opensearch.go
+++ b/aws/resources/opensearch.go
@@ -87,7 +87,7 @@ func (osd *OpenSearchDomains) getAllActiveOpenSearchDomains() ([]*opensearchserv
 // Tag an OpenSearch Domain identified by the given ARN when it's first seen by cloud-nuke
 func (osd *OpenSearchDomains) setFirstSeenTag(domainARN *string, timestamp time.Time) error {
 	logging.Debugf("Tagging the OpenSearch Domain with ARN %s with first seen timestamp", aws.StringValue(domainARN))
-	firstSeenTime := util.FormatTimestampTag(timestamp)
+	firstSeenTime := util.FormatTimestamp(timestamp)
 
 	input := &opensearchservice.AddTagsInput{
 		ARN: domainARN,
@@ -120,7 +120,7 @@ func (osd *OpenSearchDomains) getFirstSeenTag(domainARN *string) (*time.Time, er
 
 	for _, tag := range domainTags.TagList {
 		if util.IsFirstSeenTag(tag.Key) {
-			firstSeenTime, err := util.ParseTimestampTag(tag.Value)
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
 			if err != nil {
 				logging.Errorf("Error parsing the `cloud-nuke-first-seen` tag for OpenSearch Domain with ARN %s", aws.StringValue(domainARN))
 				return firstSeenTime, errors.WithStackTrace(err)
diff --git a/aws/resources/security_hub.go b/aws/resources/security_hub.go
index aabec8e..cdade3c 100644
--- a/aws/resources/security_hub.go
+++ b/aws/resources/security_hub.go
@@ -2,18 +2,17 @@ package resources
 
 import (
 	"context"
-	"github.com/gruntwork-io/cloud-nuke/config"
-	"strings"
-	"time"
-
 	"github.com/aws/aws-sdk-go/aws"
 	awsgo "github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/securityhub"
+	"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/cloud-nuke/util"
 	"github.com/gruntwork-io/go-commons/errors"
 	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
+	"strings"
 )
 
 func (sh *SecurityHub) getAll(c context.Context, configObj config.Config) ([]*string, error) {
@@ -39,14 +38,14 @@ func (sh *SecurityHub) getAll(c context.Context, configObj config.Config) ([]*st
 }
 
 func shouldIncludeHub(hub *securityhub.DescribeHubOutput, configObj config.Config) bool {
-	subscribedAt, err := time.Parse(time.RFC3339, *hub.SubscribedAt)
+	subscribedAt, err := util.ParseTimestamp(hub.SubscribedAt)
 	if err != nil {
 		logging.Debugf(
 			"Could not parse subscribedAt timestamp (%s) of security hub. Excluding from delete.", *hub.SubscribedAt)
 		return false
 	}
 
-	return configObj.SecurityHub.ShouldInclude(config.ResourceValue{Time: &subscribedAt})
+	return configObj.SecurityHub.ShouldInclude(config.ResourceValue{Time: subscribedAt})
 }
 
 func (sh *SecurityHub) getAllSecurityHubMembers() ([]*string, error) {
diff --git a/aws/resources/sns.go b/aws/resources/sns.go
index 28a3d1f..3cbacd0 100644
--- a/aws/resources/sns.go
+++ b/aws/resources/sns.go
@@ -3,6 +3,7 @@ package resources
 import (
 	"context"
 	"github.com/aws/aws-sdk-go/service/sns"
+	"github.com/gruntwork-io/cloud-nuke/util"
 	"strings"
 	"sync"
 	"time"
@@ -26,7 +27,7 @@ func (s *SNSTopic) getAll(c context.Context, configObj config.Config) ([]*string
 	var snsTopics []*string
 	err := s.Client.ListTopicsPages(&sns.ListTopicsInput{}, func(page *sns.ListTopicsOutput, lastPage bool) bool {
 		for _, topic := range page.Topics {
-			firstSeenTime, err := s.getFirstSeenSNSTopicTag(*topic.TopicArn)
+			firstSeenTime, err := s.getFirstSeenTag(*topic.TopicArn)
 			if err != nil {
 				logging.Errorf(
 					"Unable to retrieve tags for SNS Topic: %s, with error: %s", *topic.TopicArn, err)
@@ -36,7 +37,7 @@ func (s *SNSTopic) getAll(c context.Context, configObj config.Config) ([]*string
 			if firstSeenTime == nil {
 				now := time.Now().UTC()
 				firstSeenTime = &now
-				if err := s.setFirstSeenSNSTopicTag(*topic.TopicArn, now); err != nil {
+				if err := s.setFirstSeenTag(*topic.TopicArn, now); err != nil {
 					logging.Errorf(
 						"Unable to apply first seen tag SNS Topic: %s, with error: %s", *topic.TopicArn, err)
 					continue
@@ -67,7 +68,7 @@ func (s *SNSTopic) getAll(c context.Context, configObj config.Config) ([]*string
 
 // getFirstSeenSNSTopicTag will retrive the time that the topic was first seen, otherwise returning nil if the topic has not been
 // seen before.
-func (s *SNSTopic) getFirstSeenSNSTopicTag(topicArn string) (*time.Time, error) {
+func (s *SNSTopic) getFirstSeenTag(topicArn string) (*time.Time, error) {
 	response, err := s.Client.ListTagsForResource(&sns.ListTagsForResourceInput{
 		ResourceArn: &topicArn,
 	})
@@ -75,14 +76,14 @@ func (s *SNSTopic) getFirstSeenSNSTopicTag(topicArn string) (*time.Time, error)
 		return nil, err
 	}
 
-	for i := range response.Tags {
-		if *response.Tags[i].Key == firstSeenTagKey {
-			firstSeenTime, err := time.Parse(firstSeenTimeFormat, *response.Tags[i].Value)
+	for _, tag := range response.Tags {
+		if util.IsFirstSeenTag(tag.Key) {
+			firstSeenTime, err := util.ParseTimestamp(tag.Value)
 			if err != nil {
-				return nil, err
+				return nil, errors.WithStackTrace(err)
 			}
 
-			return &firstSeenTime, nil
+			return firstSeenTime, nil
 		}
 	}
 
@@ -90,16 +91,14 @@ func (s *SNSTopic) getFirstSeenSNSTopicTag(topicArn string) (*time.Time, error)
 }
 
 // setFirstSeenSNSTopic will append a tag to the SNS Topic that details the first seen time.
-func (s *SNSTopic) setFirstSeenSNSTopicTag(topicArn string, value time.Time) error {
-	timeValue := value.Format(firstSeenTimeFormat)
-
+func (s *SNSTopic) setFirstSeenTag(topicArn string, value time.Time) error {
 	_, err := s.Client.TagResource(
 		&sns.TagResourceInput{
 			ResourceArn: &topicArn,
 			Tags: []*sns.Tag{
 				{
-					Key:   aws.String(firstSeenTagKey),
-					Value: &timeValue,
+					Key:   aws.String(util.FirstSeenTagKey),
+					Value: aws.String(util.FormatTimestamp(value)),
 				},
 			},
 		},
diff --git a/aws/resources/sqs.go b/aws/resources/sqs.go
index a18a306..5d3eb0e 100644
--- a/aws/resources/sqs.go
+++ b/aws/resources/sqs.go
@@ -2,17 +2,16 @@ package resources
 
 import (
 	"context"
-	"github.com/gruntwork-io/cloud-nuke/config"
-	"github.com/gruntwork-io/cloud-nuke/telemetry"
-	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
-	"time"
-
 	"github.com/aws/aws-sdk-go/aws"
 	awsgo "github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/sqs"
+	"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/cloud-nuke/util"
 	"github.com/gruntwork-io/go-commons/errors"
+	commonTelemetry "github.com/gruntwork-io/go-commons/telemetry"
 )
 
 // Returns a formatted string of SQS Queue URLs
@@ -44,8 +43,8 @@ func (sq *SqsQueue) getAll(c context.Context, configObj config.Config) ([]*strin
 		}
 
 		// Convert string timestamp to int64
-		createdAt := *queueAttributes.Attributes["CreatedTimestamp"]
-		createdAtTime, err := time.Parse(time.RFC3339, createdAt)
+		createdAt := queueAttributes.Attributes["CreatedTimestamp"]
+		createdAtTime, err := util.ParseTimestamp(createdAt)
 		if err != nil {
 			return nil, errors.WithStackTrace(err)
 		}
@@ -53,7 +52,7 @@ func (sq *SqsQueue) getAll(c context.Context, configObj config.Config) ([]*strin
 		// Compare time as int64
 		if configObj.SQS.ShouldInclude(config.ResourceValue{
 			Name: queue,
-			Time: &createdAtTime,
+			Time: createdAtTime,
 		}) {
 			urls = append(urls, queue)
 		}
diff --git a/util/time.go b/util/time.go
index 1983d06..bf1268c 100644
--- a/util/time.go
+++ b/util/time.go
@@ -23,7 +23,7 @@ func IsFirstSeenTag(key *string) bool {
 	return aws.StringValue(key) == FirstSeenTagKey
 }
 
-func ParseTimestampTag(timestamp *string) (*time.Time, error) {
+func ParseTimestamp(timestamp *string) (*time.Time, error) {
 	parsed, err := time.Parse(firstSeenTimeFormat, aws.StringValue(timestamp))
 	if err != nil {
 		logging.Debugf("Error parsing the timestamp into a `RFC3339` Time format")
@@ -34,6 +34,6 @@ func ParseTimestampTag(timestamp *string) (*time.Time, error) {
 	return &parsed, nil
 }
 
-func FormatTimestampTag(timestamp time.Time) string {
+func FormatTimestamp(timestamp time.Time) string {
 	return timestamp.Format(firstSeenTimeFormat)
 }


Test patch:
diff --git a/aws/resources/ec2_vpc_test.go b/aws/resources/ec2_vpc_test.go
index 20473a5..0e4ee10 100644
--- a/aws/resources/ec2_vpc_test.go
+++ b/aws/resources/ec2_vpc_test.go
@@ -51,7 +51,7 @@ func TestEC2VPC_GetAll(t *testing.T) {
 							},
 							{
 								Key:   awsgo.String(util.FirstSeenTagKey),
-								Value: awsgo.String(util.FormatTimestampTag(now)),
+								Value: awsgo.String(util.FormatTimestamp(now)),
 							},
 						},
 					},
@@ -64,7 +64,7 @@ func TestEC2VPC_GetAll(t *testing.T) {
 							},
 							{
 								Key:   awsgo.String(util.FirstSeenTagKey),
-								Value: awsgo.String(util.FormatTimestampTag(now.Add(1))),
+								Value: awsgo.String(util.FormatTimestamp(now.Add(1))),
 							},
 						},
 					},
diff --git a/aws/resources/ecs_cluster_test.go b/aws/resources/ecs_cluster_test.go
index 50a6e43..9be2532 100644
--- a/aws/resources/ecs_cluster_test.go
+++ b/aws/resources/ecs_cluster_test.go
@@ -80,7 +80,7 @@ func TestEC2Cluster_GetAll(t *testing.T) {
 				Tags: []*ecs.Tag{
 					{
 						Key:   aws.String(util.FirstSeenTagKey),
-						Value: aws.String(util.FormatTimestampTag(now)),
+						Value: aws.String(util.FormatTimestamp(now)),
 					},
 				},
 			},
diff --git a/aws/resources/eip_test.go b/aws/resources/eip_test.go
index 0da5f47..030dd1c 100644
--- a/aws/resources/eip_test.go
+++ b/aws/resources/eip_test.go
@@ -51,7 +51,7 @@ func TestEIPAddress_GetAll(t *testing.T) {
 							},
 							{
 								Key:   awsgo.String(util.FirstSeenTagKey),
-								Value: awsgo.String(util.FormatTimestampTag(now)),
+								Value: awsgo.String(util.FormatTimestamp(now)),
 							},
 						},
 					},
@@ -64,7 +64,7 @@ func TestEIPAddress_GetAll(t *testing.T) {
 							},
 							{
 								Key:   awsgo.String(util.FirstSeenTagKey),
-								Value: awsgo.String(util.FormatTimestampTag(now.Add(1))),
+								Value: awsgo.String(util.FormatTimestamp(now.Add(1))),
 							},
 						},
 					},
diff --git a/aws/resources/opensearch_test.go b/aws/resources/opensearch_test.go
index 877624b..430da03 100644
--- a/aws/resources/opensearch_test.go
+++ b/aws/resources/opensearch_test.go
@@ -59,11 +59,11 @@ func TestOpenSearch_GetAll(t *testing.T) {
 				TagList: []*opensearchservice.Tag{
 					{
 						Key:   aws.String(firstSeenTagKey),
-						Value: aws.String(util.FormatTimestampTag(now)),
+						Value: aws.String(util.FormatTimestamp(now)),
 					},
 					{
 						Key:   aws.String(firstSeenTagKey),
-						Value: aws.String(util.FormatTimestampTag(now.Add(1))),
+						Value: aws.String(util.FormatTimestamp(now.Add(1))),
 					},
 				},
 			},