-
Notifications
You must be signed in to change notification settings - Fork 9.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #29099 from hashicorp/f-aws_auditmanager_assessmen…
…t_delegate New Resource: `aws_auditmanager_assessment_delegation`
- Loading branch information
Showing
5 changed files
with
780 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:new-resource | ||
aws_auditmanager_assessment_delegation | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,346 @@ | ||
package auditmanager | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"strings" | ||
|
||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/service/auditmanager" | ||
awstypes "github.com/aws/aws-sdk-go-v2/service/auditmanager/types" | ||
"github.com/hashicorp/terraform-plugin-framework/path" | ||
"github.com/hashicorp/terraform-plugin-framework/resource" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
sdkv2resource "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" | ||
"github.com/hashicorp/terraform-provider-aws/internal/create" | ||
"github.com/hashicorp/terraform-provider-aws/internal/enum" | ||
"github.com/hashicorp/terraform-provider-aws/internal/flex" | ||
"github.com/hashicorp/terraform-provider-aws/internal/framework" | ||
"github.com/hashicorp/terraform-provider-aws/internal/tfresource" | ||
"github.com/hashicorp/terraform-provider-aws/names" | ||
) | ||
|
||
func init() { | ||
_sp.registerFrameworkResourceFactory(newResourceAssessmentDelegation) | ||
} | ||
|
||
func newResourceAssessmentDelegation(_ context.Context) (resource.ResourceWithConfigure, error) { | ||
return &resourceAssessmentDelegation{}, nil | ||
} | ||
|
||
const ( | ||
ResNameAssessmentDelegation = "AssessmentDelegation" | ||
) | ||
|
||
type resourceAssessmentDelegation struct { | ||
framework.ResourceWithConfigure | ||
} | ||
|
||
func (r *resourceAssessmentDelegation) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { | ||
response.TypeName = "aws_auditmanager_assessment_delegation" | ||
} | ||
|
||
func (r *resourceAssessmentDelegation) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { | ||
resp.Schema = schema.Schema{ | ||
Attributes: map[string]schema.Attribute{ | ||
"assessment_id": schema.StringAttribute{ | ||
Required: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
}, | ||
"comment": schema.StringAttribute{ | ||
Optional: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
}, | ||
"control_set_id": schema.StringAttribute{ | ||
Required: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
}, | ||
// The AWS-generated ID for delegations has been observed to change between creation | ||
// and subseqeunt read operations. As such, this value cannot be used as the resource ID | ||
// or the input to finder functions. However, it is still required as part of the delete | ||
// request input, so will be stored as a separate computed attribute. | ||
"delegation_id": schema.StringAttribute{ | ||
Computed: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.UseStateForUnknown(), | ||
}, | ||
}, | ||
"id": framework.IDAttribute(), | ||
"role_arn": schema.StringAttribute{ | ||
Required: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
}, | ||
"role_type": schema.StringAttribute{ | ||
Required: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.RequiresReplace(), | ||
}, | ||
Validators: []validator.String{ | ||
enum.FrameworkValidate[awstypes.RoleType](), | ||
}, | ||
}, | ||
"status": schema.StringAttribute{ | ||
Computed: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.UseStateForUnknown(), | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func (r *resourceAssessmentDelegation) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { | ||
conn := r.Meta().AuditManagerClient() | ||
|
||
var plan resourceAssessmentDelegationData | ||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
delegationIn := awstypes.CreateDelegationRequest{ | ||
RoleArn: aws.String(plan.RoleARN.ValueString()), | ||
RoleType: awstypes.RoleType(plan.RoleType.ValueString()), | ||
ControlSetId: aws.String(plan.ControlSetID.ValueString()), | ||
} | ||
if !plan.Comment.IsNull() { | ||
delegationIn.Comment = aws.String(plan.Comment.ValueString()) | ||
} | ||
in := auditmanager.BatchCreateDelegationByAssessmentInput{ | ||
AssessmentId: aws.String(plan.AssessmentID.ValueString()), | ||
CreateDelegationRequests: []awstypes.CreateDelegationRequest{delegationIn}, | ||
} | ||
|
||
// Include retry handling to allow for IAM propagation | ||
// | ||
// Example: | ||
// ResourceNotFoundException: The operation tried to access a nonexistent resource. The resource | ||
// might not be specified correctly, or its status might not be active. Check and try again. | ||
var out *auditmanager.BatchCreateDelegationByAssessmentOutput | ||
err := tfresource.Retry(ctx, iamPropagationTimeout, func() *sdkv2resource.RetryError { | ||
var err error | ||
out, err = conn.BatchCreateDelegationByAssessment(ctx, &in) | ||
if err != nil { | ||
var nfe *awstypes.ResourceNotFoundException | ||
if errors.As(err, &nfe) { | ||
return sdkv2resource.RetryableError(err) | ||
} | ||
return sdkv2resource.NonRetryableError(err) | ||
} | ||
|
||
return nil | ||
}) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
create.ProblemStandardMessage(names.AuditManager, create.ErrActionCreating, ResNameAssessmentDelegation, plan.RoleARN.String(), nil), | ||
err.Error(), | ||
) | ||
return | ||
} | ||
if out == nil || len(out.Delegations) == 0 { | ||
resp.Diagnostics.AddError( | ||
create.ProblemStandardMessage(names.AuditManager, create.ErrActionCreating, ResNameAssessmentDelegation, plan.RoleARN.String(), nil), | ||
errors.New("empty output").Error(), | ||
) | ||
return | ||
} | ||
|
||
// This response object will return ALL delegations assigned to the assessment, not just those | ||
// added in this batch request. In order to write to state, the response should be filtered to | ||
// the item with a matching role_arn and control_set_id. | ||
// | ||
// Also, assessment_id is returned as null in the BatchCreateDelegationByAssessment response | ||
// object, and therefore is not included as one of the matching parameters. | ||
delegation, err := getMatchingDelegation(out.Delegations, plan.RoleARN.ValueString(), plan.ControlSetID.ValueString()) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
create.ProblemStandardMessage(names.AuditManager, create.ErrActionCreating, ResNameAssessmentDelegation, plan.RoleARN.String(), nil), | ||
err.Error(), | ||
) | ||
return | ||
} | ||
|
||
state := plan | ||
|
||
// The AWS-generated ID for delegations has been observed to change between creation | ||
// and subseqeunt read operations. As such, the ID attribute will use a combination of | ||
// attributes that are unique to a single delegation instead. | ||
id := toID(plan.AssessmentID.ValueString(), plan.RoleARN.ValueString(), plan.ControlSetID.ValueString()) | ||
state.ID = types.StringValue(id) | ||
|
||
state.refreshFromOutput(ctx, delegation) | ||
resp.Diagnostics.Append(resp.State.Set(ctx, state)...) | ||
} | ||
|
||
func (r *resourceAssessmentDelegation) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { | ||
conn := r.Meta().AuditManagerClient() | ||
|
||
var state resourceAssessmentDelegationData | ||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
out, err := FindAssessmentDelegationByID(ctx, conn, state.ID.ValueString()) | ||
if tfresource.NotFound(err) { | ||
resp.State.RemoveResource(ctx) | ||
return | ||
} | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
create.ProblemStandardMessage(names.AuditManager, create.ErrActionReading, ResNameAssessmentDelegation, state.ID.String(), nil), | ||
err.Error(), | ||
) | ||
return | ||
} | ||
|
||
state.refreshFromOutputMetadata(ctx, out) | ||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) | ||
} | ||
|
||
// There is no update API, so this method is a no-op | ||
func (r *resourceAssessmentDelegation) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { | ||
} | ||
|
||
func (r *resourceAssessmentDelegation) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { | ||
conn := r.Meta().AuditManagerClient() | ||
|
||
var state resourceAssessmentDelegationData | ||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
_, err := conn.BatchDeleteDelegationByAssessment(ctx, &auditmanager.BatchDeleteDelegationByAssessmentInput{ | ||
AssessmentId: aws.String(state.AssessmentID.ValueString()), | ||
DelegationIds: []string{state.DelegationID.ValueString()}, | ||
}) | ||
if err != nil { | ||
var nfe *awstypes.ResourceNotFoundException | ||
if errors.As(err, &nfe) { | ||
return | ||
} | ||
resp.Diagnostics.AddError( | ||
create.ProblemStandardMessage(names.AuditManager, create.ErrActionDeleting, ResNameAssessmentDelegation, state.ID.String(), nil), | ||
err.Error(), | ||
) | ||
} | ||
} | ||
|
||
func (r *resourceAssessmentDelegation) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { | ||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) | ||
} | ||
|
||
func FindAssessmentDelegationByID(ctx context.Context, conn *auditmanager.Client, id string) (*awstypes.DelegationMetadata, error) { | ||
assessmentID, roleARN, controlSetID := fromID(id) | ||
|
||
// The GetDelegations API behaves like a List* API, so the results are paged | ||
// through until an entry with a matching ID is found | ||
in := &auditmanager.GetDelegationsInput{} | ||
pages := auditmanager.NewGetDelegationsPaginator(conn, in) | ||
|
||
for pages.HasMorePages() { | ||
page, err := pages.NextPage(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
for _, d := range page.Delegations { | ||
if aws.ToString(d.AssessmentId) == assessmentID && | ||
strings.EqualFold(aws.ToString(d.RoleArn), roleARN) && // IAM role names are case-insensitive | ||
aws.ToString(d.ControlSetName) == controlSetID { | ||
return &d, nil | ||
} | ||
} | ||
} | ||
|
||
return nil, &sdkv2resource.NotFoundError{ | ||
LastRequest: in, | ||
} | ||
} | ||
|
||
// getMatchingDelegation will return the delegation matching the provided role ARN and | ||
// control set ID. If no match is found, an error is returned. | ||
func getMatchingDelegation(out []awstypes.Delegation, roleARN, controlSetID string) (*awstypes.Delegation, error) { | ||
for _, d := range out { | ||
if strings.EqualFold(aws.ToString(d.RoleArn), roleARN) && // IAM role names are case-insensitive | ||
aws.ToString(d.ControlSetId) == controlSetID { | ||
return &d, nil | ||
} | ||
} | ||
return nil, errors.New("no matching delegations in response") | ||
} | ||
|
||
func fromID(id string) (string, string, string) { | ||
parts := strings.Split(id, ",") | ||
if len(parts) != 3 { | ||
return "", "", "" | ||
} | ||
return parts[0], parts[1], parts[2] | ||
} | ||
|
||
func toID(assessmentID, roleARN, controlSetID string) string { | ||
return strings.Join([]string{assessmentID, roleARN, controlSetID}, ",") | ||
} | ||
|
||
type resourceAssessmentDelegationData struct { | ||
AssessmentID types.String `tfsdk:"assessment_id"` | ||
Comment types.String `tfsdk:"comment"` | ||
ControlSetID types.String `tfsdk:"control_set_id"` | ||
DelegationID types.String `tfsdk:"delegation_id"` | ||
ID types.String `tfsdk:"id"` | ||
RoleARN types.String `tfsdk:"role_arn"` | ||
RoleType types.String `tfsdk:"role_type"` | ||
Status types.String `tfsdk:"status"` | ||
} | ||
|
||
// refreshFromOutput writes state data from an AWS response object | ||
// | ||
// This variant of the refresh method is for use with the create operation | ||
// response type (Delegation). | ||
func (rd *resourceAssessmentDelegationData) refreshFromOutput(ctx context.Context, out *awstypes.Delegation) { | ||
if out == nil { | ||
return | ||
} | ||
|
||
// The response from create operations always includes a nil AssessmentId. This is likely | ||
// a bug in the AWS API, so for now skip using the response output and copy the state | ||
// value directly from plan. | ||
// rd.AssessmentID = flex.StringToFramework(ctx, out.AssessmentId) | ||
|
||
rd.Comment = flex.StringToFramework(ctx, out.Comment) | ||
rd.ControlSetID = flex.StringToFramework(ctx, out.ControlSetId) | ||
rd.DelegationID = flex.StringToFramework(ctx, out.Id) | ||
rd.RoleARN = flex.StringToFramework(ctx, out.RoleArn) | ||
rd.RoleType = flex.StringValueToFramework(ctx, out.RoleType) | ||
rd.Status = flex.StringValueToFramework(ctx, out.Status) | ||
} | ||
|
||
// refreshFromOutputMetadata writes state data from an AWS response object | ||
// | ||
// This variant of the refresh method is for use with the get operation | ||
// response type (DelegationMetadata). Notably, this response omits certain | ||
// attributes such as comment, control_set_id, and role_type which means | ||
// drift cannot be detected after the initial create action. | ||
func (rd *resourceAssessmentDelegationData) refreshFromOutputMetadata(ctx context.Context, out *awstypes.DelegationMetadata) { | ||
if out == nil { | ||
return | ||
} | ||
|
||
rd.AssessmentID = flex.StringToFramework(ctx, out.AssessmentId) | ||
rd.DelegationID = flex.StringToFramework(ctx, out.Id) | ||
rd.RoleARN = flex.StringToFramework(ctx, out.RoleArn) | ||
rd.Status = flex.StringValueToFramework(ctx, out.Status) | ||
} |
Oops, something went wrong.