Skip to content

Commit

Permalink
Add mcaf_aws_all_organizational_units data resource
Browse files Browse the repository at this point in the history
Currently there is no way to recursively find all OUs in an AWS
Organisation via a data resource and doesn't look like
hashicorp/terraform-provider-aws#24350 will get
merged any time soon, so mcaf_aws_all_organizational_units fills this
gap until such a resource exists in the community AWS provider.

Signed-off-by: Stephen Hoekstra <shoekstra@schubergphilis.com>
  • Loading branch information
shoekstra committed Nov 2, 2022
1 parent 157b773 commit 2647a94
Show file tree
Hide file tree
Showing 12 changed files with 391 additions and 9 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@master

- name: Setup Go env
uses: actions/setup-go@v3
with:
go-version-file: 'go.mod'

- name: Run fmtcheck
run: make fmtcheck

Expand All @@ -22,12 +24,22 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@master

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1

- name: Setup Go env
uses: actions/setup-go@v3
with:
go-version-file: 'go.mod'

- name: Run acceptance tests
run: make testacc

env:
O365_ACL_GUID: ${{ secrets.O365_ACL_GUID }}
O365_ALIAS: ${{ secrets.O365_ALIAS }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
provider "mcaf" {
aws {}
}

data "mcaf_aws_all_organizational_units" "example" {}

output "mcaf_aws_all_organizational_units" {
value = data.mcaf_aws_all_organizational_units.example.organizational_units
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
required_providers {
mcaf = {
source = "schubergphilis/mcaf"
}
}
}
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ require (
require (
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.4 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.4.0 // indirect
github.com/hashicorp/hcl/v2 v2.14.1 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.17.3 // indirect
github.com/hashicorp/terraform-json v0.14.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.14.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c // indirect
Expand All @@ -43,6 +48,7 @@ require (
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/zclconf/go-cty v1.12.0 // indirect
golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 // indirect
golang.org/x/net v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
Expand Down
89 changes: 85 additions & 4 deletions go.sum

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions internal/mcaf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"log"

"github.com/aws/aws-sdk-go/service/codebuild"
"github.com/aws/aws-sdk-go/service/organizations"
"github.com/aws/aws-sdk-go/service/servicecatalog"
awsbase "github.com/hashicorp/aws-sdk-go-base"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
Expand All @@ -19,6 +20,7 @@ type Client struct {
type AWSClient struct {
accountID string
cbconn *codebuild.CodeBuild
orgsconn *organizations.Organizations
scconn *servicecatalog.ServiceCatalog
}

Expand Down Expand Up @@ -61,6 +63,7 @@ func awsClient(aws map[string]interface{}) (*AWSClient, error) {
client := &AWSClient{
accountID: accountID,
cbconn: codebuild.New(sess.Copy()),
orgsconn: organizations.New(sess.Copy()),
scconn: servicecatalog.New(sess.Copy()),
}

Expand Down
147 changes: 147 additions & 0 deletions internal/mcaf/data_source_aws_all_organizational_units.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package mcaf

import (
"fmt"
"log"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/organizations"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

type OrganizationalUnit struct {
OrganizationalUnit *organizations.OrganizationalUnit

Path *string
}

func dataSourceAwsAllOrganizationalUnits() *schema.Resource {
return &schema.Resource{
Read: checkProvider("aws", dataSourceAwsAllOrganizationalUnitsRead),

Schema: map[string]*schema.Schema{
"organizational_units": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"id": {
Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Computed: true,
},
"path": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
},
}
}

func dataSourceAwsAllOrganizationalUnitsRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*Client).AWSClient.orgsconn

roots, err := listRoots(conn)
if err != nil {
return err
}
root_id := aws.StringValue(roots[0].Id)

var ous []*OrganizationalUnit
ous, err = listOrganizationalUnitsForParentPagesRecursive(conn, "Root", root_id, ous)
if err != nil {
return err
}

d.SetId(root_id)

if err := d.Set("organizational_units", flattenOrganizationsOrganizationalUnits(ous)); err != nil {
return fmt.Errorf("Error setting organizational_units: %s", err)
}

return nil
}

func flattenOrganizationsOrganizationalUnits(ous []*OrganizationalUnit) []map[string]interface{} {
if len(ous) == 0 {
return nil
}
var result []map[string]interface{}
for _, ou := range ous {
result = append(result, map[string]interface{}{
"arn": aws.StringValue(ou.OrganizationalUnit.Arn),
"id": aws.StringValue(ou.OrganizationalUnit.Id),
"name": aws.StringValue(ou.OrganizationalUnit.Name),
"path": aws.StringValue(ou.Path),
})
}
return result
}

func listOrganizationalUnitsForParentPagesRecursive(conn *organizations.Organizations, parentPath, parentId string, ous []*OrganizationalUnit) ([]*OrganizationalUnit, error) {
// Control Tower supports a maximum of 5 levels of nested OUs.
parentPathSplit := strings.Split(parentPath, "/")
if len(parentPathSplit) == 5 {
log.Printf("[INFO] Maximum number of levels of nested OUs reached. Skipping %s (%s)", parentPath, parentId)
return ous, nil
}

input := &organizations.ListOrganizationalUnitsForParentInput{
ParentId: aws.String(parentId),
}

log.Printf("[DEBUG] Listing OUs under parent: %s (%s)", parentPath, parentId)
err := conn.ListOrganizationalUnitsForParentPages(input, func(page *organizations.ListOrganizationalUnitsForParentOutput, lastPage bool) bool {
for _, ou := range page.OrganizationalUnits {
ouPath := fmt.Sprintf("%s/%s", parentPath, aws.StringValue(ou.Name))
ous = append(ous, &OrganizationalUnit{
OrganizationalUnit: ou,
Path: aws.String(ouPath),
})

var err error
ous, err = listOrganizationalUnitsForParentPagesRecursive(conn, ouPath, aws.StringValue(ou.Id), ous)
if err != nil {
log.Printf("[ERROR] Error listing OUs for %s (%s): %s", ouPath, aws.StringValue(ou.Id), err)
}
}

return !lastPage
})

if err != nil {
return nil, fmt.Errorf("error listing Organization Units for parent (%s): %s", parentId, err)
}

return ous, nil
}

func listRoots(conn *organizations.Organizations) ([]*organizations.Root, error) {
var roots []*organizations.Root
err := conn.ListRootsPages(&organizations.ListRootsInput{}, func(page *organizations.ListRootsOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

roots = append(roots, page.Roots...)

return !lastPage
})

if err != nil {
return nil, fmt.Errorf("error listing Organization Roots: %s", err)
}

return roots, nil
}
35 changes: 35 additions & 0 deletions internal/mcaf/data_source_aws_all_organizational_units_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package mcaf

import (
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccDataSourceAwsAllOrganizationalUnits_basic(t *testing.T) {
dataSourceName := "data.mcaf_aws_all_organizational_units.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccAwsPreCheck(t)
},
ProviderFactories: testAccProviderFactories,
CheckDestroy: nil,
Steps: []resource.TestStep{
{
Config: testAccDataSourceAwsAllOrganizationalUnitsConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, "organizational_units.#", "5"),
),
},
},
})
}

const testAccDataSourceAwsAllOrganizationalUnitsConfig = `
provider "mcaf" {
aws {}
}
data "mcaf_aws_all_organizational_units" "test" {}
`
4 changes: 4 additions & 0 deletions internal/mcaf/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func New() *schema.Provider {
},
},

