Skip to content

Commit

Permalink
resourceop account: add Delete to an account for closing it
Browse files Browse the repository at this point in the history
This will close the account and by default have 90 days to recover it.

Remove account State in favor of setting Delete
  • Loading branch information
dschofie committed May 30, 2024
1 parent b751a43 commit 19a7ce2
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 48 deletions.
8 changes: 5 additions & 3 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (
)

var (
tag string
targets string
stacks string
tag string
targets string
stacks string
allowDeleteAccount bool

// TUI
useTUI bool
Expand All @@ -29,6 +30,7 @@ func init() {
deployCmd.Flags().StringVar(&targets, "targets", "", "Filter resource types to deploy. Options: organization, scp, stacks")
deployCmd.Flags().StringVar(&orgFile, "org", "organization.yml", "Path to the organization.yml file")
deployCmd.Flags().BoolVar(&useTUI, "tui", false, "use the TUI for deploy")
deployCmd.Flags().BoolVar(&allowDeleteAccount, "allow-account-delete", false, "Allow closing an AWS account")
}

var deployCmd = &cobra.Command{
Expand Down
5 changes: 3 additions & 2 deletions cmd/provisionaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func init() {
rootCmd.AddCommand(accountProvision)
accountProvision.Flags().StringVar(&orgFile, "org", "organization.yml", "Path to the organization.yml file")
accountProvision.Flags().BoolVar(&useTUI, "tui", false, "use the TUI for diff")
accountProvision.Flags().BoolVar(&allowDeleteAccount, "allow-account-delete", false, "Allow closing an AWS account")
}

func isValidAccountArg(arg string) bool {
Expand Down Expand Up @@ -92,7 +93,7 @@ func processOrg(consoleUI runner.ConsoleUI, cmd string) {
if cmd == "diff" {
consoleUI.Print("Diffing AWS Organization", *mgmtAcct)
orgOps := resourceoperation.CollectOrganizationUnitOps(
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Diff,
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Diff, allowDeleteAccount,
)
for _, op := range resourceoperation.FlattenOperations(orgOps) {
consoleUI.Print(op.ToString(), *mgmtAcct)
Expand All @@ -105,7 +106,7 @@ func processOrg(consoleUI runner.ConsoleUI, cmd string) {
if cmd == "deploy" {
consoleUI.Print("Diffing AWS Organization", *mgmtAcct)
orgOps := resourceoperation.CollectOrganizationUnitOps(
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Deploy,
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Deploy, allowDeleteAccount,
)

for _, op := range resourceoperation.FlattenOperations(orgOps) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func ProcessOrgEndToEnd(consoleUI runner.ConsoleUI, cmd int, targets []string) e

if len(targets) == 0 || deployOrganization {
orgOps := resourceoperation.CollectOrganizationUnitOps(
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, cmd,
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, cmd, allowDeleteAccount,
)
for _, op := range resourceoperation.FlattenOperations(orgOps) {
consoleUI.Print(op.ToString(), *mgmtAcct)
Expand Down
20 changes: 9 additions & 11 deletions lib/awsorgs/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,19 +388,16 @@ func (c Client) CreateAccount(
}
}

func (c Client) CloseAccounts(ctx context.Context, accts []*organizations.Account) []error {
var errs []error
for _, acct := range accts {
fmt.Printf("Closing Account: %s Email: %s\n", *acct.Name, *acct.Email)
_, err := c.organizationClient.CloseAccountWithContext(ctx, &organizations.CloseAccountInput{
AccountId: acct.Id,
})
if err != nil {
errs = append(errs, err)
}
func (c Client) CloseAccount(ctx context.Context, acctID, acctName, acctEmail string) error {
fmt.Printf("Closing Account: %s Email: %s\n", acctName, acctEmail)
_, err := c.organizationClient.CloseAccountWithContext(ctx, &organizations.CloseAccountInput{
AccountId: &acctID,
})
if err != nil {
return oops.Wrapf(err, "closing account")
}

return errs
return nil
}

func (c Client) GetRootId() (string, error) {
Expand Down Expand Up @@ -471,6 +468,7 @@ func (c Client) FetchOUAndDescendents(ctx context.Context, ouID, mgmtAccountID s
Email: *providerAcct.Email,
Parent: &ou,
AccountName: *providerAcct.Name,
Status: aws.StringValue(providerAcct.Status),
}
if *providerAcct.Id == mgmtAccountID {
acct.ManagementAccount = true
Expand Down
19 changes: 2 additions & 17 deletions lib/ymlparser/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/organizations"
"github.com/samsarahq/go/oops"
"github.com/santiago-labs/telophasecli/lib/awsorgs"
Expand Down Expand Up @@ -116,6 +117,7 @@ func (p Parser) HydrateParsedOrg(ctx context.Context, parsedOrg *resource.Organi
for _, parsedAcct := range parsedOrg.AllDescendentAccounts() {
if parsedAcct.Email == *providerAcct.Email {
parsedAcct.AccountID = *providerAcct.Id
parsedAcct.Status = aws.StringValue(providerAcct.Status)
}
if parsedAcct.Email == mgmtAcct.Email {
parsedAcct.ManagementAccount = true
Expand Down Expand Up @@ -230,15 +232,7 @@ func WriteOrgFile(filepath string, org *resource.OrganizationUnit) error {
func validOrganization(data resource.OrganizationUnit) error {
accountEmails := map[string]struct{}{}

validStates := []string{"delete", ""}
for _, account := range data.AllDescendentAccounts() {
if ok := isOneOf(account.State,
"delete",
"",
); !ok {
return fmt.Errorf("invalid state (%s) for account %s valid states are: empty string or %v", account.State, account.AccountName, validStates)
}

if _, ok := accountEmails[account.Email]; ok {
return fmt.Errorf("duplicate account email %s", account.Email)
} else {
Expand All @@ -261,15 +255,6 @@ func validOrganization(data resource.OrganizationUnit) error {
return nil
}

func isOneOf(s string, valid ...string) bool {
for _, v := range valid {
if s == v {
return true
}
}
return false
}

func fileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
Expand Down
3 changes: 2 additions & 1 deletion mintlifydocs/config/organization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ Organization:
Accounts:
- Email: # (Required) Email used to create the account. This will be the root user for this account.
AccountName: # (Required) Name of the account.
Delete: # (Optional) Set to true if you want telophase to close the account, after closing an account it can be removed from organizations.yml.
# If deleting an account you need to pass in --allow-account-delete to telophasecli as a confirmation of the deletion.
Tags: # (Optional) Telophase label for this account. Tags translate to AWS tags with a `=` as the key value delimiter. For example, `telophase:env=prod`
Stacks: # (Optional) Terraform, Cloudformation and CDK stacks to apply to all accounts in this Organization Unit.
State: # (Optional) Can be set to `deleted` to delete an account. Experimental.
```
## Example
Expand Down
17 changes: 10 additions & 7 deletions resource/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ import (
type Account struct {
Email string `yaml:"Email"`
AccountName string `yaml:"AccountName"`
State string `yaml:"State,omitempty"`
AccountID string `yaml:"-"`

AssumeRoleName string `yaml:"AssumeRoleName,omitempty"`
Tags []string `yaml:"Tags,omitempty"`
AWSTags []string `yaml:"-"`
BaselineStacks []Stack `yaml:"Stacks,omitempty"`
ServiceControlPolicies []Stack `yaml:"ServiceControlPolicies,omitempty"`
ManagementAccount bool `yaml:"-"`
AssumeRoleName string `yaml:"AssumeRoleName,omitempty"`
Tags []string `yaml:"Tags,omitempty"`
AWSTags []string `yaml:"-"`
BaselineStacks []Stack `yaml:"Stacks,omitempty"`
ServiceControlPolicies []Stack `yaml:"ServiceControlPolicies,omitempty"`
ManagementAccount bool `yaml:"-"`

Delete bool `yaml:"Delete"`
DelegatedAdministrator bool `yaml:"DelegatedAdministrator,omitempty"`
Parent *OrganizationUnit `yaml:"-"`

Status string `yaml:"-,omitempty"`
}

func (a Account) AssumeRoleARN() string {
Expand Down
29 changes: 28 additions & 1 deletion resourceoperation/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package resourceoperation
import (
"bytes"
"context"
"fmt"
"log"
"text/template"

Expand All @@ -24,6 +25,7 @@ type accountOperation struct {
ConsoleUI runner.ConsoleUI
OrgClient *awsorgs.Client
TagsDiff *TagsDiff
AllowDelete bool
}

func NewAccountOperation(
Expand All @@ -34,7 +36,7 @@ func NewAccountOperation(
newParent *resource.OrganizationUnit,
currentParent *resource.OrganizationUnit,
tagsDiff *TagsDiff,
) ResourceOperation {
) *accountOperation {

return &accountOperation{
Account: account,
Expand All @@ -48,6 +50,10 @@ func NewAccountOperation(
}
}

func (ao *accountOperation) SetAllowDelete(allowDelete bool) {
ao.AllowDelete = allowDelete
}

func CollectAccountOps(
ctx context.Context,
consoleUI runner.ConsoleUI,
Expand Down Expand Up @@ -131,6 +137,16 @@ func (ao *accountOperation) Call(ctx context.Context) error {
}

ao.ConsoleUI.Print("Updated Tags", *ao.Account)
} else if ao.Operation == Delete {
if !ao.AllowDelete {
return fmt.Errorf("attempting to delete account: (name:%s email:%s id:%s) stopping because --allow-account-delete is not passed into telophasecli", ao.Account.AccountName, ao.Account.Email, ao.Account.AccountID)
}

// Stacks need to be cleaned up from an AWS account before its closed.
err := ao.OrgClient.CloseAccount(ctx, ao.Account.AccountID, ao.Account.AccountName, ao.Account.Email)
if err != nil {
return oops.Wrapf(err, "CloseAccounts")
}
}

for _, op := range ao.DependentOperations {
Expand Down Expand Up @@ -170,6 +186,17 @@ Email: {{ .Account.Email }}
~ Parent Name: {{ .CurrentParent.Name }} -> {{ .NewParent.Name }}
`
} else if ao.Operation == Delete {
printColor = "red"
includeDeleteStr := ""
if !ao.AllowDelete {
includeDeleteStr = " To ensure deletion run telophasecli with --allow-account-delete flag"
}
templated = "\n" + fmt.Sprintf(`(DELETE ACCOUNT)%s
- Name: {{ .Account.AccountName }}
- Email: {{ .Account.Email }}
- ID: {{ .Account.ID }}
`, includeDeleteStr)
} else if ao.Operation == UpdateTags {
// We need to compute which tags have changed
templated = "\n" + `(Updating Account Tags)
Expand Down
1 change: 1 addition & 0 deletions resourceoperation/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
Create = 2
Update = 3
UpdateTags = 6
Delete = 7

// IaC
Diff = 4
Expand Down
29 changes: 26 additions & 3 deletions resourceoperation/organization_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewOrganizationUnitOperation(
currentParent *resource.OrganizationUnit,
newName *string,
tagsDiff *TagsDiff,
) ResourceOperation {
) *organizationUnitOperation {

return &organizationUnitOperation{
OrgClient: orgClient,
Expand All @@ -64,6 +64,7 @@ func CollectOrganizationUnitOps(
mgmtAcct *resource.Account,
rootOU *resource.OrganizationUnit,
op int,
allowDelete bool,
) []ResourceOperation {

// Order of operations matters. Groups must be Created first, followed by account creation,
Expand Down Expand Up @@ -122,7 +123,6 @@ func CollectOrganizationUnitOps(

added, removed := diffTags(parsedOU)
if len(added) > 0 || len(removed) > 0 {
fmt.Println("adding new", removed, added)
operations = append(operations, NewOrganizationUnitOperation(
orgClient,
consoleUI,
Expand Down Expand Up @@ -221,7 +221,6 @@ func CollectOrganizationUnitOps(

added, removed := diffTags(parsedAcct)
if len(added) > 0 || len(removed) > 0 {
fmt.Println("added, removed ", added, removed)
operations = append(operations, NewAccountOperation(
orgClient,
consoleUI,
Expand All @@ -237,6 +236,21 @@ func CollectOrganizationUnitOps(
))
}

if found && parsedAcct.Delete && !oneOf(parsedAcct.Status, []string{"SUSPENDED", "CLOSED", "ENDED"}) {
op := NewAccountOperation(
orgClient,
consoleUI,
parsedAcct,
mgmtAcct,
Delete,
nil,
nil,
nil,
)
op.SetAllowDelete(allowDelete)
operations = append(operations, op)
}

break
}
}
Expand Down Expand Up @@ -480,3 +494,12 @@ func ignorableTag(tag string) bool {
_, ok := ignorableTags[tag]
return ok
}

func oneOf(check string, slc []string) bool {
for _, s := range slc {
if s == check {
return true
}
}
return false
}
2 changes: 1 addition & 1 deletion tests/end2end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,7 @@ func TestEndToEnd(t *testing.T) {

ymlparser.NewParser(orgClient).HydrateParsedOrg(ctx, test.OrgInitialState)
orgOps := resourceoperation.CollectOrganizationUnitOps(
ctx, consoleUI, orgClient, mgmtAcct, test.OrgInitialState, resourceoperation.Deploy,
ctx, consoleUI, orgClient, mgmtAcct, test.OrgInitialState, resourceoperation.Deploy, false,
)
for _, op := range orgOps {
err := op.Call(ctx)
Expand Down
2 changes: 1 addition & 1 deletion tests/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func compareOrganizationUnits(t *testing.T, expected, actual *resource.Organizat
func compareAccounts(t *testing.T, expected, actual *resource.Account, ignoreStacks bool) {
assert.Equal(t, expected.Email, actual.Email, "Account Emails not equal")
assert.Equal(t, expected.AccountName, actual.AccountName, "Account Name not equal")
assert.Equal(t, expected.State, actual.State, "Account State not equal")
assert.Equal(t, expected.Delete, actual.Delete, "Account delete not equal")
assert.Equal(t, expected.AssumeRoleName, actual.AssumeRoleName, "Account AssumeRoleName not equal")
assert.Equal(t, expected.ManagementAccount, actual.ManagementAccount, "Account ManagementAccount not equal")
assert.Equal(t, expected.Tags, actual.Tags, "Account Tags not equal")
Expand Down

0 comments on commit 19a7ce2

Please sign in to comment.