Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resourceop account: add Delete to an account for closing it #82

Merged
merged 1 commit into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading