From 726d0064c6769f0bae17ed52ef292c4521d264c4 Mon Sep 17 00:00:00 2001 From: Nirdosh Date: Sun, 15 Aug 2021 21:36:31 +0545 Subject: [PATCH 1/6] Initial working setup. --- .gitignore | 2 + LICENSE | 223 ++++++++++-- Makefile | 6 + README.md | 181 +++++++++- cmd/deleteStacks.go | 78 +++++ cmd/listDependencies.go | 63 ++++ cmd/root.go | 151 ++++++++ go.mod | 11 + go.sum | 754 ++++++++++++++++++++++++++++++++++++++++ main.go | 22 ++ models/cfn.go | 79 +++++ models/nuke.go | 30 ++ utils/cloudformation.go | 277 +++++++++++++++ utils/deleter.go | 481 +++++++++++++++++++++++++ utils/notifier.go | 304 ++++++++++++++++ utils/s3.go | 105 ++++++ 16 files changed, 2744 insertions(+), 23 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/deleteStacks.go create mode 100644 cmd/listDependencies.go create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 models/cfn.go create mode 100644 models/nuke.go create mode 100644 utils/cloudformation.go create mode 100644 utils/deleter.go create mode 100644 utils/notifier.go create mode 100644 utils/s3.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6436c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +stack_teardown_details.json +cfn-teardown diff --git a/LICENSE b/LICENSE index 310cbb3..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -MIT License - -Copyright (c) 2021 Nirdosh Gautam - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28a4b42 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +build: + go mod download && \ + go build -o cfn-teardown . + +run: build + ./cfn-teardown diff --git a/README.md b/README.md index b6f7c44..b8ef1b3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,179 @@ -# cfn-teardown -Tool to teardown CloudFormation stacks by preparing a dependency tree +# CFN Teardown + +CFN Teardown is a tool to delete CloudFormation stacks respecting the stack dependencies. + +If you deploy all of you intrastructure using CloudFormation with a `consistent naming convention` for stacks, then you can use this tool to tear down the environment. + +Teardown of huge number of stacks using this tool is considerably faster that applying brute force. + +**Example of consistent stack naming:** + +- qa-bucket-users +- qa-service-user-management +- qa-service-user-search + +You can supply stack pattern as `qa-` in this tool to delete these stacks. + + +## Features + +- Matches stack pattern and builds dependency tree for intelligent/faster teardown. + +- Stack dependencies are respected during deletion. No brute force strategy. + +- Multiple safety checks to prevent accidental deletion. + +- Generates a file `stack_teardown_details` listing stack dependencies which can be watched live to get an idea of how the script is working. It contains useful details like time taken to delete each stacks, delete attempts, failure reason and many more. + +- Supports slack notification via webhook. + + +## Install + +```bash + +go get github.com/nirdosh17/cfn-teardown + +``` + +**OR** download binary from [HERE](https://github.com/nirdosh17/cfn-teardown/releases) + + + +## Using CFN Teardown + +Required global flags for all commands: `stackPattern`, `awsRegion`, `awsProfile` + +1. Run `cfn-teardown -h` and see available commands and needed parameters. + +2. Listing stack dependencies: `cfn-teardown listDependencies` + + _Generates dependencies in `stack_teardown_details.json` file (printed in terminal as well)_ + +2. Tear down stacks: `cfn-teardown deleteStacks` + + _Deletes matching stacks and updates status in the teardown details file._ + + + +## Configuration + +Configuration for this command can be set in three different ways in the precedence order defined below: +1. Environment variables(same as flag name) +2. Flags e.g. `cfn-teardown deleteStacks --stackPattern=qaenv-` +3. Supplied YAML Config file (default: ~/.cfn-teardown.yaml) +
+ Minimal config file + + ```yaml + awsRegion: us-east-1 + awsProfile: staging + stackPattern: qa- + ``` +
+
+ All configs present + + ```yaml + awsRegion: us-east-1 + awsProfile: staging + awsAccountId: 121212121212 + stackPattern: qa- + abortWaitTimeMinutes: 20 + stackWaitTimeSeconds: 30 + maxDeleteRetryCount: 5 + notificationWebhookURL: https://hooks.slack.com/services/dummy/dummy/long_hash + roleARN: + dryRun: false + ``` +
+ +See Available configurations via: + +```bash +cfn-teardown --help +cfn-teardown listDependencies --help +cfn-teardown deleteStacks --help +``` + +## How it works? + +1. Scans all stacks in your account. + +2. Prepares of list of stack with their dependencies. + +
+ It looks something like this: + + ```json + { + "staging-bucket-archived-items": { + "StackName": "staging-bucket-archived-items", + "Status": "CREATE_COMPLETE", + "StackStatusReason": "", + "DeleteStartedAt": "2021-02-07T03:35:43Z", + "DeleteCompletedAt": "", + "DeletionTimeInMinutes": "", + "DeleteAttempt": 0, + "Exports": [ + "staging:ItemsArchiveBucket", + "staging:ItemsArchiveBucketArn" + ], + "ActiveImporterStacks": { + "staging-products-service": {} + }, + "CFNConsoleLink": "https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/stackinfo?stackId=staging-bucket-archived-items" + }, + "staging-products-service": { + "StackName": "staging-products-service", + "Status": "CREATE_COMPLETE", + "StackStatusReason": "", + "DeleteStartedAt": "2021-02-07T03:30:54Z", + "DeleteCompletedAt": "", + "DeletionTimeInMinutes": "", + "DeleteAttempt": 0, + "Exports": [ + "staging:ProductsServiceEndpoint" + ], + "ActiveImporterStacks": {}, + "CFNConsoleLink": "https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/stackinfo?stackId=staging-products-service" + } + } + ``` +
+ +3. Alerts slack channel(if provided) and waits for the specified time before initiating deletion. If wait time is not provided, it starts deleting stacks immediately. + +4. Finds stacks which are eligible for deletion. Eligibility criteria is that the stack shouldn't have it's exports imported by any other stacks. In simple terms, it should have no dependencies. + +5. Initiates delete requests concurrently for eligible stacks. + +6. Waits for 30 seconds(can be configurable) before scanning eligible stacks again. Checks If the stack has been already deleted and if deleted updates stack stack in the dependency tree. + + + +## Assume Role + +By default it tries to use the IAM role of environment it is being run. e.g. Codebuild, EC2 instance. We can also supply role arn if we want the script to assume a different role. + + + +## Safety Checks for Accidental Deletion + +- `dryRun` flag must be explicitely set to `false` to activate delete functionality + +- `abortWaitTimeMinutes` flag lets us to decide how much to wait before initiating delete as you might want to confirm the stacks that are about to get deleted + +- `awsAccountId` flag will check the supplied account id with aws session account id during runtime to confirm that we are deleting stacks in the desired non production account + + +## Edge Case +If a stack can't be deleted from the AWS Console itself due to some dependencies, then it won't be deleted by this tool as well. In such case, manual intervention is required which is notified by this tool. + + +## Caution :warning: +_With great power, comes great responsibility_ + +- Use this tool with great caution. **Don't ever** run this in production environment with the intention of deleting a subset of stacks. +- First try within small number of test stacks in dry run mode. +- Use redundant safety flags `dryRun`, `awsAccountId` and `abortWaitTimeMinutes`. diff --git a/cmd/deleteStacks.go b/cmd/deleteStacks.go new file mode 100644 index 0000000..67be472 --- /dev/null +++ b/cmd/deleteStacks.go @@ -0,0 +1,78 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "fmt" + + "github.com/nirdosh17/cfn-teardown/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// deleteStacksCmd represents the deleteStacks command +var deleteStacksCmd = &cobra.Command{ + Use: "deleteStacks", + Short: "Deletes matching cloudformation stacks", + Long: `Deletes cloudformation stacks and their dependencies whose names match with the given pattern. +The pattern must be provided otherwise it will scan all stacks in the region. +Example: +If your stacks to be deleted follow this naming convention: qa-{{component name}} +Supply stack pattern as: 'qa-' + `, + Example: "cfn-teardown deleteStacks --stackPattern='qa-' --awsProfile='staging' --region=us-east-1", + Args: func(cmd *cobra.Command, args []string) error { + // validate your arguments here + return validateConfigs(config) + }, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Executing command: deleteStacks") + fmt.Println() + utils.InitiateTearDown(config) + }, +} + +func init() { + rootCmd.AddCommand(deleteStacksCmd) + + deleteStacksCmd.Flags().Int("stackWaitTimeSeconds", 30, "Seconds to wait after delete requests are submitted to CFN") + viper.BindPFlag("stackWaitTimeSeconds", deleteStacksCmd.Flags().Lookup("stackWaitTimeSeconds")) + + deleteStacksCmd.Flags().String("awsAccountId", "", "[Safety Check] Validates against account id in current aws session and provided ID") + viper.BindPFlag("awsAccountId", deleteStacksCmd.Flags().Lookup("awsAccountId")) + + deleteStacksCmd.Flags().Int("maxDeleteRetryCount", 5, "Max stack delete attempts") + viper.BindPFlag("maxDeleteRetryCount", deleteStacksCmd.Flags().Lookup("maxDeleteRetryCount")) + + deleteStacksCmd.Flags().Int("abortWaitTimeMinutes", 10, "[Safety Check] Minutes to wait before initiating deletion") + viper.BindPFlag("abortWaitTimeMinutes", deleteStacksCmd.Flags().Lookup("abortWaitTimeMinutes")) + + deleteStacksCmd.Flags().String("notificationWebhookURL", "", "Send status alerts to Slack channel") + viper.BindPFlag("notificationWebhookURL", deleteStacksCmd.Flags().Lookup("notificationWebhookURL")) + + deleteStacksCmd.Flags().String("dryRun", "true", "[Safety Check] To delete stacks, it needs to be explicitely set to false") + viper.BindPFlag("dryRun", deleteStacksCmd.Flags().Lookup("dryRun")) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // deleteStacksCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // deleteStacksCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/listDependencies.go b/cmd/listDependencies.go new file mode 100644 index 0000000..9d7970b --- /dev/null +++ b/cmd/listDependencies.go @@ -0,0 +1,63 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "fmt" + + "github.com/nirdosh17/cfn-teardown/utils" + "github.com/spf13/cobra" +) + +// listDependenciesCmd represents the listDependencies command +var listDependenciesCmd = &cobra.Command{ + Use: "listDependencies", + Short: "Scan stacks and list stacks and their dependencies", + Long: `Scan stacks and list stacks and their dependencies. +A stack pattern must be provided otherwise it will scan all stacks in the region. +Example: +If your stacks to be deleted follow this naming convention: qa-{{component name}} +Supply stack pattern as: 'qa-' + `, + Example: "cfn-teardown listDependencies --stackPattern='qa-' --awsProfile='staging' --region=us-east-1", + + Args: func(cmd *cobra.Command, args []string) error { + // validate your arguments here + return validateConfigs(config) + }, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Executing command: listDependencies") + fmt.Println() + + // for safety + config.DryRun = "true" + + utils.InitiateTearDown(config) + }, +} + +func init() { + rootCmd.AddCommand(listDependenciesCmd) + + // Here you will define your flags and configuration settings. + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // listDependenciesCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // listDependenciesCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..f37f336 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,151 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "errors" + "fmt" + "log" + "os" + "strings" + + "github.com/spf13/cobra" + + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" + + "github.com/nirdosh17/cfn-teardown/models" +) + +// config vars +var ( + cfgFile string +) + +var config models.Config + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "cfn-teardown", + Short: "Delete cloudformation stacks with matching names", + Long: `Finds and deletes stacks whose name matches with the given pattern. + + First, It prepares the list of stacks and their dependencies. + Then, It recursively searches for importer stacks until stacks in leaf node has no importer stacks. + Finally, the stack in the leaf nodes are deleted concurrently. + `, + Args: func(cmd *cobra.Command, args []string) error { + // validate your arguments here + return nil + }, + // Uncomment the following line if your bare application + // has an action associated with it: + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + cmd.Help() + os.Exit(0) + } + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func validateConfigs(config models.Config) (err error) { + emptyFlags := []string{} + + if config.StackPattern == "" { + emptyFlags = append(emptyFlags, "stackPattern") + } + + if config.AWSProfile == "" { + emptyFlags = append(emptyFlags, "awsProfile") + } + + if config.AWSRegion == "" { + emptyFlags = append(emptyFlags, "awsRegion") + } + + if len(emptyFlags) > 0 { + err = errors.New("required flag(s) " + strings.Join(emptyFlags, ", ") + " not set") + } + + return +} + +func init() { + cobra.OnInitialize(initConfig) + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + rootCmd.PersistentFlags().String("stackPattern", "", "Pattern to match stack name e.g. 'staging-'") + viper.BindPFlag("stackPattern", rootCmd.PersistentFlags().Lookup("stackPattern")) + + rootCmd.PersistentFlags().String("awsRegion", "", "AWS Region where the stacks are present") + viper.BindPFlag("awsRegion", rootCmd.PersistentFlags().Lookup("awsRegion")) + + rootCmd.PersistentFlags().String("awsProfile", "", "AWS Profile") + viper.BindPFlag("awsProfile", rootCmd.PersistentFlags().Lookup("awsProfile")) + + rootCmd.PersistentFlags().String("roleARN", "", "Assume this role to scan and delete stacks if provided") + viper.BindPFlag("roleARN", rootCmd.PersistentFlags().Lookup("roleARN")) + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cfn-teardown.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // Search config in home directory with name ".cfn-teardown.yaml" + viper.AddConfigPath(home) + viper.SetConfigName(".cfn-teardown.yaml") + viper.SetConfigType("yaml") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file: ", viper.ConfigFileUsed()) + err = viper.Unmarshal(&config) + if err != nil { + log.Fatalf("Error parsing config, %s", err) + } + } else { + log.Fatalf("Error reading config file, %s", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3ecc5ef --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/nirdosh17/cfn-teardown + +go 1.16 + +require ( + github.com/aws/aws-sdk-go v1.40.22 + github.com/briandowns/spinner v1.16.0 + github.com/mitchellh/go-homedir v1.1.0 + github.com/spf13/cobra v0.0.5 + github.com/spf13/viper v1.8.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..be55514 --- /dev/null +++ b/go.sum @@ -0,0 +1,754 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0 h1:PQcPefKFdaIzjQFbiyOgAqyx8q5djaE7x9Sqe712DPA= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0 h1:9x7Bx0A9R5/M9jibeJeZWqjeVEIxYW9fZYqB9a70/bY= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e h1:QEF07wC0T1rKkctt1RINW/+RMTVmiwxETico2l3gxJA= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 h1:G1bPvciwNyF7IUmKXNt9Ak3m6u9DE1rF+RmtIkBpVdA= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.40.22 h1:iit4tJ1hjL2GlNCrbE4aJza6jTmvEE2pDTnShct/yyY= +github.com/aws/aws-sdk-go v1.40.22/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs= +github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMux2sDi4oo5YOo= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d h1:QyzYnTnPE15SQyUeqU6qLbWxMkwyAyu+vGksa0b7j00= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5 h1:zIaiqGYDQwa4HVx5wGRTXbx38Pqxjemn4BP98wpzpXo= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1 h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0 h1:WhIgCr5a7AaVH6jPUwjtRuuE7/RDufnUvzIr48smyxs= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3 h1:EmmoJme1matNzb+hMpDuR/0sbJSUisxyqBGG676r31M= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +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= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0 h1:lfGJxY7ToLJQjHHwi0EX6uYBdK78egf954SQl13PQJc= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +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/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f h1:UFr9zpz4xgTnIE5yIMtWAMngCdZ9p/+q6lTbgelo80M= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/etcd/api/v3 v3.5.0 h1:GsV3S+OfZEOCNXdtNkBSR7kgLobAa/SO6tCxRa0GAYw= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0 h1:2aQv6F436YnN7I4VbI8PPYrBhu+SmrTaADcf8Mi/6PU= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0 h1:URs6qR1lAxDsqWITsQXI4ZkGiYJ5dHtRNiCpfs2OeKA= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d6853a4 --- /dev/null +++ b/main.go @@ -0,0 +1,22 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 main + +import "github.com/nirdosh17/cfn-teardown/cmd" + +func main() { + cmd.Execute() +} diff --git a/models/cfn.go b/models/cfn.go new file mode 100644 index 0000000..d31a0e8 --- /dev/null +++ b/models/cfn.go @@ -0,0 +1,79 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +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 + DeleteAttempt int16 + Exports []string + ActiveImporterStacks map[string]struct{} // active(not deleted) stacks which are importing exports from THIS stack + CFNConsoleLink string +} + +// 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 +var ActiveStatuses = []*string{ + &CREATE_IN_PROGRESS, + &CREATE_FAILED, + &CREATE_COMPLETE, + &ROLLBACK_IN_PROGRESS, + &ROLLBACK_FAILED, + &ROLLBACK_COMPLETE, + &DELETE_IN_PROGRESS, + &DELETE_FAILED, + &UPDATE_IN_PROGRESS, + &UPDATE_COMPLETE_CLEANUP_IN_PROGRESS, + &UPDATE_COMPLETE, + &UPDATE_ROLLBACK_IN_PROGRESS, + &UPDATE_ROLLBACK_FAILED, + &UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS, + &UPDATE_ROLLBACK_COMPLETE, + &REVIEW_IN_PROGRESS, + &IMPORT_IN_PROGRESS, + &IMPORT_COMPLETE, + &IMPORT_ROLLBACK_IN_PROGRESS, + &IMPORT_ROLLBACK_FAILED, + &IMPORT_ROLLBACK_COMPLETE, +} diff --git a/models/nuke.go b/models/nuke.go new file mode 100644 index 0000000..b1ddc28 --- /dev/null +++ b/models/nuke.go @@ -0,0 +1,30 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +// Config represents all the parameters supported by cfn-teardown +type Config struct { + AWSProfile string `mapstructure:"awsProfile"` + AWSRegion string `mapstructure:"awsRegion"` + AWSAccountId string `mapstructure:"awsAccountId"` + StackPattern string `mapstructure:"stackPattern"` + StackWaitTimeSeconds int16 `mapstructure:"stackWaitTimeSeconds"` + MaxDeleteRetryCount int16 `mapstructure:"maxDeleteRetryCount"` + AbortWaitTimeMinutes int16 `mapstructure:"abortWaitTimeMinutes"` + NotificationWebhookURL string `mapstructure:"notificationWebhookURL"` + RoleARN string `mapstructure:"roleARN"` + DryRun string `mapstructure:"dryRun"` +} diff --git a/utils/cloudformation.go b/utils/cloudformation.go new file mode 100644 index 0000000..67588f4 --- /dev/null +++ b/utils/cloudformation.go @@ -0,0 +1,277 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "log" + "os" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/aws/aws-sdk-go/service/sts" + + . "github.com/nirdosh17/cfn-teardown/models" +) + +type CFNManager struct { + ExpectedAccountID string + NukeRoleARN string + StackPattern string + AWSRegion string +} + +func (dm CFNManager) DescribeStack(stackName string) (*cloudformation.Stack, error) { + cfn, err := dm.Session() + if err != nil { + return nil, err + } + + resp, err := cfn.DescribeStacks(&cloudformation.DescribeStacksInput{StackName: &stackName}) + if err != nil { + return nil, err + } + return resp.Stacks[0], err +} + +func (dm CFNManager) ListStackResources(stackName string) ([]*cloudformation.StackResourceSummary, error) { + cfn, err := dm.Session() + if err != nil { + return nil, err + } + + allResources := []*cloudformation.StackResourceSummary{} + resp, err := cfn.ListStackResources(&cloudformation.ListStackResourcesInput{StackName: &stackName}) + if err != nil { + return nil, err + } + allResources = append(allResources, resp.StackResourceSummaries...) + + nextToken := resp.NextToken + for nextToken != nil { + // sending next token for pagination + resp, err := cfn.ListStackResources(&cloudformation.ListStackResourcesInput{StackName: &stackName, NextToken: nextToken}) + if err != nil { + break + } + allResources = append(allResources, resp.StackResourceSummaries...) + nextToken = resp.NextToken + } + + if err != nil { + log.Printf("Error listing resources of stack '%v': %v\n", stackName, err) + } + + return resp.StackResourceSummaries, err +} + +func (dm CFNManager) ListImports(exportNames []string) (map[string]struct{}, error) { + importers := make(map[string]struct{}) + var err error + cfn, err := dm.Session() + if err != nil { + return importers, err + } + + for _, export := range exportNames { + resp, err := cfn.ListImports(&cloudformation.ListImportsInput{ExportName: &export}) + if err != nil { + // no imports = eligible for deletion + if !strings.Contains(err.Error(), "is not imported by any stack") { + return importers, err + } + } + for _, stackName := range resp.Imports { + // using map for faster access and empty struct due to its null memory consumption + importers[*stackName] = struct{}{} + } + } + + 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 +func (dm CFNManager) DeleteStack(stackName string) error { + log.Printf("Submitting delete request for stack: %v\n", stackName) + cfn, err := dm.Session() + if err != nil { + return err + } + input := cloudformation.DeleteStackInput{StackName: &stackName} + // stack delete output is an empty struct + _, err = cfn.DeleteStack(&input) + return err +} + +func (dm CFNManager) ListEnvironmentStacks() (map[string]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{} + + cfn, err := dm.Session() + if err != nil { + return envStacks, err + } + + input := cloudformation.ListStacksInput{StackStatusFilter: ActiveStatuses} + // only returns first 100 stacks. Need to use NextToken + listStackOutput, err := cfn.ListStacks(&input) + if err != nil { + return envStacks, err + } + + for _, details := range listStackOutput.StackSummaries { + // select stacks of our concern + stackName := *details.StackName + if dm.RegexMatch(stackName) { + sd := StackDetails{ + StackName: stackName, + Status: *details.StackStatus, + CFNConsoleLink: (CFNConsoleBaseURL + stackName), + } + envStacks[stackName] = sd + } + } + + if err != nil { + log.Printf("Failed listing stacks with pattern: '%v', Error: '%v'\n", dm.StackPattern, err) + return envStacks, err + } + + nextToken := listStackOutput.NextToken + for nextToken != nil { + // sending next token for pagination + input = cloudformation.ListStacksInput{NextToken: nextToken, StackStatusFilter: ActiveStatuses} + listStackOutput, err = cfn.ListStacks(&input) + if err != nil { + break + } + for _, details := range listStackOutput.StackSummaries { + // select stacks of our concern + stackName := *details.StackName + if dm.RegexMatch(stackName) { + sd := StackDetails{ + StackName: stackName, + Status: *details.StackStatus, + CFNConsoleLink: (CFNConsoleBaseURL + stackName), + } + envStacks[stackName] = sd + } + } + nextToken = listStackOutput.NextToken + } + + if err != nil { + log.Printf("Error listing '%v' environment stacks: %v\n", dm.StackPattern, err) + } + return envStacks, err +} + +// { "stack-1-name": ["export-1", "export-2"], "stack-2-name": []} +func (dm CFNManager) ListEnvironmentExports() (map[string][]string, error) { + exports := map[string][]string{} + + cfn, err := dm.Session() + if err != nil { + return exports, err + } + + input := cloudformation.ListExportsInput{} + // only returns first 100 stacks. Need to use NextToken + listExportOutput, err := cfn.ListExports(&input) + + for _, details := range listExportOutput.Exports { + stackArn := *details.ExportingStackId + stackName := strings.Split(stackArn, "/")[1] + exportName := *details.Name + exports[stackName] = append(exports[stackName], exportName) + } + + if err != nil { + log.Printf("Error listing '%v' environment stack exports: %v\n", dm.StackPattern, err) + return exports, err + } + + nextToken := listExportOutput.NextToken + + for nextToken != nil { + // sending next token for pagination + input := cloudformation.ListExportsInput{NextToken: nextToken} + listExportOutput, err = cfn.ListExports(&input) + + if err != nil { + break + } + + for _, details := range listExportOutput.Exports { + stackArn := *details.ExportingStackId + stackName := strings.Split(stackArn, "/")[1] + exportName := *details.Name + exports[stackName] = append(exports[stackName], exportName) + } + nextToken = listExportOutput.NextToken + } + return exports, err +} + +func (dm CFNManager) RegexMatch(stackName string) bool { + match, _ := regexp.MatchString(dm.StackPattern, stackName) + return match +} + +// assumes staging nuke role +func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { + sess := session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{Region: aws.String(os.Getenv("AWS_REGION"))}, + SharedConfigState: session.SharedConfigEnable, + Profile: os.Getenv("AWS_PROFILE"), + })) + + isStaging, err := dm.IsDesiredAWSAccount(sess) + if err != nil { + return nil, err + } + + // to make things easy while running this script locally + if isStaging { + return cloudformation.New(sess), nil + } else { + // Create the credentials from AssumeRoleProvider to assume the role referenced by the "NukeRoleARN" ARN. + 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 + } + +} + +func (dm CFNManager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { + svc := sts.New(sess) + result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) + if err != nil { + log.Printf("Error requesting AWS caller identity: %v", err.Error()) + return false, err + } + + if *result.Account == dm.ExpectedAccountID { + return true, err + } + return false, err +} diff --git a/utils/deleter.go b/utils/deleter.go new file mode 100644 index 0000000..78562b1 --- /dev/null +++ b/utils/deleter.go @@ -0,0 +1,481 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "strings" + "time" + + "github.com/briandowns/spinner" + . "github.com/nirdosh17/cfn-teardown/models" +) + +// -------------- configs --------------- +const ( + STACK_DELETION_WAIT_TIME_IN_SEC int16 = 30 + MAX_DELETE_RETRY_COUNT int16 = 5 +) + +var ( + NUKE_START_TIME = CurrentUTCDateTime() + NUKE_END_TIME = CurrentUTCDateTime() + AWS_SDK_MAX_RETRY int = 5 + + // stats + TOTAL_STACK_COUNT int + DELETED_STACK_COUNT int + ACTIVE_STACK_COUNT int + NUKE_DURATION_IN_HRS float64 +) + +// A stack is eligible for deletion when it's exports has not been imported by any other stacks +func InitiateTearDown(config Config) { + loading := spinner.New( + spinner.CharSets[7], + 100*time.Millisecond, + ) + loading.Color("red", "bold") + defer loading.Stop() + + s3 := S3Manager{ExpectedAccountID: config.AWSAccountId, NukeRoleARN: config.RoleARN} + notifier := NotificationManager{StackPattern: config.StackPattern, NotificationWebHookURL: config.NotificationWebhookURL} + cfn := CFNManager{StackPattern: config.StackPattern, ExpectedAccountID: config.AWSAccountId, NukeRoleARN: config.RoleARN, AWSRegion: config.AWSRegion} + + var dependencyTree = map[string]StackDetails{} + + // generate dependencies for matching stacks + loading.Start() + dt, err := prepareDependencyTree(config.StackPattern, cfn) + loading.Stop() + + if err != nil { + UpdateNukeStats(dependencyTree) + msg := fmt.Sprintf("Unable to prepare dependencies. Error: %v", err.Error()) + notifier.ErrorAlert(AlertMessage{Message: msg}) + log.Fatal(msg) + } + dependencyTree = dt // need to do this for global scope + writeToJSON(config.StackPattern, dependencyTree) + + TOTAL_STACK_COUNT = len(dependencyTree) + UpdateNukeStats(dependencyTree) + fmt.Printf("Total Stack Count: '%v'\n", ACTIVE_STACK_COUNT) + + if ACTIVE_STACK_COUNT == 0 { + UpdateNukeStats(dependencyTree) + fmt.Printf("Successfully deleted '%v' stacks!", TOTAL_STACK_COUNT) + notifier.SuccessAlert(AlertMessage{}) + return + } + + // safety check for accidental run + if config.DryRun != "false" { + fmt.Println("\nFollowing stacks are eligible for deletion:") + for stackName, _ := range dependencyTree { + fmt.Println(" -", stackName) + } + fmt.Println("\nCheck 'stack_teardown_details.json' file for more details.") + return + } + + msg := fmt.Sprintf("Waiting for `%v minutes` before initiating deletion...", config.AbortWaitTimeMinutes) + notifier.StartAlert(AlertMessage{Message: msg}) + fmt.Println(msg) + loading.Start() + // TODO FEATURE: Add a countdown timer + time.Sleep(time.Duration(config.AbortWaitTimeMinutes) * time.Minute) + fmt.Println("\n\n------------------------- Deletion Started ----------------------------------") + for { + // Algorithm: + // 1. Scan stacks who has zero importing stacks i.e. last leaf in the dependency tree + toDelete := stacksEligibleToDelete(dependencyTree) + + // 2. Delete stacks + // 2.1 If stack has S3 bucket resource, then delete bucket contents first + // 2.2 Then send request to delete stack + // 2.3 Change stack status to DELETE_IN_PROGRESS + fmt.Println("\n-----------------------------------------------------------------------------") + fmt.Printf("Searching stacks with no importers(dependencies): %v", len(toDelete)) + for _, sName := range toDelete { + stack := dependencyTree[sName] + bktErr := deleteBucketIfPresent(sName, cfn, s3) + if bktErr != nil { + stack.StackStatusReason = bktErr.Error() + UpdateNukeStats(dependencyTree) + msg := fmt.Sprintf("Unable to empty bucket from stack '%v'", sName) + notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) + log.Fatalln(msg) // abort! + } + + err := cfn.DeleteStack(sName) + if err != nil { + UpdateNukeStats(dependencyTree) + msg = fmt.Sprintf("Unable to send delete request for stack '%v' Error: %v", sName, err) + stack.StackStatusReason = msg + notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) + log.Fatalln(msg) + } + stack.Status = DELETE_IN_PROGRESS + stack.DeleteStartedAt = CurrentUTCDateTime() + stack.DeleteAttempt = stack.DeleteAttempt + 1 + dependencyTree[sName] = stack + writeToJSON(config.StackPattern, dependencyTree) + } + + // 3. Wait for 30 seconds + fmt.Println("\n-----------------------------------------------------------------------------") + fmt.Printf("Waiting for %v seconds...", STACK_DELETION_WAIT_TIME_IN_SEC) + time.Sleep(time.Duration(STACK_DELETION_WAIT_TIME_IN_SEC) * time.Second) + + // 4. Get list of stacks in DELETE_IN_PROGRESS and describe stack + // 4.1. If status is still DELETE_IN_PROGRESS, skip + // 4.2. If stack is not found or already deleted + // 4.2.1 Change status to DELETE_COMPLETE + // 4.2.2 Remove stack from importer list + // 4.3. If stack status is not 'DELETE_IN_PROGRESS' or 'DELETE_COMPLETE' + // Mark this as failure. Get stack reason. Alert in the notification channel. Abort env deletion. + dipStacks := deleteInProgressStacks(dependencyTree) + for _, sName := range dipStacks { + stack := dependencyTree[sName] + // fetch latest stack details + details, err := cfn.DescribeStack(sName) + + var dne bool + if err != nil { + // this error means stack has already been deleted + dne = strings.Contains(err.Error(), "does not exist") + // means that the error is related to SDK. in that case we would want to notify error and exit + if !dne { + UpdateNukeStats(dependencyTree) + msg := fmt.Sprintf("Unable to describe stack '%v'", sName) + stack.StackStatusReason = msg + notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) + log.Fatal(msg) + } + } + + var newStatus string + // does not exist means the stack was deleted + if dne { + newStatus = DELETE_COMPLETE + } else { + newStatus = *details.StackStatus + } + + if newStatus == DELETE_IN_PROGRESS { + // skip now. check again later + continue + } else if newStatus == DELETE_COMPLETE { + // update local copy + stack.Status = newStatus + stack.DeleteCompletedAt = CurrentUTCDateTime() + stack.DeletionTimeInMinutes = TimeDiff(stack.DeleteStartedAt, stack.DeleteCompletedAt) + + // updating stack details to dependency tree + dependencyTree[sName] = stack + + // removing this stack from list of importers of all stacks and updating dependency tree + dependencyTree = updateImporterList(sName, dependencyTree) + writeToJSON(config.StackPattern, dependencyTree) + fmt.Printf("Stack successfully deleted: %v", sName) + } else { + if stack.DeleteAttempt >= MAX_DELETE_RETRY_COUNT { + stack.Status = newStatus + statusReason := *details.StackStatusReason + stack.StackStatusReason = statusReason + + dependencyTree[sName] = stack + writeToJSON(config.StackPattern, dependencyTree) + + UpdateNukeStats(dependencyTree) + msg := fmt.Sprintf("Failed to delete stack `%v`. Reason: %v", sName, statusReason) + notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) + log.Fatal(msg) + } else { + // In some cases cloud9 stacks can't be deleted due to security group being manually attached to other resources like elastic search or redis + // In such case it is better to wait for dependent resource's(mostly datastore or cache) stack and security group to get deleted and retry again + newDeleteAttempt := stack.DeleteAttempt + 1 + fmt.Printf("Retrying deleting stack: %v Delete Attempt: %v/%v\n", sName, newDeleteAttempt, MAX_DELETE_RETRY_COUNT) + err := cfn.DeleteStack(sName) + if err != nil { + UpdateNukeStats(dependencyTree) + msg = fmt.Sprintf("Unable to send delete retry request for stack '%v' Error: %v", sName, err) + stack.StackStatusReason = msg + notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) + log.Fatalln(msg) + } + stack.Status = DELETE_IN_PROGRESS + stack.DeleteStartedAt = CurrentUTCDateTime() + stack.DeleteAttempt = newDeleteAttempt + dependencyTree[sName] = stack + writeToJSON(config.StackPattern, dependencyTree) + } + } + } + + // 5. If all stacks have already been deleted, stop execution. Else Go to step 1 + if isEnvNuked(dependencyTree) { + UpdateNukeStats(dependencyTree) + fmt.Printf("Successfully deleted '%v' stacks matching with '%v' pattern!", DELETED_STACK_COUNT, config.StackPattern) + notifier.SuccessAlert(AlertMessage{}) + break + } + + // 6. Check if nuke is stuck + if isNukeStuck(dependencyTree) { + UpdateNukeStats(dependencyTree) + msg := "No stacks are eligible for deletion. Please find and delete stacks which do not have follow given pattern: " + config.StackPattern + notifier.StuckAlert(AlertMessage{Message: msg}) + log.Fatal(msg) + break + } + } +} + +// 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 { + for _, stackDetails := range dt { + importers := stackDetails.ActiveImporterStacks + delete(importers, deletedStackName) + stackDetails.ActiveImporterStacks = importers + } + return dt +} + +func deleteBucketIfPresent(stackName string, cfn CFNManager, s3 S3Manager) error { + resources, _ := cfn.ListStackResources(stackName) + + var objDeleteError error + for _, resource := range resources { + // if a stack is in ROLLBACK_COMPLETE state. Some of the resources might not have physical resource ID + // so checking this first. If there is no resource. No need to empty the bucket + if resource.PhysicalResourceId != nil && resource.ResourceType != nil { + rType := *resource.ResourceType + rName := *resource.PhysicalResourceId + // bucket should be empty before we delete the cfn stack, thus emptying bucket here + if rType == "AWS::S3::Bucket" { + objDeleteError = s3.EmptyBucket(rName) + if objDeleteError != nil { + msg := fmt.Sprintf("Failed to empty bucket '%v' from stack '%v'. Error: %v", rName, stackName, objDeleteError.Error()) + fmt.Println(msg) + break + } + } + } + } + return objDeleteError +} + +func isNukeStuck(dt map[string]StackDetails) bool { + if len(deleteInProgressStacks(dt)) == 0 && len(stacksEligibleToDelete(dt)) == 0 { + return true + } else { + return false + } +} + +func stacksEligibleToDelete(dt map[string]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 { + deleteReady = append(deleteReady, stackName) + } + } + } + return deleteReady +} + +func deleteInProgressStacks(dt map[string]StackDetails) []string { + dip := []string{} + for stackName, stackDetails := range dt { + if stackDetails.Status == DELETE_IN_PROGRESS { + dip = append(dip, stackName) + } + } + return dip +} + +// all stacks have status DELETE_COMPLETE +func isEnvNuked(dt map[string]StackDetails) bool { + nuked := true + for _, stackDetails := range dt { + if stackDetails.Status != DELETE_COMPLETE { + nuked = false + break + } + } + return nuked +} + +func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDetails, error) { + CFNConsoleBaseURL := "https://console.aws.amazon.com/cloudformation/home?region=" + cfn.AWSRegion + "#/stacks/stackinfo?stackId=" + + fmt.Printf("Listing stacks matching with '%v'...\n", envLabel) + dependencyTree, err := cfn.ListEnvironmentStacks() + totalStackCount := len(dependencyTree) + + if err != nil { + UpdateNukeStats(dependencyTree) + fmt.Printf("Failed listing stacks! Error: %v\n", err) + return dependencyTree, err + } + + fmt.Println("Listing all exports...") + stackExports, err := cfn.ListEnvironmentExports() + if err != nil { + fmt.Printf("Failed listing exports! Error: %v", err) + return dependencyTree, err + } + + fmt.Println("Listing all imports...") + stackCount := 0 + var listImportErr error + for stackName, stack := range dependencyTree { + // populate exports + if _, ok := stackExports[stackName]; ok { + if len(stackExports[stackName]) > 0 { + stack.Exports = stackExports[stackName] + } + } + + // listing all importers. making single api call at a time to avoid rate limiting + importingStacks, listImportErr := cfn.ListImports(stack.Exports) + if listImportErr != nil { + fmt.Printf("Failed listing imports! Error: %v", listImportErr) + break + } + + stack.ActiveImporterStacks = importingStacks + dependencyTree[stackName] = stack + stackCount++ + fmt.Println("Listing imports | ", stackCount, "/", totalStackCount, " stacks complete") + } + + if listImportErr != nil { + return dependencyTree, listImportErr + } + + // check if any stack is present in the importers list but not present in the dependency tree. If yes add it to dependency tree along with its dependent stacks + // this can happen if a stackname does not begin match with given pattern i.e. not following the naming convention + missing := getStackWithMissingDependencies(dependencyTree) + for len(missing) != 0 { + // TODO: better logging for this. include this in readme as well + // fmt.Printf("Stack '%v' does not match pattern '%v' and imports from stacks selected for deletion", missing, cfn.EnvLabel) + // fmt.Printf("Included '%v' stack in the deletion list", missing) + for mStk := range missing { + totalStackCount++ + sDetails, err := cfn.DescribeStack(mStk) + if err != nil { + dne := strings.Contains(err.Error(), "does not exist") + if !dne { + fmt.Printf("Error describing stack %v", mStk) + break // real error. + } + dependencyTree[mStk] = StackDetails{ + StackName: mStk, + Status: "DELETE_COMPLETE", + CFNConsoleLink: (CFNConsoleBaseURL + mStk), + } + } else { + // list exports + exports := []string{} + for _, output := range sDetails.Outputs { + exports = append(exports, *output.ExportName) + } + + // list imports + importingStacks, listImportErr := cfn.ListImports(exports) + if listImportErr != nil { + fmt.Println("Failed listing imports!") + break + } + + dependencyTree[mStk] = StackDetails{ + StackName: mStk, + Status: *sDetails.StackStatus, + Exports: exports, + ActiveImporterStacks: importingStacks, + CFNConsoleLink: (CFNConsoleBaseURL + mStk), + } + } + } + missing = getStackWithMissingDependencies(dependencyTree) + } + + return dependencyTree, listImportErr +} + +// --------------------- Utility functions --------------------------- + +func getStackWithMissingDependencies(dt map[string]StackDetails) map[string]struct{} { + allImporterStacks := map[string]struct{}{} + notListed := map[string]struct{}{} + for _, details := range dt { + ais := details.ActiveImporterStacks + for k := range ais { + allImporterStacks[k] = struct{}{} + } + } + + // select importer stacks which are not listed in dependency tree + for sn := range allImporterStacks { + if _, ok := dt[sn]; !ok { + notListed[sn] = struct{}{} + } + } + + return notListed +} + +func writeToJSON(envLabel string, data map[string]StackDetails) { + file, _ := json.MarshalIndent(data, "", " ") + _ = ioutil.WriteFile("stack_teardown_details.json", file, 0644) +} + +func CurrentUTCDateTime() string { + return time.Now().UTC().Format("2006-01-02T15:04:05Z") +} + +func TimeDiff(startTime, endTime string) string { + st, _ := time.Parse(time.RFC3339, startTime) + et, _ := time.Parse(time.RFC3339, endTime) + diff := et.Sub(st) + return fmt.Sprintf("%.2f", diff.Minutes()) +} + +// updating global vars used for alerts +func UpdateNukeStats(dt map[string]StackDetails) { + NUKE_END_TIME = CurrentUTCDateTime() + st, _ := time.Parse(time.RFC3339, NUKE_START_TIME) + et, _ := time.Parse(time.RFC3339, NUKE_END_TIME) + NUKE_DURATION_IN_HRS = et.Sub(st).Hours() + + deletedStackCount := 0 + for _, stackDetails := range dt { + if stackDetails.Status == DELETE_COMPLETE { + deletedStackCount++ + } + } + DELETED_STACK_COUNT = deletedStackCount + ACTIVE_STACK_COUNT = TOTAL_STACK_COUNT - DELETED_STACK_COUNT +} diff --git a/utils/notifier.go b/utils/notifier.go new file mode 100644 index 0000000..e5c83c4 --- /dev/null +++ b/utils/notifier.go @@ -0,0 +1,304 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/nirdosh17/cfn-teardown/models" +) + +type NotificationManager struct { + StackPattern string + NotificationWebHookURL string // Webhook url is specific to channel +} + +type AlertMessage struct { + Message string // Long message with details about the event + Event string // Start | Complete | Error + FailedStack models.StackDetails + Attachment map[string]interface{} +} + +// building slack messages: https://app.slack.com/block-kit-builder +type SlackMessage struct { + Attachments []map[string]interface{} `json:"attachments"` +} + +var ColorMapping map[string]string = map[string]string{"Start": "#f0e62e", "Complete": "#25db2e", "Error": "#e81e1e"} + +func (nm NotificationManager) StartAlert(am AlertMessage) { + am.Event = "Start" + am.Attachment = map[string]interface{}{ + "color": ColorMapping[am.Event], + "blocks": []map[string]interface{}{ + { + "type": "header", + "text": map[string]string{ + "type": "plain_text", + "text": "Stack Deletion Started", + }, + }, + { + "type": "context", + "elements": []map[string]string{ + { + "type": "mrkdwn", + "text": am.Message, + }, + }, + }, + { + "type": "divider", + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": ("*Stack Pattern* \n " + nm.StackPattern), + }, + { + "type": "mrkdwn", + "text": fmt.Sprintf("*Stack Count* \n %v", TOTAL_STACK_COUNT), + }, + }, + }, + }, + } + nm.Alert(am) +} + +func (nm NotificationManager) ErrorAlert(am AlertMessage) { + am.Event = "Error" + am.Attachment = map[string]interface{}{ + "color": ColorMapping[am.Event], + "blocks": []map[string]interface{}{ + { + "type": "header", + "text": map[string]string{ + "type": "plain_text", + "text": "Stack Deletion Failed", + }, + }, + { + "type": "context", + "elements": []map[string]string{ + { + "type": "mrkdwn", + "text": "Manual Intervention Required", + }, + }, + }, + { + "type": "divider", + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": ("*Stack Pattern* \n " + nm.StackPattern), + }, + { + "type": "mrkdwn", + "text": "*Runtime* \n" + fmt.Sprintf("%.2f Hour/s", NUKE_DURATION_IN_HRS), + }, + }, + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": "*Stacks Deleted* \n" + fmt.Sprintf("%v/%v", DELETED_STACK_COUNT, TOTAL_STACK_COUNT), + }, + { + "type": "mrkdwn", + "text": "*Failed Stack* \n" + fmt.Sprintf("<%v|%v>", am.FailedStack.CFNConsoleLink, am.FailedStack.StackName), + }, + }, + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": "*Reason* \n" + am.FailedStack.StackStatusReason, + }, + }, + }, + }, + } + nm.Alert(am) +} + +func (nm NotificationManager) StuckAlert(am AlertMessage) { + am.Event = "Error" + am.Attachment = map[string]interface{}{ + "color": ColorMapping[am.Event], + "blocks": []map[string]interface{}{ + { + "type": "header", + "text": map[string]string{ + "type": "plain_text", + "text": "Stack Deletion Stuck", + }, + }, + { + "type": "context", + "elements": []map[string]string{ + { + "type": "mrkdwn", + "text": am.Message, + }, + }, + }, + { + "type": "divider", + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": ("*Stack Pattern* \n " + nm.StackPattern), + }, + { + "type": "mrkdwn", + "text": "*Runtime* \n" + fmt.Sprintf("%.2f Hour/s", NUKE_DURATION_IN_HRS), + }, + }, + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": "*Stacks Deleted* \n" + fmt.Sprintf("%v/%v", DELETED_STACK_COUNT, TOTAL_STACK_COUNT), + }, + }, + }, + }, + } + nm.Alert(am) +} + +func (nm NotificationManager) SuccessAlert(am AlertMessage) { + am.Event = "Complete" + am.Attachment = map[string]interface{}{ + "color": ColorMapping[am.Event], + "blocks": []map[string]interface{}{ + { + "type": "header", + "text": map[string]string{ + "type": "plain_text", + "text": "Stack Deletion Completed", + }, + }, + { + "type": "divider", + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": ("*Stack Pattern* \n " + nm.StackPattern), + }, + { + "type": "mrkdwn", + "text": fmt.Sprintf("*Deleted Stacks* \n %v", TOTAL_STACK_COUNT), + }, + }, + }, + { + "type": "section", + "fields": []map[string]string{ + { + "type": "mrkdwn", + "text": ("*Started At* \n " + NUKE_START_TIME), + }, + { + "type": "mrkdwn", + "text": ("*Completed At* \n " + NUKE_END_TIME + fmt.Sprintf("(%.2f Hour/s)", NUKE_DURATION_IN_HRS)), + }, + }, + }, + }, + } + nm.Alert(am) +} + +func (nm NotificationManager) GenericAlert(am AlertMessage) { + am.Event = "Error" + am.Attachment = map[string]interface{}{ + "color": ColorMapping[am.Event], + "blocks": []map[string]interface{}{ + { + "type": "header", + "text": map[string]string{ + "type": "plain_text", + "text": "Stack Deletion Error", + }, + }, + { + "type": "context", + "elements": []map[string]string{ + { + "type": "mrkdwn", + "text": am.Message, + }, + }, + }, + }, + } + nm.Alert(am) +} + +func (nm NotificationManager) Alert(am AlertMessage) error { + msgBody := SlackMessage{ + Attachments: []map[string]interface{}{am.Attachment}, + } + + postBody, err := json.Marshal(msgBody) + if err != nil { + log.Printf("[Alert] Error marshaling request body: %v", err) + return err + } + + resp, err := http.Post(nm.NotificationWebHookURL, "application/json", bytes.NewBuffer(postBody)) + if err != nil { + log.Printf("Error posting message to Slack: %v", err) + return err + } + defer resp.Body.Close() + + //Read the response body + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != 200 { + log.Printf("Got %v status code from Slack, Response body: %v\n", resp.StatusCode, string(body)) + log.Printf("Request body: %v\n", string(postBody)) + return fmt.Errorf("Failed to publish message %+v to Slack", msgBody) + } + + return nil +} diff --git a/utils/s3.go b/utils/s3.go new file mode 100644 index 0000000..9934cae --- /dev/null +++ b/utils/s3.go @@ -0,0 +1,105 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "fmt" + "log" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/aws/aws-sdk-go/service/sts" +) + +type S3Manager struct { + ExpectedAccountID string + NukeRoleARN string +} + +func (sm S3Manager) EmptyBucket(bucketName string) error { + svc, err := sm.Session() + if err != nil { + return err + } + + log.Printf("Emptying bucket '%v'...\n", bucketName) + + // Setup BatchDeleteIterator to iterate through a list of objects + iterator := s3manager.NewDeleteListIterator(svc, &s3.ListObjectsInput{Bucket: aws.String(bucketName)}) + err = s3manager.NewBatchDeleteWithClient(svc).Delete(aws.BackgroundContext(), iterator) + if err != nil { + log.Printf("Unable to delete objects from bucket '%v': %v\n", bucketName, err) + return err + } + + // check if the bucket is empty + resp, err := svc.ListObjectsV2(&s3.ListObjectsV2Input{ + Bucket: &bucketName, + }) + if err != nil { + return fmt.Errorf("Error listing objects from bucket '%v': %v", bucketName, err) + } + + if len(resp.Contents) != 0 { + return fmt.Errorf("Failed to empty bucket. Number of items left: %v", len(resp.Contents)) + } + + log.Printf("Bucket '%v' emptied successfully\n", bucketName) + + return nil +} + +// assumes staging nuke role +func (sm S3Manager) Session() (*s3.S3, error) { + sess := session.Must(session.NewSessionWithOptions(session.Options{ + Config: aws.Config{Region: aws.String(os.Getenv("AWS_REGION"))}, + SharedConfigState: session.SharedConfigEnable, + Profile: os.Getenv("AWS_PROFILE"), + })) + + isStaging, err := sm.IsDesiredAWSAccount(sess) + if err != nil { + return nil, err + } + + // to make things easy while running this script locally + if isStaging { + return s3.New(sess), err + } else { + // Create the credentials from AssumeRoleProvider to assume the role referenced by the "NukeRoleARN" ARN. + 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}), err + } +} + +func (sm S3Manager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { + svc := sts.New(sess) + result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) + if err != nil { + log.Printf("Error requesting AWS caller identity: %v", err.Error()) + return false, err + } + + if *result.Account == sm.ExpectedAccountID { + return true, err + } + return false, err +} From ff5823c025951c5a7ce27961e0031310b074f5a0 Mon Sep 17 00:00:00 2001 From: Nirdosh Date: Mon, 16 Aug 2021 19:43:33 +0545 Subject: [PATCH 2/6] Change flag names to full caps. --- README.md | 43 +++++++++++++++++++++-------------------- cmd/deleteStacks.go | 30 +++++++++++++++------------- cmd/listDependencies.go | 5 +++-- cmd/root.go | 22 ++++++++++----------- models/nuke.go | 20 +++++++++---------- utils/cloudformation.go | 37 +++++++++++++++++------------------ utils/deleter.go | 27 ++++++++++++++------------ utils/notifier.go | 24 +++++++++++++++-------- utils/s3.go | 28 +++++++++++++-------------- 9 files changed, 126 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index b8ef1b3..5ddb7e6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CFN Teardown -CFN Teardown is a tool to delete CloudFormation stacks respecting the stack dependencies. +CFN Teardown is a tool to delete CloudFormation stacks respecting stack dependencies. If you deploy all of you intrastructure using CloudFormation with a `consistent naming convention` for stacks, then you can use this tool to tear down the environment. @@ -42,7 +42,7 @@ go get github.com/nirdosh17/cfn-teardown ## Using CFN Teardown -Required global flags for all commands: `stackPattern`, `awsRegion`, `awsProfile` +Required global flags for all commands: `STACK_PATTERN`, `AWS_REGION`, `AWS_PROFILE` 1. Run `cfn-teardown -h` and see available commands and needed parameters. @@ -60,31 +60,31 @@ Required global flags for all commands: `stackPattern`, `awsRegion`, `awsProfile Configuration for this command can be set in three different ways in the precedence order defined below: 1. Environment variables(same as flag name) -2. Flags e.g. `cfn-teardown deleteStacks --stackPattern=qaenv-` +2. Flags e.g. `cfn-teardown deleteStacks --STACK_PATTERN=qaenv-` 3. Supplied YAML Config file (default: ~/.cfn-teardown.yaml)
Minimal config file ```yaml - awsRegion: us-east-1 - awsProfile: staging - stackPattern: qa- + AWS_REGION: us-east-1 + AWS_PROFILE: staging + STACK_PATTERN: qa- ```
All configs present ```yaml - awsRegion: us-east-1 - awsProfile: staging - awsAccountId: 121212121212 - stackPattern: qa- - abortWaitTimeMinutes: 20 - stackWaitTimeSeconds: 30 - maxDeleteRetryCount: 5 - notificationWebhookURL: https://hooks.slack.com/services/dummy/dummy/long_hash - roleARN: - dryRun: false + AWS_REGION: us-east-1 + AWS_PROFILE: staging + TARGET_ACCOUNT_ID: 121212121212 + STACK_PATTERN: qa- + ABORT_WAIT_TIME_MINUTES: 20 + STACK_WAIT_TIME_SECONDS: 30 + MAX_DELETE_RETRY_COUNT: 5 + SLACK_WEBHOOK_URL: https://hooks.slack.com/services/dummy/dummy/long_hash + ROLE_ARN: "" + DRY_RUN: "false" ```
@@ -160,15 +160,16 @@ By default it tries to use the IAM role of environment it is being run. e.g. Cod ## Safety Checks for Accidental Deletion -- `dryRun` flag must be explicitely set to `false` to activate delete functionality +- `DRY_RUN` flag must be explicitely set to `false` to activate delete functionality -- `abortWaitTimeMinutes` flag lets us to decide how much to wait before initiating delete as you might want to confirm the stacks that are about to get deleted +- `ABORT_WAIT_TIME_MINUTES` flag lets us to decide how much to wait before initiating delete as you might want to confirm the stacks that are about to get deleted -- `awsAccountId` flag will check the supplied account id with aws session account id during runtime to confirm that we are deleting stacks in the desired non production account +- `TARGET_ACCOUNT_ID` flag will check the supplied account id with aws session account id during runtime to confirm that we are deleting stacks in the desired aws account ## Edge Case -If a stack can't be deleted from the AWS Console itself due to some dependencies, then it won't be deleted by this tool as well. In such case, manual intervention is required which is notified by this tool. +- If a stack can't be deleted from the AWS Console itself due to some dependencies or error, then it won't be deleted by this tool as well. In such case, manual intervention is required. +- To delete a stack with S3 bucket, this script empties the bucket first and then deletes the stack since CFN does not allow to delete stack with non-empty bucket. ## Caution :warning: @@ -176,4 +177,4 @@ _With great power, comes great responsibility_ - Use this tool with great caution. **Don't ever** run this in production environment with the intention of deleting a subset of stacks. - First try within small number of test stacks in dry run mode. -- Use redundant safety flags `dryRun`, `awsAccountId` and `abortWaitTimeMinutes`. +- Use redundant safety flags `DRY_RUN`, `TARGET_ACCOUNT_ID` and `ABORT_WAIT_TIME_MINUTES`. diff --git a/cmd/deleteStacks.go b/cmd/deleteStacks.go index 67be472..9d0fac1 100644 --- a/cmd/deleteStacks.go +++ b/cmd/deleteStacks.go @@ -33,7 +33,7 @@ Example: If your stacks to be deleted follow this naming convention: qa-{{component name}} Supply stack pattern as: 'qa-' `, - Example: "cfn-teardown deleteStacks --stackPattern='qa-' --awsProfile='staging' --region=us-east-1", + Example: "cfn-teardown deleteStacks --STACK_PATTERN='^qa-' --AWS_PROFILE=staging --AWS_REGION=us-east-1", Args: func(cmd *cobra.Command, args []string) error { // validate your arguments here return validateConfigs(config) @@ -41,6 +41,10 @@ Supply stack pattern as: 'qa-' Run: func(cmd *cobra.Command, args []string) { fmt.Println("Executing command: deleteStacks") fmt.Println() + if config.DryRun != "false" { + fmt.Println("Running in dry run mode. Set dry run to 'false' to actually delete stacks.") + } + utils.InitiateTearDown(config) }, } @@ -48,23 +52,23 @@ Supply stack pattern as: 'qa-' func init() { rootCmd.AddCommand(deleteStacksCmd) - deleteStacksCmd.Flags().Int("stackWaitTimeSeconds", 30, "Seconds to wait after delete requests are submitted to CFN") - viper.BindPFlag("stackWaitTimeSeconds", deleteStacksCmd.Flags().Lookup("stackWaitTimeSeconds")) + deleteStacksCmd.Flags().Int("STACK_WAIT_TIME_SECONDS", 30, "Seconds to wait after delete requests are submitted to CFN") + viper.BindPFlag("STACK_WAIT_TIME_SECONDS", deleteStacksCmd.Flags().Lookup("STACK_WAIT_TIME_SECONDS")) - deleteStacksCmd.Flags().String("awsAccountId", "", "[Safety Check] Validates against account id in current aws session and provided ID") - viper.BindPFlag("awsAccountId", deleteStacksCmd.Flags().Lookup("awsAccountId")) + deleteStacksCmd.Flags().String("TARGET_ACCOUNT_ID", "", "[Safety Check] Confirmes that account id from aws session and intented target aws account are the same") + viper.BindPFlag("TARGET_ACCOUNT_ID", deleteStacksCmd.Flags().Lookup("TARGET_ACCOUNT_ID")) - deleteStacksCmd.Flags().Int("maxDeleteRetryCount", 5, "Max stack delete attempts") - viper.BindPFlag("maxDeleteRetryCount", deleteStacksCmd.Flags().Lookup("maxDeleteRetryCount")) + deleteStacksCmd.Flags().Int("MAX_DELETE_RETRY_COUNT", 5, "Max stack delete attempts") + viper.BindPFlag("MAX_DELETE_RETRY_COUNT", deleteStacksCmd.Flags().Lookup("MAX_DELETE_RETRY_COUNT")) - deleteStacksCmd.Flags().Int("abortWaitTimeMinutes", 10, "[Safety Check] Minutes to wait before initiating deletion") - viper.BindPFlag("abortWaitTimeMinutes", deleteStacksCmd.Flags().Lookup("abortWaitTimeMinutes")) + deleteStacksCmd.Flags().Int("ABORT_WAIT_TIME_MINUTES", 10, "[Safety Check] Minutes to wait before initiating deletion") + viper.BindPFlag("ABORT_WAIT_TIME_MINUTES", deleteStacksCmd.Flags().Lookup("ABORT_WAIT_TIME_MINUTES")) - deleteStacksCmd.Flags().String("notificationWebhookURL", "", "Send status alerts to Slack channel") - viper.BindPFlag("notificationWebhookURL", deleteStacksCmd.Flags().Lookup("notificationWebhookURL")) + deleteStacksCmd.Flags().String("SLACK_WEBHOOK_URL", "", "Send status alerts to Slack channel") + viper.BindPFlag("SLACK_WEBHOOK_URL", deleteStacksCmd.Flags().Lookup("SLACK_WEBHOOK_URL")) - deleteStacksCmd.Flags().String("dryRun", "true", "[Safety Check] To delete stacks, it needs to be explicitely set to false") - viper.BindPFlag("dryRun", deleteStacksCmd.Flags().Lookup("dryRun")) + deleteStacksCmd.Flags().String("DRY_RUN", "true", "[Safety Check] To delete stacks, it needs to be explicitely set to false") + viper.BindPFlag("DRY_RUN", deleteStacksCmd.Flags().Lookup("DRY_RUN")) // Here you will define your flags and configuration settings. diff --git a/cmd/listDependencies.go b/cmd/listDependencies.go index 9d7970b..003333c 100644 --- a/cmd/listDependencies.go +++ b/cmd/listDependencies.go @@ -32,18 +32,19 @@ Example: If your stacks to be deleted follow this naming convention: qa-{{component name}} Supply stack pattern as: 'qa-' `, - Example: "cfn-teardown listDependencies --stackPattern='qa-' --awsProfile='staging' --region=us-east-1", + Example: "cfn-teardown listDependencies --STACK_PATTERN='qa-' --AWS_PROFILE='staging' --AWS_REGION=us-east-1", Args: func(cmd *cobra.Command, args []string) error { // validate your arguments here return validateConfigs(config) }, Run: func(cmd *cobra.Command, args []string) { + fmt.Println() fmt.Println("Executing command: listDependencies") fmt.Println() - // for safety config.DryRun = "true" + fmt.Println("Running in dry run mode...") utils.InitiateTearDown(config) }, diff --git a/cmd/root.go b/cmd/root.go index f37f336..20b64d2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -74,15 +74,15 @@ func validateConfigs(config models.Config) (err error) { emptyFlags := []string{} if config.StackPattern == "" { - emptyFlags = append(emptyFlags, "stackPattern") + emptyFlags = append(emptyFlags, "STACK_PATTERN") } if config.AWSProfile == "" { - emptyFlags = append(emptyFlags, "awsProfile") + emptyFlags = append(emptyFlags, "AWS_PROFILE") } if config.AWSRegion == "" { - emptyFlags = append(emptyFlags, "awsRegion") + emptyFlags = append(emptyFlags, "AWS_REGION") } if len(emptyFlags) > 0 { @@ -98,17 +98,17 @@ func init() { // Cobra supports persistent flags, which, if defined here, // will be global for your application. - rootCmd.PersistentFlags().String("stackPattern", "", "Pattern to match stack name e.g. 'staging-'") - viper.BindPFlag("stackPattern", rootCmd.PersistentFlags().Lookup("stackPattern")) + rootCmd.PersistentFlags().String("STACK_PATTERN", "", "Pattern to match stack name e.g. 'staging-'") + viper.BindPFlag("STACK_PATTERN", rootCmd.PersistentFlags().Lookup("STACK_PATTERN")) - rootCmd.PersistentFlags().String("awsRegion", "", "AWS Region where the stacks are present") - viper.BindPFlag("awsRegion", rootCmd.PersistentFlags().Lookup("awsRegion")) + rootCmd.PersistentFlags().String("AWS_REGION", "", "AWS Region where the stacks are present") + viper.BindPFlag("AWS_REGION", rootCmd.PersistentFlags().Lookup("AWS_REGION")) - rootCmd.PersistentFlags().String("awsProfile", "", "AWS Profile") - viper.BindPFlag("awsProfile", rootCmd.PersistentFlags().Lookup("awsProfile")) + rootCmd.PersistentFlags().String("AWS_PROFILE", "", "AWS Profile") + viper.BindPFlag("AWS_PROFILE", rootCmd.PersistentFlags().Lookup("AWS_PROFILE")) - rootCmd.PersistentFlags().String("roleARN", "", "Assume this role to scan and delete stacks if provided") - viper.BindPFlag("roleARN", rootCmd.PersistentFlags().Lookup("roleARN")) + rootCmd.PersistentFlags().String("ROLE_ARN", "", "Assume this role to scan and delete stacks if provided") + viper.BindPFlag("ROLE_ARN", rootCmd.PersistentFlags().Lookup("ROLE_ARN")) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cfn-teardown.yaml)") diff --git a/models/nuke.go b/models/nuke.go index b1ddc28..54e3b46 100644 --- a/models/nuke.go +++ b/models/nuke.go @@ -17,14 +17,14 @@ package models // Config represents all the parameters supported by cfn-teardown type Config struct { - AWSProfile string `mapstructure:"awsProfile"` - AWSRegion string `mapstructure:"awsRegion"` - AWSAccountId string `mapstructure:"awsAccountId"` - StackPattern string `mapstructure:"stackPattern"` - StackWaitTimeSeconds int16 `mapstructure:"stackWaitTimeSeconds"` - MaxDeleteRetryCount int16 `mapstructure:"maxDeleteRetryCount"` - AbortWaitTimeMinutes int16 `mapstructure:"abortWaitTimeMinutes"` - NotificationWebhookURL string `mapstructure:"notificationWebhookURL"` - RoleARN string `mapstructure:"roleARN"` - DryRun string `mapstructure:"dryRun"` + AWSProfile string `mapstructure:"AWS_PROFILE"` + AWSRegion string `mapstructure:"AWS_REGION"` + TargetAccountId string `mapstructure:"TARGET_ACCOUNT_ID"` + StackPattern string `mapstructure:"STACK_PATTERN"` + StackWaitTimeSeconds int16 `mapstructure:"STACK_WAIT_TIME_SECONDS"` + MaxDeleteRetryCount int16 `mapstructure:"MAX_DELETE_RETRY_COUNT"` + AbortWaitTimeMinutes int16 `mapstructure:"ABORT_WAIT_TIME_MINUTES"` + SlackWebhookURL string `mapstructure:"SLACK_WEBHOOK_URL"` + RoleARN string `mapstructure:"ROLE_ARN"` + DryRun string `mapstructure:"DRY_RUN"` } diff --git a/utils/cloudformation.go b/utils/cloudformation.go index 67588f4..97e685b 100644 --- a/utils/cloudformation.go +++ b/utils/cloudformation.go @@ -16,8 +16,7 @@ limitations under the License. package utils import ( - "log" - "os" + "fmt" "regexp" "strings" @@ -31,10 +30,11 @@ import ( ) type CFNManager struct { - ExpectedAccountID string - NukeRoleARN string - StackPattern string - AWSRegion string + TargetAccountId string + NukeRoleARN string + StackPattern string + AWSProfile string + AWSRegion string } func (dm CFNManager) DescribeStack(stackName string) (*cloudformation.Stack, error) { @@ -75,7 +75,7 @@ func (dm CFNManager) ListStackResources(stackName string) ([]*cloudformation.Sta } if err != nil { - log.Printf("Error listing resources of stack '%v': %v\n", stackName, err) + fmt.Printf("Error listing resources of stack '%v': %v\n", stackName, err) } return resp.StackResourceSummaries, err @@ -109,7 +109,7 @@ func (dm CFNManager) ListImports(exportNames []string) (map[string]struct{}, err // No error means, delete request sent to cloudformation // If the stack we are trying to delete has already been deleted, returns success func (dm CFNManager) DeleteStack(stackName string) error { - log.Printf("Submitting delete request for stack: %v\n", stackName) + fmt.Printf("Submitting delete request for stack: %v\n", stackName) cfn, err := dm.Session() if err != nil { return err @@ -152,7 +152,7 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) { } if err != nil { - log.Printf("Failed listing stacks with pattern: '%v', Error: '%v'\n", dm.StackPattern, err) + fmt.Printf("Failed listing stacks with pattern: '%v', Error: '%v'\n", dm.StackPattern, err) return envStacks, err } @@ -180,7 +180,7 @@ func (dm CFNManager) ListEnvironmentStacks() (map[string]StackDetails, error) { } if err != nil { - log.Printf("Error listing '%v' environment stacks: %v\n", dm.StackPattern, err) + fmt.Printf("Error listing '%v' environment stacks: %v\n", dm.StackPattern, err) } return envStacks, err } @@ -206,7 +206,7 @@ func (dm CFNManager) ListEnvironmentExports() (map[string][]string, error) { } if err != nil { - log.Printf("Error listing '%v' environment stack exports: %v\n", dm.StackPattern, err) + fmt.Printf("Error listing '%v' environment stack exports: %v\n", dm.StackPattern, err) return exports, err } @@ -240,37 +240,36 @@ func (dm CFNManager) RegexMatch(stackName string) bool { // assumes staging nuke role func (dm CFNManager) Session() (*cloudformation.CloudFormation, error) { sess := session.Must(session.NewSessionWithOptions(session.Options{ - Config: aws.Config{Region: aws.String(os.Getenv("AWS_REGION"))}, + Config: aws.Config{Region: aws.String(dm.AWSRegion)}, SharedConfigState: session.SharedConfigEnable, - Profile: os.Getenv("AWS_PROFILE"), + Profile: dm.AWSProfile, })) - isStaging, err := dm.IsDesiredAWSAccount(sess) + desiredAccount, err := dm.IsDesiredAWSAccount(sess) if err != nil { return nil, err } // to make things easy while running this script locally - if isStaging { + if desiredAccount { return cloudformation.New(sess), nil } else { - // Create the credentials from AssumeRoleProvider to assume the role referenced by the "NukeRoleARN" ARN. + // 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 } - } func (dm CFNManager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { - log.Printf("Error requesting AWS caller identity: %v", err.Error()) + fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) return false, err } - if *result.Account == dm.ExpectedAccountID { + if *result.Account == dm.TargetAccountId { return true, err } return false, err diff --git a/utils/deleter.go b/utils/deleter.go index 78562b1..d14d868 100644 --- a/utils/deleter.go +++ b/utils/deleter.go @@ -54,9 +54,9 @@ func InitiateTearDown(config Config) { loading.Color("red", "bold") defer loading.Stop() - s3 := S3Manager{ExpectedAccountID: config.AWSAccountId, NukeRoleARN: config.RoleARN} - notifier := NotificationManager{StackPattern: config.StackPattern, NotificationWebHookURL: config.NotificationWebhookURL} - cfn := CFNManager{StackPattern: config.StackPattern, ExpectedAccountID: config.AWSAccountId, NukeRoleARN: config.RoleARN, AWSRegion: config.AWSRegion} + 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} var dependencyTree = map[string]StackDetails{} @@ -76,7 +76,6 @@ func InitiateTearDown(config Config) { TOTAL_STACK_COUNT = len(dependencyTree) UpdateNukeStats(dependencyTree) - fmt.Printf("Total Stack Count: '%v'\n", ACTIVE_STACK_COUNT) if ACTIVE_STACK_COUNT == 0 { UpdateNukeStats(dependencyTree) @@ -85,21 +84,25 @@ func InitiateTearDown(config Config) { return } + fmt.Println() + fmt.Println() + fmt.Printf("Following %v stacks are eligible for deletion:\n", ACTIVE_STACK_COUNT) + for stackName, _ := range dependencyTree { + fmt.Println(" -", stackName) + } + fmt.Println("\nCheck 'stack_teardown_details.json' file for more details.") + // safety check for accidental run if config.DryRun != "false" { - fmt.Println("\nFollowing stacks are eligible for deletion:") - for stackName, _ := range dependencyTree { - fmt.Println(" -", stackName) - } - fmt.Println("\nCheck 'stack_teardown_details.json' file for more details.") return } msg := fmt.Sprintf("Waiting for `%v minutes` before initiating deletion...", config.AbortWaitTimeMinutes) notifier.StartAlert(AlertMessage{Message: msg}) + fmt.Println() fmt.Println(msg) loading.Start() - // TODO FEATURE: Add a countdown timer + time.Sleep(time.Duration(config.AbortWaitTimeMinutes) * time.Minute) fmt.Println("\n\n------------------------- Deletion Started ----------------------------------") for { @@ -112,7 +115,7 @@ func InitiateTearDown(config Config) { // 2.2 Then send request to delete stack // 2.3 Change stack status to DELETE_IN_PROGRESS fmt.Println("\n-----------------------------------------------------------------------------") - fmt.Printf("Searching stacks with no importers(dependencies): %v", len(toDelete)) + fmt.Printf("Searching stacks with no importers(dependencies): %v\n", len(toDelete)) for _, sName := range toDelete { stack := dependencyTree[sName] bktErr := deleteBucketIfPresent(sName, cfn, s3) @@ -141,7 +144,7 @@ func InitiateTearDown(config Config) { // 3. Wait for 30 seconds fmt.Println("\n-----------------------------------------------------------------------------") - fmt.Printf("Waiting for %v seconds...", STACK_DELETION_WAIT_TIME_IN_SEC) + fmt.Printf("Waiting for %v seconds...\n", STACK_DELETION_WAIT_TIME_IN_SEC) time.Sleep(time.Duration(STACK_DELETION_WAIT_TIME_IN_SEC) * time.Second) // 4. Get list of stacks in DELETE_IN_PROGRESS and describe stack diff --git a/utils/notifier.go b/utils/notifier.go index e5c83c4..06ddde2 100644 --- a/utils/notifier.go +++ b/utils/notifier.go @@ -20,15 +20,15 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "net/http" "github.com/nirdosh17/cfn-teardown/models" ) type NotificationManager struct { - StackPattern string - NotificationWebHookURL string // Webhook url is specific to channel + StackPattern string + DryRun string + SlackWebHookURL string // Webhook url is specific to channel } type AlertMessage struct { @@ -275,19 +275,27 @@ func (nm NotificationManager) GenericAlert(am AlertMessage) { } func (nm NotificationManager) Alert(am AlertMessage) error { + if nm.DryRun != "false" { + return nil + } + if nm.SlackWebHookURL == "" { + // do not make api request + return nil + } + msgBody := SlackMessage{ Attachments: []map[string]interface{}{am.Attachment}, } postBody, err := json.Marshal(msgBody) if err != nil { - log.Printf("[Alert] Error marshaling request body: %v", err) + fmt.Printf("[Alert] Error marshaling request body: %v", err) return err } - resp, err := http.Post(nm.NotificationWebHookURL, "application/json", bytes.NewBuffer(postBody)) + resp, err := http.Post(nm.SlackWebHookURL, "application/json", bytes.NewBuffer(postBody)) if err != nil { - log.Printf("Error posting message to Slack: %v", err) + fmt.Printf("Error posting message to Slack: %v", err) return err } defer resp.Body.Close() @@ -295,8 +303,8 @@ func (nm NotificationManager) Alert(am AlertMessage) error { //Read the response body body, _ := ioutil.ReadAll(resp.Body) if resp.StatusCode != 200 { - log.Printf("Got %v status code from Slack, Response body: %v\n", resp.StatusCode, string(body)) - log.Printf("Request body: %v\n", string(postBody)) + fmt.Printf("Got %v status code from Slack, Response body: %v\n", resp.StatusCode, string(body)) + fmt.Printf("Request body: %v\n", string(postBody)) return fmt.Errorf("Failed to publish message %+v to Slack", msgBody) } diff --git a/utils/s3.go b/utils/s3.go index 9934cae..2962a96 100644 --- a/utils/s3.go +++ b/utils/s3.go @@ -17,8 +17,6 @@ package utils import ( "fmt" - "log" - "os" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" @@ -29,8 +27,10 @@ import ( ) type S3Manager struct { - ExpectedAccountID string - NukeRoleARN string + TargetAccountId string + NukeRoleARN string + AWSProfile string + AWSRegion string } func (sm S3Manager) EmptyBucket(bucketName string) error { @@ -39,13 +39,13 @@ func (sm S3Manager) EmptyBucket(bucketName string) error { return err } - log.Printf("Emptying bucket '%v'...\n", bucketName) + fmt.Printf("Emptying bucket '%v'...\n", bucketName) // Setup BatchDeleteIterator to iterate through a list of objects iterator := s3manager.NewDeleteListIterator(svc, &s3.ListObjectsInput{Bucket: aws.String(bucketName)}) err = s3manager.NewBatchDeleteWithClient(svc).Delete(aws.BackgroundContext(), iterator) if err != nil { - log.Printf("Unable to delete objects from bucket '%v': %v\n", bucketName, err) + fmt.Printf("Unable to delete objects from bucket '%v': %v\n", bucketName, err) return err } @@ -61,7 +61,7 @@ func (sm S3Manager) EmptyBucket(bucketName string) error { return fmt.Errorf("Failed to empty bucket. Number of items left: %v", len(resp.Contents)) } - log.Printf("Bucket '%v' emptied successfully\n", bucketName) + fmt.Printf("Bucket '%v' emptied successfully\n", bucketName) return nil } @@ -69,21 +69,21 @@ func (sm S3Manager) EmptyBucket(bucketName string) error { // assumes staging nuke role func (sm S3Manager) Session() (*s3.S3, error) { sess := session.Must(session.NewSessionWithOptions(session.Options{ - Config: aws.Config{Region: aws.String(os.Getenv("AWS_REGION"))}, + Config: aws.Config{Region: aws.String(sm.AWSRegion)}, SharedConfigState: session.SharedConfigEnable, - Profile: os.Getenv("AWS_PROFILE"), + Profile: sm.AWSProfile, })) - isStaging, err := sm.IsDesiredAWSAccount(sess) + desiredAccount, err := sm.IsDesiredAWSAccount(sess) if err != nil { return nil, err } // to make things easy while running this script locally - if isStaging { + if desiredAccount { return s3.New(sess), err } else { - // Create the credentials from AssumeRoleProvider to assume the role referenced by the "NukeRoleARN" ARN. + // Create the credentials from AssumeRoleProvider to assume the role referenced by the "NukeROLE_ARN" ARN. 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}), err @@ -94,11 +94,11 @@ func (sm S3Manager) IsDesiredAWSAccount(sess *session.Session) (bool, error) { svc := sts.New(sess) result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}) if err != nil { - log.Printf("Error requesting AWS caller identity: %v", err.Error()) + fmt.Printf("Error requesting AWS caller identity: %v", err.Error()) return false, err } - if *result.Account == sm.ExpectedAccountID { + if *result.Account == sm.TargetAccountId { return true, err } return false, err From 44ae89347da105aa6c17d2b8de787b82dc8e015e Mon Sep 17 00:00:00 2001 From: Nirdosh Date: Mon, 16 Aug 2021 21:46:56 +0545 Subject: [PATCH 3/6] Fixes. --- go.mod | 1 - go.sum | 10 ---------- utils/deleter.go | 20 ++++---------------- utils/s3.go | 4 ++-- 4 files changed, 6 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 3ecc5ef..3f04898 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.16 require ( github.com/aws/aws-sdk-go v1.40.22 - github.com/briandowns/spinner v1.16.0 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.8.1 diff --git a/go.sum b/go.sum index be55514..492e2ac 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,6 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4 h1:w/jqZtC9YD4DS/Vp9GhWfWcCpuAL58oTnLoI8vE9YHU= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs= -github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= @@ -268,12 +266,8 @@ github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaW github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= @@ -301,7 +295,6 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -469,7 +462,6 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -503,7 +495,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -551,7 +542,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/utils/deleter.go b/utils/deleter.go index d14d868..5bcd121 100644 --- a/utils/deleter.go +++ b/utils/deleter.go @@ -23,7 +23,6 @@ import ( "strings" "time" - "github.com/briandowns/spinner" . "github.com/nirdosh17/cfn-teardown/models" ) @@ -47,23 +46,14 @@ var ( // A stack is eligible for deletion when it's exports has not been imported by any other stacks func InitiateTearDown(config Config) { - loading := spinner.New( - spinner.CharSets[7], - 100*time.Millisecond, - ) - loading.Color("red", "bold") - defer loading.Stop() - 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} + notifier := NotificationManager{StackPattern: config.StackPattern, SlackWebHookURL: config.SlackWebhookURL, DryRun: config.DryRun} var dependencyTree = map[string]StackDetails{} // generate dependencies for matching stacks - loading.Start() dt, err := prepareDependencyTree(config.StackPattern, cfn) - loading.Stop() if err != nil { UpdateNukeStats(dependencyTree) @@ -79,7 +69,7 @@ func InitiateTearDown(config Config) { if ACTIVE_STACK_COUNT == 0 { UpdateNukeStats(dependencyTree) - fmt.Printf("Successfully deleted '%v' stacks!", TOTAL_STACK_COUNT) + fmt.Printf("\nNo matching stacks to delete! Stack count: %v\n", TOTAL_STACK_COUNT) notifier.SuccessAlert(AlertMessage{}) return } @@ -101,8 +91,6 @@ func InitiateTearDown(config Config) { notifier.StartAlert(AlertMessage{Message: msg}) fmt.Println() fmt.Println(msg) - loading.Start() - time.Sleep(time.Duration(config.AbortWaitTimeMinutes) * time.Minute) fmt.Println("\n\n------------------------- Deletion Started ----------------------------------") for { @@ -197,7 +185,7 @@ func InitiateTearDown(config Config) { // removing this stack from list of importers of all stacks and updating dependency tree dependencyTree = updateImporterList(sName, dependencyTree) writeToJSON(config.StackPattern, dependencyTree) - fmt.Printf("Stack successfully deleted: %v", sName) + fmt.Printf("Stack successfully deleted: %v\n", sName) } else { if stack.DeleteAttempt >= MAX_DELETE_RETRY_COUNT { stack.Status = newStatus @@ -236,7 +224,7 @@ func InitiateTearDown(config Config) { // 5. If all stacks have already been deleted, stop execution. Else Go to step 1 if isEnvNuked(dependencyTree) { UpdateNukeStats(dependencyTree) - fmt.Printf("Successfully deleted '%v' stacks matching with '%v' pattern!", DELETED_STACK_COUNT, config.StackPattern) + fmt.Printf("\nStack Teardown Successful! Deleted Stacks: %v\n", DELETED_STACK_COUNT) notifier.SuccessAlert(AlertMessage{}) break } diff --git a/utils/s3.go b/utils/s3.go index 2962a96..fba89d7 100644 --- a/utils/s3.go +++ b/utils/s3.go @@ -83,9 +83,9 @@ func (sm S3Manager) Session() (*s3.S3, error) { if desiredAccount { return s3.New(sess), err } else { - // Create the credentials from AssumeRoleProvider to assume the role referenced by the "NukeROLE_ARN" ARN. + // 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. + // Create service client value configured for credentials from assumed role return s3.New(sess, &aws.Config{Credentials: creds, MaxRetries: &AWS_SDK_MAX_RETRY}), err } } From 1852ad506757cff98ff1733070416944d8e99de1 Mon Sep 17 00:00:00 2001 From: Nirdosh Date: Mon, 16 Aug 2021 23:05:46 +0545 Subject: [PATCH 4/6] Colorize. --- cmd/deleteStacks.go | 3 +- cmd/listDependencies.go | 3 +- go.mod | 1 + go.sum | 4 +++ utils/deleter.go | 63 +++++++++++++++++++++++------------------ 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/cmd/deleteStacks.go b/cmd/deleteStacks.go index 9d0fac1..f22e4fc 100644 --- a/cmd/deleteStacks.go +++ b/cmd/deleteStacks.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/gookit/color" "github.com/nirdosh17/cfn-teardown/utils" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -39,7 +40,7 @@ Supply stack pattern as: 'qa-' return validateConfigs(config) }, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Executing command: deleteStacks") + color.Red.Println("Executing command: deleteStacks") fmt.Println() if config.DryRun != "false" { fmt.Println("Running in dry run mode. Set dry run to 'false' to actually delete stacks.") diff --git a/cmd/listDependencies.go b/cmd/listDependencies.go index 003333c..48d86fd 100644 --- a/cmd/listDependencies.go +++ b/cmd/listDependencies.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/gookit/color" "github.com/nirdosh17/cfn-teardown/utils" "github.com/spf13/cobra" ) @@ -40,7 +41,7 @@ Supply stack pattern as: 'qa-' }, Run: func(cmd *cobra.Command, args []string) { fmt.Println() - fmt.Println("Executing command: listDependencies") + color.Green.Println("Executing command: listDependencies") fmt.Println() // for safety config.DryRun = "true" diff --git a/go.mod b/go.mod index 3f04898..2b24e40 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/aws/aws-sdk-go v1.40.22 + github.com/gookit/color v1.4.2 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.8.1 diff --git a/go.sum b/go.sum index 492e2ac..78f215b 100644 --- a/go.sum +++ b/go.sum @@ -191,6 +191,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= @@ -350,6 +352,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 h1:ESFSdwYZvkeru3RtdrYueztKhOBCSAAzS4Gf+k0tEow= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/utils/deleter.go b/utils/deleter.go index 5bcd121..342c5b1 100644 --- a/utils/deleter.go +++ b/utils/deleter.go @@ -19,10 +19,11 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" + "os" "strings" "time" + "github.com/gookit/color" . "github.com/nirdosh17/cfn-teardown/models" ) @@ -59,7 +60,8 @@ func InitiateTearDown(config Config) { UpdateNukeStats(dependencyTree) msg := fmt.Sprintf("Unable to prepare dependencies. Error: %v", err.Error()) notifier.ErrorAlert(AlertMessage{Message: msg}) - log.Fatal(msg) + color.Error.Println(msg) + os.Exit(1) } dependencyTree = dt // need to do this for global scope writeToJSON(config.StackPattern, dependencyTree) @@ -69,30 +71,29 @@ func InitiateTearDown(config Config) { if ACTIVE_STACK_COUNT == 0 { UpdateNukeStats(dependencyTree) - fmt.Printf("\nNo matching stacks to delete! Stack count: %v\n", TOTAL_STACK_COUNT) + color.Yellow.Printf("\nNo matching stacks to delete! Stack count: %v\n", TOTAL_STACK_COUNT) notifier.SuccessAlert(AlertMessage{}) return } fmt.Println() - fmt.Println() - fmt.Printf("Following %v stacks are eligible for deletion:\n", ACTIVE_STACK_COUNT) + fmt.Printf("Following stacks are eligible for deletion | Stack count: %v\n", ACTIVE_STACK_COUNT) for stackName, _ := range dependencyTree { - fmt.Println(" -", stackName) + color.Gray.Println(" -", stackName) } - fmt.Println("\nCheck 'stack_teardown_details.json' file for more details.") + color.Style{color.Yellow, color.OpItalic}.Println("\nCheck 'stack_teardown_details.json' file for more details.") + fmt.Println() // safety check for accidental run if config.DryRun != "false" { return } - msg := fmt.Sprintf("Waiting for `%v minutes` before initiating deletion...", config.AbortWaitTimeMinutes) + msg := fmt.Sprintf("Waiting for `%v minutes` before starting deletion. Abort if necessary.", config.AbortWaitTimeMinutes) notifier.StartAlert(AlertMessage{Message: msg}) - fmt.Println() - fmt.Println(msg) + color.Red.Println(msg) time.Sleep(time.Duration(config.AbortWaitTimeMinutes) * time.Minute) - fmt.Println("\n\n------------------------- Deletion Started ----------------------------------") + color.Green.Println("\n\n---------------------------- Deletion Started -------------------------------") for { // Algorithm: // 1. Scan stacks who has zero importing stacks i.e. last leaf in the dependency tree @@ -112,7 +113,8 @@ func InitiateTearDown(config Config) { UpdateNukeStats(dependencyTree) msg := fmt.Sprintf("Unable to empty bucket from stack '%v'", sName) notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) - log.Fatalln(msg) // abort! + color.Error.Println(msg) + os.Exit(1) } err := cfn.DeleteStack(sName) @@ -121,7 +123,8 @@ func InitiateTearDown(config Config) { msg = fmt.Sprintf("Unable to send delete request for stack '%v' Error: %v", sName, err) stack.StackStatusReason = msg notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) - log.Fatalln(msg) + color.Error.Println(msg) + os.Exit(1) } stack.Status = DELETE_IN_PROGRESS stack.DeleteStartedAt = CurrentUTCDateTime() @@ -158,7 +161,8 @@ func InitiateTearDown(config Config) { msg := fmt.Sprintf("Unable to describe stack '%v'", sName) stack.StackStatusReason = msg notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) - log.Fatal(msg) + color.Error.Println(msg) + os.Exit(1) } } @@ -198,7 +202,8 @@ func InitiateTearDown(config Config) { UpdateNukeStats(dependencyTree) msg := fmt.Sprintf("Failed to delete stack `%v`. Reason: %v", sName, statusReason) notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) - log.Fatal(msg) + color.Error.Println(msg) + os.Exit(1) } else { // In some cases cloud9 stacks can't be deleted due to security group being manually attached to other resources like elastic search or redis // In such case it is better to wait for dependent resource's(mostly datastore or cache) stack and security group to get deleted and retry again @@ -210,7 +215,8 @@ func InitiateTearDown(config Config) { msg = fmt.Sprintf("Unable to send delete retry request for stack '%v' Error: %v", sName, err) stack.StackStatusReason = msg notifier.ErrorAlert(AlertMessage{Message: msg, FailedStack: stack}) - log.Fatalln(msg) + color.Error.Println(msg) + os.Exit(1) } stack.Status = DELETE_IN_PROGRESS stack.DeleteStartedAt = CurrentUTCDateTime() @@ -224,7 +230,7 @@ func InitiateTearDown(config Config) { // 5. If all stacks have already been deleted, stop execution. Else Go to step 1 if isEnvNuked(dependencyTree) { UpdateNukeStats(dependencyTree) - fmt.Printf("\nStack Teardown Successful! Deleted Stacks: %v\n", DELETED_STACK_COUNT) + color.Green.Printf("\n---------- STACK TEARDOWN SUCCESSFUL! STACKS DELETED: (%v) ----------\n\n", DELETED_STACK_COUNT) notifier.SuccessAlert(AlertMessage{}) break } @@ -232,9 +238,11 @@ func InitiateTearDown(config Config) { // 6. Check if nuke is stuck if isNukeStuck(dependencyTree) { UpdateNukeStats(dependencyTree) + // TODO: better messaging msg := "No stacks are eligible for deletion. Please find and delete stacks which do not have follow given pattern: " + config.StackPattern notifier.StuckAlert(AlertMessage{Message: msg}) - log.Fatal(msg) + color.Error.Println(msg) + os.Exit(1) break } } @@ -321,24 +329,25 @@ func isEnvNuked(dt map[string]StackDetails) bool { func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDetails, error) { CFNConsoleBaseURL := "https://console.aws.amazon.com/cloudformation/home?region=" + cfn.AWSRegion + "#/stacks/stackinfo?stackId=" - fmt.Printf("Listing stacks matching with '%v'...\n", envLabel) + fmt.Printf("-------------- Listing Stacks | Match Pattern: [%v] --------------\n", color.Gray.Render(envLabel)) + dependencyTree, err := cfn.ListEnvironmentStacks() totalStackCount := len(dependencyTree) if err != nil { UpdateNukeStats(dependencyTree) - fmt.Printf("Failed listing stacks! Error: %v\n", err) + color.Error.Printf(" Failed listing stacks! Error: %v\n", err) return dependencyTree, err } - fmt.Println("Listing all exports...") + color.Gray.Println(" Listing all exports...") stackExports, err := cfn.ListEnvironmentExports() if err != nil { - fmt.Printf("Failed listing exports! Error: %v", err) + color.Error.Printf(" Failed listing exports! Error: %v", err) return dependencyTree, err } - fmt.Println("Listing all imports...") + color.Gray.Println(" Listing all imports...") stackCount := 0 var listImportErr error for stackName, stack := range dependencyTree { @@ -352,14 +361,14 @@ func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDet // listing all importers. making single api call at a time to avoid rate limiting importingStacks, listImportErr := cfn.ListImports(stack.Exports) if listImportErr != nil { - fmt.Printf("Failed listing imports! Error: %v", listImportErr) + color.Error.Printf(" Failed listing imports! Error: %v", listImportErr) break } stack.ActiveImporterStacks = importingStacks dependencyTree[stackName] = stack stackCount++ - fmt.Println("Listing imports | ", stackCount, "/", totalStackCount, " stacks complete") + color.Gray.Println(" Listing imports | ", stackCount, "/", totalStackCount, " stacks complete") } if listImportErr != nil { @@ -379,7 +388,7 @@ func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDet if err != nil { dne := strings.Contains(err.Error(), "does not exist") if !dne { - fmt.Printf("Error describing stack %v", mStk) + color.Error.Printf(" Error describing stack %v", mStk) break // real error. } dependencyTree[mStk] = StackDetails{ @@ -397,7 +406,7 @@ func prepareDependencyTree(envLabel string, cfn CFNManager) (map[string]StackDet // list imports importingStacks, listImportErr := cfn.ListImports(exports) if listImportErr != nil { - fmt.Println("Failed listing imports!") + color.Error.Println(" Failed listing imports!") break } From 54ce09ff8e3f02d876809e2d56e72fefc3a99279 Mon Sep 17 00:00:00 2001 From: Nirdosh Date: Tue, 17 Aug 2021 21:05:06 +0545 Subject: [PATCH 5/6] Add version command. --- cmd/version.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 cmd/version.go diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..6dc6f77 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,38 @@ +/* +Copyright © 2021 Nirdosh Gautam + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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 + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var Version = "development" + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Shows cli command version", + Long: "Shows cli command version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Version: ", Version) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} From 92e176bf6430e68ae7ebe0b73d2d045b0285a53e Mon Sep 17 00:00:00 2001 From: Nirdosh Date: Tue, 17 Aug 2021 21:10:46 +0545 Subject: [PATCH 6/6] Setup Go releaser. --- .github/workflows/release.yaml | 33 +++++++++++++++++++++++++++++++++ .goreleaser.yml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 .github/workflows/release.yaml create mode 100644 .goreleaser.yml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..2128dac --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,33 @@ +name: goreleaser + +on: + push: + branches: + - 'main' + tags: + - 'v*' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..9fb563e --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,31 @@ +# For more info about this file, check the documentation at http://goreleaser.com +before: + hooks: + - go mod download +builds: + - ldflags: + - -X "github.com/nirdosh17/cfn-teardown/cmd.Version={{ .Tag }}" + env: + - CGO_ENABLED=0 + binary: cfn-teardown + goos: + - linux + - windows + - darwin +archives: + - replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:'