DataSourcesMap: map[string]*schema.Resource{
"mcaf_aws_all_organizational_units": dataSourceAwsAllOrganizationalUnits(),
},

ResourcesMap: map[string]*schema.Resource{
"mcaf_aws_account": resourceAWSAccount(),
"mcaf_aws_codebuild_trigger": resourceAWSCodeBuildTrigger(),
Expand Down
53 changes: 48 additions & 5 deletions internal/mcaf/provider_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package mcaf

import (
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var testAccProviders map[string]*schema.Provider
var testAccProvider *schema.Provider
const (
// Provider name for single configuration testing
ProviderName = "mcaf"
)

var testAccProviderFactories map[string]func() (*schema.Provider, error)

func init() {
testAccProvider = New()
testAccProviders = map[string]*schema.Provider{
"mcaf": testAccProvider,
// Always allocate a new provider instance each invocation, otherwise gRPC
// ProviderConfigure() can overwrite configuration during concurrent testing.
testAccProviderFactories = map[string]func() (*schema.Provider, error){
ProviderName: func() (*schema.Provider, error) { return New(), nil }, //nolint:unparam
}
}

Expand All @@ -25,3 +31,40 @@ func TestProvider(t *testing.T) {
func TestProvider_impl(t *testing.T) {
var _ = New()
}

func testAccAwsPreCheck(t *testing.T) {
if v := os.Getenv("AWS_ACCESS_KEY_ID"); v == "" {
t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests")
}
if v := os.Getenv("AWS_SECRET_ACCESS_KEY"); v == "" {
t.Fatal("AWS_SECRET_ACCESS_KEY must be set for acceptance tests")
}
}

// testAccAwsClient configures and returns a fully initialized AWSClient.
func testAccAwsClient() (*AWSClient, error) {
region := "us-east-1"
if v := os.Getenv("AWS_DEFAULT_REGION"); v != "" {
region = v
}

aws := map[string]interface{}{
"access_key": "",
"secret_key": "",
"profile": "",
"region": region,
"max_retries": 3,
"shared_credentials_file": "",
"skip_credentials_validation": false,
"skip_metadata_api_check": false,
"skip_requesting_account_id": false,
"token": "",
}

client, err := awsClient(aws)
if err != nil {
return nil, err
}

return client, nil
}
Loading

0 comments on commit 2647a94

Please sign in to comment.