From 1f8f6d24617426b2eb646d27c4ba3d41a60c6110 Mon Sep 17 00:00:00 2001 From: Nirdosh Gautam Date: Sat, 21 Aug 2021 13:44:09 +0545 Subject: [PATCH] Document/Refactor code (#3) * Document/Refactor code. --- README.md | 3 ++ cmd/deleteStacks.go | 2 + cmd/listDependencies.go | 2 + cmd/root.go | 2 + cmd/version.go | 4 ++ models/cfn.go | 103 +++++++++++++++++++++++++++++----------- models/nuke.go | 2 + utils/cloudformation.go | 50 ++++++++++++------- utils/deleter.go | 88 ++++++++++++++++++---------------- utils/notifier.go | 15 +++++- utils/s3.go | 19 +++++--- 11 files changed, 199 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index ad14159..6b7bb9e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # CFN Teardown +[![Go Report Card](https://goreportcard.com/badge/github.com/nirdosh17/cfn-teardown)](https://goreportcard.com/report/github.com/nirdosh17/cfn-teardown) +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache-yellow.svg)](https://opensource.org/licenses/Apache-2.0) +![Latest GitHub Release](https://img.shields.io/github/release/nirdosh17/cfn-teardown) CFN Teardown is a tool to delete matching CloudFormation stacks respecting stack dependencies. diff --git a/cmd/deleteStacks.go b/cmd/deleteStacks.go index f22e4fc..74fe4ea 100644 --- a/cmd/deleteStacks.go +++ b/cmd/deleteStacks.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package cmd provides interface to register and define actions for all cli commands package cmd import ( diff --git a/cmd/listDependencies.go b/cmd/listDependencies.go index 48d86fd..2950874 100644 --- a/cmd/listDependencies.go +++ b/cmd/listDependencies.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package cmd provides interface to register and define actions for all cli commands package cmd import ( diff --git a/cmd/root.go b/cmd/root.go index 20b64d2..3ce4dd9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package cmd provides interface to register and define actions for all cli commands package cmd import ( diff --git a/cmd/version.go b/cmd/version.go index 6dc6f77..334ec4a 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package cmd provides interface to register and define actions for all cli commands package cmd import ( @@ -21,6 +23,8 @@ import ( "github.com/spf13/cobra" ) +// Version is the current CLI version. +// This is overwrriten by semantic version tag while building binaries. var Version = "development" // versionCmd represents the version command diff --git a/models/cfn.go b/models/cfn.go index d31a0e8..8dcb071 100644 --- a/models/cfn.go +++ b/models/cfn.go @@ -13,47 +13,94 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package models has definition of entities used in the process of teardown package models +// StackDetails represents a cloudformation stack, it's state and dependencies. type StackDetails struct { StackName string Status string StackStatusReason string // useful for failed cases DeleteStartedAt string - DeleteCompletedAt string // must be fetched from describe status command. Wait time should not be considered - DeletionTimeInMinutes string // total minutes taken to delete the stack + DeleteCompletedAt string + DeletionTimeInMinutes string DeleteAttempt int16 Exports []string - ActiveImporterStacks map[string]struct{} // active(not deleted) stacks which are importing exports from THIS stack + ActiveImporterStacks map[string]struct{} // active(not deleted) stacks which are importing exports from this stack CFNConsoleLink string } +// ---------- Stack statuses and their eligibility for deletion ------------ // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html -// Stack status and eligibility for deletion -var CREATE_IN_PROGRESS string = "CREATE_IN_PROGRESS" // Wait -var CREATE_FAILED string = "CREATE_FAILED" // Eligible for deletion -var CREATE_COMPLETE string = "CREATE_COMPLETE" // Eligible for deletion -var ROLLBACK_IN_PROGRESS string = "ROLLBACK_IN_PROGRESS" // Wait -var ROLLBACK_FAILED string = "ROLLBACK_FAILED" // Eligible for deletion -var ROLLBACK_COMPLETE string = "ROLLBACK_COMPLETE" // Eligible for deletion -var DELETE_IN_PROGRESS string = "DELETE_IN_PROGRESS" // Wait -var DELETE_FAILED string = "DELETE_FAILED" // Cannot be deleted. Manual Intervention Required. Post Message in RC. -var DELETE_COMPLETE string = "DELETE_COMPLETE" // Skip -var UPDATE_IN_PROGRESS string = "UPDATE_IN_PROGRESS" // Wait -var UPDATE_COMPLETE_CLEANUP_IN_PROGRESS string = "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS" // Wait -var UPDATE_COMPLETE string = "UPDATE_COMPLETE" // Eligible for deletion -var UPDATE_ROLLBACK_IN_PROGRESS string = "UPDATE_ROLLBACK_IN_PROGRESS" // Wait -var UPDATE_ROLLBACK_FAILED string = "UPDATE_ROLLBACK_FAILED" // Cannot be deleted. Manual Intervention Required. Post Message in RC. -var UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS string = "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" // Wait -var UPDATE_ROLLBACK_COMPLETE string = "UPDATE_ROLLBACK_COMPLETE" // Eligible for deletion -var REVIEW_IN_PROGRESS string = "REVIEW_IN_PROGRESS" // Wait -var IMPORT_IN_PROGRESS string = "IMPORT_IN_PROGRESS" // Wait -var IMPORT_COMPLETE string = "IMPORT_COMPLETE" // Wait -var IMPORT_ROLLBACK_IN_PROGRESS string = "IMPORT_ROLLBACK_IN_PROGRESS" // Wait -var IMPORT_ROLLBACK_FAILED string = "IMPORT_ROLLBACK_FAILED" // Cannot be deleted. Manual Intervention Required. Post Message in RC. -var IMPORT_ROLLBACK_COMPLETE string = "IMPORT_ROLLBACK_COMPLETE" // Wait - -// all statuses except DELETE_COMPLETE + +// CREATE_IN_PROGRESS stack status requires waiting before sending delete request. +var CREATE_IN_PROGRESS string = "CREATE_IN_PROGRESS" + +// CREATE_FAILED stack status is eligible for deletion. +var CREATE_FAILED string = "CREATE_FAILED" + +// CREATE_COMPLETE stack status is eligible for deletion. +var CREATE_COMPLETE string = "CREATE_COMPLETE" + +// ROLLBACK_IN_PROGRESS stack status requires waiting before sending delete request. +var ROLLBACK_IN_PROGRESS string = "ROLLBACK_IN_PROGRESS" + +// ROLLBACK_FAILED stack status is eligible for deletion. +var ROLLBACK_FAILED string = "ROLLBACK_FAILED" + +// ROLLBACK_COMPLETE stack status is eligible for deletion. +var ROLLBACK_COMPLETE string = "ROLLBACK_COMPLETE" + +// DELETE_IN_PROGRESS stack status requires waiting before taking any action +var DELETE_IN_PROGRESS string = "DELETE_IN_PROGRESS" + +// DELETE_FAILED stack status after max delete attempts is unactionable and requires manual intervention +var DELETE_FAILED string = "DELETE_FAILED" + +// DELETE_COMPLETE stack status can be skipped as the stack has already been deleted +var DELETE_COMPLETE string = "DELETE_COMPLETE" + +// UPDATE_IN_PROGRESS stack status requires waiting before sending delete request. +var UPDATE_IN_PROGRESS string = "UPDATE_IN_PROGRESS" + +// UPDATE_COMPLETE_CLEANUP_IN_PROGRESS stack status requires waiting before sending delete request. +var UPDATE_COMPLETE_CLEANUP_IN_PROGRESS string = "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS" + +// UPDATE_COMPLETE stack status is eligible for deletion. +var UPDATE_COMPLETE string = "UPDATE_COMPLETE" + +// UPDATE_ROLLBACK_IN_PROGRESS stack status requires waiting before sending delete request. +var UPDATE_ROLLBACK_IN_PROGRESS string = "UPDATE_ROLLBACK_IN_PROGRESS" + +// UPDATE_ROLLBACK_FAILED stack status is eligible for deletion. +var UPDATE_ROLLBACK_FAILED string = "UPDATE_ROLLBACK_FAILED" + +// UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS stack status requires waiting before sending delete request. +var UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS string = "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" + +// UPDATE_ROLLBACK_COMPLETE stack status is eligible for deletion. +var UPDATE_ROLLBACK_COMPLETE string = "UPDATE_ROLLBACK_COMPLETE" + +// REVIEW_IN_PROGRESS stack status requires waiting before sending delete request. +var REVIEW_IN_PROGRESS string = "REVIEW_IN_PROGRESS" + +// IMPORT_IN_PROGRESS stack status requires waiting before sending delete request. +var IMPORT_IN_PROGRESS string = "IMPORT_IN_PROGRESS" + +// IMPORT_COMPLETE stack status requires waiting before sending delete request. +var IMPORT_COMPLETE string = "IMPORT_COMPLETE" + +// IMPORT_ROLLBACK_IN_PROGRESS stack status requires waiting before sending delete request. +var IMPORT_ROLLBACK_IN_PROGRESS string = "IMPORT_ROLLBACK_IN_PROGRESS" + +// IMPORT_ROLLBACK_FAILED stack status is eligible for deletion. +var IMPORT_ROLLBACK_FAILED string = "IMPORT_ROLLBACK_FAILED" + +// IMPORT_ROLLBACK_COMPLETE stack status requires waiting before sending delete request. +var IMPORT_ROLLBACK_COMPLETE string = "IMPORT_ROLLBACK_COMPLETE" + +// ActiveStatuses includes all stack statuses except DELETE_COMPLETE var ActiveStatuses = []*string{ &CREATE_IN_PROGRESS, &CREATE_FAILED, diff --git a/models/nuke.go b/models/nuke.go index 54e3b46..7e324ff 100644 --- a/models/nuke.go +++ b/models/nuke.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package models has definition of entities used in the process of teardown package models // Config represents all the parameters supported by cfn-teardown diff --git a/utils/cloudformation.go b/utils/cloudformation.go index cb65d2d..e8899e5 100644 --- a/utils/cloudformation.go +++ b/utils/cloudformation.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package utils provides cli specifics methods for interacting with AWS services package utils import ( @@ -26,9 +28,10 @@ import ( "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/sts" - . "github.com/nirdosh17/cfn-teardown/models" + "github.com/nirdosh17/cfn-teardown/models" ) +// CFNManager exposes methods to interact with CloudFormation via SDK. type CFNManager struct { TargetAccountId string NukeRoleARN string @@ -37,6 +40,7 @@ type CFNManager struct { AWSRegion string } +// DescribeStack returns description for particular stack. func (dm CFNManager) DescribeStack(stackName string) (*cloudformation.Stack, error) { cfn, err := dm.Session() if err != nil { @@ -50,6 +54,7 @@ func (dm CFNManager) DescribeStack(stackName string) (*cloudformation.Stack, err return resp.Stacks[0], err } +// ListStackResources lists description of all resources in a stack. func (dm CFNManager) ListStackResources(stackName string) ([]*cloudformation.StackResourceSummary, error) { cfn, err := dm.Session() if err != nil { @@ -81,6 +86,7 @@ func (dm CFNManager) ListStackResources(stackName string) ([]*cloudformation.Sta return resp.StackResourceSummaries, err } +// ListImports lists all stacks importing given exported names. func (dm CFNManager) ListImports(exportNames []string) (map[string]struct{}, error) { importers := make(map[string]struct{}) var err error @@ -106,8 +112,8 @@ func (dm CFNManager) ListImports(exportNames []string) (map[string]struct{}, err return importers, err } -// No error means, delete request sent to cloudformation -// If the stack we are trying to delete has already been deleted, returns success +// DeleteStack sends delete request for a stack. +// Returns success if the stack we are trying to delete has already been deleted. func (dm CFNManager) DeleteStack(stackName string) error { fmt.Printf("Submitting delete request for stack: %v\n", stackName) cfn, err := dm.Session() @@ -117,21 +123,25 @@ func (dm CFNManager) DeleteStack(stackName string) error { input := cloudformation.DeleteStackInput{StackName: &stackName} // stack delete output is an empty struct _, err = cfn.DeleteStack(&input) + + // No error only means that the delete request was sent + // It does not gurantee that the stack will be deleted return err } -func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) { +// ListEnvironmentStacks lists matching stacks for the given regex. +func (dm CFNManager) ListEnvironmentStacks() (map[string]models.StackDetails, error) { CFNConsoleBaseURL := "https://console.aws.amazon.com/cloudformation/home?region=" + dm.AWSRegion + "#/stacks/stackinfo?stackId=" // using stack name as key for easy traversal - envStacks := map[string]StackDetails{} + envStacks := map[string]models.StackDetails{} cfn, err := dm.Session() if err != nil { return envStacks, err } - input := cloudformation.ListStacksInput{StackStatusFilter: ActiveStatuses} + input := cloudformation.ListStacksInput{StackStatusFilter: models.ActiveStatuses} // only returns first 100 stacks. Need to use NextToken listStackOutput, err := cfn.ListStacks(&input) if err != nil { @@ -142,7 +152,7 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) { // select stacks of our concern stackName := *details.StackName if dm.RegexMatch(stackName) { - sd := StackDetails{ + sd := models.StackDetails{ StackName: stackName, Status: *details.StackStatus, CFNConsoleLink: (CFNConsoleBaseURL + stackName), @@ -159,7 +169,7 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) { nextToken := listStackOutput.NextToken for nextToken != nil { // sending next token for pagination - input = cloudformation.ListStacksInput{NextToken: nextToken, StackStatusFilter: ActiveStatuses} + input = cloudformation.ListStacksInput{NextToken: nextToken, StackStatusFilter: models.ActiveStatuses} listStackOutput, err = cfn.ListStacks(&input) if err != nil { break @@ -168,7 +178,7 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) { // select stacks of our concern stackName := *details.StackName if dm.RegexMatch(stackName) { - sd := StackDetails{ + sd := models.StackDetails{ StackName: stackName, Status: *details.StackStatus, CFNConsoleLink: (CFNConsoleBaseURL + stackName), @@ -185,7 +195,11 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) { return envStacks, err } -// { "stack-1-name": ["export-1", "export-2"], "stack-2-name": []} +// ListEnvironmentExports finds all exported values for our matching stacks in this format: +// { +// "stack-1-name": ["export-1", "export-2"], +// "stack-2-name": [] +// } func (dm CFNManager) ListEnvironmentExports() (map[string][]string, error) { exports := map[string][]string{} @@ -232,12 +246,15 @@ func (dm CFNManager) ListEnvironmentExports() (map[string][]string, error) { return exports, err } +// RegexMatch matches stack name with the supplied regex so that we can filter desired stacks for deletion. func (dm CFNManager) RegexMatch(stackName string) bool { match, _ := regexp.MatchString(dm.StackPattern, stackName) return match } -// assumes staging nuke role +// Session creates a new aws cloudformation session. +// By default it uses given aws profile and region but it also provides option to assume a different role. +// It also has validation for target account id to ensure we are deleting in the correct aws account. func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { sess := session.Must(session.NewSessionWithOptions(session.Options{ Config: aws.Config{Region: aws.String(dm.AWSRegion)}, @@ -265,14 +282,15 @@ func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { if dm.NukeRoleARN == "" { // this means, we are using given aws profile return cloudformation.New(sess), nil - } else { - // Create the credentials from AssumeRoleProvider if nuke role arn is provided - creds := stscreds.NewCredentials(sess, dm.NukeRoleARN) - // Create service client value configured for credentials from assumed role. - return cloudformation.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), nil } + + // Create the credentials from AssumeRoleProvider if nuke role arn is provided + creds := stscreds.NewCredentials(sess, dm.NukeRoleARN) + // Create service client value configured for credentials from assumed role. + return cloudformation.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), nil } +// AWSSessionAccountID fetches account id from current aws session func (dm CFNManager) AWSSessionAccountID(sess *session.Session) (acID string, err error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) diff --git a/utils/deleter.go b/utils/deleter.go index 342c5b1..48f29b7 100644 --- a/utils/deleter.go +++ b/utils/deleter.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package utils provides cli specifics methods for interacting with AWS services package utils import ( @@ -24,34 +26,35 @@ import ( "time" "github.com/gookit/color" - . "github.com/nirdosh17/cfn-teardown/models" + "github.com/nirdosh17/cfn-teardown/models" ) // -------------- configs --------------- const ( - STACK_DELETION_WAIT_TIME_IN_SEC int16 = 30 - MAX_DELETE_RETRY_COUNT int16 = 5 + STACK_DELETION_WAIT_TIME_IN_SEC int16 = 30 // STACK_DELETION_WAIT_TIME_IN_SEC is the time to wait for stacks before peforming status checks after delete requests have been sent. + MAX_DELETE_RETRY_COUNT int16 = 5 // MAX_DELETE_RETRY_COUNT specifies the number of times we should retry deleting a stack before giving up. ) var ( - NUKE_START_TIME = CurrentUTCDateTime() - NUKE_END_TIME = CurrentUTCDateTime() - AWS_SDK_MAX_RETRY int = 5 + NUKE_START_TIME = CurrentUTCDateTime() // NUKE_START_TIME is the start timestamp of teardown. + NUKE_END_TIME = CurrentUTCDateTime() // NUKE_END_TIME is the end timestamp of teardown. + AWS_SDK_MAX_RETRY int = 5 // AWS_SDK_MAX_RETRY is max retry count for AWS SDK. // stats - TOTAL_STACK_COUNT int - DELETED_STACK_COUNT int - ACTIVE_STACK_COUNT int - NUKE_DURATION_IN_HRS float64 + TOTAL_STACK_COUNT int // TOTAL_STACK_COUNT is the number of stacks found to be eligible for deletion/ + DELETED_STACK_COUNT int // DELETED_STACK_COUNT is the number of stacks deleted so far. + ACTIVE_STACK_COUNT int // ACTIVE_STACK_COUNT is the number of stacks yet to be deleted or in the process of being deleted. + NUKE_DURATION_IN_HRS float64 // NUKE_DURATION_IN_HRS is the total run time of teardown until now. ) -// A stack is eligible for deletion when it's exports has not been imported by any other stacks -func InitiateTearDown(config Config) { +// InitiateTearDown scans and deletes cloudformation stacks respecting the dependencies. +// A stack is eligible for deletion when it's exports has not been imported by any other stacks. +func InitiateTearDown(config models.Config) { cfn := CFNManager{StackPattern: config.StackPattern, TargetAccountId: config.TargetAccountId, NukeRoleARN: config.RoleARN, AWSProfile: config.AWSProfile, AWSRegion: config.AWSRegion} s3 := S3Manager{TargetAccountId: config.TargetAccountId, NukeRoleARN: config.RoleARN, AWSProfile: config.AWSProfile, AWSRegion: config.AWSRegion} notifier := NotificationManager{StackPattern: config.StackPattern, SlackWebHookURL: config.SlackWebhookURL, DryRun: config.DryRun} - var dependencyTree = map[string]StackDetails{} + var dependencyTree = map[string]models.StackDetails{} // generate dependencies for matching stacks dt, err := prepareDependencyTree(config.StackPattern, cfn) @@ -78,7 +81,7 @@ func InitiateTearDown(config Config) { fmt.Println() fmt.Printf("Following stacks are eligible for deletion | Stack count: %v\n", ACTIVE_STACK_COUNT) - for stackName, _ := range dependencyTree { + for stackName := range dependencyTree { color.Gray.Println(" -", stackName) } color.Style{color.Yellow, color.OpItalic}.Println("\nCheck 'stack_teardown_details.json' file for more details.") @@ -126,7 +129,7 @@ func InitiateTearDown(config Config) { color.Error.Println(msg) os.Exit(1) } - stack.Status = DELETE_IN_PROGRESS + stack.Status = models.DELETE_IN_PROGRESS stack.DeleteStartedAt = CurrentUTCDateTime() stack.DeleteAttempt = stack.DeleteAttempt + 1 dependencyTree[sName] = stack @@ -169,15 +172,15 @@ func InitiateTearDown(config Config) { var newStatus string // does not exist means the stack was deleted if dne { - newStatus = DELETE_COMPLETE + newStatus = models.DELETE_COMPLETE } else { newStatus = *details.StackStatus } - if newStatus == DELETE_IN_PROGRESS { + if newStatus == models.DELETE_IN_PROGRESS { // skip now. check again later continue - } else if newStatus == DELETE_COMPLETE { + } else if newStatus == models.DELETE_COMPLETE { // update local copy stack.Status = newStatus stack.DeleteCompletedAt = CurrentUTCDateTime() @@ -218,7 +221,7 @@ func InitiateTearDown(config Config) { color.Error.Println(msg) os.Exit(1) } - stack.Status = DELETE_IN_PROGRESS + stack.Status = models.DELETE_IN_PROGRESS stack.DeleteStartedAt = CurrentUTCDateTime() stack.DeleteAttempt = newDeleteAttempt dependencyTree[sName] = stack @@ -248,9 +251,9 @@ func InitiateTearDown(config Config) { } } -// when a stack is deleted, we can safely remove it from list of importers -// so that the parent stack is free of dependencies and becomes eligible for deletion in the next cycle -func updateImporterList(deletedStackName string, dt map[string]StackDetails) map[string]StackDetails { +// When a stack is deleted, we can safely remove it from list of importers +// so that the parent stack is free of dependencies and becomes eligible for deletion in the next cycle. +func updateImporterList(deletedStackName string, dt map[string]models.StackDetails) map[string]models.StackDetails { for _, stackDetails := range dt { importers := stackDetails.ActiveImporterStacks delete(importers, deletedStackName) @@ -259,6 +262,7 @@ func updateImporterList(deletedStackName string, dt map[string]StackDetails) map return dt } +// In order to a stack with S3 bucket, we need to empty it first which is done by this method. func deleteBucketIfPresent(stackName string, cfn CFNManager, s3 S3Manager) error { resources, _ := cfn.ListStackResources(stackName) @@ -283,20 +287,21 @@ func deleteBucketIfPresent(stackName string, cfn CFNManager, s3 S3Manager) error return objDeleteError } -func isNukeStuck(dt map[string]StackDetails) bool { +// In some cases, there could be no stacks which are eligible for deletion. This can happen due to cyclic dependency. In such case, we abort nuke and notify the user for manual intervention. +func isNukeStuck(dt map[string]models.StackDetails) bool { if len(deleteInProgressStacks(dt)) == 0 && len(stacksEligibleToDelete(dt)) == 0 { return true - } else { - return false } + return false } -func stacksEligibleToDelete(dt map[string]StackDetails) []string { +// stacksEligibleToDelete selects stacks for deletion which have no dependencies +func stacksEligibleToDelete(dt map[string]models.StackDetails) []string { deleteReady := []string{} for stackName, stackDetails := range dt { if len(stackDetails.ActiveImporterStacks) == 0 { // not filtering out delete failed here as it is being handled in main.go - if stackDetails.Status != DELETE_COMPLETE && stackDetails.Status != DELETE_IN_PROGRESS { + if stackDetails.Status != models.DELETE_COMPLETE && stackDetails.Status != models.DELETE_IN_PROGRESS { deleteReady = append(deleteReady, stackName) } } @@ -304,21 +309,21 @@ func stacksEligibleToDelete(dt map[string]StackDetails) []string { return deleteReady } -func deleteInProgressStacks(dt map[string]StackDetails) []string { +func deleteInProgressStacks(dt map[string]models.StackDetails) []string { dip := []string{} for stackName, stackDetails := range dt { - if stackDetails.Status == DELETE_IN_PROGRESS { + if stackDetails.Status == models.DELETE_IN_PROGRESS { dip = append(dip, stackName) } } return dip } -// all stacks have status DELETE_COMPLETE -func isEnvNuked(dt map[string]StackDetails) bool { +// isEnvNuked checks if all stacks have status DELETE_COMPLETE to mark the end of teardown +func isEnvNuked(dt map[string]models.StackDetails) bool { nuked := true for _, stackDetails := range dt { - if stackDetails.Status != DELETE_COMPLETE { + if stackDetails.Status != models.DELETE_COMPLETE { nuked = false break } @@ -326,7 +331,8 @@ func isEnvNuked(dt map[string]StackDetails) bool { return nuked } -func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDetails, error) { +// prepareDependencyTree generates list of stacks and their dependencies which is useful to determine the order of deletion +func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]models.StackDetails, error) { CFNConsoleBaseURL := "https://console.aws.amazon.com/cloudformation/home?region=" + cfn.AWSRegion + "#/stacks/stackinfo?stackId=" fmt.Printf("-------------- Listing Stacks | Match Pattern: [%v] --------------\n", color.Gray.Render(envLabel)) @@ -391,7 +397,7 @@ func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDet color.Error.Printf(" Error describing stack %v", mStk) break // real error. } - dependencyTree[mStk] = StackDetails{ + dependencyTree[mStk] = models.StackDetails{ StackName: mStk, Status: "DELETE_COMPLETE", CFNConsoleLink: (CFNConsoleBaseURL + mStk), @@ -410,7 +416,7 @@ func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDet break } - dependencyTree[mStk] = StackDetails{ + dependencyTree[mStk] = models.StackDetails{ StackName: mStk, Status: *sDetails.StackStatus, Exports: exports, @@ -427,7 +433,7 @@ func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDet // --------------------- Utility functions --------------------------- -func getStackWithMissingDependencies(dt map[string]StackDetails) map[string]struct{} { +func getStackWithMissingDependencies(dt map[string]models.StackDetails) map[string]struct{} { allImporterStacks := map[string]struct{}{} notListed := map[string]struct{}{} for _, details := range dt { @@ -447,15 +453,17 @@ func getStackWithMissingDependencies(dt map[string]StackDetails) map[string]stru return notListed } -func writeToJSON(envLabel string, data map[string]StackDetails) { +func writeToJSON(envLabel string, data map[string]models.StackDetails) { file, _ := json.MarshalIndent(data, "", " ") _ = ioutil.WriteFile("stack_teardown_details.json", file, 0644) } +// CurrentUTCDateTime returns current time in ISO string func CurrentUTCDateTime() string { return time.Now().UTC().Format("2006-01-02T15:04:05Z") } +// TimeDiff returns difference of two timestamps in minutes func TimeDiff(startTime, endTime string) string { st, _ := time.Parse(time.RFC3339, startTime) et, _ := time.Parse(time.RFC3339, endTime) @@ -463,8 +471,8 @@ func TimeDiff(startTime, endTime string) string { return fmt.Sprintf("%.2f", diff.Minutes()) } -// updating global vars used for alerts -func UpdateNukeStats(dt map[string]StackDetails) { +// UpdateNukeStats updates global variables used for capturing teardown stats +func UpdateNukeStats(dt map[string]models.StackDetails) { NUKE_END_TIME = CurrentUTCDateTime() st, _ := time.Parse(time.RFC3339, NUKE_START_TIME) et, _ := time.Parse(time.RFC3339, NUKE_END_TIME) @@ -472,7 +480,7 @@ func UpdateNukeStats(dt map[string]StackDetails) { deletedStackCount := 0 for _, stackDetails := range dt { - if stackDetails.Status == DELETE_COMPLETE { + if stackDetails.Status == models.DELETE_COMPLETE { deletedStackCount++ } } diff --git a/utils/notifier.go b/utils/notifier.go index 06ddde2..1e785d9 100644 --- a/utils/notifier.go +++ b/utils/notifier.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package utils provides cli specifics methods for interacting with AWS services package utils import ( @@ -25,12 +27,14 @@ import ( "github.com/nirdosh17/cfn-teardown/models" ) +// NotificationManager exposes methods for sending alerts to slack channel. type NotificationManager struct { StackPattern string DryRun string SlackWebHookURL string // Webhook url is specific to channel } +// AlertMessage is the structure of a alert event which is translated to slack message later. type AlertMessage struct { Message string // Long message with details about the event Event string // Start | Complete | Error @@ -38,13 +42,16 @@ type AlertMessage struct { Attachment map[string]interface{} } -// building slack messages: https://app.slack.com/block-kit-builder +// SlackMessage is the structure accepted by Slack post message api. +// More info: https://app.slack.com/block-kit-builder type SlackMessage struct { Attachments []map[string]interface{} `json:"attachments"` } +// ColorMapping is the mapping of slack message color based on teardown event types 'Start', 'Complete', 'Error' var ColorMapping map[string]string = map[string]string{"Start": "#f0e62e", "Complete": "#25db2e", "Error": "#e81e1e"} +// StartAlert prepares slack message for teardown start event func (nm NotificationManager) StartAlert(am AlertMessage) { am.Event = "Start" am.Attachment = map[string]interface{}{ @@ -87,6 +94,7 @@ func (nm NotificationManager) StartAlert(am AlertMessage) { nm.Alert(am) } +// ErrorAlert prepares slack message for stack deletion error func (nm NotificationManager) ErrorAlert(am AlertMessage) { am.Event = "Error" am.Attachment = map[string]interface{}{ @@ -151,6 +159,7 @@ func (nm NotificationManager) ErrorAlert(am AlertMessage) { nm.Alert(am) } +// StuckAlert prepares slack message when stack teardown is stuck func (nm NotificationManager) StuckAlert(am AlertMessage) { am.Event = "Error" am.Attachment = map[string]interface{}{ @@ -202,6 +211,7 @@ func (nm NotificationManager) StuckAlert(am AlertMessage) { nm.Alert(am) } +// SuccessAlert prepares slack message for successful completion of stack teardown func (nm NotificationManager) SuccessAlert(am AlertMessage) { am.Event = "Complete" am.Attachment = map[string]interface{}{ @@ -248,6 +258,7 @@ func (nm NotificationManager) SuccessAlert(am AlertMessage) { nm.Alert(am) } +// GenericAlert prepares slack message for a generic message func (nm NotificationManager) GenericAlert(am AlertMessage) { am.Event = "Error" am.Attachment = map[string]interface{}{ @@ -274,6 +285,8 @@ func (nm NotificationManager) GenericAlert(am AlertMessage) { nm.Alert(am) } +// Alert posts message to Slack channel using webhook +// Only posts the message if it's not a dry run and webhook url is present func (nm NotificationManager) Alert(am AlertMessage) error { if nm.DryRun != "false" { return nil diff --git a/utils/s3.go b/utils/s3.go index ac469cf..296547f 100644 --- a/utils/s3.go +++ b/utils/s3.go @@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +// Package utils provides cli specifics methods for interacting with AWS services package utils import ( @@ -26,6 +28,7 @@ import ( "github.com/aws/aws-sdk-go/service/sts" ) +// S3Manager exposes methods to interact with AWS S3 service via SDK. type S3Manager struct { TargetAccountId string NukeRoleARN string @@ -33,6 +36,7 @@ type S3Manager struct { AWSRegion string } +// EmptyBucket deletes all objects from a particular S3 bucket. func (sm S3Manager) EmptyBucket(bucketName string) error { svc, err := sm.Session() if err != nil { @@ -66,7 +70,9 @@ func (sm S3Manager) EmptyBucket(bucketName string) error { return nil } -// assumes staging nuke role +// Session creates a new aws S3 session. +// By default, it uses given aws profile and region but it also provides option to assume a different role. +// It also has validation for target account id to ensure we are deleting in the correct aws account. func (sm S3Manager) Session() (*s3.S3, error) { sess := session.Must(session.NewSessionWithOptions(session.Options{ Config: aws.Config{Region: aws.String(sm.AWSRegion)}, @@ -94,14 +100,15 @@ func (sm S3Manager) Session() (*s3.S3, error) { if sm.NukeRoleARN == "" { // this means, we are using given aws profile return s3.New(sess), nil - } else { - // Create the credentials from AssumeRoleProvider if nuke role arn is provided - creds := stscreds.NewCredentials(sess, sm.NukeRoleARN) - // Create service client value configured for credentials from assumed role - return s3.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), nil } + + // Create the credentials from AssumeRoleProvider if nuke role arn is provided + creds := stscreds.NewCredentials(sess, sm.NukeRoleARN) + // Create service client value configured for credentials from assumed role + return s3.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), nil } +// AWSSessionAccountID fetches account id from current aws session func (sm S3Manager) AWSSessionAccountID(sess *session.Session) (acID string, err error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{})