Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add table aws_organizations_organizational_unit and aws_organizations_root. closes #1674 #1677

Merged
merged 18 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
"aws_oam_sink": tableAwsOAMSink(ctx),
"aws_opensearch_domain": tableAwsOpenSearchDomain(ctx),
"aws_organizations_account": tableAwsOrganizationsAccount(ctx),
"aws_organizations_organizational_unit": tableAwsOrganizationsOrganizationalUnit(ctx),
"aws_organizations_policy": tableAwsOrganizationsPolicy(ctx),
"aws_pinpoint_app": tableAwsPinpointApp(ctx),
"aws_pipes_pipe": tableAwsPipes(ctx),
Expand Down
181 changes: 181 additions & 0 deletions aws/table_aws_organizations_organizational_unit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package aws

import (
"context"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/organizations"
"github.com/aws/aws-sdk-go-v2/service/organizations/types"

"github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto"
"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
"github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform"
)

func tableAwsOrganizationsOrganizationalUnit(_ context.Context) *plugin.Table {
return &plugin.Table{
Name: "aws_organizations_organizational_unit",
Description: "AWS Organizations Organizational Unit",
Get: &plugin.GetConfig{
KeyColumns: plugin.SingleColumn("id"),
Hydrate: getOrganizationsOrganizationalUnit,
IgnoreConfig: &plugin.IgnoreConfig{
ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"InvalidInputException"}),
},
},
List: &plugin.ListConfig{
Hydrate: listOrganizationsOrganizationalUnits,
KeyColumns: plugin.SingleColumn("parent_id"),
IgnoreConfig: &plugin.IgnoreConfig{
ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"ParentNotFoundException", "InvalidInputException"}),
},
},
Columns: awsGlobalRegionColumns([]*plugin.Column{
{
Name: "name",
Description: "The friendly name of this OU.",
Type: proto.ColumnType_STRING,
},
{
Name: "id",
Description: "The unique identifier (ID) associated with this OU.",
Type: proto.ColumnType_STRING,
},
{
Name: "arn",
Description: "The Amazon Resource Name (ARN) of this OU.",
Type: proto.ColumnType_STRING,
},
{
Name: "parent_id",
Description: "The unique identifier (ID) of the root or OU whose child OUs you want to list.",
Type: proto.ColumnType_STRING,
Hydrate: getOrganizationsOrganizationalUnit,
Transform: transform.From(getParentId),
},

// Steampipe standard columns
{
Name: "title",
Description: resourceInterfaceDescription("title"),
Type: proto.ColumnType_STRING,
Transform: transform.FromField("Name"),
},
{
Name: "akas",
Description: resourceInterfaceDescription("akas"),
Type: proto.ColumnType_JSON,
Transform: transform.FromField("Arn").Transform(transform.EnsureStringArray),
},
}),
}
}

//// LIST FUNCTION

func listOrganizationsOrganizationalUnits(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
// Get Client
svc, err := OrganizationClient(ctx, d)
if err != nil {
plugin.Logger(ctx).Error("aws_organizations_organizational_unit.listOrganizationsOrganizationalUnits", "client_error", err)
return nil, err
}

parentId := d.EqualsQualString("parent_id")

// Empty Check
if parentId == "" {
return nil, nil
}

// Limiting the result
maxItems := int32(20)

// Reduce the basic request limit down if the user has only requested a small number of rows
if d.QueryContext.Limit != nil {
limit := int32(*d.QueryContext.Limit)
if limit < maxItems {
maxItems = int32(limit)
}
}

params := &organizations.ListOrganizationalUnitsForParentInput{
ParentId: aws.String(parentId),
MaxResults: &maxItems,
}

paginator := organizations.NewListOrganizationalUnitsForParentPaginator(svc, params, func(o *organizations.ListOrganizationalUnitsForParentPaginatorOptions) {
o.Limit = maxItems
o.StopOnDuplicateToken = true
})

for paginator.HasMorePages() {
output, err := paginator.NextPage(ctx)
if err != nil {
plugin.Logger(ctx).Error("aws_organizations_organizational_unit.listOrganizationsOrganizationalUnits", "api_error", err)
return nil, err
}

for _, unit := range output.OrganizationalUnits {
d.StreamListItem(ctx, unit)

// Context may get cancelled due to manual cancellation or if the limit has been reached
if d.RowsRemaining(ctx) == 0 {
return nil, nil
}
}
}

return nil, nil
}

