├── mintlifydocs
├── images
│ ├── tui.jpg
│ ├── logo.png
│ ├── tui.png
│ ├── favicon.ico
│ └── tui-tags.png
├── .gitignore
├── favicon.png
├── features
│ ├── tui.mdx
│ ├── Manage-AWS-Organizations.mdx
│ ├── scps.mdx
│ ├── localstack.mdx
│ └── Assign-IaC-Blueprints-To-Accounts.mdx
├── commands
│ ├── account-import.mdx
│ ├── diff.mdx
│ └── deploy.mdx
├── mint.json
├── introduction.mdx
├── quickstart.mdx
└── config
│ └── organization.mdx
├── examples
├── tf
│ ├── buildkite
│ │ ├── apply.sh
│ │ ├── plan.sh
│ │ ├── blueprint_terraform
│ │ │ ├── provider.tf
│ │ │ ├── main.tf
│ │ │ └── vpc.tf
│ │ ├── pipeline.yml
│ │ └── organization.yml
│ ├── awsconfig
│ │ ├── provider.tf
│ │ ├── variables.tf
│ │ ├── baseconfig
│ │ │ ├── variables.tf
│ │ │ └── main.tf
│ │ ├── bucket.tf
│ │ ├── iam.tf
│ │ └── main.tf
│ ├── configaggregator
│ │ ├── provider.tf
│ │ └── main.tf
│ ├── db-per-tenant
│ │ ├── provider.tf
│ │ └── tenant.tf
│ └── SCPs
│ │ ├── restrict_to_us
│ │ └── main.tf
│ │ └── restrict_to_eu
│ │ └── main.tf
├── localstack
│ ├── tf
│ │ ├── ci_iam
│ │ │ ├── provider.tf
│ │ │ └── main.tf
│ │ └── workspace
│ │ │ └── main.tf
│ ├── s3-remote-state
│ │ ├── README.md
│ │ ├── go.mod
│ │ ├── app.go
│ │ ├── cdk.json
│ │ └── go.sum
│ ├── organization.yml
│ ├── setup.sh
│ └── README.mdx
├── README.md
├── cdk
│ └── tf-s3-backend
│ │ ├── README.md
│ │ ├── go.mod
│ │ ├── app.go
│ │ ├── cdk.json
│ │ └── go.sum
├── cloudformation
│ ├── iam
│ │ └── role.yml
│ └── dynamo
│ │ └── table.yml
└── organization-config-everywhere.yml
├── .dockerignore
├── tests
├── teardown.sh
├── tf
│ ├── s3-test
│ │ └── main.tf
│ └── scp-test
│ │ └── main.tf
├── cdk
│ └── dynamo
│ │ ├── README.md
│ │ ├── dyanmo_test.go
│ │ ├── go.mod
│ │ ├── dynamo.go
│ │ ├── cdk.json
│ │ └── go.sum
├── setup.sh
└── cloudformation
│ └── table.yml
├── resource
├── interface.go
├── organization_unit.go
├── stack_test.go
├── stack.go
└── account.go
├── lib
├── ymlparser
│ ├── testdata
│ │ ├── organization-child2.yml
│ │ ├── organization-one-child.yml
│ │ ├── organization-with-filepath.yml
│ │ ├── organization-child.yml
│ │ └── organization-basic.yml
│ ├── organization_test.go
│ └── organization.go
├── localstack
│ └── localstack.go
├── cdk
│ └── local.go
├── colors
│ └── color.go
├── awssts
│ └── awssts.go
├── awssess
│ └── awssess.go
├── telophase
│ └── account.go
├── metrics
│ └── metrics.go
├── terraform
│ └── local.go
└── awsorgs
│ └── awsorgsmock
│ └── organizationmock.go
├── .gitignore
├── cmd
├── runner
│ ├── interface.go
│ ├── stdout.go
│ └── tui.go
├── auth.go
├── iac.go
├── diff.go
├── deploy.go
├── provisionaccounts.go
└── root.go
├── main.go
├── resourceoperation
├── interface.go
├── organization_unit_test.go
├── cdk.go
├── terraform.go
└── scp.go
├── .github
└── workflows
│ ├── go.yml
│ ├── release.yml
│ └── dockerimage.yml
├── Dockerfile
├── go.mod
├── .goreleaser.yaml
├── setup.sh
└── README.md
/mintlifydocs/images/tui.jpg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mintlifydocs/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 |
--------------------------------------------------------------------------------
/examples/tf/buildkite/apply.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | telophasecli account apply
4 |
--------------------------------------------------------------------------------
/examples/tf/buildkite/plan.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | telophasecli account diff
4 |
--------------------------------------------------------------------------------
/mintlifydocs/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/telophasehq/telophasecli/HEAD/mintlifydocs/favicon.png
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Ignore everything
2 | *
3 |
4 | # But not .go files
5 | !**/*.go
6 | !go.mod
7 | !go.sum
8 |
--------------------------------------------------------------------------------
/mintlifydocs/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/telophasehq/telophasecli/HEAD/mintlifydocs/images/logo.png
--------------------------------------------------------------------------------
/mintlifydocs/images/tui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/telophasehq/telophasecli/HEAD/mintlifydocs/images/tui.png
--------------------------------------------------------------------------------
/mintlifydocs/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/telophasehq/telophasecli/HEAD/mintlifydocs/images/favicon.ico
--------------------------------------------------------------------------------
/tests/teardown.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eux
4 |
5 | localstack stop
6 | rm organization.yml
7 | rm -rf telophasedirs/
--------------------------------------------------------------------------------
/mintlifydocs/images/tui-tags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/telophasehq/telophasecli/HEAD/mintlifydocs/images/tui-tags.png
--------------------------------------------------------------------------------
/resource/interface.go:
--------------------------------------------------------------------------------
1 | package resource
2 |
3 | type Resource interface {
4 | ID() string
5 | Name() string
6 | Type() string
7 | }
8 |
--------------------------------------------------------------------------------
/lib/ymlparser/testdata/organization-child2.yml:
--------------------------------------------------------------------------------
1 | Name: ExampleOU2
2 |
3 | Accounts:
4 | - Email: test3@example.com
5 | AccountName: test3
6 |
--------------------------------------------------------------------------------
/tests/tf/s3-test/main.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = "us-east-1"
3 | }
4 |
5 | resource "aws_s3_bucket" "test" {
6 | bucket = "test"
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | organization*.yml
2 | .DS_STORE
3 |
4 | telophasecli
5 |
6 | .terraform
7 | telophasedirs
8 | .terraform.lock.hcl
9 |
10 | # CDK Ignores
11 | cdk.out
12 |
13 |
14 | dist/
15 |
--------------------------------------------------------------------------------
/examples/localstack/tf/ci_iam/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "s3" {
3 | bucket = "tfstate-${telophase.account_id}"
4 | key = "terraform.tfstate"
5 | region = "us-west-2"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/tf/awsconfig/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "s3" {
3 | bucket = "terraform-state-${telophase.account_id}"
4 | key = "terraform.tfstate"
5 | region = "us-east-1"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/tf/buildkite/blueprint_terraform/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "s3" {
3 | bucket = "tfstate-${telophase.account_id}"
4 | key = "terraform.tfstate"
5 | region = "us-west-2"
6 | }
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/lib/ymlparser/testdata/organization-one-child.yml:
--------------------------------------------------------------------------------
1 | Organization:
2 | OrganizationUnits:
3 | - OUFilepath: "./testdata/organization-child.yml"
4 |
5 | - Name: ExampleOU2
6 | Accounts:
7 | - Email: test3@example.com
8 | AccountName: test3
9 |
--------------------------------------------------------------------------------
/cmd/runner/interface.go:
--------------------------------------------------------------------------------
1 | package runner
2 |
3 | import (
4 | "os/exec"
5 |
6 | "github.com/santiago-labs/telophasecli/resource"
7 | )
8 |
9 | type ConsoleUI interface {
10 | Print(string, resource.Account)
11 | RunCmd(*exec.Cmd, resource.Account) error
12 | Start()
13 | }
14 |
--------------------------------------------------------------------------------
/lib/ymlparser/testdata/organization-with-filepath.yml:
--------------------------------------------------------------------------------
1 | Organization:
2 | Name: root
3 |
4 | OrganizationUnits:
5 | # Path needs to be relative to where telophase is run
6 | - OUFilepath: "./testdata/organization-child.yml"
7 | - OUFilepath: "./testdata/organization-child2.yml"
8 |
--------------------------------------------------------------------------------
/examples/tf/configaggregator/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "s3" {
3 | bucket = "terraform-state-${telophase.account_id}"
4 | key = "configaggregator/terraform.tfstate"
5 | region = "us-west-2"
6 | }
7 | }
8 |
9 |
10 | provider "aws" {
11 | region = "us-east-1"
12 | }
13 |
--------------------------------------------------------------------------------
/examples/tf/db-per-tenant/provider.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | neon = {
4 | source = "terraform-community-providers/neon"
5 | }
6 | }
7 | backend "s3" {
8 | bucket = "tfstate-${telophase.account_id}"
9 | key = "terraform.tfstate"
10 | region = "us-west-2"
11 | }
12 | }
13 |
14 | provider "neon" {}
15 |
--------------------------------------------------------------------------------
/examples/tf/awsconfig/variables.tf:
--------------------------------------------------------------------------------
1 | variable "bucket_name" {
2 | type = string
3 | description = "name of the S3 bucket"
4 | default = "telophase-awsconfig-bucket-${telophase.account_id}"
5 | }
6 |
7 | variable "tags" {
8 | default = {}
9 | description = "Tags to add to resources that support it"
10 | type = map(string)
11 | }
12 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os/exec"
5 |
6 | "github.com/santiago-labs/telophasecli/cmd"
7 | )
8 |
9 | func main() {
10 | cmdStr := "cdk"
11 | cdkArgs := []string{"--version"}
12 |
13 | cmdCdk := exec.Command(cmdStr, cdkArgs...)
14 | if err := cmdCdk.Run(); err != nil {
15 | panic("install cdk before running telophasecli. You can install by running `npm install -g aws-cdk`")
16 | }
17 | cmd.Execute()
18 | }
19 |
--------------------------------------------------------------------------------
/mintlifydocs/features/tui.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Terminal UI'
3 | icon: 'computer-classic'
4 | ---
5 |
6 | Telophase comes with an interactive terminal UI (TUI). Enabled the TUI by passing in the `--tui` flag. For example, `telophasecli deploy --tui`
7 |
8 |
9 |
10 | The TUI is helpful to organize output across the multiple accounts that are being deployed in parallel.
11 |
--------------------------------------------------------------------------------
/lib/localstack/localstack.go:
--------------------------------------------------------------------------------
1 | package localstack
2 |
3 | import "os"
4 |
5 | func UsingLocalStack() bool {
6 | if os.Getenv("LOCALSTACK") != "" {
7 | return true
8 | }
9 | return false
10 | }
11 |
12 | func CdkCmd() string {
13 | if UsingLocalStack() {
14 | return "cdklocal"
15 | }
16 | return "cdk"
17 | }
18 |
19 | func TfCmd() string {
20 | if UsingLocalStack() {
21 | return "tflocal"
22 | }
23 | return "terraform"
24 | }
25 |
--------------------------------------------------------------------------------
/examples/tf/awsconfig/baseconfig/variables.tf:
--------------------------------------------------------------------------------
1 | variable "iam_role" {
2 | type = string
3 | description = "IAM role ARN for AWS Config"
4 | }
5 |
6 | variable "global_resource_collector_region" {
7 | type = string
8 | description = "value of the global resource collector region"
9 | default = "us-east-1"
10 | }
11 |
12 | variable "bucket_name" {
13 | type = string
14 | description = "name of the S3 bucket"
15 | }
16 |
--------------------------------------------------------------------------------
/mintlifydocs/commands/account-import.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'telophasecli account import'
3 | ---
4 |
5 | ```
6 | Usage:
7 | telophasecli account import [flags]
8 |
9 | Flags:
10 | -h, --help help for account
11 | --org string Path to the organization.yml file (default "organization.yml")
12 | ```
13 |
14 | This command reads your AWS Organization and writes an `organization.yml` file locally. It must be run in your AWS Management Account.
15 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 | The examples directory contains example Cloudformation, terraform and CDK code that can be referenced in an organization.yml
3 |
4 | ## organization-config-everywhere.yml
5 | `organization-config-everywhere.yml` stands up an example Org structure where:
6 | - Applies delegated admin to the Audit account.
7 | - Provisions an organization-wide config aggregator in the Audit account.
8 | - AWS Config is enabled in every region of every telophase managed Account.
9 |
--------------------------------------------------------------------------------
/examples/tf/buildkite/pipeline.yml:
--------------------------------------------------------------------------------
1 | steps:
2 | - label: "Terraform Plan"
3 | # This will output:
4 | # 1. Changes required to AWS Organization.
5 | # 2. Output of terraform plan.
6 | # This will not output the output of a terraform plan if the account has not
7 | # been provisioned.
8 | - command: "telophasecli diff"
9 |
10 | - wait: ~
11 | - block: "unblock to apply"
12 |
13 | - label: "Apply new Accounts and Terraform"
14 | - command: "telophasecli deploy"
15 |
--------------------------------------------------------------------------------
/tests/cdk/dynamo/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your CDK Go project!
2 |
3 | This is a blank project for CDK development with Go.
4 |
5 | The `cdk.json` file tells the CDK toolkit how to execute your app.
6 |
7 | ## Useful commands
8 |
9 | * `cdk deploy` deploy this stack to your default AWS account/region
10 | * `cdk diff` compare deployed stack with current state
11 | * `cdk synth` emits the synthesized CloudFormation template
12 | * `go test` run unit tests
13 |
--------------------------------------------------------------------------------
/examples/cdk/tf-s3-backend/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your CDK Go project!
2 |
3 | This is a blank project for CDK development with Go.
4 |
5 | The `cdk.json` file tells the CDK toolkit how to execute your app.
6 |
7 | ## Useful commands
8 |
9 | * `cdk deploy` deploy this stack to your default AWS account/region
10 | * `cdk diff` compare deployed stack with current state
11 | * `cdk synth` emits the synthesized CloudFormation template
12 | * `go test` run unit tests
13 |
--------------------------------------------------------------------------------
/tests/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eux
4 |
5 | if ! nc -z localhost 4566; then
6 | localstack start -d
7 | sleep 5
8 | fi
9 |
10 | # Pre-create tf-test-state table to avoid concurrency bug in localstack.
11 | aws dynamodb create-table --table-name tf-test-state \
12 | --attribute-definitions AttributeName=id,AttributeType=S \
13 | --key-schema AttributeName=id,KeyType=HASH \
14 | --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
15 | --endpoint-url http://localhost:4566
16 |
--------------------------------------------------------------------------------
/examples/localstack/s3-remote-state/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your CDK Go project!
2 |
3 | This is a blank project for CDK development with Go.
4 |
5 | The `cdk.json` file tells the CDK toolkit how to execute your app.
6 |
7 | ## Useful commands
8 |
9 | * `cdk deploy` deploy this stack to your default AWS account/region
10 | * `cdk diff` compare deployed stack with current state
11 | * `cdk synth` emits the synthesized CloudFormation template
12 | * `go test` run unit tests
13 |
--------------------------------------------------------------------------------
/lib/ymlparser/testdata/organization-child.yml:
--------------------------------------------------------------------------------
1 | Name: ExampleOU
2 | Tags:
3 | - "ou=ExampleTenants"
4 |
5 | Stacks:
6 | - Type: "CDK"
7 | Path: "examples/localstack/s3-remote-state"
8 | Name: "example"
9 |
10 | Accounts:
11 | - Email: test1@example.com
12 | AccountName: test1
13 | Stacks:
14 | - Type: "CDK"
15 | Path: "examples/cdk/sqs"
16 | Name: "example"
17 | Region: "us-west-2,us-east-1"
18 |
19 | - Email: test2@example.com
20 | AccountName: test2
21 |
--------------------------------------------------------------------------------
/lib/cdk/local.go:
--------------------------------------------------------------------------------
1 | package cdk
2 |
3 | import (
4 | "crypto/sha256"
5 | "encoding/hex"
6 | "fmt"
7 | "path"
8 |
9 | "github.com/santiago-labs/telophasecli/resource"
10 | )
11 |
12 | func TmpPath(acct resource.Account, filePath string) string {
13 | hasher := sha256.New()
14 | hasher.Write([]byte(filePath))
15 | hashBytes := hasher.Sum(nil)
16 | hashString := hex.EncodeToString(hashBytes)
17 |
18 | return path.Join("telophasedirs", fmt.Sprintf("cdk-tmp%s-%s", acct.AccountID, hashString))
19 | }
20 |
--------------------------------------------------------------------------------
/examples/tf/buildkite/blueprint_terraform/main.tf:
--------------------------------------------------------------------------------
1 | resource "aws_iam_role" "cross_account_role" {
2 | name = "CrossAccountRole"
3 |
4 | # This buildkite role can be assumed by ACCOUNT_iD
5 | assume_role_policy = jsonencode({
6 | Version = "2012-10-17"
7 | Statement = [
8 | {
9 | Action = "sts:AssumeRole"
10 | Effect = "Allow"
11 | Principal = {
12 | AWS = "arn:aws:iam::ACCOUNT_ID:role/BuildkiteRole"
13 | }
14 | },
15 | ]
16 | })
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/resourceoperation/interface.go:
--------------------------------------------------------------------------------
1 | package resourceoperation
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | const (
8 | // Accounts
9 | UpdateParent = 1
10 | Create = 2
11 | Update = 3
12 | UpdateTags = 6
13 | Delete = 7
14 | DelegateAdmin = 8
15 |
16 | // IaC
17 | Diff = 4
18 | Deploy = 5
19 | )
20 |
21 | type ResourceOperation interface {
22 | Call(context.Context) error
23 | ToString() string
24 | AddDependent(ResourceOperation)
25 | ListDependents() []ResourceOperation
26 | }
27 |
--------------------------------------------------------------------------------
/examples/localstack/tf/ci_iam/main.tf:
--------------------------------------------------------------------------------
1 | resource "aws_iam_role" "test_role" {
2 | name = "ci_role"
3 |
4 | # Terraform's "jsonencode" function converts a
5 | # Terraform expression result to valid JSON syntax.
6 | assume_role_policy = jsonencode({
7 | Version = "2012-10-17"
8 | Statement = [
9 | {
10 | Action = "sts:AssumeRole"
11 | Effect = "Allow"
12 | Sid = ""
13 | Principal = {
14 | Service = "ec2.amazonaws.com"
15 | }
16 | },
17 | ]
18 | })
19 |
20 | tags = {
21 | ci = "true"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/colors/color.go:
--------------------------------------------------------------------------------
1 | package colors
2 |
3 | import (
4 | "hash/fnv"
5 |
6 | "github.com/fatih/color"
7 | )
8 |
9 | func DeterministicColorFunc(id string) func(format string, a ...interface{}) string {
10 | options := []func(format string, a ...interface{}) string{
11 | color.CyanString,
12 | color.GreenString,
13 | color.BlueString,
14 | color.MagentaString,
15 | color.HiBlueString,
16 | color.HiCyanString,
17 | color.HiGreenString,
18 | color.HiMagentaString,
19 | }
20 |
21 | h := fnv.New32a()
22 | h.Write([]byte(id))
23 | hashedValue := h.Sum32()
24 |
25 | return options[hashedValue%uint32(len(options))]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Go
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 |
18 | - name: Set up Go
19 | uses: actions/setup-go@v4
20 | with:
21 | go-version-file: go.mod
22 |
23 | - name: Run Setup
24 | run: ./setup.sh
25 |
26 | - name: Build
27 | run: go build -v ./...
28 |
--------------------------------------------------------------------------------
/mintlifydocs/commands/diff.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'telophasecli diff'
3 | ---
4 |
5 | ```
6 | Usage:
7 | telophasecli diff [flags]
8 |
9 | Flags:
10 | -h, --help help for diff
11 | --org string Path to the organization.yml file (default "organization.yml")
12 | --stacks string Filter stacks to diff
13 | --tag string Filter accounts and account groups to diff via a comma separated list.
14 | --tui use the TUI for diff
15 | ```
16 |
17 | This command will read `organization.yml` and **output**:
18 | 1) Changes required to AWS Organization.
19 | 2) Output of `cdk diff`.
20 | 3) Output of `terraform plan`.
--------------------------------------------------------------------------------
/examples/tf/db-per-tenant/tenant.tf:
--------------------------------------------------------------------------------
1 | resource "neon_project" "tenant" {
2 | name = telophase.account_name
3 | region_id = "aws-us-east-1"
4 | branch = {
5 | endpoint = {
6 | suspend_timeout = 300
7 | }
8 | }
9 | }
10 |
11 | resource "neon_role" "tenant" {
12 | name = "${telophase.account_name}_user"
13 | branch_id = neon_project.tenant.branch.id
14 | project_id = neon_project.tenant.id
15 | }
16 |
17 | resource "neon_database" "tenant" {
18 | name = "${telophase.account_name}db"
19 | owner_name = neon_role.tenant.name
20 | branch_id = neon_project.tenant.branch.id
21 | project_id = neon_project.tenant.id
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/examples/cdk/tf-s3-backend/go.mod:
--------------------------------------------------------------------------------
1 | module tf-s3-backend
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/aws/aws-cdk-go/awscdk v1.204.0-devpreview
7 | github.com/aws/jsii-runtime-go v1.89.0
8 | )
9 |
10 | require (
11 | github.com/Masterminds/semver/v3 v3.2.1 // indirect
12 | github.com/aws/constructs-go/constructs/v3 v3.4.232 // indirect
13 | github.com/fatih/color v1.15.0 // indirect
14 | github.com/mattn/go-colorable v0.1.13 // indirect
15 | github.com/mattn/go-isatty v0.0.19 // indirect
16 | github.com/yuin/goldmark v1.4.13 // indirect
17 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
18 | golang.org/x/mod v0.12.0 // indirect
19 | golang.org/x/sys v0.12.0 // indirect
20 | golang.org/x/tools v0.13.0 // indirect
21 | )
22 |
--------------------------------------------------------------------------------
/tests/cdk/dynamo/dyanmo_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // import (
4 | // "testing"
5 |
6 | // "github.com/aws/aws-cdk-go/awscdk/v2"
7 | // "github.com/aws/aws-cdk-go/awscdk/v2/assertions"
8 | // "github.com/aws/jsii-runtime-go"
9 | // )
10 |
11 | // example tests. To run these tests, uncomment this file along with the
12 | // example resource in dynamo_test.go
13 | // func TestSqsStack(t *testing.T) {
14 | // // GIVEN
15 | // app := awscdk.NewApp(nil)
16 |
17 | // // WHEN
18 | // stack := NewSqsStack(app, "MyStack", nil)
19 |
20 | // // THEN
21 | // template := assertions.Template_FromStack(stack, nil)
22 |
23 | // template.HasResourceProperties(jsii.String("AWS::SQS::Queue"), map[string]interface{}{
24 | // "VisibilityTimeout": 300,
25 | // })
26 | // }
27 |
--------------------------------------------------------------------------------
/lib/ymlparser/testdata/organization-basic.yml:
--------------------------------------------------------------------------------
1 | Organization:
2 | OrganizationUnits:
3 | - Name: ExampleOU
4 | Tags:
5 | - "ou=ExampleTenants"
6 |
7 | Stacks:
8 | - Type: "CDK"
9 | Path: "examples/localstack/s3-remote-state"
10 | Name: "example"
11 |
12 | Accounts:
13 | - Email: test1@example.com
14 | AccountName: test1
15 | Stacks:
16 | - Type: "CDK"
17 | Path: "examples/cdk/sqs"
18 | Name: "example"
19 | Region: "us-west-2,us-east-1"
20 |
21 | - Email: test2@example.com
22 | AccountName: test2
23 |
24 | - Name: ExampleOU2
25 | Accounts:
26 | - Email: test3@example.com
27 | AccountName: test3
28 |
--------------------------------------------------------------------------------
/examples/localstack/tf/workspace/main.tf:
--------------------------------------------------------------------------------
1 | resource "aws_dynamodb_table" "example" {
2 | name = "${terraform.workspace}-eu"
3 |
4 | hash_key = "TestTableHashKey"
5 | billing_mode = "PAY_PER_REQUEST"
6 | stream_enabled = true
7 | stream_view_type = "NEW_AND_OLD_IMAGES"
8 |
9 | attribute {
10 | name = "TestTableHashKey"
11 | type = "S"
12 | }
13 | }
14 |
15 | locals {
16 | region = split("_",terraform.workspace)[1]
17 | }
18 |
19 | provider "aws" {
20 | # Two options can use ${telophase.region} or look at local config
21 | region = "${telophase.region}"
22 | }
23 |
24 | terraform {
25 | backend "s3" {
26 | bucket = "tfstate-${telophase.account_id}"
27 | key = "workspace/terraform.tfstate"
28 | region = "us-west-2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/cloudformation/iam/role.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Description: Administrator access service role for CloudFormation
3 | Resources:
4 | CFNAdminRole:
5 | Type: "AWS::IAM::Role"
6 | Properties:
7 | AssumeRolePolicyDocument:
8 | Version: "2012-10-17"
9 | Statement:
10 | - Effect: "Allow"
11 | Principal:
12 | Service: ["cloudformation.amazonaws.com"]
13 | Action: "sts:AssumeRole"
14 | Path: "/"
15 | ManagedPolicyArns:
16 | - 'arn:aws:iam::aws:policy/AdministratorAccess'
17 | Outputs:
18 | CFNAdminRole:
19 | Description: CloudFormation admin access service role.
20 | Value: !Ref CFNAdminRole
21 | CFNAdminRoleArn:
22 | Description: CloudFormation admin access service role ARN.
23 | Value: !GetAtt CFNAdminRole.Arn
--------------------------------------------------------------------------------
/examples/tf/SCPs/restrict_to_us/main.tf:
--------------------------------------------------------------------------------
1 | data "aws_iam_policy_document" "restrict_regions" {
2 | statement {
3 | sid = "RegionRestriction"
4 | effect = "Deny"
5 | actions = ["*"]
6 | resources = ["*"]
7 |
8 | condition {
9 | test = "StringNotEquals"
10 | variable = "aws:RequestedRegion"
11 |
12 | values = [
13 | "us-east-1"
14 | ]
15 | }
16 | }
17 | }
18 |
19 | resource "aws_organizations_policy" "restrict_regions" {
20 | name = "restrict_regions"
21 | description = "Deny all regions except US East 1."
22 | content = data.aws_iam_policy_document.restrict_regions.json
23 | }
24 |
25 | resource "aws_organizations_policy_attachment" "restrict_regions" {
26 | policy_id = aws_organizations_policy.restrict_regions.id
27 | target_id = telophase.organization_unit_id
28 | }
29 |
--------------------------------------------------------------------------------
/examples/localstack/s3-remote-state/go.mod:
--------------------------------------------------------------------------------
1 | module tf-s3-backend
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/aws/aws-cdk-go/awscdk v1.204.0-devpreview
7 | github.com/aws/jsii-runtime-go v1.89.0
8 | )
9 |
10 | require (
11 | github.com/Masterminds/semver/v3 v3.2.1 // indirect
12 | github.com/aws/aws-sdk-go v1.48.1 // indirect
13 | github.com/aws/constructs-go/constructs/v3 v3.4.232 // indirect
14 | github.com/fatih/color v1.15.0 // indirect
15 | github.com/jmespath/go-jmespath v0.4.0 // indirect
16 | github.com/mattn/go-colorable v0.1.13 // indirect
17 | github.com/mattn/go-isatty v0.0.19 // indirect
18 | github.com/yuin/goldmark v1.4.13 // indirect
19 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
20 | golang.org/x/mod v0.12.0 // indirect
21 | golang.org/x/sys v0.12.0 // indirect
22 | golang.org/x/tools v0.13.0 // indirect
23 | )
24 |
--------------------------------------------------------------------------------
/examples/tf/SCPs/restrict_to_eu/main.tf:
--------------------------------------------------------------------------------
1 | data "aws_iam_policy_document" "restrict_regions" {
2 | statement {
3 | sid = "RegionRestriction"
4 | effect = "Deny"
5 | actions = ["*"]
6 | resources = ["*"]
7 |
8 | condition {
9 | test = "StringNotEquals"
10 | variable = "aws:RequestedRegion"
11 |
12 | values = [
13 | "eu-west-1"
14 | ]
15 | }
16 | }
17 | }
18 |
19 | resource "aws_organizations_policy" "restrict_regions" {
20 | name = "restrict_regions_${telophase.organization_unit_id}"
21 | description = "Deny all regions except EU West 1."
22 | content = data.aws_iam_policy_document.restrict_regions.json
23 | }
24 |
25 | resource "aws_organizations_policy_attachment" "restrict_regions" {
26 | policy_id = aws_organizations_policy.restrict_regions.id
27 | target_id = telophase.organization_unit_id
28 | }
29 |
--------------------------------------------------------------------------------
/examples/tf/awsconfig/baseconfig/main.tf:
--------------------------------------------------------------------------------
1 | data "aws_region" "this" {}
2 |
3 | data "aws_caller_identity" "this" {}
4 |
5 | data "aws_partition" "current" {}
6 |
7 | locals {
8 | is_global_recorder_region = var.global_resource_collector_region == data.aws_region.this.name
9 | partition = data.aws_partition.current.partition
10 | }
11 |
12 | resource "aws_config_configuration_recorder" "recorder" {
13 | name = "telophase-configuration-recorder"
14 | role_arn = var.iam_role
15 |
16 | recording_group {
17 | all_supported = true
18 | include_global_resource_types = local.is_global_recorder_region
19 | }
20 | }
21 |
22 |
23 | resource "aws_config_delivery_channel" "this" {
24 | name = "telophase-config-delivery-channel"
25 | s3_bucket_name = var.bucket_name
26 |
27 | depends_on = [aws_config_configuration_recorder.recorder]
28 | }
29 |
--------------------------------------------------------------------------------
/tests/tf/scp-test/main.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = "us-east-1"
3 | }
4 |
5 | data "aws_iam_policy_document" "restrict_regions" {
6 | statement {
7 | sid = "RegionRestriction"
8 | effect = "Deny"
9 | actions = ["*"]
10 | resources = ["*"]
11 |
12 | condition {
13 | test = "StringNotEquals"
14 | variable = "aws:RequestedRegion"
15 |
16 | values = [
17 | "us-east-1"
18 | ]
19 | }
20 | }
21 | }
22 |
23 | resource "aws_organizations_policy" "restrict_regions" {
24 | name = "restrict_regions"
25 | description = "Deny all regions except US East 1."
26 | content = data.aws_iam_policy_document.restrict_regions.json
27 | }
28 |
29 | resource "aws_organizations_policy_attachment" "restrict_regions" {
30 | policy_id = aws_organizations_policy.restrict_regions.id
31 | target_id = telophase.organization_unit_id
32 | }
33 |
--------------------------------------------------------------------------------
/mintlifydocs/features/Manage-AWS-Organizations.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Manage AWS Organizations'
3 | icon: 'sitemap'
4 | ---
5 |
6 | `telophasecli` allows you to define your AWS Organization Unit structure via code.
7 |
8 | ## Example `organization.yml`
9 | ```yml
10 | Organization:
11 | OrganizationUnits:
12 | - Name: Production
13 | Accounts:
14 | - Email: safety+firmware@example.app
15 | AccountName: Safety Firmware
16 | - Email: safety+ingestion@example.app
17 | AccountName: Safety Ingestion Team
18 | - Name: Development
19 | Accounts:
20 | - Email: eng1@example.app
21 | AccountName: Engineer A
22 |
23 | ```
24 |
25 | The configuration above will create
26 | 1) `Production` Organizational Unit
27 | 2) `Safety Firmware` and `Safety Ingestion Team` accounts in the `Production` OU
28 | 3) `Development` Organizational Unit
29 | 4) `Engineer A` account in the `Development` OU
30 |
31 | These accounts can be provisioned via `telophasecli account deploy`
--------------------------------------------------------------------------------
/examples/tf/configaggregator/main.tf:
--------------------------------------------------------------------------------
1 | resource "aws_config_configuration_aggregator" "organization" {
2 | depends_on = [aws_iam_role_policy_attachment.organization]
3 |
4 | name = "telophase-config-aggregator" # Required
5 |
6 | organization_aggregation_source {
7 | all_regions = true
8 | role_arn = aws_iam_role.organization.arn
9 | }
10 | }
11 |
12 | data "aws_iam_policy_document" "assume_role" {
13 | statement {
14 | effect = "Allow"
15 |
16 | principals {
17 | type = "Service"
18 | identifiers = ["config.amazonaws.com"]
19 | }
20 |
21 | actions = ["sts:AssumeRole"]
22 | }
23 | }
24 |
25 | resource "aws_iam_role" "organization" {
26 | name = "telophase-config-aggregator-role"
27 | assume_role_policy = data.aws_iam_policy_document.assume_role.json
28 | }
29 |
30 | resource "aws_iam_role_policy_attachment" "organization" {
31 | role = aws_iam_role.organization.name
32 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSConfigRoleForOrganizations"
33 | }
34 |
--------------------------------------------------------------------------------
/tests/cdk/dynamo/go.mod:
--------------------------------------------------------------------------------
1 | module dynamo
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/aws/aws-cdk-go/awscdk v1.204.0-devpreview
7 | github.com/aws/aws-cdk-go/awscdk/v2 v2.135.0
8 | github.com/aws/constructs-go/constructs/v10 v10.3.0
9 | github.com/aws/jsii-runtime-go v1.96.0
10 | )
11 |
12 | require (
13 | github.com/Masterminds/semver/v3 v3.2.1 // indirect
14 | github.com/aws/constructs-go/constructs/v3 v3.4.232 // indirect
15 | github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.202 // indirect
16 | github.com/cdklabs/awscdk-asset-kubectl-go/kubectlv20/v2 v2.1.2 // indirect
17 | github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.0.1 // indirect
18 | github.com/fatih/color v1.16.0 // indirect
19 | github.com/mattn/go-colorable v0.1.13 // indirect
20 | github.com/mattn/go-isatty v0.0.20 // indirect
21 | github.com/yuin/goldmark v1.4.13 // indirect
22 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
23 | golang.org/x/mod v0.16.0 // indirect
24 | golang.org/x/sys v0.18.0 // indirect
25 | golang.org/x/tools v0.19.0 // indirect
26 | )
27 |
--------------------------------------------------------------------------------
/mintlifydocs/features/scps.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Service Control Policies'
3 | icon: 'police-box'
4 | ---
5 |
6 | Service Control Policies defined in Terraform can be applied to Organization Units and Accounts in `organization.yml`.
7 |
8 | ## Example
9 | ```yml
10 | Organization:
11 | AccountGroups:
12 | - Name: Production
13 | ServiceControlPolicies:
14 | - Name: DisableEURegion # This SCP will be applied to the `Production` Organization Unit.
15 | Path: path/to/scp
16 | Type: Terraform
17 | Accounts:
18 | - Email: safety+firmware@example.app
19 | AccountName: Safety Firmware
20 | - Email: safety+ingestion@example.app
21 | AccountName: Safety Ingestion Team
22 | - Name: Development
23 | Accounts:
24 | - Email: eng1@example.app
25 | AccountName: Engineer A
26 | ServiceControlPolicies:
27 | - Name: DisableGPUInstances # This SCP will be applied to `Engineer A` account only.
28 | Path: path/to/scp
29 | Type: Terraform
30 | ```
31 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.24rc1
2 |
3 | WORKDIR /telophasecli
4 |
5 | COPY ./ /telophasecli
6 |
7 | RUN go mod download
8 |
9 | # run go install instead so that we can reuse the PATH to from the baseto from
10 | # the base image.
11 | RUN go install
12 |
13 | RUN apt-get update
14 | # We need npm to install the CDK and terraform
15 | RUN apt-get install -y npm
16 | RUN npm install -g aws-cdk
17 |
18 | # Install Terraform https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli
19 | RUN apt-get install -y gnupg software-properties-common
20 | RUN wget -O- https://apt.releases.hashicorp.com/gpg | \
21 | gpg --dearmor | \
22 | tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
23 |
24 | RUN gpg --no-default-keyring \
25 | --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \
26 | --fingerprint
27 |
28 | RUN echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
29 | https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
30 | tee /etc/apt/sources.list.d/hashicorp.list
31 |
32 | RUN apt-get update
33 | RUN apt-get install terraform
34 |
--------------------------------------------------------------------------------
/examples/cdk/tf-s3-backend/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/aws/aws-cdk-go/awscdk"
7 | "github.com/aws/aws-cdk-go/awscdk/awss3"
8 | "github.com/aws/jsii-runtime-go"
9 | )
10 |
11 | type TerraformStateBucketStackProps struct {
12 | awscdk.StackProps
13 | }
14 |
15 | func NewTerraformStateBucketStack(scope awscdk.Construct, id string, props *TerraformStateBucketStackProps) awscdk.Stack {
16 | var sprops awscdk.StackProps
17 | if props != nil {
18 | sprops = props.StackProps
19 | }
20 | stack := awscdk.NewStack(scope, &id, &sprops)
21 |
22 | accountId := stack.Node().TryGetContext(jsii.String("telophaseAccountId")).(string)
23 | awss3.NewBucket(stack, jsii.String("TerraformStateBucket"), &awss3.BucketProps{
24 | Versioned: jsii.Bool(true),
25 | BucketName: jsii.String(fmt.Sprintf("tfstate-%s", accountId)),
26 | })
27 |
28 | return stack
29 | }
30 |
31 | func main() {
32 | app := awscdk.NewApp(nil)
33 |
34 | NewTerraformStateBucketStack(app, "TerraformStateBucketStackExample", &TerraformStateBucketStackProps{
35 | awscdk.StackProps{
36 | Env: env(),
37 | },
38 | })
39 |
40 | app.Synth(nil)
41 | }
42 |
43 | func env() *awscdk.Environment {
44 | return nil
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/release.yml
2 | name: goreleaser
3 |
4 | on:
5 | push:
6 | # run only against tags
7 | tags:
8 | - "v*.*.*"
9 |
10 | permissions:
11 | contents: write
12 | # packages: write
13 | # issues: write
14 |
15 | jobs:
16 | goreleaser:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 0
23 | - name: Set up Go
24 | uses: actions/setup-go@v5
25 | with:
26 | go-version: stable
27 | # More assembly might be required: Docker logins, GPG, etc.
28 | # It all depends on your needs.
29 | - name: Run GoReleaser
30 | uses: goreleaser/goreleaser-action@v5
31 | with:
32 | # either 'goreleaser' (default) or 'goreleaser-pro'
33 | distribution: goreleaser
34 | # 'latest', 'nightly', or a semver
35 | version: latest
36 | args: release --clean
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }}
39 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
40 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
--------------------------------------------------------------------------------
/lib/awssts/awssts.go:
--------------------------------------------------------------------------------
1 | package awssts
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/aws/aws-sdk-go/service/sts"
7 | "github.com/santiago-labs/telophasecli/lib/localstack"
8 | )
9 |
10 | func SetEnvironCreds(currEnv []string,
11 | creds *sts.Credentials,
12 | awsRegion *string) []string {
13 | var newEnv []string
14 |
15 | for _, e := range currEnv {
16 | if creds != nil {
17 | if strings.Contains(e, "AWS_ACCESS_KEY_ID=") ||
18 | strings.Contains(e, "AWS_SECRET_ACCESS_KEY=") ||
19 | strings.Contains(e, "AWS_SESSION_TOKEN=") {
20 | continue
21 | }
22 | }
23 |
24 | if awsRegion != nil && strings.Contains(e, "AWS_REGION=") {
25 | continue
26 | }
27 |
28 | newEnv = append(newEnv, e)
29 | }
30 |
31 | if creds != nil {
32 | newEnv = append(newEnv,
33 | "AWS_ACCESS_KEY_ID="+*creds.AccessKeyId,
34 | "AWS_SECRET_ACCESS_KEY="+*creds.SecretAccessKey,
35 | "AWS_SESSION_TOKEN="+*creds.SessionToken,
36 | )
37 | }
38 |
39 | if awsRegion != nil {
40 | newEnv = append(newEnv, *awsRegion)
41 | }
42 |
43 | if localstack.UsingLocalStack() {
44 | // We need to set this to true for localstack so that tflocal will use
45 | // the AWS key for the proper account.
46 | newEnv = append(newEnv, "CUSTOMIZE_ACCESS_KEY=true")
47 | }
48 |
49 | return newEnv
50 | }
51 |
--------------------------------------------------------------------------------
/examples/tf/buildkite/organization.yml:
--------------------------------------------------------------------------------
1 | Organization:
2 | Name: root
3 | OrganizationUnits:
4 | - Name: Security
5 | Accounts:
6 | - Email: aws+audit@example.com
7 | AccountName: Audit
8 |
9 | - Email: aws+logs@example.com
10 | AccountName: Log Archive
11 |
12 | - Name: ProductionTenants
13 | Accounts:
14 | - Email: ethan+example@example.com
15 | AccountName: example-app
16 | - Path: ./blueprint_terraform
17 | Type: Terraform
18 | Tags:
19 | - "production"
20 |
21 | - Email: ethan+derp@example.com
22 | AccountName: example-app-eu
23 | - Path: ./blueprint_terraform
24 | Type: Terraform
25 | Tags:
26 | - "production"
27 |
28 | - Name: Team SRE
29 | Accounts:
30 | - Email: aws+sre1@example.com
31 | AccountName: aws-SRE
32 | Tags:
33 | - "sre"
34 | AssumeRoleOverride: "TelophaseAdminAccess"
35 |
36 | - Email: aws+sre2@example.com
37 | AccountName: aws-SRE2
38 | Tags:
39 | - "sre"
40 | AssumeRoleOverride: "TelophaseAdminAccess"
41 |
42 |
--------------------------------------------------------------------------------
/examples/organization-config-everywhere.yml:
--------------------------------------------------------------------------------
1 | Organization:
2 | # All OUs/accounts will inherit these stacks and apply to every account.
3 | Stacks:
4 | # This stack creates an S3 bucket and dynamo DB table in every account.
5 | - Type: "Cloudformation"
6 | Path: "cloudformation/s3/bucket.yml"
7 | Name: "s3-remote-state-terraform"
8 | CloudformationCapabilities:
9 | - "CAPABILITY_NAMED_IAM"
10 |
11 | # This stack enables AWS config in every region in every account.
12 | - Type: "Cloudformation"
13 | - Type: "Terraform"
14 | Path: "tf/awsconfig"
15 | Name: "AWS-Config-Every-Region"
16 |
17 | OrganizationUnits:
18 | - Name: Security
19 | Accounts:
20 | - Email: example+audit@example.com
21 | AccountName: Audit
22 | # This account will have config and config-multiaccountsetup delegated.
23 | DelegatedAdministratorServices:
24 | - "config.amazonaws.com"
25 | - "config-multiaccountsetup.amazonaws.com"
26 | Stacks:
27 | # This stack creates the aggregator for the organization in the delegated admin account.
28 | - Type: "Terraform"
29 | Path: "tf/configaggregator"
30 | Name: "aggregator"
31 |
32 | - Email: example+logarchive@example.com
33 | AccountName: Log Archive
34 |
--------------------------------------------------------------------------------
/examples/tf/awsconfig/bucket.tf:
--------------------------------------------------------------------------------
1 | resource "aws_s3_bucket" "bucket" {
2 | bucket = var.bucket_name
3 |
4 | tags = merge(var.tags, {
5 | "Name" = var.bucket_name
6 | })
7 | force_destroy = true
8 | }
9 |
10 | resource "aws_s3_bucket_server_side_encryption_configuration" "encryption" {
11 | bucket = aws_s3_bucket.bucket.id
12 |
13 | rule {
14 | apply_server_side_encryption_by_default {
15 | sse_algorithm = "aws:kms"
16 | }
17 | }
18 | }
19 |
20 | resource "aws_s3_bucket_public_access_block" "block" {
21 | bucket = aws_s3_bucket.bucket.id
22 | block_public_acls = true
23 | block_public_policy = true
24 | ignore_public_acls = true
25 | restrict_public_buckets = true
26 | }
27 |
28 |
29 | resource "aws_s3_bucket_versioning" "versioning_example" {
30 | bucket = aws_s3_bucket.bucket.id
31 | versioning_configuration {
32 | status = "Enabled"
33 | }
34 | }
35 |
36 | resource "aws_s3_bucket_lifecycle_configuration" "example" {
37 | bucket = aws_s3_bucket.bucket.id
38 |
39 | rule {
40 | id = "expire-old-logs"
41 | status = "Enabled"
42 |
43 | transition {
44 | days = 90
45 | storage_class = "GLACIER"
46 | }
47 |
48 | noncurrent_version_expiration {
49 | noncurrent_days = 120
50 | }
51 |
52 | expiration {
53 | # Default to 7 years
54 | days = 2555
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/santiago-labs/telophasecli
2 |
3 | go 1.22
4 |
5 | require (
6 | github.com/aws/aws-sdk-go v1.46.0
7 | github.com/fatih/color v1.15.0
8 | github.com/google/go-cmp v0.6.0
9 | github.com/google/uuid v1.4.0
10 | github.com/pkg/errors v0.9.1
11 | github.com/posthog/posthog-go v0.0.0-20230801140217-d607812dee69
12 | github.com/rivo/tview v0.0.0-20231031172508-2dfe06011790
13 | github.com/samsarahq/go/oops v0.0.0-20220211150445-4b291d6feac4
14 | github.com/spf13/cobra v1.7.0
15 | github.com/stretchr/testify v1.8.4
16 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
17 | gopkg.in/yaml.v3 v3.0.1
18 | )
19 |
20 | require (
21 | github.com/davecgh/go-spew v1.1.1 // indirect
22 | github.com/gdamore/encoding v1.0.0 // indirect
23 | github.com/gdamore/tcell/v2 v2.6.0 // indirect
24 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
25 | github.com/jmespath/go-jmespath v0.4.0 // indirect
26 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
27 | github.com/mattn/go-colorable v0.1.13 // indirect
28 | github.com/mattn/go-isatty v0.0.19 // indirect
29 | github.com/mattn/go-runewidth v0.0.14 // indirect
30 | github.com/pmezard/go-difflib v1.0.0 // indirect
31 | github.com/rivo/uniseg v0.4.3 // indirect
32 | github.com/spf13/pflag v1.0.5 // indirect
33 | golang.org/x/sys v0.15.0 // indirect
34 | golang.org/x/term v0.15.0 // indirect
35 | golang.org/x/text v0.14.0 // indirect
36 | gopkg.in/yaml.v2 v2.4.0 // indirect
37 | )
38 |
--------------------------------------------------------------------------------
/examples/tf/awsconfig/iam.tf:
--------------------------------------------------------------------------------
1 | data "aws_iam_policy_document" "iam" {
2 | statement {
3 | effect = "Allow"
4 | actions = ["s3:PutObject"]
5 | resources = [
6 | "${aws_s3_bucket.bucket.arn}/*",
7 | ]
8 | condition {
9 | test = "StringEquals"
10 | variable = "s3:x-amz-acl"
11 | values = ["bucket-owner-full-control"]
12 | }
13 | }
14 | statement {
15 | effect = "Allow"
16 | actions = ["s3:GetBucketAcl"]
17 | resources = [
18 | aws_s3_bucket.bucket.arn,
19 | ]
20 | }
21 | }
22 |
23 | resource "aws_iam_policy" "iam" {
24 | name_prefix = "telophase-config-role"
25 | policy = data.aws_iam_policy_document.iam.json
26 | }
27 |
28 | data "aws_iam_policy_document" "assume" {
29 | statement {
30 | effect = "Allow"
31 | actions = ["sts:AssumeRole"]
32 |
33 | principals {
34 | type = "Service"
35 | identifiers = ["config.amazonaws.com"]
36 | }
37 | }
38 | }
39 |
40 | resource "aws_iam_role" "iam" {
41 | name_prefix = "TelophaseConfigRole"
42 | assume_role_policy = data.aws_iam_policy_document.assume.json
43 | }
44 |
45 | resource "aws_iam_role_policy_attachment" "awsconfig_managed_policy" {
46 | role = aws_iam_role.iam.name
47 | policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWS_ConfigRole"
48 | }
49 |
50 |
51 | resource "aws_iam_role_policy_attachment" "iam" {
52 | role = aws_iam_role.iam.name
53 | policy_arn = aws_iam_policy.iam.arn
54 | }
55 |
--------------------------------------------------------------------------------
/cmd/auth.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os/exec"
7 | "runtime"
8 |
9 | "github.com/spf13/cobra"
10 | )
11 |
12 | const TELOPHASE_URL = "https://app.telophase.dev"
13 |
14 | func init() {
15 | rootCmd.AddCommand(authCommand)
16 | }
17 |
18 | func isValidAuthArg(arg string) bool {
19 | switch arg {
20 | case "signup":
21 | return true
22 | default:
23 | return false
24 | }
25 | }
26 |
27 | var authCommand = &cobra.Command{
28 | Use: "auth",
29 | Short: "auth - Signup for Telophase (Optional, but we would love your feedback!)",
30 | Args: func(cmd *cobra.Command, args []string) error {
31 | if len(args) < 1 {
32 | return errors.New("requires at least one arg")
33 | }
34 | if isValidAuthArg(args[0]) {
35 | return nil
36 | }
37 | return fmt.Errorf("invalid color specified: %s", args[0])
38 | },
39 | Run: func(cmd *cobra.Command, args []string) {
40 | if args[0] == "signup" {
41 | if err := openSignup(); err != nil {
42 | panic(fmt.Sprintf("error opening signup page: %s. Please visit https://app.telophase.dev", err))
43 | }
44 | }
45 | },
46 | }
47 |
48 | // https://gist.github.com/sevkin/9798d67b2cb9d07cb05f89f14ba682f8
49 | func openSignup() error {
50 | var cmd string
51 | var args []string
52 |
53 | switch runtime.GOOS {
54 | case "windows":
55 | cmd = "cmd"
56 | args = []string{"/c", "start"}
57 | case "darwin":
58 | cmd = "open"
59 | default: // "linux", "freebsd", "openbsd", "netbsd"
60 | cmd = "xdg-open"
61 | }
62 | args = append(args, TELOPHASE_URL)
63 | return exec.Command(cmd, args...).Start()
64 | }
65 |
--------------------------------------------------------------------------------
/examples/localstack/organization.yml:
--------------------------------------------------------------------------------
1 | Organization:
2 | Name: root
3 | OrganizationUnits:
4 | - Name: ProductionTenants
5 | Stacks:
6 | # This stack provisions an S3 bucket to be used for teraform remote
7 | # state for every production tenant.
8 | - Type: "CDK"
9 | Path: "./s3-remote-state"
10 | - Type: "Terraform"
11 | Path: "./tf/ci_iam"
12 | Name: "Default IAM Roles for CI"
13 | - Type: "Terraform"
14 | Path: "./tf/workspace"
15 | Region: "eu-west-1"
16 | Workspace: "${telophase.account_id}_${telophase.region}"
17 | Tags:
18 | - "production"
19 | Accounts:
20 | - Email: production+us0@example.com
21 | AccountName: US0
22 | - Email: production+us1@example.com
23 | AccountName: US1
24 | - Email: production+us2@example.com
25 | AccountName: US2
26 | - Email: production+us3@example.com
27 | AccountName: US3
28 | - Name: Development
29 | Stacks:
30 | # This stack provisions an S3 bucket to be used for teraform remote
31 | # state for every production tenant.
32 | - Type: "CDK"
33 | Path: "./s3-remote-state"
34 | Tags:
35 | - "dev"
36 | Accounts:
37 | - Email: alice@example.com
38 | AccountName: Alice
39 | - Email: bob@example.com
40 | AccountName: Bob
41 | - Email: ethan@example.com
42 | AccountName: Ethan
43 |
44 |
--------------------------------------------------------------------------------
/examples/localstack/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eux
4 |
5 | print_red() {
6 | echo -e "\033[0;31m$1\033[0m"
7 | }
8 |
9 | print_green() {
10 | echo -e "\033[0;32m$1\033[0m"
11 | }
12 |
13 | if command -v brew &> /dev/null
14 | then
15 | echo "Brew is installed, proceeding with awscli and terraform installation."
16 | brew install awscli awscli-local terraform terraform-local localstack/tap/localstack-cli
17 | else
18 | print_red "Brew is not installed please install awscli and terraform manually"
19 | exit 1
20 | fi
21 |
22 | if command -v npm &> /dev/null
23 | then
24 | echo "Installing aws-cdk and aws-cdk-local."
25 | npm install -g aws-cdk aws-cdk-local
26 | else
27 | print_red "npm is not installed, please install it and then return"
28 | exit 1
29 | fi
30 |
31 | if command -v go &> /dev/null
32 | then
33 | echo "Installing telophasecli..."
34 | go install github.com/santiago-labs/telophasecli@latest
35 | else
36 | print_red "go is not installed. Please install the language then return."
37 | exit 1
38 | fi
39 |
40 | localstack start -d
41 |
42 | # Pre-create tf-test-state table to avoid concurrency bug in localstack.
43 | aws dynamodb create-table --table-name tf-test-state \
44 | --attribute-definitions AttributeName=id,AttributeType=S \
45 | --key-schema AttributeName=id,KeyType=HASH \
46 | --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
47 | --endpoint-url http://localhost:4566
48 |
49 | awslocal organizations create-organization --feature-set ALL
50 |
51 | print_green "Setup complete! You can now run telophasecli :)"
52 |
--------------------------------------------------------------------------------
/lib/awssess/awssess.go:
--------------------------------------------------------------------------------
1 | package awssess
2 |
3 | import (
4 | "os"
5 | "strings"
6 |
7 | "github.com/aws/aws-sdk-go/aws"
8 | "github.com/aws/aws-sdk-go/aws/session"
9 | "github.com/aws/aws-sdk-go/service/sts"
10 | "github.com/samsarahq/go/oops"
11 | )
12 |
13 | func DefaultSession(cfgs ...*aws.Config) (*session.Session, error) {
14 | if os.Getenv("LOCALSTACK") != "" {
15 | cfg := aws.NewConfig()
16 | cfg.Endpoint = aws.String("http://localhost:4566")
17 | cfgs = append(cfgs, cfg)
18 | }
19 |
20 | sess, err := session.NewSession(cfgs...)
21 | if err != nil {
22 | return nil, oops.Wrapf(err, "new session")
23 | }
24 | return sess, nil
25 | }
26 |
27 | func AssumeRole(svc *sts.STS, input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
28 | if os.Getenv("LOCALSTACK") != "" {
29 | // Localstack doesn't handle IAM checks so let everything through.
30 | return &sts.AssumeRoleOutput{
31 | Credentials: &sts.Credentials{
32 | // The accessKeyId needs to equal the target accountID for operations to happen on multiple accounts.
33 |
34 | AccessKeyId: aws.String(RoleARNToAccountID(*input.RoleArn)),
35 | SecretAccessKey: aws.String("fake"),
36 | SessionToken: aws.String("fake"),
37 | },
38 | }, nil
39 | }
40 |
41 | result, err := svc.AssumeRole(input)
42 | if err != nil {
43 | return nil, oops.Wrapf(err, "assume role")
44 | }
45 | return result, nil
46 | }
47 |
48 | // RoleARN to accountID.
49 | func RoleARNToAccountID(roleARN string) string {
50 | parts := strings.Split(roleARN, ":")
51 | if len(parts) < 4 {
52 | return ""
53 | }
54 |
55 | return parts[4]
56 | }
57 |
--------------------------------------------------------------------------------
/cmd/iac.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sync"
7 |
8 | "github.com/samsarahq/go/oops"
9 | "github.com/santiago-labs/telophasecli/cmd/runner"
10 | "github.com/santiago-labs/telophasecli/resource"
11 | "github.com/santiago-labs/telophasecli/resourceoperation"
12 | )
13 |
14 | func runIAC(
15 | ctx context.Context,
16 | consoleUI runner.ConsoleUI,
17 | cmd int,
18 | accts []resource.Account,
19 | ) error {
20 | var wg sync.WaitGroup
21 |
22 | var once sync.Once
23 | var retError error
24 |
25 | for i := range accts {
26 | wg.Add(1)
27 | go func(acct resource.Account) {
28 | defer wg.Done()
29 | if !acct.IsProvisioned() {
30 | consoleUI.Print(fmt.Sprintf("skipping account: %s because it hasn't been provisioned yet", acct.AccountName), acct)
31 | return
32 | }
33 |
34 | ops, err := resourceoperation.CollectAccountOps(ctx, consoleUI, cmd, &acct, stacks)
35 | if err != nil {
36 | panic(oops.Wrapf(err, "error collecting account ops for acct: %s", acct.AccountID))
37 | }
38 |
39 | if len(ops) == 0 {
40 | consoleUI.Print("No stacks to deploy\n", acct)
41 | return
42 | }
43 |
44 | for _, op := range ops {
45 | if err := op.Call(ctx); err != nil {
46 | once.Do(func() {
47 | retError = err
48 | })
49 | consoleUI.Print(fmt.Sprintf("%v", err), acct)
50 | return
51 | }
52 | }
53 | }(accts[i])
54 | }
55 |
56 | wg.Wait()
57 |
58 | return retError
59 | }
60 | func contains(e string, s []string) bool {
61 | for _, a := range s {
62 | if a == e {
63 | return true
64 | }
65 | }
66 | return false
67 | }
68 |
--------------------------------------------------------------------------------
/resourceoperation/organization_unit_test.go:
--------------------------------------------------------------------------------
1 | package resourceoperation
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/santiago-labs/telophasecli/resource"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestDiffTags(t *testing.T) {
12 | tests := []struct {
13 | description string
14 | input resource.OrganizationUnit
15 |
16 | wantAdded []string
17 | wantRemoved []string
18 | }{
19 | {
20 | description: "adding basic tag",
21 | input: resource.OrganizationUnit{
22 | Accounts: []*resource.Account{
23 | {
24 | AccountName: "mgmt",
25 | Tags: []string{
26 | "ou=mgmt",
27 | },
28 | },
29 | },
30 | },
31 | wantAdded: []string{"ou=mgmt"},
32 | },
33 | {
34 | description: "removing basic tag",
35 | input: resource.OrganizationUnit{
36 | Accounts: []*resource.Account{
37 | {
38 | AccountName: "mgmt",
39 | AWSTags: []string{
40 | "ou=mgmt",
41 | },
42 | },
43 | },
44 | },
45 | wantRemoved: []string{"ou=mgmt"},
46 | },
47 | {
48 | description: "no tag diff",
49 | input: resource.OrganizationUnit{
50 | Accounts: []*resource.Account{
51 | {
52 | AccountName: "mgmt",
53 | Tags: []string{
54 | "ou=mgmt",
55 | },
56 | AWSTags: []string{
57 | "ou=mgmt",
58 | },
59 | },
60 | },
61 | },
62 | },
63 | }
64 |
65 | for _, tc := range tests {
66 | added, removed := diffTags(tc.input.Accounts[0])
67 | fmt.Println("added", added)
68 | fmt.Println("removed", removed)
69 | assert.Equal(t, tc.wantAdded, added, "added: "+tc.description)
70 | assert.Equal(t, tc.wantRemoved, removed, "removed: "+tc.description)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/examples/localstack/s3-remote-state/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/aws/aws-cdk-go/awscdk"
8 | "github.com/aws/aws-cdk-go/awscdk/awss3"
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/aws/session"
11 | "github.com/aws/aws-sdk-go/service/sts"
12 | "github.com/aws/jsii-runtime-go"
13 | )
14 |
15 | type TerraformStateBucketStackProps struct {
16 | awscdk.StackProps
17 | }
18 |
19 | func fetchAccountID() string {
20 | cfg := aws.NewConfig()
21 | if os.Getenv("LOCALSTACK") != "" {
22 | cfg.Endpoint = aws.String("http://localhost:4566")
23 | }
24 | sess := session.Must(session.NewSession(
25 | cfg,
26 | ))
27 | svc := sts.New(sess)
28 | result, err := svc.GetCallerIdentity(&sts.GetCallerIdentityInput{})
29 | if err != nil {
30 | panic(fmt.Sprintf("Failed to get caller identity: %s", err))
31 | }
32 |
33 | return *result.Account
34 | }
35 |
36 | func NewTerraformStateBucketStack(scope awscdk.Construct, id string, props *TerraformStateBucketStackProps) awscdk.Stack {
37 | var sprops awscdk.StackProps
38 | if props != nil {
39 | sprops = props.StackProps
40 | }
41 | stack := awscdk.NewStack(scope, &id, &sprops)
42 |
43 | awss3.NewBucket(stack, jsii.String("TerraformStateBucket"), &awss3.BucketProps{
44 | Versioned: jsii.Bool(true),
45 | BucketName: jsii.String(fmt.Sprintf("tfstate-%s", fetchAccountID())),
46 | })
47 |
48 | return stack
49 | }
50 |
51 | func main() {
52 | app := awscdk.NewApp(nil)
53 |
54 | NewTerraformStateBucketStack(app, "TerraformStateBucketStackExample", &TerraformStateBucketStackProps{
55 | awscdk.StackProps{
56 | Env: env(),
57 | },
58 | })
59 |
60 | app.Synth(nil)
61 | }
62 |
63 | func env() *awscdk.Environment {
64 | return nil
65 | }
66 |
--------------------------------------------------------------------------------
/.github/workflows/dockerimage.yml:
--------------------------------------------------------------------------------
1 | name: release-docker-image
2 |
3 | on:
4 | push:
5 | # run only against tags
6 | tags:
7 | - "v*.*.*"
8 |
9 | env:
10 | AWS_REGION: us-west-2
11 |
12 | jobs:
13 | buildx:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: Configure AWS credentials
20 | uses: aws-actions/configure-aws-credentials@v1
21 | with:
22 | aws-access-key-id: ${{ secrets.ECR_AWS_ACCESS_KEY_ID }}
23 | aws-secret-access-key: ${{ secrets.ECR_AWS_SECRET_ACCESS_KEY }}
24 | aws-region: ${{ env.AWS_REGION }}
25 |
26 | - name: Set up QEMU
27 | uses: docker/setup-qemu-action@v3
28 |
29 | - name: Set up Docker Buildx
30 | uses: docker/setup-buildx-action@v3
31 |
32 | - name: Login to Amazon ECR Public
33 | id: login-ecr-public
34 | uses: aws-actions/amazon-ecr-login@v2
35 | with:
36 | registry-type: public
37 | # ECR login has to be done in us-east-1
38 | env:
39 | AWS_REGION: us-east-1
40 |
41 | - name: Build, tag, and push docker image to Amazon ECR Public
42 | env:
43 | REGISTRY: ${{ steps.login-ecr-public.outputs.registry }}
44 | REGISTRY_ALIAS: w0i7g3v8
45 | REPOSITORY: telophase
46 | # ref_name is the tag
47 | IMAGE_TAG: ${{ github.ref_name }}
48 | AWS_REGION: ${{ env.AWS_REGION }}
49 | run: |
50 | docker buildx build \
51 | --platform linux/arm/v7,linux/arm64/v8,linux/amd64 \
52 | --tag $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG \
53 | --provenance=false \
54 | --push .
55 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example .goreleaser.yml file with some sensible defaults.
2 | # Make sure to check the documentation at https://goreleaser.com
3 |
4 | # The lines below are called `modelines`. See `:help modeline`
5 | # Feel free to remove those if you don't want/need to use them.
6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj
8 |
9 | before:
10 | hooks:
11 | # You may remove this if you don't use go modules.
12 | - go mod tidy
13 | # you may remove this if you don't need go generate
14 | - go generate ./...
15 |
16 | builds:
17 | - env:
18 | - CGO_ENABLED=0
19 | goos:
20 | - linux
21 | - windows
22 | - darwin
23 |
24 | archives:
25 | - format: tar.gz
26 | # this name template makes the OS and Arch compatible with the results of `uname`.
27 | name_template: >-
28 | {{ .ProjectName }}_
29 | {{- title .Os }}_
30 | {{- if eq .Arch "amd64" }}x86_64
31 | {{- else if eq .Arch "386" }}i386
32 | {{- else }}{{ .Arch }}{{ end }}
33 | {{- if .Arm }}v{{ .Arm }}{{ end }}
34 | # use zip for windows archives
35 | format_overrides:
36 | - goos: windows
37 | format: zip
38 |
39 | changelog:
40 | sort: asc
41 | filters:
42 | exclude:
43 | - "^docs:"
44 | - "^test:"
45 |
46 | brews:
47 | - name: telophasecli
48 | description: "Open-Source AWS Control Tower"
49 | homepage: "https://github.com/Santiago-Labs/telophasecli"
50 | license: "GPL-3.0"
51 |
52 | commit_author:
53 | name: dschofie
54 | email: danny@telophase.dev
55 |
56 | repository:
57 | # Repository owner.
58 | owner: Santiago-Labs
59 | # Repository name.
60 | name: homebrew-telophase
61 | branch: main
62 |
--------------------------------------------------------------------------------
/cmd/diff.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strings"
8 |
9 | "github.com/santiago-labs/telophasecli/cmd/runner"
10 | "github.com/santiago-labs/telophasecli/resourceoperation"
11 | "golang.org/x/sync/errgroup"
12 |
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | func init() {
17 | rootCmd.AddCommand(diffCmd)
18 | diffCmd.Flags().StringVar(&stacks, "stacks", "", "Filter stacks to deploy")
19 | diffCmd.Flags().StringVar(&tag, "tag", "", "Filter accounts and organization units to deploy with a comma separated list")
20 | diffCmd.Flags().StringVar(&targets, "targets", "", "Filter resource types to deploy. Options: organization, scp, stacks")
21 | diffCmd.Flags().StringVar(&orgFile, "org", "organization.yml", "Path to the organization.yml file")
22 | diffCmd.Flags().BoolVar(&useTUI, "tui", false, "use the TUI for diff")
23 | }
24 |
25 | var diffCmd = &cobra.Command{
26 | Use: "diff",
27 | Short: "diff - Show accounts to create/update and CDK and/or TF changes for each account.",
28 | Run: func(cmd *cobra.Command, args []string) {
29 |
30 | if err := validateTargets(); err != nil {
31 | log.Fatal("error validating targets err:", err)
32 | }
33 | var consoleUI runner.ConsoleUI
34 | parsedTargets := filterEmptyStrings(strings.Split(targets, ","))
35 |
36 | var g errgroup.Group
37 |
38 | if useTUI {
39 | consoleUI = runner.NewTUI()
40 | g.Go(func() error {
41 | return ProcessOrgEndToEnd(consoleUI, resourceoperation.Diff, parsedTargets)
42 | })
43 | } else {
44 | consoleUI = runner.NewSTDOut()
45 | if err := ProcessOrgEndToEnd(consoleUI, resourceoperation.Diff, parsedTargets); err != nil {
46 | fmt.Println(err)
47 | os.Exit(1)
48 | }
49 | }
50 |
51 | consoleUI.Start()
52 | if err := g.Wait(); err != nil {
53 | os.Exit(1)
54 | }
55 | },
56 | }
57 |
--------------------------------------------------------------------------------
/mintlifydocs/mint.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://mintlify.com/schema.json",
3 | "name": "Telophase",
4 | "logo": {
5 | "dark": "/logo/logo.png",
6 | "light": "/logo/light-telophase.svg"
7 | },
8 | "favicon": "/favicon.png",
9 | "colors": {
10 | "primary": "#000000",
11 | "light": "#07C983",
12 | "dark": "#0D9373",
13 | "anchors": {
14 | "from": "#0D9373",
15 | "to": "#07C983"
16 | }
17 | },
18 | "topbarLinks": [
19 | {
20 | "name": "Contact Support",
21 | "url": "mailto:support@telophase.dev"
22 | }
23 | ],
24 | "topbarCtaButton": {
25 | "type": "github",
26 | "url": "https://github.com/Santiago-Labs/telophasecli"
27 | },
28 | "navigation": [
29 | {
30 | "group": "Get Started",
31 | "pages": [
32 | "introduction",
33 | "quickstart"
34 | ]
35 | },
36 | {
37 | "group": "Features",
38 | "pages": [
39 | "features/Manage-AWS-Organizations",
40 | "features/Assign-IaC-Blueprints-To-Accounts",
41 | "features/scps",
42 | "features/tui",
43 | "features/localstack"
44 | ]
45 | },
46 | {
47 | "group": "Configuration",
48 | "pages": [
49 | "config/organization"
50 | ]
51 | },
52 | {
53 | "group": "Commands",
54 | "pages": [
55 | "commands/diff",
56 | "commands/deploy",
57 | "commands/account-import"
58 | ]
59 | }
60 | ],
61 | "footerSocials": {
62 | "twitter": "https://twitter.com/telophaseHQ",
63 | "github": "https://github.com/Santiago-Labs/telophasecli",
64 | "linkedin": "https://www.linkedin.com/company/95748888"
65 | },
66 | "modeToggle": {
67 | "default": "light",
68 | "isHidden": true
69 | },
70 | "analytics": {
71 | "posthog": {
72 | "apiKey": "phc_whf6xThJIHQw6Og5F9dsP3eb7dkrh3N4oZT7nzDhMR0"
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/examples/tf/buildkite/blueprint_terraform/vpc.tf:
--------------------------------------------------------------------------------
1 | provider "aws" {
2 | region = "us-east-1"
3 | }
4 |
5 | resource "aws_vpc" "example_vpc" {
6 | cidr_block = "10.0.0.0/16"
7 | enable_dns_support = true
8 | enable_dns_hostnames = true
9 |
10 | tags = {
11 | Name = "ExampleVPC"
12 | }
13 | }
14 |
15 | resource "aws_internet_gateway" "example_igw" {
16 | vpc_id = aws_vpc.example_vpc.id
17 |
18 | tags = {
19 | Name = "ExampleIGW"
20 | }
21 | }
22 |
23 | resource "aws_subnet" "example_public_subnet" {
24 | count = 3
25 | vpc_id = aws_vpc.example_vpc.id
26 | cidr_block = cidrsubnet(aws_vpc.example_vpc.cidr_block, 3, count.index)
27 | map_public_ip_on_launch = true
28 | availability_zone = element(split(",", data.aws_availability_zones.available.names), count.index)
29 |
30 | tags = {
31 | Name = "PublicSubnet-${count.index}"
32 | }
33 | }
34 |
35 | resource "aws_subnet" "example_private_subnet" {
36 | count = 3
37 | vpc_id = aws_vpc.example_vpc.id
38 | cidr_block = cidrsubnet(aws_vpc.example_vpc.cidr_block, 3, count.index + 3)
39 | availability_zone = element(split(",", data.aws_availability_zones.available.names), count.index)
40 |
41 | tags = {
42 | Name = "PrivateSubnet-${count.index}"
43 | }
44 | }
45 |
46 | resource "aws_route_table" "example_public_rt" {
47 | vpc_id = aws_vpc.example_vpc.id
48 |
49 | route {
50 | cidr_block = "0.0.0.0/0"
51 | gateway_id = aws_internet_gateway.example_igw.id
52 | }
53 |
54 | tags = {
55 | Name = "PublicRouteTable"
56 | }
57 | }
58 |
59 | resource "aws_route_table_association" "example_public_rta" {
60 | count = length(aws_subnet.example_public_subnet.*.id)
61 | subnet_id = aws_subnet.example_public_subnet.*.id[count.index]
62 | route_table_id = aws_route_table.example_public_rt.id
63 | }
64 |
65 | # Fetching availability zones
66 | data "aws_availability_zones" "available" {}
67 |
68 |
--------------------------------------------------------------------------------
/examples/cloudformation/dynamo/table.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: "2010-09-09"
2 |
3 | Description: 'AWS CloudFormation Sample Template DynamoDB_Table: This template demonstrates the creation of a DynamoDB table. **WARNING** This template creates an Amazon DynamoDB table. You will be billed for the AWS resources used if you create a stack from this template.'
4 |
5 | Metadata:
6 | License: Apache-2.0
7 |
8 | Parameters:
9 | HashKeyElementName:
10 | Description: HashType PrimaryKey Name
11 | Type: String
12 | AllowedPattern: '[a-zA-Z0-9]*'
13 | MinLength: "1"
14 | MaxLength: "2048"
15 | ConstraintDescription: must contain only alphanumberic characters
16 |
17 | HashKeyElementType:
18 | Description: HashType PrimaryKey Type
19 | Type: String
20 | Default: S
21 | AllowedPattern: '[S|N]'
22 | MinLength: "1"
23 | MaxLength: "1"
24 | ConstraintDescription: must be either S or N
25 |
26 | ReadCapacityUnits:
27 | Description: Provisioned read throughput
28 | Type: Number
29 | Default: "5"
30 | MinValue: "5"
31 | MaxValue: "10000"
32 | ConstraintDescription: must be between 5 and 10000
33 |
34 | WriteCapacityUnits:
35 | Description: Provisioned write throughput
36 | Type: Number
37 | Default: "10"
38 | MinValue: "5"
39 | MaxValue: "10000"
40 | ConstraintDescription: must be between 5 and 10000
41 |
42 | Resources:
43 | myDynamoDBTable:
44 | Type: AWS::DynamoDB::Table
45 | Properties:
46 | AttributeDefinitions:
47 | - AttributeName: !Ref HashKeyElementName
48 | AttributeType: !Ref HashKeyElementType
49 | KeySchema:
50 | - AttributeName: !Ref HashKeyElementName
51 | KeyType: HASH
52 | ProvisionedThroughput:
53 | ReadCapacityUnits: !Ref ReadCapacityUnits
54 | WriteCapacityUnits: !Ref WriteCapacityUnits
55 |
56 | Outputs:
57 | TableName:
58 | Description: Table name of the newly created DynamoDB table
59 | Value: !Ref myDynamoDBTable
60 |
61 |
--------------------------------------------------------------------------------
/lib/telophase/account.go:
--------------------------------------------------------------------------------
1 | package telophase
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "os"
10 | )
11 |
12 | func ValidTelophaseToken(token string) bool {
13 | if token == "" ||
14 | token == "ignore" {
15 | return false
16 | }
17 |
18 | return true
19 | }
20 |
21 | func UpsertAccount(accountID string, accountName string) {
22 | token := os.Getenv("TELOPHASE_TOKEN")
23 | if ValidTelophaseToken(token) {
24 | reqBody, _ := json.Marshal(map[string]string{
25 | "account_id": accountID,
26 | "name": accountName,
27 | })
28 | client := &http.Client{}
29 | req, _ := http.NewRequest("POST", "https://api.telophase.dev/cloudAccount", bytes.NewBuffer(reqBody))
30 | req.Header.Set("Content-Type", "application/json")
31 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
32 | resp, err := client.Do(req)
33 | if err != nil {
34 | fmt.Printf("error creating account in telophase: %s\n", err)
35 | }
36 | if resp.StatusCode != 200 {
37 | defer resp.Body.Close()
38 | body, err := io.ReadAll(resp.Body)
39 | if err != nil {
40 | fmt.Println(err)
41 | }
42 | fmt.Printf("error creating account in telophase: %s\n", string(body))
43 | }
44 | }
45 | }
46 |
47 | func RecordDeploy(accountID string, accountName string) {
48 | token := os.Getenv("TELOPHASE_TOKEN")
49 | if ValidTelophaseToken(token) {
50 | reqBody, _ := json.Marshal(map[string]string{})
51 | client := &http.Client{}
52 | req, _ := http.NewRequest("PATCH", fmt.Sprintf("https://api.telophase.dev/cloudAccount/%s", accountID), bytes.NewBuffer(reqBody))
53 | req.Header.Set("Content-Type", "application/json")
54 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
55 | resp, err := client.Do(req)
56 | if err != nil {
57 | fmt.Printf("error creating account in telophase: %s\n", err)
58 | }
59 | if resp.StatusCode != 200 {
60 | defer resp.Body.Close()
61 | body, err := io.ReadAll(resp.Body)
62 | if err != nil {
63 | fmt.Println(err)
64 | }
65 | fmt.Printf("error creating account in telophase: %s\n", string(body))
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/cmd/deploy.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "strings"
8 |
9 | "github.com/santiago-labs/telophasecli/cmd/runner"
10 | "github.com/santiago-labs/telophasecli/resourceoperation"
11 | "golang.org/x/sync/errgroup"
12 |
13 | "github.com/spf13/cobra"
14 | )
15 |
16 | var (
17 | tag string
18 | targets string
19 | stacks string
20 | allowDeleteAccount bool
21 |
22 | // TUI
23 | useTUI bool
24 | )
25 |
26 | func init() {
27 | rootCmd.AddCommand(deployCmd)
28 | deployCmd.Flags().StringVar(&stacks, "stacks", "", "Filter stacks to deploy")
29 | deployCmd.Flags().StringVar(&tag, "tag", "", "Filter accounts and organization units to deploy with a comma separated list")
30 | deployCmd.Flags().StringVar(&targets, "targets", "", "Filter resource types to deploy. Options: organization, scp, stacks")
31 | deployCmd.Flags().StringVar(&orgFile, "org", "organization.yml", "Path to the organization.yml file")
32 | 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")
34 | }
35 |
36 | var deployCmd = &cobra.Command{
37 | Use: "deploy",
38 | Short: "deploy - Deploy a CDK and/or TF stacks to your AWS account(s). Accounts in organization.yml will be created if they do not exist.",
39 | Run: func(cmd *cobra.Command, args []string) {
40 |
41 | if err := validateTargets(); err != nil {
42 | log.Fatal("error validating targets err:", err)
43 | }
44 | var consoleUI runner.ConsoleUI
45 | parsedTargets := filterEmptyStrings(strings.Split(targets, ","))
46 | var g errgroup.Group
47 |
48 | if useTUI {
49 | consoleUI = runner.NewTUI()
50 | g.Go(func() error {
51 | return ProcessOrgEndToEnd(consoleUI, resourceoperation.Deploy, parsedTargets)
52 | })
53 | } else {
54 | consoleUI = runner.NewSTDOut()
55 | if err := ProcessOrgEndToEnd(consoleUI, resourceoperation.Deploy, parsedTargets); err != nil {
56 | fmt.Println(err)
57 | os.Exit(1)
58 | }
59 | }
60 |
61 | consoleUI.Start()
62 | if err := g.Wait(); err != nil {
63 | fmt.Println(err)
64 | os.Exit(1)
65 | }
66 | },
67 | }
68 |
--------------------------------------------------------------------------------
/tests/cloudformation/table.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: "2010-09-09"
2 |
3 | Description: 'AWS CloudFormation Sample Template DynamoDB_Table: This template demonstrates the creation of a DynamoDB table. **WARNING** This template creates an Amazon DynamoDB table. You will be billed for the AWS resources used if you create a stack from this template.'
4 |
5 | Metadata:
6 | License: Apache-2.0
7 |
8 | Parameters:
9 | HashKeyElementName:
10 | Description: HashType PrimaryKey Name
11 | Type: String
12 | AllowedPattern: '[a-zA-Z0-9]*'
13 | MinLength: "1"
14 | MaxLength: "2048"
15 | ConstraintDescription: must contain only alphanumberic characters
16 |
17 | TableName:
18 | Description: TableName
19 | Type: String
20 | AllowedPattern: '[a-zA-Z0-9]*'
21 | MinLength: "1"
22 | MaxLength: "2048"
23 | ConstraintDescription: must contain only alphanumberic characters
24 |
25 | HashKeyElementType:
26 | Description: HashType PrimaryKey Type
27 | Type: String
28 | Default: S
29 | AllowedPattern: '[S|N]'
30 | MinLength: "1"
31 | MaxLength: "1"
32 | ConstraintDescription: must be either S or N
33 |
34 | ReadCapacityUnits:
35 | Description: Provisioned read throughput
36 | Type: Number
37 | Default: "5"
38 | MinValue: "5"
39 | MaxValue: "10000"
40 | ConstraintDescription: must be between 5 and 10000
41 |
42 | WriteCapacityUnits:
43 | Description: Provisioned write throughput
44 | Type: Number
45 | Default: "10"
46 | MinValue: "5"
47 | MaxValue: "10000"
48 | ConstraintDescription: must be between 5 and 10000
49 |
50 | Resources:
51 | myDynamoDBTable:
52 | Type: AWS::DynamoDB::Table
53 | Properties:
54 | AttributeDefinitions:
55 | - AttributeName: !Ref HashKeyElementName
56 | AttributeType: !Ref HashKeyElementType
57 | KeySchema:
58 | - AttributeName: !Ref HashKeyElementName
59 | KeyType: HASH
60 | ProvisionedThroughput:
61 | ReadCapacityUnits: !Ref ReadCapacityUnits
62 | WriteCapacityUnits: !Ref WriteCapacityUnits
63 | TableName: !Ref TableName
64 |
65 | Outputs:
66 | TableName:
67 | Description: Table name of the newly created DynamoDB table
68 | Value: !Ref myDynamoDBTable
69 |
70 |
--------------------------------------------------------------------------------
/examples/localstack/README.mdx:
--------------------------------------------------------------------------------
1 | # Example: Localstack
2 | This example will walk you through using localstack with Telophase to create AWS Accounts and OUs (see: [`organization.yml`](./organization.yml)), and deploy CDK and Terraform to them.
3 |
4 | # Getting Started with Localstack + Telophase
5 | 1. Install Dependencies
6 | ```bash
7 | ./setup.sh
8 | ```
9 |
10 | 2. Start up Localstack in the background. Ensure your pro license is set. Learn more [here](https://docs.localstack.cloud/getting-started/auth-token/)
11 | ```bash
12 | localstack start -d
13 | ```
14 |
15 | 3. Localstack Setup
16 | Create your root organization in Localstack
17 | ```bash
18 | awslocal organizations create-organization --feature-set ALL
19 | ```
20 |
21 | 4. Setup an AWS_REGION if not set
22 | ```bash
23 | export AWS_REGION=us-east-1
24 | ```
25 |
26 | 5. Deploy Infra in the Accounts
27 |
28 | Telophase will:
29 | 1. Create the accounts and OUs listed in `organization.yml`
30 | 2. A CDK stack will create an S3 bucket for a terraform state will be create in each account. Additionally, a Terraform stack will create a CI deploy role in each account in the `Production` OU.
31 | ```bash
32 | LOCALSTACK=true telophasecli deploy --tui
33 | ```
34 |
35 | 6. Inspect the accounts using `awslocal`
36 |
37 | Learn how Localstack handles multi-account auth [here](https://docs.localstack.cloud/references/multi-account-setups/)
38 | ```bash
39 | # View the organizations you have created
40 | awslocal organizations list-accounts
41 |
42 | # Each account will have its own tfstate bucket
43 | AWS_ACCESS_KEY_ID=$AN_ORG_FROM_ABOVE awslocal s3 ls
44 |
45 | # New roles will be created in each account
46 | AWS_ACCESS_KEY_ID=$AN_ORG_FROM_ABOVE awslocal iam list-roles
47 | ```
48 |
49 | ### Common Errors
50 | #### Cannot create preexisting table / Cannot do operations on a non-existent table
51 | These are triggered by a concurrency bug in localstack. You can resolve it by pre-creating the `tf-test-state` table and running Telophase again:
52 | ```bash
53 | aws dynamodb create-table --table-name tf-test-state \
54 | --attribute-definitions AttributeName=id,AttributeType=S \
55 | --key-schema AttributeName=id,KeyType=HASH \
56 | --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
57 | --endpoint-url http://localhost:4566
58 |
59 | LOCALSTACK=true telophasecli deploy
60 | ```
--------------------------------------------------------------------------------
/lib/metrics/metrics.go:
--------------------------------------------------------------------------------
1 | package metrics
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path"
7 |
8 | "github.com/google/uuid"
9 | "github.com/pkg/errors"
10 | "github.com/posthog/posthog-go"
11 | )
12 |
13 | type client struct {
14 | posthog.Client
15 | }
16 |
17 | var c *client
18 |
19 | func Init() {
20 | if !isEnabled() {
21 | return
22 | }
23 |
24 | ph, err := posthog.NewWithConfig(
25 | "phc_whf6xThJIHQw6Og5F9dsP3eb7dkrh3N4oZT7nzDhMR0",
26 | posthog.Config{
27 | Endpoint: "https://app.posthog.com",
28 | },
29 | )
30 | if err != nil {
31 | fmt.Fprintln(os.Stderr, errors.Wrap(err, "fail to init telemetry"))
32 | }
33 |
34 | c = &client{ph}
35 | }
36 |
37 | func isEnabled() bool {
38 | return false
39 | }
40 |
41 | func Close() {
42 | if c == nil || !isEnabled() {
43 | return
44 | }
45 |
46 | err := c.Close()
47 | if err != nil {
48 | fmt.Fprintln(os.Stderr, errors.Wrap(err, "fail to close telemetry"))
49 | }
50 | }
51 |
52 | type Event string
53 |
54 | const EventRunCommand Event = "telophasecli"
55 |
56 | func Push(event Event, properties posthog.Properties) {
57 | if c == nil || !isEnabled() {
58 | return
59 | }
60 |
61 | if err := c.Enqueue(posthog.Capture{
62 | DistinctId: getDistinctId(),
63 | Event: string(event),
64 | Properties: properties,
65 | }); err != nil {
66 | fmt.Fprintln(os.Stderr, errors.Wrap(err, "fail to enqueue telemetry"))
67 | }
68 | }
69 |
70 | func RegisterCommand() {
71 | command := ""
72 | if len(os.Args) > 1 {
73 | command = os.Args[1]
74 | }
75 |
76 | Push(
77 | EventRunCommand,
78 | map[string]interface{}{
79 | "$set": map[string]interface{}{
80 | "command": command,
81 | },
82 | },
83 | )
84 | }
85 |
86 | func getDistinctId() string {
87 | id := uuid.NewString()
88 |
89 | homedir, err := os.UserHomeDir()
90 | if err != nil {
91 | return id
92 | }
93 |
94 | telpohaseDir := path.Join(homedir, ".telophase")
95 | err = os.MkdirAll(telpohaseDir, os.ModePerm)
96 | if err != nil {
97 | return id
98 | }
99 |
100 | fpath := path.Join(telpohaseDir, "userid")
101 |
102 | bs, err := os.ReadFile(fpath)
103 | if err != nil {
104 | os.WriteFile(fpath, []byte(id), 0644)
105 | return id
106 | }
107 |
108 | prevId, err := uuid.ParseBytes(bs)
109 | if err != nil {
110 | os.WriteFile(fpath, []byte(id), 0644)
111 | return id
112 | }
113 |
114 | return prevId.String()
115 | }
116 |
--------------------------------------------------------------------------------
/cmd/runner/stdout.go:
--------------------------------------------------------------------------------
1 | package runner
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os/exec"
7 | "sync"
8 |
9 | "github.com/santiago-labs/telophasecli/lib/colors"
10 | "github.com/santiago-labs/telophasecli/resource"
11 | )
12 |
13 | func NewSTDOut() ConsoleUI {
14 | return &stdOut{
15 | coloredId: make(map[string]string),
16 | lock: sync.Mutex{},
17 | }
18 | }
19 |
20 | type stdOut struct {
21 | coloredId map[string]string
22 | lock sync.Mutex
23 | }
24 |
25 | func (s *stdOut) ColoredId(acct resource.Account) string {
26 | s.lock.Lock()
27 | defer s.lock.Unlock()
28 |
29 | coloredId, ok := s.coloredId[acct.ID()]
30 | if !ok {
31 | colorFunc := colors.DeterministicColorFunc(acct.AccountID)
32 | if acct.AccountName != "" {
33 | coloredId = colorFunc(fmt.Sprintf("[Account: %s (%s)]", acct.ID(), acct.AccountName))
34 | } else {
35 | coloredId = colorFunc("[Account: " + acct.ID() + "]")
36 | }
37 | s.coloredId[acct.ID()] = coloredId
38 | }
39 | return coloredId
40 | }
41 |
42 | // RunCmd takes the command and acct and runs it while prepending the
43 | // coloredAccountID from stderr and stdout and printing it.
44 | func (s *stdOut) RunCmd(cmd *exec.Cmd, acct resource.Account) error {
45 | stdoutPipe, err := cmd.StdoutPipe()
46 | if err != nil {
47 | return fmt.Errorf("[ERROR] %s %v", s.ColoredId(acct), err)
48 | }
49 | stdoutScanner := bufio.NewScanner(stdoutPipe)
50 |
51 | stderrPipe, err := cmd.StderrPipe()
52 | if err != nil {
53 | return fmt.Errorf("[ERROR] %s %v", s.ColoredId(acct), err)
54 | }
55 | stderrScanner := bufio.NewScanner(stderrPipe)
56 |
57 | if err := cmd.Start(); err != nil {
58 | return fmt.Errorf("[ERROR] %s %v", s.ColoredId(acct), err)
59 | }
60 |
61 | var scannerWg sync.WaitGroup
62 | scannerWg.Add(2)
63 | scanF := func(scanner *bufio.Scanner, _ string) {
64 | defer scannerWg.Done()
65 | for scanner.Scan() {
66 | fmt.Printf("%s %s\n", s.ColoredId(acct), scanner.Text())
67 | }
68 | if err := scanner.Err(); err != nil {
69 | fmt.Printf("[ERROR] %s %v\n", s.ColoredId(acct), err)
70 | return
71 | }
72 | }
73 |
74 | go scanF(stdoutScanner, "stdout")
75 | go scanF(stderrScanner, "stderr")
76 | scannerWg.Wait()
77 |
78 | if err := cmd.Wait(); err != nil {
79 | return fmt.Errorf("[ERROR] %s %v", s.ColoredId(acct), err)
80 | }
81 |
82 | return nil
83 | }
84 |
85 | func (s *stdOut) Print(msg string, acct resource.Account) {
86 | fmt.Printf("%s %v\n", s.ColoredId(acct), msg)
87 | }
88 |
89 | func (s *stdOut) Start() {}
90 |
--------------------------------------------------------------------------------
/mintlifydocs/commands/deploy.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'telophasecli deploy'
3 | ---
4 |
5 | ```
6 | Usage:
7 | telophasecli deploy [flags]
8 |
9 | Flags:
10 | -h, --help help for deploy
11 | --org string Path to the organization.yml file (default "organization.yml")
12 | --stacks string Filter stacks to deploy
13 | --tag string Filter accounts and account groups to deploy via a comma separated list
14 | --tui use the TUI for deploy
15 | ```
16 |
17 | This command will read `organization.yml` and **perform**:
18 | 1) Changes required to AWS Organization, provisioning/deprovisioning new accounts.
19 | 2) Provision Resources within the accounts:
20 | - CDK deploy. Telophase runs `cdk bootstrap` and `cdk synth` on every deploy.
21 | - Terraform apply. Telophase automatically runs `terraform plan` if no plan exists.
22 | - `telophasecli diff` does _NOT_ need to be run before `telophasecli deploy`.
23 |
24 | # Examples
25 | For the following examples, we will use the following `organization.yml`.
26 |
27 | ```yml organization.yml
28 | Organization:
29 | Name: root
30 | OrganizationUnits:
31 | - Name: ProductionTenants
32 | Stacks:
33 | - Type: "CDK"
34 | Path: "./s3-remote-state"
35 | - Type: "Terraform"
36 | Path: "./tf/ci_iam"
37 | Name: "Default IAM Roles for CI"
38 | # Tags are specified here
39 | Tags:
40 | - "env=production"
41 | Accounts:
42 | - Email: production+us0@example.com
43 | AccountName: US0
44 | - Email: production+us1@example.com
45 | AccountName: US1
46 | - Email: production+us2@example.com
47 | AccountName: US2
48 | - Email: production+us3@example.com
49 | AccountName: US3
50 |
51 | - Name: Development
52 | Stacks:
53 | - Type: "CDK"
54 | Path: "./s3-remote-state"
55 | Tags:
56 | - "dev"
57 | Accounts:
58 | - Email: alice@example.com
59 | AccountName: Alice
60 | - Email: bob@example.com
61 | AccountName: Bob
62 | - Email: ethan@example.com
63 | AccountName: Ethan
64 | ```
65 | ## Using Tags
66 |
67 | 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:
68 |
69 |
70 |
--------------------------------------------------------------------------------
/mintlifydocs/features/localstack.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Testing'
3 | icon: 'vial'
4 | ---
5 |
6 | # Example: Localstack
7 | This example will walk you through using localstack with Telophase to create AWS Accounts and OUs (see: [`organization.yml`](https://github.com/Santiago-Labs/telophasecli/blob/main/examples/localstack/organization.yml)), and Stacks to them.
8 |
9 | # Getting Started with Localstack + Telophase
10 | 1. Install Dependencies
11 | ```bash
12 | ./setup.sh
13 | ```
14 |
15 | 2. Start up Localstack in the background. Ensure your pro license is set. Learn more [here](https://docs.localstack.cloud/getting-started/auth-token/)
16 | ```bash
17 | localstack start -d
18 | ```
19 |
20 | 3. Localstack Setup
21 | Create your root organization in Localstack
22 | ```bash
23 | awslocal organizations create-organization --feature-set ALL
24 | ```
25 |
26 | 4. Setup an AWS_REGION if not set
27 | ```bash
28 | export AWS_REGION=us-east-1
29 | ```
30 |
31 | 5. Deploy Infra in the Accounts
32 |
33 | Telophase will:
34 | 1. Create the accounts and OUs listed in [`organization.yml`](https://github.com/Santiago-Labs/telophasecli/blob/main/examples/localstack/organization.yml)
35 | 2. A CDK stack will create an S3 bucket for a terraform state will be create in each account. Additionally, a Terraform stack will create a CI deploy role in each account in the `Production` OU.
36 | ```bash
37 | LOCALSTACK=true telophasecli deploy --tui
38 | ```
39 |
40 | 6. Inspect the accounts using `awslocal`
41 |
42 | Learn how Localstack handles multi-account auth [here](https://docs.localstack.cloud/references/multi-account-setups/)
43 | ```bash
44 | # View the organizations you have created
45 | awslocal organizations list-accounts
46 |
47 | # Each account will have its own tfstate bucket
48 | AWS_ACCESS_KEY_ID=$AN_ORG_FROM_ABOVE awslocal s3 ls
49 |
50 | # New roles will be created in each account
51 | AWS_ACCESS_KEY_ID=$AN_ORG_FROM_ABOVE awslocal iam list-roles
52 | ```
53 |
54 | ### Common Errors
55 | #### Cannot create preexisting table / Cannot do operations on a non-existent table
56 | These are triggered by a concurrency bug in localstack. You can resolve it by pre-creating the `tf-test-state` table and running Telophase again:
57 | ```bash
58 | aws dynamodb create-table --table-name tf-test-state \
59 | --attribute-definitions AttributeName=id,AttributeType=S \
60 | --key-schema AttributeName=id,KeyType=HASH \
61 | --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \
62 | --endpoint-url http://localhost:4566
63 |
64 | LOCALSTACK=true telophasecli deploy
65 | ```
66 |
67 |
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -eux
4 |
5 | print_red() {
6 | echo -e "\033[0;31m$1\033[0m"
7 | }
8 |
9 | print_green() {
10 | echo -e "\033[0;32m$1\033[0m"
11 | }
12 |
13 | if command -v brew &> /dev/null; then
14 | echo "Brew is installed, proceeding with awscli and terraform installation."
15 | brew install awscli awscli-local terraform terraform-local localstack/tap/localstack-cli
16 | elif [ "$(uname)" = "Linux" ]; then
17 | if [ -f /etc/debian_version ]; then
18 | echo "Debian-based Linux detected. Installing packages with apt."
19 | sudo snap install aws-cli --classic
20 | pip3 install awscli-local
21 | pip3 install terraform-local
22 | sudo snap install terraform --classic
23 | curl -Lo localstack-cli-3.3.0-linux-amd64-onefile.tar.gz https://github.com/localstack/localstack-cli/releases/download/v3.3.0/localstack-cli-3.3.0-linux-amd64-onefile.tar.gz
24 | sudo tar xvzf localstack-cli-3.3.0-linux-*-onefile.tar.gz -C /usr/local/bin
25 | elif [ -f /etc/redhat-release ]; then
26 | echo "Red Hat-based Linux detected. Installing packages with dnf."
27 | sudo dnf install -y awscli
28 | print_red "Please manually install Terraform following the official instructions."
29 | fi
30 | else
31 | print_red "Brew is not installed, or you're not on a supported Linux distribution. Please install awscli and terraform manually."
32 | exit 1
33 | fi
34 |
35 | if command -v npm &> /dev/null
36 | then
37 | echo "Installing aws-cdk and aws-cdk-local."
38 | npm install -g aws-cdk aws-cdk-local
39 | else
40 | print_red "npm is not installed. Please install it and then return."
41 | exit 1
42 | fi
43 |
44 | if command -v go &> /dev/null
45 | then
46 | echo "Installing telophasecli..."
47 | go install github.com/santiago-labs/telophasecli@latest
48 | else
49 | if [ "$(uname)" = "Darwin" ]; then
50 | print_red "Go is not installed. On Mac, you can install Go using Homebrew with: brew install go. Then, return to run this script again."
51 | elif [ "$(uname)" = "Linux" ]; then
52 | print_red "Go is not installed. On Linux, you can generally install Go by running: sudo snap install go --classic (Debian/Ubuntu) or sudo dnf install golang (Fedora/RHEL). Then, return to run this script again."
53 | else
54 | print_red "Go is not installed. Please visit https://golang.org/doc/install for instructions on how to install Go on your system."
55 | fi
56 | exit 1
57 | fi
58 |
59 | print_green "Setup complete! You can now run telophasecli :)"
--------------------------------------------------------------------------------
/tests/cdk/dynamo/dynamo.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/aws/aws-cdk-go/awscdk/v2"
5 | "github.com/aws/aws-cdk-go/awscdk/v2/awsdynamodb"
6 |
7 | "github.com/aws/constructs-go/constructs/v10"
8 | "github.com/aws/jsii-runtime-go"
9 | )
10 |
11 | type DynamoStackProps struct {
12 | awscdk.StackProps
13 | }
14 |
15 | func NewDynamoStack(scope constructs.Construct, id string, props *DynamoStackProps) awscdk.Stack {
16 | var sprops awscdk.StackProps
17 | if props != nil {
18 | sprops = props.StackProps
19 | }
20 | stack := awscdk.NewStack(scope, &id, &sprops)
21 |
22 | // The code that defines your stack goes here
23 |
24 | // example resource
25 | name := "cdktesttable"
26 | awsdynamodb.NewTable(stack, jsii.String("Table"), &awsdynamodb.TableProps{
27 | TableName: &name,
28 | PartitionKey: &awsdynamodb.Attribute{
29 | Name: jsii.String("pk"),
30 | Type: awsdynamodb.AttributeType_STRING,
31 | },
32 | })
33 |
34 | return stack
35 | }
36 |
37 | func main() {
38 | defer jsii.Close()
39 |
40 | app := awscdk.NewApp(nil)
41 |
42 | NewDynamoStack(app, "DynamoStack", &DynamoStackProps{
43 | awscdk.StackProps{
44 | Env: env(),
45 | },
46 | })
47 |
48 | app.Synth(nil)
49 | }
50 |
51 | // env determines the AWS environment (account+region) in which our stack is to
52 | // be deployed. For more information see: https://docs.aws.amazon.com/cdk/latest/guide/environments.html
53 | func env() *awscdk.Environment {
54 | // If unspecified, this stack will be "environment-agnostic".
55 | // Account/Region-dependent features and context lookups will not work, but a
56 | // single synthesized template can be deployed anywhere.
57 | //---------------------------------------------------------------------------
58 | return nil
59 |
60 | // Uncomment if you know exactly what account and region you want to deploy
61 | // the stack to. This is the recommendation for production stacks.
62 | //---------------------------------------------------------------------------
63 | // return &awscdk.Environment{
64 | // Account: jsii.String("123456789012"),
65 | // Region: jsii.String("us-east-1"),
66 | // }
67 |
68 | // Uncomment to specialize this stack for the AWS Account and Region that are
69 | // implied by the current CLI configuration. This is recommended for dev
70 | // stacks.
71 | //---------------------------------------------------------------------------
72 | // return &awscdk.Environment{
73 | // Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")),
74 | // Region: jsii.String(os.Getenv("CDK_DEFAULT_REGION")),
75 | // }
76 | }
77 |
--------------------------------------------------------------------------------
/examples/cdk/tf-s3-backend/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "go mod download && go run app.go",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "go.mod",
11 | "go.sum",
12 | "**/*test.go"
13 | ]
14 | },
15 | "context": {
16 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
17 | "@aws-cdk/core:checkSecretUsage": true,
18 | "@aws-cdk/core:target-partitions": [
19 | "aws",
20 | "aws-cn"
21 | ],
22 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
23 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
24 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
25 | "@aws-cdk/aws-iam:minimizePolicies": true,
26 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
27 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
28 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
29 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
30 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
31 | "@aws-cdk/core:enablePartitionLiterals": true,
32 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
33 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
34 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
35 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
36 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
37 | "@aws-cdk/aws-route53-patters:useCertificate": true,
38 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
39 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
40 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
41 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
42 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
43 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
44 | "@aws-cdk/aws-redshift:columnId": true,
45 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
46 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
47 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
48 | "@aws-cdk/aws-kms:aliasNameRef": true,
49 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
50 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
51 | "@aws-cdk/aws-efs:denyAnonymousAccess": true,
52 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
53 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
54 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
55 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
56 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
57 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
58 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true
59 | }
60 | }
--------------------------------------------------------------------------------
/examples/localstack/s3-remote-state/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "go mod download && go run app.go",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "go.mod",
11 | "go.sum",
12 | "**/*test.go"
13 | ]
14 | },
15 | "context": {
16 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
17 | "@aws-cdk/core:checkSecretUsage": true,
18 | "@aws-cdk/core:target-partitions": [
19 | "aws",
20 | "aws-cn"
21 | ],
22 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
23 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
24 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
25 | "@aws-cdk/aws-iam:minimizePolicies": true,
26 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
27 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
28 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
29 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
30 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
31 | "@aws-cdk/core:enablePartitionLiterals": true,
32 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
33 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true,
34 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
35 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
36 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
37 | "@aws-cdk/aws-route53-patters:useCertificate": true,
38 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
39 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
40 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
41 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
42 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
43 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
44 | "@aws-cdk/aws-redshift:columnId": true,
45 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
46 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
47 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
48 | "@aws-cdk/aws-kms:aliasNameRef": true,
49 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
50 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
51 | "@aws-cdk/aws-efs:denyAnonymousAccess": true,
52 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
53 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
54 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
55 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
56 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
57 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
58 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true
59 | }
60 | }
--------------------------------------------------------------------------------
/lib/ymlparser/organization_test.go:
--------------------------------------------------------------------------------
1 | package ymlparser
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "testing"
7 |
8 | "github.com/aws/aws-sdk-go/aws"
9 | "github.com/google/go-cmp/cmp"
10 | "github.com/google/go-cmp/cmp/cmpopts"
11 | "github.com/santiago-labs/telophasecli/lib/awsorgs"
12 | "github.com/santiago-labs/telophasecli/lib/awsorgs/awsorgsmock"
13 | "github.com/santiago-labs/telophasecli/resource"
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | func basicOU() resource.OrganizationUnit {
18 | return resource.OrganizationUnit{
19 | OUName: "root",
20 | OUID: aws.String("r-0000"),
21 | ChildOUs: []*resource.OrganizationUnit{
22 | {
23 | OUName: "ExampleOU",
24 | BaselineStacks: []resource.Stack{
25 | {
26 | Type: "CDK",
27 | Path: "examples/localstack/s3-remote-state",
28 | Name: "example",
29 | },
30 | },
31 | Tags: []string{
32 | "ou=ExampleTenants",
33 | },
34 | Accounts: []*resource.Account{
35 | {
36 | Email: "test1@example.com",
37 | AccountName: "test1",
38 | BaselineStacks: []resource.Stack{
39 | {
40 | Type: "CDK",
41 | Path: "examples/cdk/sqs",
42 | Name: "example",
43 | Region: "us-west-2,us-east-1",
44 | },
45 | },
46 | AccountID: "10000000000",
47 | },
48 | {
49 | Email: "test2@example.com",
50 | AccountName: "test2",
51 | AccountID: "20000000000",
52 | },
53 | },
54 | },
55 | {
56 | OUName: "ExampleOU2",
57 | Accounts: []*resource.Account{
58 | {
59 | Email: "test3@example.com",
60 | AccountName: "test3",
61 | AccountID: "30000000000",
62 | },
63 | },
64 | },
65 | },
66 | }
67 | }
68 |
69 | func TestParseOrganization(t *testing.T) {
70 | tests := []struct {
71 | name string
72 | orgPath string
73 | want resource.OrganizationUnit
74 | }{
75 | {
76 | name: "basic OU",
77 | orgPath: "./testdata/organization-basic.yml",
78 | want: basicOU(),
79 | },
80 | {
81 | name: "OU with child filepaths",
82 | orgPath: "./testdata/organization-with-filepath.yml",
83 | want: basicOU(),
84 | },
85 | {
86 | name: "OU with one child inline and one filepath",
87 | orgPath: "./testdata/organization-one-child.yml",
88 | want: basicOU(),
89 | },
90 | }
91 | for _, tc := range tests {
92 | mockClient := awsorgs.New(&awsorgs.Config{
93 | OrganizationClient: awsorgsmock.New(),
94 | })
95 |
96 | parser := NewParser(mockClient)
97 |
98 | actual, err := parser.ParseOrganization(context.Background(), tc.orgPath)
99 | require.NoError(t, err)
100 | ignoreFields := []cmp.Option{
101 | cmpopts.IgnoreFields(resource.OrganizationUnit{}, "Parent"),
102 | cmpopts.IgnoreFields(resource.Account{}, "Parent"),
103 | }
104 |
105 | if diff := cmp.Diff(tc.want.ChildOUs, actual.ChildOUs, ignoreFields...); diff != "" {
106 | t.Errorf(fmt.Sprintf("expected no diff for %s got diff: %+v", tc.name, diff))
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/resource/organization_unit.go:
--------------------------------------------------------------------------------
1 | package resource
2 |
3 | type OrganizationUnit struct {
4 | OUID *string `yaml:"-"`
5 | OUName string `yaml:"Name,omitempty"`
6 | ChildGroups []*OrganizationUnit `yaml:"AccountGroups,omitempty"` // Deprecated. Use `OrganizationUnits`
7 | ChildOUs []*OrganizationUnit `yaml:"OrganizationUnits,omitempty"`
8 | Tags []string `yaml:"Tags,omitempty"`
9 | AWSTags []string `yaml:"-"`
10 | Accounts []*Account `yaml:"Accounts,omitempty"`
11 | BaselineStacks []Stack `yaml:"Stacks,omitempty"`
12 | ServiceControlPolicies []Stack `yaml:"ServiceControlPolicies,omitempty"`
13 | Parent *OrganizationUnit `yaml:"-"`
14 |
15 | OUFilepath *string `yaml:"OUFilepath,omitempty"`
16 | }
17 |
18 | func (grp OrganizationUnit) ID() string {
19 | if grp.OUID != nil {
20 | return *grp.OUID
21 | }
22 | return ""
23 | }
24 |
25 | func (grp OrganizationUnit) Name() string {
26 | return grp.OUName
27 | }
28 |
29 | func (grp OrganizationUnit) Type() string {
30 | return "Organization Unit"
31 | }
32 |
33 | func (grp OrganizationUnit) AllTags() []string {
34 | var tags []string
35 | tags = append(tags, grp.Tags...)
36 | if grp.Parent != nil {
37 | tags = append(tags, grp.Parent.AllTags()...)
38 | }
39 | return tags
40 | }
41 |
42 | func (grp OrganizationUnit) AllAWSTags() []string {
43 | var tags []string
44 | tags = append(tags, grp.AWSTags...)
45 | if grp.Parent != nil {
46 | tags = append(tags, grp.Parent.AllAWSTags()...)
47 | }
48 | return tags
49 | }
50 |
51 | func (grp OrganizationUnit) AllBaselineStacks() []Stack {
52 | var stacks []Stack
53 | if grp.Parent != nil {
54 | stacks = append(stacks, grp.Parent.AllBaselineStacks()...)
55 | }
56 | stacks = append(stacks, grp.BaselineStacks...)
57 | return stacks
58 | }
59 |
60 | func (grp OrganizationUnit) AllDescendentAccounts() []*Account {
61 | var accounts []*Account
62 | accounts = append(accounts, grp.Accounts...)
63 |
64 | for _, ou := range grp.ChildOUs {
65 | accounts = append(accounts, ou.AllDescendentAccounts()...)
66 | }
67 |
68 | return accounts
69 | }
70 |
71 | // This should only be called from the Root OU.
72 | func (grp OrganizationUnit) DelegatedAdministrator() *Account {
73 | for _, acct := range grp.AllDescendentAccounts() {
74 | if acct.DelegatedAdministrator {
75 | return acct
76 | }
77 | }
78 |
79 | return nil
80 | }
81 |
82 | // This should only be called from the Root OU.
83 | func (grp OrganizationUnit) ManagementAccount() *Account {
84 | for _, acct := range grp.AllDescendentAccounts() {
85 | if acct.ManagementAccount {
86 | return acct
87 | }
88 | }
89 |
90 | return nil
91 | }
92 |
93 | func (grp OrganizationUnit) AllDescendentOUs() []*OrganizationUnit {
94 | var OUs []*OrganizationUnit
95 | OUs = append(OUs, grp.ChildOUs...)
96 |
97 | for _, childOU := range grp.ChildOUs {
98 | OUs = append(OUs, childOU.AllDescendentOUs()...)
99 | }
100 |
101 | return OUs
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/mintlifydocs/introduction.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | description: "Telophase is an open-source AWS Control Tower"
4 | ---
5 |
6 | # Why Telophase?
7 | Automation and Compliance are key concerns when adopting multi-account AWS. Telophase orchestrates the management of AWS Organizations alongside your infrastructure-as-code (IaC) provider, like Terraform or CDK. Using a single tool for these allows:
8 | 1. **Workflow Automation**: Automates account creation and decommissioning, integrating with existing automation workflows, like CI or ServiceNow.
9 | 2. **IaC & Account Binding**: Enables binding accounts to specific IaC blueprints for automatic provisioning of baseline resources.
10 | 3. **Easier Compliance Deployment**: Enables binding Service Control Policies (SCPs) to accounts as part of your Account provisioning workflow to make sure every Account is compliant. We make it easy to test SCPs before they are deployed.
11 |
12 | Currently, Telophase is a CLI tool only. In the future, we plan to offer a web UI.
13 |
14 | # Install
15 | If you'd like another method, please let us know by opening an issue!
16 |
17 | ## Go
18 | ```bash
19 | go install github.com/santiago-labs/telophasecli@latest
20 | ```
21 |
22 | ## Homebrew
23 | ```
24 | brew tap Santiago-Labs/telophasecli
25 | brew install telophasecli
26 | ```
27 |
28 | # Features
29 |