Skip to content

Commit 19a7ce2

Browse files
committed
resourceop account: add Delete to an account for closing it
This will close the account and by default have 90 days to recover it. Remove account State in favor of setting Delete
1 parent b751a43 commit 19a7ce2

File tree

12 files changed

+89
-48
lines changed

12 files changed

+89
-48
lines changed

cmd/deploy.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import (
1414
)
1515

1616
var (
17-
tag string
18-
targets string
19-
stacks string
17+
tag string
18+
targets string
19+
stacks string
20+
allowDeleteAccount bool
2021

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

3436
var deployCmd = &cobra.Command{

cmd/provisionaccounts.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func init() {
2020
rootCmd.AddCommand(accountProvision)
2121
accountProvision.Flags().StringVar(&orgFile, "org", "organization.yml", "Path to the organization.yml file")
2222
accountProvision.Flags().BoolVar(&useTUI, "tui", false, "use the TUI for diff")
23+
accountProvision.Flags().BoolVar(&allowDeleteAccount, "allow-account-delete", false, "Allow closing an AWS account")
2324
}
2425

2526
func isValidAccountArg(arg string) bool {
@@ -92,7 +93,7 @@ func processOrg(consoleUI runner.ConsoleUI, cmd string) {
9293
if cmd == "diff" {
9394
consoleUI.Print("Diffing AWS Organization", *mgmtAcct)
9495
orgOps := resourceoperation.CollectOrganizationUnitOps(
95-
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Diff,
96+
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Diff, allowDeleteAccount,
9697
)
9798
for _, op := range resourceoperation.FlattenOperations(orgOps) {
9899
consoleUI.Print(op.ToString(), *mgmtAcct)
@@ -105,7 +106,7 @@ func processOrg(consoleUI runner.ConsoleUI, cmd string) {
105106
if cmd == "deploy" {
106107
consoleUI.Print("Diffing AWS Organization", *mgmtAcct)
107108
orgOps := resourceoperation.CollectOrganizationUnitOps(
108-
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Deploy,
109+
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, resourceoperation.Deploy, allowDeleteAccount,
109110
)
110111

111112
for _, op := range resourceoperation.FlattenOperations(orgOps) {

cmd/root.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func ProcessOrgEndToEnd(consoleUI runner.ConsoleUI, cmd int, targets []string) e
8383

8484
if len(targets) == 0 || deployOrganization {
8585
orgOps := resourceoperation.CollectOrganizationUnitOps(
86-
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, cmd,
86+
ctx, consoleUI, orgClient, mgmtAcct, rootAWSOU, cmd, allowDeleteAccount,
8787
)
8888
for _, op := range resourceoperation.FlattenOperations(orgOps) {
8989
consoleUI.Print(op.ToString(), *mgmtAcct)

lib/awsorgs/organization.go

+9-11
Original file line numberDiff line numberDiff line change
@@ -388,19 +388,16 @@ func (c Client) CreateAccount(
388388
}
389389
}
390390

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

403-
return errs
400+
return nil
404401
}
405402

406403
func (c Client) GetRootId() (string, error) {
@@ -471,6 +468,7 @@ func (c Client) FetchOUAndDescendents(ctx context.Context, ouID, mgmtAccountID s
471468
Email: *providerAcct.Email,
472469
Parent: &ou,
473470
AccountName: *providerAcct.Name,
471+
Status: aws.StringValue(providerAcct.Status),
474472
}
475473
if *providerAcct.Id == mgmtAccountID {
476474
acct.ManagementAccount = true

lib/ymlparser/organization.go

+2-17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io/ioutil"
88
"os"
99

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

233-
validStates := []string{"delete", ""}
234235
for _, account := range data.AllDescendentAccounts() {
235-
if ok := isOneOf(account.State,
236-
"delete",
237-
"",
238-
); !ok {
239-
return fmt.Errorf("invalid state (%s) for account %s valid states are: empty string or %v", account.State, account.AccountName, validStates)
240-
}
241-
242236
if _, ok := accountEmails[account.Email]; ok {
243237
return fmt.Errorf("duplicate account email %s", account.Email)
244238
} else {
@@ -261,15 +255,6 @@ func validOrganization(data resource.OrganizationUnit) error {
261255
return nil
262256
}
263257

264-
func isOneOf(s string, valid ...string) bool {
265-
for _, v := range valid {
266-
if s == v {
267-
return true
268-
}
269-
}
270-
return false
271-
}
272-
273258
func fileExists(filename string) bool {
274259
_, err := os.Stat(filename)
275260
if os.IsNotExist(err) {

mintlifydocs/config/organization.mdx

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ Organization:
4545
Accounts:
4646
- Email: # (Required) Email used to create the account. This will be the root user for this account.
4747
AccountName: # (Required) Name of the account.
48+
Delete: # (Optional) Set to true if you want telophase to close the account, after closing an account it can be removed from organizations.yml.
49+
# If deleting an account you need to pass in --allow-account-delete to telophasecli as a confirmation of the deletion.
4850
Tags: # (Optional) Telophase label for this account. Tags translate to AWS tags with a `=` as the key value delimiter. For example, `telophase:env=prod`
4951
Stacks: # (Optional) Terraform, Cloudformation and CDK stacks to apply to all accounts in this Organization Unit.
50-
State: # (Optional) Can be set to `deleted` to delete an account. Experimental.
5152
```
5253
5354
## Example

resource/account.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@ import (
1313
type Account struct {
1414
Email string `yaml:"Email"`
1515
AccountName string `yaml:"AccountName"`
16-
State string `yaml:"State,omitempty"`
1716
AccountID string `yaml:"-"`
1817

19-
AssumeRoleName string `yaml:"AssumeRoleName,omitempty"`
20-
Tags []string `yaml:"Tags,omitempty"`
21-
AWSTags []string `yaml:"-"`
22-
BaselineStacks []Stack `yaml:"Stacks,omitempty"`
23-
ServiceControlPolicies []Stack `yaml:"ServiceControlPolicies,omitempty"`
24-
ManagementAccount bool `yaml:"-"`
18+
AssumeRoleName string `yaml:"AssumeRoleName,omitempty"`
19+
Tags []string `yaml:"Tags,omitempty"`
20+
AWSTags []string `yaml:"-"`
21+
BaselineStacks []Stack `yaml:"Stacks,omitempty"`
22+
ServiceControlPolicies []Stack `yaml:"ServiceControlPolicies,omitempty"`
23+
ManagementAccount bool `yaml:"-"`
24+
25+
Delete bool `yaml:"Delete"`
2526
DelegatedAdministrator bool `yaml:"DelegatedAdministrator,omitempty"`
2627
Parent *OrganizationUnit `yaml:"-"`
28+
29+
Status string `yaml:"-,omitempty"`
2730
}
2831

2932
func (a Account) AssumeRoleARN() string {

resourceoperation/account.go

+28-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package resourceoperation
33
import (
44
"bytes"
55
"context"
6+
"fmt"
67
"log"
78
"text/template"
89

@@ -24,6 +25,7 @@ type accountOperation struct {
2425
ConsoleUI runner.ConsoleUI
2526
OrgClient *awsorgs.Client
2627
TagsDiff *TagsDiff
28+
AllowDelete bool
2729
}
2830

2931
func NewAccountOperation(
@@ -34,7 +36,7 @@ func NewAccountOperation(
3436
newParent *resource.OrganizationUnit,
3537
currentParent *resource.OrganizationUnit,
3638
tagsDiff *TagsDiff,
37-
) ResourceOperation {
39+
) *accountOperation {
3840

3941
return &accountOperation{
4042
Account: account,
@@ -48,6 +50,10 @@ func NewAccountOperation(
4850
}
4951
}
5052

53+
func (ao *accountOperation) SetAllowDelete(allowDelete bool) {
54+
ao.AllowDelete = allowDelete
55+
}
56+
5157
func CollectAccountOps(
5258
ctx context.Context,
5359
consoleUI runner.ConsoleUI,
@@ -131,6 +137,16 @@ func (ao *accountOperation) Call(ctx context.Context) error {
131137
}
132138

133139
ao.ConsoleUI.Print("Updated Tags", *ao.Account)
140+
} else if ao.Operation == Delete {
141+
if !ao.AllowDelete {
142+
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)
143+
}
144+
145+
// Stacks need to be cleaned up from an AWS account before its closed.
146+
err := ao.OrgClient.CloseAccount(ctx, ao.Account.AccountID, ao.Account.AccountName, ao.Account.Email)
147+
if err != nil {
148+
return oops.Wrapf(err, "CloseAccounts")
149+
}
134150
}
135151

136152
for _, op := range ao.DependentOperations {
@@ -170,6 +186,17 @@ Email: {{ .Account.Email }}
170186
~ Parent Name: {{ .CurrentParent.Name }} -> {{ .NewParent.Name }}
171187
172188
`
189+
} else if ao.Operation == Delete {
190+
printColor = "red"
191+
includeDeleteStr := ""
192+
if !ao.AllowDelete {
193+
includeDeleteStr = " To ensure deletion run telophasecli with --allow-account-delete flag"
194+
}
195+
templated = "\n" + fmt.Sprintf(`(DELETE ACCOUNT)%s
196+
- Name: {{ .Account.AccountName }}
197+
- Email: {{ .Account.Email }}
198+
- ID: {{ .Account.ID }}
199+
`, includeDeleteStr)
173200
} else if ao.Operation == UpdateTags {
174201
// We need to compute which tags have changed
175202
templated = "\n" + `(Updating Account Tags)

resourceoperation/interface.go

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const (
1010
Create = 2
1111
Update = 3
1212
UpdateTags = 6
13+
Delete = 7
1314

1415
// IaC
1516
Diff = 4

resourceoperation/organization_unit.go

+26-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func NewOrganizationUnitOperation(
4242
currentParent *resource.OrganizationUnit,
4343
newName *string,
4444
tagsDiff *TagsDiff,
45-
) ResourceOperation {
45+
) *organizationUnitOperation {
4646

4747
return &organizationUnitOperation{
4848
OrgClient: orgClient,
@@ -64,6 +64,7 @@ func CollectOrganizationUnitOps(
6464
mgmtAcct *resource.Account,
6565
rootOU *resource.OrganizationUnit,
6666
op int,
67+
allowDelete bool,
6768
) []ResourceOperation {
6869

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

123124
added, removed := diffTags(parsedOU)
124125
if len(added) > 0 || len(removed) > 0 {
125-
fmt.Println("adding new", removed, added)
126126
operations = append(operations, NewOrganizationUnitOperation(
127127
orgClient,
128128
consoleUI,
@@ -221,7 +221,6 @@ func CollectOrganizationUnitOps(
221221

222222
added, removed := diffTags(parsedAcct)
223223
if len(added) > 0 || len(removed) > 0 {
224-
fmt.Println("added, removed ", added, removed)
225224
operations = append(operations, NewAccountOperation(
226225
orgClient,
227226
consoleUI,
@@ -237,6 +236,21 @@ func CollectOrganizationUnitOps(
237236
))
238237
}
239238

239+
if found && parsedAcct.Delete && !oneOf(parsedAcct.Status, []string{"SUSPENDED", "CLOSED", "ENDED"}) {
240+
op := NewAccountOperation(
241+
orgClient,
242+
consoleUI,
243+
parsedAcct,
244+
mgmtAcct,
245+
Delete,
246+
nil,
247+
nil,
248+
nil,
249+
)
250+
op.SetAllowDelete(allowDelete)
251+
operations = append(operations, op)
252+
}
253+
240254
break
241255
}
242256
}
@@ -480,3 +494,12 @@ func ignorableTag(tag string) bool {
480494
_, ok := ignorableTags[tag]
481495
return ok
482496
}
497+
498+
func oneOf(check string, slc []string) bool {
499+
for _, s := range slc {
500+
if s == check {
501+
return true
502+
}
503+
}
504+
return false
505+
}

tests/end2end_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ func TestEndToEnd(t *testing.T) {
10551055

10561056
ymlparser.NewParser(orgClient).HydrateParsedOrg(ctx, test.OrgInitialState)
10571057
orgOps := resourceoperation.CollectOrganizationUnitOps(
1058-
ctx, consoleUI, orgClient, mgmtAcct, test.OrgInitialState, resourceoperation.Deploy,
1058+
ctx, consoleUI, orgClient, mgmtAcct, test.OrgInitialState, resourceoperation.Deploy, false,
10591059
)
10601060
for _, op := range orgOps {
10611061
err := op.Call(ctx)

tests/main_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ func compareOrganizationUnits(t *testing.T, expected, actual *resource.Organizat
215215
func compareAccounts(t *testing.T, expected, actual *resource.Account, ignoreStacks bool) {
216216
assert.Equal(t, expected.Email, actual.Email, "Account Emails not equal")
217217
assert.Equal(t, expected.AccountName, actual.AccountName, "Account Name not equal")
218-
assert.Equal(t, expected.State, actual.State, "Account State not equal")
218+
assert.Equal(t, expected.Delete, actual.Delete, "Account delete not equal")
219219
assert.Equal(t, expected.AssumeRoleName, actual.AssumeRoleName, "Account AssumeRoleName not equal")
220220
assert.Equal(t, expected.ManagementAccount, actual.ManagementAccount, "Account ManagementAccount not equal")
221221
assert.Equal(t, expected.Tags, actual.Tags, "Account Tags not equal")

0 commit comments

Comments
 (0)