Skip to content

Commit

Permalink
Merge pull request #38071 from hashicorp/f-controltower_control_param…
Browse files Browse the repository at this point in the history
…eters

r/controltower_control: add `parameters` attribute
  • Loading branch information
johnsonaj authored Jun 24, 2024
2 parents cc67231 + a856ac7 commit bd1368a
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 13 deletions.
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 {
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 {
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

0 comments on commit bd1368a

Please sign in to comment.