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 9 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` attribute
johnsonaj marked this conversation as resolved.
Show resolved Hide resolved
```
198 changes: 189 additions & 9 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,7 @@ 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)
}

targetIdentifier, controlIdentifier := parts[0], parts[1]
output, err := findEnabledControlByTwoPartKey(ctx, conn, targetIdentifier, controlIdentifier)
output, err := findEnabledControlByARN(ctx, conn, d.Get(names.AttrARN).(string))

if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] ControlTower Control %s not found, removing from state", d.Id())
Expand All @@ -110,11 +157,49 @@ func resourceControlRead(ctx context.Context, d *schema.ResourceData, meta inter
}

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 +233,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 +353,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
17 changes: 16 additions & 1 deletion website/docs/r/controltower_control.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,35 @@ resource "aws_controltower_control" "example" {
for x in data.aws_organizations_organizational_units.example.children :
x.arn if x.name == "Infrastructure"
][0]

parameters {
key = "AllowedRegions"
value = jsonencode(["us-east-1"])
}
}
```

## Argument Reference

This resource supports the following arguments:
This following arguments are required:

* `control_identifier` - (Required) The ARN of the control. Only Strongly recommended and Elective controls are permitted, with the exception of the Region deny guardrail.
* `target_identifier` - (Required) The ARN of the organizational unit.

The following arguments are optional:

* `parameters` - (Optional) Parameter values which are specified to configure the control when you enable it. See [Parameters](#parameters) for more details.

### Parameters

* `key` - (Required) The name of the parameter.
* `value` - (Required) The value of the parameter.

## Attribute Reference

This resource exports the following attributes in addition to the arguments above:

* `arn` - The ARN of the EnabledControl resource.
* `id` - The ARN of the organizational unit.

## Import
Expand Down
Loading