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

r/controltower_control: add parameters attribute #38071

Merged
merged 15 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .changelog/38071.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_controltower_control: Add `parameters` argument and `arn` attribute
```
215 changes: 207 additions & 8 deletions internal/service/controltower/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ package controltower

import (
"context"
"encoding/json"
"errors"
"log"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/controltower"
"github.com/aws/aws-sdk-go-v2/service/controltower/document"
"github.com/aws/aws-sdk-go-v2/service/controltower/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
Expand All @@ -23,31 +25,72 @@ import (
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @SDKResource("aws_controltower_control", name="Control")
func resourceControl() *schema.Resource {
johnsonaj marked this conversation as resolved.
Show resolved Hide resolved
return &schema.Resource{
CreateWithoutTimeout: resourceControlCreate,
ReadWithoutTimeout: resourceControlRead,
UpdateWithoutTimeout: resourceControlUpdate,
DeleteWithoutTimeout: resourceControlDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
conn := meta.(*conns.AWSClient).ControlTowerClient(ctx)

parts, err := flex.ExpandResourceId(d.Id(), controlResourceIDPartCount, false)
if err != nil {
return nil, err
}

targetIdentifier, controlIdentifier := parts[0], parts[1]
output, err := findEnabledControlByTwoPartKey(ctx, conn, targetIdentifier, controlIdentifier)
if err != nil {
return nil, err
}

d.Set(names.AttrARN, output.Arn)

return []*schema.ResourceData{d}, nil
},
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(60 * time.Minute),
Update: schema.DefaultTimeout(60 * time.Minute),
Delete: schema.DefaultTimeout(60 * time.Minute),
},

Schema: map[string]*schema.Schema{
names.AttrARN: {
Type: schema.TypeString,
Computed: true,
},
"control_identifier": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: verify.ValidARN,
},
names.AttrParameters: {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
names.AttrKey: {
Type: schema.TypeString,
Required: true,
},
names.AttrValue: {
Type: schema.TypeString,
Required: true,
ValidateFunc: verify.ValidStringIsJSONOrYAML,
},
},
},
},
"target_identifier": {
Type: schema.TypeString,
Required: true,
Expand All @@ -71,13 +114,23 @@ func resourceControlCreate(ctx context.Context, d *schema.ResourceData, meta int
TargetIdentifier: aws.String(targetIdentifier),
}

if v, ok := d.GetOk(names.AttrParameters); ok && v.(*schema.Set).Len() > 0 {
p, err := expandControlParameters(v.(*schema.Set).List())
if err != nil {
return sdkdiag.AppendErrorf(diags, "creating ControlTower Control (%s): %s", id, err)
}

input.Parameters = p
}

output, err := conn.EnableControl(ctx, input)

if err != nil {
return sdkdiag.AppendErrorf(diags, "creating ControlTower Control (%s): %s", id, err)
}

d.SetId(id)
d.Set(names.AttrARN, output.Arn)

if _, err := waitOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutCreate)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for ControlTower Control (%s) create: %s", d.Id(), err)
Expand All @@ -91,13 +144,25 @@ func resourceControlRead(ctx context.Context, d *schema.ResourceData, meta inter

conn := meta.(*conns.AWSClient).ControlTowerClient(ctx)

parts, err := flex.ExpandResourceId(d.Id(), controlResourceIDPartCount, false)
if err != nil {
return sdkdiag.AppendFromErr(diags, err)
}
var output *types.EnabledControlDetails
var err error
if v, ok := d.GetOk(names.AttrARN); ok {
output, err = findEnabledControlByARN(ctx, conn, v.(string))
} else {
// backwards compatibility if ARN is not set from existing state
parts, internalErr := flex.ExpandResourceId(d.Id(), controlResourceIDPartCount, false)
if internalErr != nil {
return sdkdiag.AppendFromErr(diags, err)
}

targetIdentifier, controlIdentifier := parts[0], parts[1]
output, err := findEnabledControlByTwoPartKey(ctx, conn, targetIdentifier, controlIdentifier)
targetIdentifier, controlIdentifier := parts[0], parts[1]
out, internalErr := findEnabledControlByTwoPartKey(ctx, conn, targetIdentifier, controlIdentifier)
if internalErr != nil {
return sdkdiag.AppendErrorf(diags, "reading ControlTower Control (%s): %s", d.Id(), err)
}

output, err = findEnabledControlByARN(ctx, conn, aws.ToString(out.Arn))
}

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] ControlTower Control %s not found, removing from state", d.Id())
Expand All @@ -109,12 +174,51 @@ func resourceControlRead(ctx context.Context, d *schema.ResourceData, meta inter
return sdkdiag.AppendErrorf(diags, "reading ControlTower Control (%s): %s", d.Id(), err)
}

d.Set(names.AttrARN, output.Arn)
d.Set("control_identifier", output.ControlIdentifier)
d.Set("target_identifier", targetIdentifier)

parameters, err := flattenControlParameters(output.Parameters)
if err != nil {
return sdkdiag.AppendErrorf(diags, "flattening ControlTower Control (%s) parameters: %s", d.Id(), err)
}

d.Set(names.AttrParameters, parameters)
d.Set("target_identifier", output.TargetIdentifier)

return diags
}

func resourceControlUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
Copy link
Member

Choose a reason for hiding this comment

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

Very nice! Update makes this a lot more useful.

var diags diag.Diagnostics

conn := meta.(*conns.AWSClient).ControlTowerClient(ctx)

if d.HasChange(names.AttrParameters) {
input := &controltower.UpdateEnabledControlInput{
EnabledControlIdentifier: aws.String(d.Get(names.AttrARN).(string)),
}

p, err := expandControlParameters(d.Get(names.AttrParameters).(*schema.Set).List())
if err != nil {
return sdkdiag.AppendErrorf(diags, "updating ControlTower Control (%s): %s", d.Id(), err)
}

input.Parameters = p

output, err := conn.UpdateEnabledControl(ctx, input)

if err != nil {
return sdkdiag.AppendErrorf(diags, "updating ControlTower Control (%s): %s", d.Id(), err)
}

if _, err := waitOperationSucceeded(ctx, conn, aws.ToString(output.OperationIdentifier), d.Timeout(schema.TimeoutUpdate)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for ControlTower Control (%s) delete: %s", d.Id(), err)
}
}

return append(diags, resourceControlRead(ctx, d, meta)...)
}

func resourceControlDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics

Expand Down Expand Up @@ -148,6 +252,77 @@ const (
controlResourceIDPartCount = 2
)

func expandControlParameters(input []any) ([]types.EnabledControlParameter, error) {
if len(input) == 0 {
return nil, nil
}

var output []types.EnabledControlParameter

for _, v := range input {
val := v.(map[string]any)
e := types.EnabledControlParameter{
Key: aws.String(val[names.AttrKey].(string)),
}

var out any
err := json.Unmarshal([]byte(val[names.AttrValue].(string)), &out)
if err != nil {
return nil, err
}

e.Value = document.NewLazyDocument(out)
output = append(output, e)
}

return output, nil
}

func flattenControlParameters(input []types.EnabledControlParameterSummary) (*schema.Set, error) {
if len(input) == 0 {
return nil, nil
}

res := &schema.Resource{
Schema: map[string]*schema.Schema{
names.AttrKey: {
Type: schema.TypeString,
Required: true,
},
names.AttrValue: {
Type: schema.TypeString,
Required: true,
},
},
}

var output []any

for _, v := range input {
val := map[string]any{
names.AttrKey: aws.ToString(v.Key),
}

var va any
err := v.Value.UnmarshalSmithyDocument(&va)

if err != nil {
log.Printf("[WARN] Error unmarshalling control parameter value: %s", err)
return nil, err
}

out, err := json.Marshal(va)
if err != nil {
return nil, err
}

val[names.AttrValue] = string(out)
output = append(output, val)
}

return schema.NewSet(schema.HashResource(res), output), nil
}

func findEnabledControlByTwoPartKey(ctx context.Context, conn *controltower.Client, targetIdentifier, controlIdentifier string) (*types.EnabledControlSummary, error) {
input := &controltower.ListEnabledControlsInput{
TargetIdentifier: aws.String(targetIdentifier),
Expand Down Expand Up @@ -197,6 +372,30 @@ func findEnabledControls(ctx context.Context, conn *controltower.Client, input *
return output, nil
}

func findEnabledControlByARN(ctx context.Context, conn *controltower.Client, arn string) (*types.EnabledControlDetails, error) {
input := &controltower.GetEnabledControlInput{
EnabledControlIdentifier: aws.String(arn),
}

output, err := conn.GetEnabledControl(ctx, input)

if errs.IsA[*types.ResourceNotFoundException](err) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if output == nil || output.EnabledControlDetails == nil {
return nil, tfresource.NewEmptyResultError(input)
}

return output.EnabledControlDetails, nil
}
func findControlOperationByID(ctx context.Context, conn *controltower.Client, id string) (*types.ControlOperation, error) {
input := &controltower.GetControlOperationInput{
OperationIdentifier: aws.String(id),
Expand Down
15 changes: 11 additions & 4 deletions internal/service/controltower/control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func testAccControl_basic(t *testing.T) {
resourceName := "aws_controltower_control.test"
controlName := "AWS-GR_EC2_VOLUME_INUSE_CHECK"
ouName := "Security"
region := "us-west-2" //lintignore:AWSAT003

resource.Test(t, resource.TestCase{
PreCheck: func() {
Expand All @@ -49,7 +50,7 @@ func testAccControl_basic(t *testing.T) {
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccControlConfig_basic(controlName, ouName),
Config: testAccControlConfig_basic(controlName, ouName, region),
Check: resource.ComposeTestCheckFunc(
testAccCheckControlExists(ctx, resourceName, &control),
resource.TestCheckResourceAttrSet(resourceName, "control_identifier"),
Expand All @@ -65,6 +66,7 @@ func testAccControl_disappears(t *testing.T) {
resourceName := "aws_controltower_control.test"
controlName := "AWS-GR_EC2_VOLUME_INUSE_CHECK"
ouName := "Security"
region := "us-west-2" //lintignore:AWSAT003

resource.Test(t, resource.TestCase{
PreCheck: func() {
Expand All @@ -77,7 +79,7 @@ func testAccControl_disappears(t *testing.T) {
CheckDestroy: testAccCheckControlDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccControlConfig_basic(controlName, ouName),
Config: testAccControlConfig_basic(controlName, ouName, region),
Check: resource.ComposeTestCheckFunc(
testAccCheckControlExists(ctx, resourceName, &control),
acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcontroltower.ResourceControl(), resourceName),
Expand Down Expand Up @@ -135,7 +137,7 @@ func testAccCheckControlDestroy(ctx context.Context) resource.TestCheckFunc {
}
}

func testAccControlConfig_basic(controlName string, ouName string) string {
func testAccControlConfig_basic(controlName, ouName, region string) string {
return fmt.Sprintf(`
data "aws_region" "current" {}

Expand All @@ -153,6 +155,11 @@ resource "aws_controltower_control" "test" {
for x in data.aws_organizations_organizational_units.test.children :
x.arn if x.name == "%[2]s"
][0]

parameters {
key = "AllowedRegions"
value = jsonencode([%[3]q])
}
}
`, controlName, ouName)
`, controlName, ouName, region)
}
Loading
Loading