diff --git a/cmd/root.go b/cmd/root.go index 690a1bb..5773786 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/samsarahq/go/oops" "github.com/santiago-labs/telophasecli/cmd/runner" "github.com/santiago-labs/telophasecli/lib/awsorgs" "github.com/santiago-labs/telophasecli/lib/metrics" @@ -162,7 +163,7 @@ func resolveMgmtAcct( fetchedMgmtAcct, err := orgClient.FetchManagementAccount(ctx) if err != nil { - return nil, err + return nil, oops.Wrapf(err, "FetchManagementAccount") } return fetchedMgmtAcct, nil } diff --git a/lib/awsorgs/organization.go b/lib/awsorgs/organization.go index 1042599..e65e2fd 100644 --- a/lib/awsorgs/organization.go +++ b/lib/awsorgs/organization.go @@ -141,6 +141,28 @@ func (c Client) GetOrganizationUnit(ctx context.Context, OUId string) (*organiza return out.OrganizationalUnit, nil } +func (c Client) GetTags(ctx context.Context, id string) ([]string, error) { + var tags []string + err := c.organizationClient.ListTagsForResourcePagesWithContext(ctx, &organizations.ListTagsForResourceInput{ + ResourceId: &id, + }, + func(page *organizations.ListTagsForResourceOutput, lastPage bool) bool { + for _, tag := range page.Tags { + if aws.StringValue(tag.Value) != "" { + tags = append(tags, aws.StringValue(tag.Key)+"="+aws.StringValue(tag.Value)) + } else { + tags = append(tags, aws.StringValue(tag.Key)) + } + } + return !lastPage + }, + ) + if err != nil { + return nil, oops.Wrapf(err, "listing tags for id: %s", id) + } + return tags, nil +} + func (c Client) GetOrganizationUnitChildren(ctx context.Context, OUId string) ([]*organizations.OrganizationalUnit, error) { var childOUs []*organizations.OrganizationalUnit @@ -189,11 +211,13 @@ func (c Client) CreateOrganizationUnit( consoleUI runner.ConsoleUI, mgmtAcct resource.Account, ouName, newParentId string, + tags []string, ) (*organizations.OrganizationalUnit, error) { consoleUI.Print(fmt.Sprintf("Creating OU: Name=%s\n", ouName), mgmtAcct) out, err := c.organizationClient.CreateOrganizationalUnitWithContext(ctx, &organizations.CreateOrganizationalUnitInput{ Name: &ouName, ParentId: &newParentId, + Tags: buildTags(tags), }) if err != nil { return nil, err @@ -207,8 +231,9 @@ func (c Client) RecreateOU( consoleUI runner.ConsoleUI, mgmtAcct resource.Account, ouID, ouName, newParentId string, + tags []string, ) error { - newOU, err := c.CreateOrganizationUnit(ctx, consoleUI, mgmtAcct, ouName, newParentId) + newOU, err := c.CreateOrganizationUnit(ctx, consoleUI, mgmtAcct, ouName, newParentId, tags) if err != nil { return err } @@ -229,7 +254,7 @@ func (c Client) RecreateOU( return err } for _, childOU := range childOUs { - err := c.RecreateOU(ctx, consoleUI, mgmtAcct, *childOU.Id, *childOU.Name, *newOU.Id) + err := c.RecreateOU(ctx, consoleUI, mgmtAcct, *childOU.Id, *childOU.Name, *newOU.Id, tags) if err != nil { return err } @@ -248,22 +273,71 @@ func (c Client) UpdateOrganizationUnit(ctx context.Context, ouID, newName string return err } +func buildTags(tags []string) []*organizations.Tag { + var awsTags []*organizations.Tag + for _, t := range tags { + parts := strings.Split(t, "=") + key := parts[0] + value := "" + if len(parts) == 2 { + value = parts[1] + } + + awsTags = append(awsTags, &organizations.Tag{ + Key: aws.String(key), + Value: aws.String(value), + }) + } + + return awsTags +} + +func (c Client) TagResource(ctx context.Context, id string, tags []string) error { + _, err := c.organizationClient.TagResourceWithContext(ctx, + &organizations.TagResourceInput{ + ResourceId: aws.String(id), + Tags: buildTags(tags), + }) + if err != nil { + return oops.Wrapf(err, "tagging: %s", id) + } + return nil +} + +func (c Client) UntagResources(ctx context.Context, id string, tags []string) error { + if len(tags) == 0 { + return nil + } + var tagKeys []*string + for _, t := range tags { + parts := strings.Split(t, "=") + key := parts[0] + tagKeys = append(tagKeys, &key) + } + + _, err := c.organizationClient.UntagResourceWithContext(ctx, + &organizations.UntagResourceInput{ + ResourceId: aws.String(id), + TagKeys: tagKeys, + }) + if err != nil { + return oops.Wrapf(err, "tagging: %s", id) + } + return nil +} + func (c Client) CreateAccount( ctx context.Context, consoleUI runner.ConsoleUI, mgmtAcct resource.Account, acct *organizations.Account, + tags []string, ) (string, error) { consoleUI.Print(fmt.Sprintf("Creating Account: Name=%s Email=%s\n", *acct.Name, *acct.Email), mgmtAcct) out, err := c.organizationClient.CreateAccount(&organizations.CreateAccountInput{ AccountName: acct.Name, Email: acct.Email, - Tags: []*organizations.Tag{ - { - Key: aws.String("TelophaseManaged"), - Value: aws.String("true"), - }, - }, + Tags: buildTags(tags), }) if err != nil { consoleUI.Print(fmt.Sprintf("Error creating account: %s.\n", err.Error()), mgmtAcct) diff --git a/lib/ymlparser/organization.go b/lib/ymlparser/organization.go index 7f5abbb..e2f74bd 100644 --- a/lib/ymlparser/organization.go +++ b/lib/ymlparser/organization.go @@ -91,6 +91,9 @@ func (p Parser) HydrateParsedOrg(ctx context.Context, parsedOrg *resource.Organi } } + // We have to hydrate tags after account ID to get the right tags. + p.hydrateTags(ctx, parsedOrg) + return nil } @@ -143,6 +146,28 @@ func (p Parser) hydrateOUID(parsedOU *resource.OrganizationUnit, providerOU *org return nil } +func (p Parser) hydrateTags(ctx context.Context, ou *resource.OrganizationUnit) error { + tags, err := p.orgClient.GetTags(ctx, ou.ID()) + if err != nil { + return oops.Wrapf(err, "GetTags OUID: %s", ou.ID()) + } + ou.AWSTags = tags + + for _, parsedChild := range ou.AllDescendentOUs() { + p.hydrateTags(ctx, parsedChild) + } + + for _, acct := range ou.AllDescendentAccounts() { + tags, err := p.orgClient.GetTags(ctx, acct.ID()) + if err != nil { + return oops.Wrapf(err, "GetTags Account ID: %s", acct.ID()) + } + acct.AWSTags = tags + } + + return nil +} + func hydrateOUParent(parsedOU *resource.OrganizationUnit) { for _, parsedChild := range parsedOU.ChildOUs { parsedChild.Parent = parsedOU diff --git a/mintlifydocs/commands/deploy.mdx b/mintlifydocs/commands/deploy.mdx index 471f0d5..759277a 100644 --- a/mintlifydocs/commands/deploy.mdx +++ b/mintlifydocs/commands/deploy.mdx @@ -37,7 +37,7 @@ Organization: Name: "Default IAM Roles for CI" # Tags are specified here Tags: - - "production" + - "env=production" Accounts: - Email: production+us0@example.com AccountName: US0 @@ -64,6 +64,6 @@ Organization: ``` ## Using Tags -Running `telophasecli deploy --tag=production` will only deploy terraform and CDK changes for the accounts named `US0`, `US1`, `US2`, `US3`. The resulting TUI looks like: +Running `telophasecli deploy --tag="env=production"` will only deploy terraform and CDK changes for the accounts named `US0`, `US1`, `US2`, `US3`. The resulting TUI looks like: diff --git a/mintlifydocs/config/organization.mdx b/mintlifydocs/config/organization.mdx index 64ad36d..a17744a 100644 --- a/mintlifydocs/config/organization.mdx +++ b/mintlifydocs/config/organization.mdx @@ -45,7 +45,7 @@ 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. - Tags: # (Optional) Telophase label for this account. + 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) CDK or Terraform stacks to apply to all accounts in this Organization Unit. State: # (Optional) Can be set to `deleted` to delete an account. Experimental. ``` @@ -126,7 +126,9 @@ This will run two separate applies in the `us-prod` account: 2. `tf/default-vpc` Terraform stack. # Tags -Tags can be used to perform operations on groups of accounts. `Account`s and `OrganizationUnits`s can be tagged. Tags do _not_ represent AWS `Tag`s. +Tags can be used to perform operations on groups of accounts. `Account`s and `OrganizationUnits`s can be tagged. Tags represent AWS `Tag`s. +Telophase Tags map to AWS tags with a key, value pair delimited by an `=`. For example, `env=dev` will translate to an AWS tag on an Account or OU with the key `env` and value `dev`. + Telophase commands optionally take tags as inputs, allowing you to limit the scope of the operation. @@ -136,13 +138,12 @@ Telophase commands optionally take tags as inputs, allowing you to limit the sco - Email: newdev+1@telophase.dev AccountName: newdev1 Tags: - - "dev" + - "env=dev" - Email: newdev+2@telophase.dev AccountName: newdev2 - Tags: - - "dev" + - Email: production@telophase.dev AccountName: production ``` -`telophasecli diff --tag dev` will show a `diff` for `newdev1` and `newdev2` accounts only. \ No newline at end of file +`telophasecli diff --tag "env=dev"` will show a `diff` for only the `newdev1` account. diff --git a/resource/account.go b/resource/account.go index d96adaa..4954b6d 100644 --- a/resource/account.go +++ b/resource/account.go @@ -18,6 +18,7 @@ type Account struct { 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:"-"` @@ -67,6 +68,41 @@ func (a Account) AllTags() []string { return tags } +func (a Account) AllAWSTags() []string { + var tags []string + tags = append(tags, a.AWSTags...) + if a.Parent != nil { + tags = append(tags, a.Parent.AllAWSTags()...) + } + return tags +} + +func (a Account) CurrentTags() []string { + // Default tags for every account + var tags []string + currTags := make(map[string]struct{}) + for _, tag := range a.Tags { + key := strings.Split(tag, "=")[0] + if _, exists := currTags[key]; exists { + panic(fmt.Sprintf("duplicate tag key: %s on account with email: %s", key, a.Email)) + } + } + + tags = append(tags, a.Tags...) + if a.Parent != nil { + for _, tag := range a.Parent.AllTags() { + key := strings.Split(tag, "=")[0] + if _, exists := currTags[key]; exists { + panic(fmt.Sprintf("duplicate tag key: %s on account with email : %s inherited from parent tree", key, a.Email)) + } + + tags = append(tags, tag) + } + } + + return tags +} + func (a Account) AllBaselineStacks() ([]Stack, error) { var stacks []Stack if a.Parent != nil { diff --git a/resource/organization_unit.go b/resource/organization_unit.go index 2661a46..b593808 100644 --- a/resource/organization_unit.go +++ b/resource/organization_unit.go @@ -6,6 +6,7 @@ type OrganizationUnit struct { ChildGroups []*OrganizationUnit `yaml:"AccountGroups,omitempty"` // Deprecated. Use `OrganizationUnits` ChildOUs []*OrganizationUnit `yaml:"OrganizationUnits,omitempty"` Tags []string `yaml:"Tags,omitempty"` + AWSTags []string `yaml:"-"` Accounts []*Account `yaml:"Accounts,omitempty"` BaselineStacks []Stack `yaml:"Stacks,omitempty"` ServiceControlPolicies []Stack `yaml:"ServiceControlPolicies,omitempty"` @@ -36,6 +37,15 @@ func (grp OrganizationUnit) AllTags() []string { return tags } +func (grp OrganizationUnit) AllAWSTags() []string { + var tags []string + tags = append(tags, grp.AWSTags...) + if grp.Parent != nil { + tags = append(tags, grp.Parent.AllAWSTags()...) + } + return tags +} + func (grp OrganizationUnit) AllBaselineStacks() []Stack { var stacks []Stack if grp.Parent != nil { diff --git a/resourceoperation/account.go b/resourceoperation/account.go index 394111e..31ba5bc 100644 --- a/resourceoperation/account.go +++ b/resourceoperation/account.go @@ -3,11 +3,12 @@ package resourceoperation import ( "bytes" "context" - "html/template" "log" + "text/template" "github.com/aws/aws-sdk-go/service/organizations" "github.com/fatih/color" + "github.com/samsarahq/go/oops" "github.com/santiago-labs/telophasecli/cmd/runner" "github.com/santiago-labs/telophasecli/lib/awsorgs" "github.com/santiago-labs/telophasecli/resource" @@ -22,6 +23,7 @@ type accountOperation struct { DependentOperations []ResourceOperation ConsoleUI runner.ConsoleUI OrgClient *awsorgs.Client + TagsDiff *TagsDiff } func NewAccountOperation( @@ -31,6 +33,7 @@ func NewAccountOperation( operation int, newParent *resource.OrganizationUnit, currentParent *resource.OrganizationUnit, + tagsDiff *TagsDiff, ) ResourceOperation { return &accountOperation{ @@ -41,6 +44,7 @@ func NewAccountOperation( ConsoleUI: consoleUI, OrgClient: &orgClient, MgmtAccount: mgmtAcct, + TagsDiff: tagsDiff, } } @@ -93,7 +97,7 @@ func (ao *accountOperation) Call(ctx context.Context) error { Email: &ao.Account.Email, Name: &ao.Account.AccountName, } - acctID, err := ao.OrgClient.CreateAccount(ctx, ao.ConsoleUI, *ao.MgmtAccount, acct) + acctID, err := ao.OrgClient.CreateAccount(ctx, ao.ConsoleUI, *ao.MgmtAccount, acct, ao.Account.AllTags()) if err != nil { return err } @@ -114,6 +118,17 @@ func (ao *accountOperation) Call(ctx context.Context) error { if err != nil { return err } + } else if ao.Operation == UpdateTags { + err := ao.OrgClient.TagResource(ctx, ao.Account.AccountID, ao.Account.AllTags()) + if err != nil { + return oops.Wrapf(err, "UpdateTags") + } + err = ao.OrgClient.UntagResources(ctx, ao.Account.AccountID, ao.TagsDiff.Removed) + if err != nil { + return oops.Wrapf(err, "UntagResources") + } + + ao.ConsoleUI.Print("Updated Tags", *ao.Account) } for _, op := range ao.DependentOperations { @@ -135,8 +150,15 @@ func (ao *accountOperation) ToString() string { + Email: {{ .Account.Email }} + Parent ID: {{ if .NewParent.ID }}{{ .NewParent.ID }}{{else}}{{end}} + Parent Name: {{ .NewParent.Name }} +` + if len(ao.Account.AllTags()) > 0 { + templated = templated + + `+ Tags: {{ range AllTags }} ++ - {{ . }}{{ end }} ` + + } } else if ao.Operation == UpdateParent { templated = "\n" + `(Update Account Parent) ID: {{ .Account.AccountID }} @@ -146,9 +168,39 @@ Email: {{ .Account.Email }} ~ Parent Name: {{ .CurrentParent.Name }} -> {{ .NewParent.Name }} ` + } else if ao.Operation == UpdateTags { + // We need to compute which tags have changed + templated = "\n" + `(Updating Account Tags) +ID: {{ .Account.AccountID }} +Name: {{ .Account.AccountName }} +Tags: ` + + if ao.TagsDiff.Added != nil { + templated = templated + `(Added Tags){{ range .TagsDiff.Added }} ++ {{ . }}{{ end }} +` + if ao.TagsDiff.Removed == nil { + printColor = "green" + } + } + + if ao.TagsDiff.Removed != nil { + templated = templated + `(Removed Tags){{ range .TagsDiff.Removed}} +- {{ . }}{{end}} +` + if ao.TagsDiff.Added == nil { + printColor = "red" + } + } } - tpl, err := template.New("operation").Parse(templated) + tpl, err := template.New("operation").Funcs(template.FuncMap{ + "AllTags": func() []string { + tags := ao.Account.AllTags() + + return tags + }, + }).Parse(templated) if err != nil { log.Fatal(err) } @@ -159,5 +211,8 @@ Email: {{ .Account.Email }} if printColor == "yellow" { return color.YellowString(buf.String()) } + if printColor == "red" { + return color.RedString(buf.String()) + } return color.GreenString(buf.String()) } diff --git a/resourceoperation/interface.go b/resourceoperation/interface.go index 5888108..6318fc7 100644 --- a/resourceoperation/interface.go +++ b/resourceoperation/interface.go @@ -9,6 +9,7 @@ const ( UpdateParent = 1 Create = 2 Update = 3 + UpdateTags = 6 // IaC Diff = 4 diff --git a/resourceoperation/organization_unit.go b/resourceoperation/organization_unit.go index c95324b..0cda770 100644 --- a/resourceoperation/organization_unit.go +++ b/resourceoperation/organization_unit.go @@ -4,8 +4,8 @@ import ( "bytes" "context" "fmt" - "html/template" "log" + "text/template" "github.com/fatih/color" "github.com/santiago-labs/telophasecli/cmd/runner" @@ -23,6 +23,13 @@ type organizationUnitOperation struct { OrgClient awsorgs.Client ConsoleUI runner.ConsoleUI DependentOperations []ResourceOperation + TagsDiff *TagsDiff +} + +// TagsDiff needs to be exported so it can be read by the template. +type TagsDiff struct { + Added []string + Removed []string } func NewOrganizationUnitOperation( @@ -34,6 +41,7 @@ func NewOrganizationUnitOperation( newParent *resource.OrganizationUnit, currentParent *resource.OrganizationUnit, newName *string, + tagsDiff *TagsDiff, ) ResourceOperation { return &organizationUnitOperation{ @@ -45,6 +53,7 @@ func NewOrganizationUnitOperation( CurrentParent: currentParent, NewName: newName, MgmtAccount: mgmtAcct, + TagsDiff: tagsDiff, } } @@ -90,6 +99,7 @@ func CollectOrganizationUnitOps( parsedOU.Parent, providerOU.Parent, nil, + nil, )) } } @@ -105,9 +115,29 @@ func CollectOrganizationUnitOps( parsedOU.Parent, providerOU.Parent, nil, + nil, ), ) } + + added, removed := diffTags(parsedOU) + if len(added) > 0 || len(removed) > 0 { + fmt.Println("adding new", removed, added) + operations = append(operations, NewOrganizationUnitOperation( + orgClient, + consoleUI, + parsedOU, + mgmtAcct, + UpdateTags, + parsedOU.Parent, + providerOU.Parent, + nil, + &TagsDiff{ + Added: added, + Removed: removed, + }, + )) + } break } } @@ -129,6 +159,7 @@ func CollectOrganizationUnitOps( parsedOU.Parent, nil, nil, + nil, )) } } @@ -143,6 +174,7 @@ func CollectOrganizationUnitOps( parsedOU.Parent, nil, nil, + nil, ), ) } @@ -170,6 +202,7 @@ func CollectOrganizationUnitOps( UpdateParent, parsedAcct.Parent, providerAcct.Parent, + nil, )) } } @@ -182,8 +215,28 @@ func CollectOrganizationUnitOps( UpdateParent, parsedAcct.Parent, providerAcct.Parent, + nil, + )) + } + + added, removed := diffTags(parsedAcct) + if len(added) > 0 || len(removed) > 0 { + fmt.Println("added, removed ", added, removed) + operations = append(operations, NewAccountOperation( + orgClient, + consoleUI, + parsedAcct, + mgmtAcct, + UpdateTags, + parsedAcct.Parent, + providerAcct.Parent, + &TagsDiff{ + Added: added, + Removed: removed, + }, )) } + break } } @@ -204,6 +257,7 @@ func CollectOrganizationUnitOps( Create, parsedAcct.Parent, nil, + nil, ) newOU.AddDependent(newAcct) } @@ -217,6 +271,7 @@ func CollectOrganizationUnitOps( Create, parsedAcct.Parent, nil, + nil, ) operations = append(operations, newAcct) } @@ -236,13 +291,13 @@ func (ou *organizationUnitOperation) ListDependents() []ResourceOperation { func (ou *organizationUnitOperation) Call(ctx context.Context) error { if ou.Operation == Create { - newOrg, err := ou.OrgClient.CreateOrganizationUnit(ctx, ou.ConsoleUI, *ou.MgmtAccount, ou.OrganizationUnit.OUName, *ou.OrganizationUnit.Parent.OUID) + newOrg, err := ou.OrgClient.CreateOrganizationUnit(ctx, ou.ConsoleUI, *ou.MgmtAccount, ou.OrganizationUnit.OUName, *ou.OrganizationUnit.Parent.OUID, ou.OrganizationUnit.AllTags()) if err != nil { return err } ou.OrganizationUnit.OUID = newOrg.Id } else if ou.Operation == UpdateParent { - err := ou.OrgClient.RecreateOU(ctx, ou.ConsoleUI, *ou.MgmtAccount, *ou.OrganizationUnit.OUID, ou.OrganizationUnit.OUName, *ou.OrganizationUnit.Parent.OUID) + err := ou.OrgClient.RecreateOU(ctx, ou.ConsoleUI, *ou.MgmtAccount, *ou.OrganizationUnit.OUID, ou.OrganizationUnit.OUName, *ou.OrganizationUnit.Parent.OUID, ou.OrganizationUnit.AllTags()) if err != nil { return err } @@ -251,6 +306,17 @@ func (ou *organizationUnitOperation) Call(ctx context.Context) error { if err != nil { return err } + } else if ou.Operation == UpdateTags { + err := ou.OrgClient.TagResource(ctx, *ou.OrganizationUnit.OUID, ou.OrganizationUnit.AllTags()) + if err != nil { + return err + } + err = ou.OrgClient.UntagResources(ctx, *ou.OrganizationUnit.OUID, ou.TagsDiff.Removed) + if err != nil { + return err + } + + runner.ConsoleUI.Print(ou.ConsoleUI, "updated tags", *ou.MgmtAccount) } for _, op := range ou.DependentOperations { @@ -270,9 +336,15 @@ func (ou *organizationUnitOperation) ToString() string { templated = "\n" + `(Create Organizational Unit) + Name: {{ .OrganizationUnit.Name }} + Parent ID: {{ if .NewParent.ID }}{{ .NewParent.ID }}{{else}}{{end}} -+ Parent Name: {{ .NewParent.Name }} - ++ Parent Name: {{ .NewParent.Name }}` + if len(ou.OrganizationUnit.AllTags()) > 0 { + templated = templated + "\n" + ` ++ Tags: {{ range AWSTags }} ++ - {{ . }}{{ end }} ` + + } + } else if ou.Operation == UpdateParent { templated = "\n" + `(Update Organizational Unit Parent) ID: {{ .OrganizationUnit.ID }} @@ -287,9 +359,34 @@ ID: {{ .OrganizationUnit.ID }} ~ Name: {{ .OrganizationUnit.Name }} -> {{ .NewName }} ` + } else if ou.Operation == UpdateTags { + templated = "\n" + `(Update OU Tags) +ID: {{ .OrganizationUnit.ID }} +Tags: ` + if ou.TagsDiff.Added != nil { + // TODO: ensure that just the tag line is green/red to more clearly denote the diff. Do this in account as well. + templated = templated + `(Adding Tags){{ range .TagsDiff.Added}} ++ {{ . }}{{ end }} +` + if ou.TagsDiff.Removed == nil { + printColor = "green" + } + } + if ou.TagsDiff.Removed != nil { + templated = templated + `(Removing Tags){{ range .TagsDiff.Removed }} +- {{ . }}{{ end }} +` + if ou.TagsDiff.Added == nil { + printColor = "red" + } + } } - tpl, err := template.New("operation").Parse(templated) + tpl, err := template.New("operation").Funcs(template.FuncMap{ + "AWSTags": func() []string { + return ou.OrganizationUnit.AllTags() + }, + }).Parse(templated) if err != nil { log.Fatal(err) } @@ -300,6 +397,9 @@ ID: {{ .OrganizationUnit.ID }} if printColor == "yellow" { return color.YellowString(buf.String()) } + if printColor == "red" { + return color.RedString(buf.String()) + } return color.GreenString(buf.String()) } @@ -313,3 +413,70 @@ func FlattenOperations(topList []ResourceOperation) []ResourceOperation { return finalOperations } + +type Taggable interface { + AllTags() []string + AllAWSTags() []string +} + +func diffTags(taggable Taggable) (added, removed []string) { + oldMap := make(map[string]struct{}) + for _, tag := range taggable.AllAWSTags() { + if ignorableTag(tag) { + continue + } + oldMap[tag] = struct{}{} + } + + taggableMap := make(map[string]struct{}) + for _, tag := range taggable.AllTags() { + taggableMap[tag] = struct{}{} + } + + for _, tag := range taggable.AllTags() { + if _, ok := oldMap[tag]; !ok { + if contains(added, tag) { + // There can be duplicates when tags are inherited from an OU + continue + } + // All the added tags don't exist in the + added = append(added, tag) + continue + } + } + + for _, tag := range taggable.AllAWSTags() { + if ignorableTag(tag) { + continue + } + if _, ok := taggableMap[tag]; !ok { + if contains(removed, tag) { + // There can be duplicates when tags are inherited from an OU + continue + } + // All the removed tags don't exist on the taggable + removed = append(removed, tag) + continue + } + } + + return added, removed +} + +func contains(slc []string, check string) bool { + for _, s := range slc { + if s == check { + return true + } + } + return false +} + +func ignorableTag(tag string) bool { + ignorableTags := map[string]struct{}{ + "TelophaseManaged=true": {}, + } + + _, ok := ignorableTags[tag] + return ok +}