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
+}