Skip to content

Commit

Permalink
*: support OU and account AWS Tags
Browse files Browse the repository at this point in the history
This PR supports tagging at the OU and Account level. You can now:
- Tag resources in telophase and result in tagging within AWS via an `=`
  delimited string.
    - For example, `env=prod` will create a tag with key `env` and value
  `prod`
    - If there is no `=` then the value will be empty on the AWS tag.

- Account tags will inherit tags from their OU parent tree
  • Loading branch information
dschofie committed May 1, 2024
1 parent 4d46fc6 commit 2f3c13e
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 26 deletions.
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
90 changes: 82 additions & 8 deletions lib/awsorgs/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions lib/ymlparser/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mintlifydocs/commands/deploy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Organization:
Name: "Default IAM Roles for CI"
# Tags are specified here
Tags:
- "production"
- "env=production"
Accounts:
- Email: [email protected]
AccountName: US0
Expand All @@ -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:

<img src="/images/tui-tags.png" style={{ borderRadius: '0.5rem' }} />
13 changes: 7 additions & 6 deletions mintlifydocs/config/organization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
```
Expand Down Expand Up @@ -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.

Expand All @@ -136,13 +138,12 @@ Telophase commands optionally take tags as inputs, allowing you to limit the sco
- Email: [email protected]
AccountName: newdev1
Tags:
- "dev"
- "env=dev"
- Email: [email protected]
AccountName: newdev2
Tags:
- "dev"
- Email: [email protected]
AccountName: production
```

`telophasecli diff --tag dev` will show a `diff` for `newdev1` and `newdev2` accounts only.
`telophasecli diff --tag "env=dev"` will show a `diff` for only the `newdev1` account.
36 changes: 36 additions & 0 deletions resource/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions resource/organization_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 2f3c13e

Please sign in to comment.