//// HYDRATE FUNCTIONS

func getOrganizationsOrganizationalUnit(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
var orgUnitId string

if h.Item != nil {
orgUnitId = *h.Item.(types.OrganizationalUnit).Id
} else {
orgUnitId = d.EqualsQuals["id"].GetStringValue()
}

// Get Client
svc, err := OrganizationClient(ctx, d)
if err != nil {
plugin.Logger(ctx).Error("aws_organizations_organizational_unit.getOrganizationsOrganizationalUnit", "client_error", err)
return nil, err
}

params := &organizations.DescribeOrganizationalUnitInput{
OrganizationalUnitId: aws.String(orgUnitId),
}

op, err := svc.DescribeOrganizationalUnit(ctx, params)
if err != nil {
plugin.Logger(ctx).Error("aws_organizations_organizational_unit.getOrganizationsOrganizationalUnit", "api_error", err)
return nil, err
}

return *op.OrganizationalUnit, nil
}

//// TRANSFORM FUNCTION

func getParentId(_ context.Context, d *transform.TransformData) (interface{}, error) {
quals := d.KeyColumnQuals["parent_id"]
for _, data := range quals {
parentId := data.Value.GetStringValue()
if parentId != "" {
return parentId, nil
}
}

if d.HydrateItem != nil {
data := d.HydrateItem.(types.OrganizationalUnit)
return strings.Split(*data.Arn, "/")[2], nil
}

return nil, nil
}
21 changes: 21 additions & 0 deletions docs/tables/aws_organizations_organizational_unit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Table: aws_organizations_organizational_unit

A container for accounts within a root. An OU also can contain other OUs, enabling you to create a hierarchy that resembles an upside-down tree, with a root at the top and branches of OUs that reach down, ending in accounts that are the leaves of the tree. When you attach a policy to one of the nodes in the hierarchy, it flows down and affects all the branches (OUs) and leaves (accounts) beneath it. An OU can have exactly one parent, and currently each account can be a member of exactly one OU.

**Note**: The `parent_id` is the required to make the API call. It is the unique identifier (ID) of the root or OU whose child OUs you want to list.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your second example in this ticket does not specify parent_id, how come it worked?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your second example in this ticket does not specify parent_id, how come it worked?

In the table definition we have two config one is List Config and another one is Get Config.
In List config we generally make list API call and in Get config we make the get API call.

Here we are using ListAccountsForParent API in the List config, for making the ListAccountsForParent API call we must need to pass the ParentId in param of this API call, so parent_id is required to make list API call.

In Get config we are using the DescribeOrganizationalUnit API call, for making this API call we must have to pass the OrganizationalUnitId, we do not need ParentId for this API call.

The Get API will be called if we are providing id as query parameter, so in second example we need to pass the parent_id.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it for the logic underneath ; may I suggest a working like this?

Suggested change
**Note**: The `parent_id` is the required to make the API call. It is the unique identifier (ID) of the root or OU whose child OUs you want to list.
You **_must_** specify a single `parent_id` or `id` in a where or join clause in order to use this table.

I found similar wording/format for aws_route53_record, aws_iam_access_advisor, aws_cloudtrail_trail_event and others

Also from a user perspective I find it odd that we can't have the listing without a parent_id, but looking at API reference it looks tricky to get the root id reliably...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah @ajoga, this looks better, will push the changes. Thanks!


## Examples

### Basic info

```sql
select
name,
id,
arn,
parent_id,
title,
akas
from
aws_organizations_organizational_unit;
```