Skip to content

Commit

Permalink
r/aws_iam_group_policy_attachments_exclusive: new resource (#39732)
Browse files Browse the repository at this point in the history
This resource will allow practitioners to retain exclusive ownership of customer managed policy attachments to IAM groups via Terraform.

```console
% make testacc PKG=iam TESTS=TestAccIAMGroupPolicyAttachmentsExclusive_
make: Verifying source code with gofmt...
==> Checking that code complies with gofmt requirements...
TF_ACC=1 go1.23.2 test ./internal/service/iam/... -v -count 1 -parallel 20 -run='TestAccIAMGroupPolicyAttachmentsExclusive_'  -timeout 360m
2024/10/15 15:41:33 Initializing Terraform AWS Provider...

--- PASS: TestAccIAMGroupPolicyAttachmentsExclusive_empty (14.72s)
--- PASS: TestAccIAMGroupPolicyAttachmentsExclusive_disappears_Policy (15.88s)
--- PASS: TestAccIAMGroupPolicyAttachmentsExclusive_disappears_Group (15.92s)
--- PASS: TestAccIAMGroupPolicyAttachmentsExclusive_basic (16.48s)
--- PASS: TestAccIAMGroupPolicyAttachmentsExclusive_outOfBandRemoval (23.58s)
--- PASS: TestAccIAMGroupPolicyAttachmentsExclusive_outOfBandAddition (24.17s)
--- PASS: TestAccIAMGroupPolicyAttachmentsExclusive_multiple (24.29s)
PASS
ok      github.com/hashicorp/terraform-provider-aws/internal/service/iam        30.887s
```
  • Loading branch information
jar-b authored Oct 16, 2024
1 parent 8493020 commit 14689f2
Show file tree
Hide file tree
Showing 6 changed files with 826 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/39732.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_iam_group_policy_attachments_exclusive
```
1 change: 1 addition & 0 deletions internal/service/iam/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var (
FindEntitiesForPolicyByARN = findEntitiesForPolicyByARN
FindGroupByName = findGroupByName
FindGroupPoliciesByName = findGroupPoliciesByName
FindGroupPolicyAttachmentsByName = findGroupPolicyAttachmentsByName
FindInstanceProfileByName = findInstanceProfileByName
FindOpenIDConnectProviderByARN = findOpenIDConnectProviderByARN
FindPolicyByARN = findPolicyByARN
Expand Down
211 changes: 211 additions & 0 deletions internal/service/iam/group_policy_attachments_exclusive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package iam

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/iam"
awstypes "github.com/aws/aws-sdk-go-v2/service/iam/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/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-provider-aws/internal/create"
"github.com/hashicorp/terraform-provider-aws/internal/errs"
intflex "github.com/hashicorp/terraform-provider-aws/internal/flex"
"github.com/hashicorp/terraform-provider-aws/internal/framework"
"github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkResource("aws_iam_group_policy_attachments_exclusive", name="Group Policy Attachments Exclusive")
func newResourceGroupPolicyAttachmentsExclusive(_ context.Context) (resource.ResourceWithConfigure, error) {
return &resourceGroupPolicyAttachmentsExclusive{}, nil
}

const (
ResNameGroupPolicyAttachmentsExclusive = "Group Policy Attachments Exclusive"
)

type resourceGroupPolicyAttachmentsExclusive struct {
framework.ResourceWithConfigure
framework.WithNoOpDelete
}

func (r *resourceGroupPolicyAttachmentsExclusive) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "aws_iam_group_policy_attachments_exclusive"
}

func (r *resourceGroupPolicyAttachmentsExclusive) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
names.AttrGroupName: schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"policy_arns": schema.SetAttribute{
ElementType: types.StringType,
Required: true,
},
},
}
}

func (r *resourceGroupPolicyAttachmentsExclusive) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan resourceGroupPolicyAttachmentsExclusiveData
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

var policyARNs []string
resp.Diagnostics.Append(plan.PolicyARNs.ElementsAs(ctx, &policyARNs, false)...)
if resp.Diagnostics.HasError() {
return
}

err := r.syncAttachments(ctx, plan.GroupName.ValueString(), policyARNs)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.IAM, create.ErrActionCreating, ResNameGroupPolicyAttachmentsExclusive, plan.GroupName.String(), err),
err.Error(),
)
return
}

resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
}

func (r *resourceGroupPolicyAttachmentsExclusive) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
conn := r.Meta().IAMClient(ctx)

var state resourceGroupPolicyAttachmentsExclusiveData
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

out, err := findGroupPolicyAttachmentsByName(ctx, conn, state.GroupName.ValueString())
if tfresource.NotFound(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.IAM, create.ErrActionReading, ResNameGroupPolicyAttachmentsExclusive, state.GroupName.String(), err),
err.Error(),
)
return
}

state.PolicyARNs = flex.FlattenFrameworkStringValueSetLegacy(ctx, out)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func (r *resourceGroupPolicyAttachmentsExclusive) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state resourceGroupPolicyAttachmentsExclusiveData
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

if !plan.PolicyARNs.Equal(state.PolicyARNs) {
var policyARNs []string
resp.Diagnostics.Append(plan.PolicyARNs.ElementsAs(ctx, &policyARNs, false)...)
if resp.Diagnostics.HasError() {
return
}

err := r.syncAttachments(ctx, plan.GroupName.ValueString(), policyARNs)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.IAM, create.ErrActionUpdating, ResNameGroupPolicyAttachmentsExclusive, plan.GroupName.String(), err),
err.Error(),
)
return
}
}

resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

// syncAttachments handles keeping the configured customer managed policy
// attachments in sync with the remote resource.
//
// Customer managed policies defined on this resource but not attached to
// the group will be added. Policies attached to the group but not configured
// on this resource will be removed.
func (r *resourceGroupPolicyAttachmentsExclusive) syncAttachments(ctx context.Context, groupName string, want []string) error {
conn := r.Meta().IAMClient(ctx)

have, err := findGroupPolicyAttachmentsByName(ctx, conn, groupName)
if err != nil {
return err
}

create, remove, _ := intflex.DiffSlices(have, want, func(s1, s2 string) bool { return s1 == s2 })

for _, arn := range create {
err := attachPolicyToGroup(ctx, conn, groupName, arn)
if err != nil {
return err
}
}

for _, arn := range remove {
err := detachPolicyFromGroup(ctx, conn, groupName, arn)
if err != nil {
return err
}
}

return nil
}

func (r *resourceGroupPolicyAttachmentsExclusive) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root(names.AttrGroupName), req, resp)
}

func findGroupPolicyAttachmentsByName(ctx context.Context, conn *iam.Client, groupName string) ([]string, error) {
in := &iam.ListAttachedGroupPoliciesInput{
GroupName: aws.String(groupName),
}

var policyARNs []string
paginator := iam.NewListAttachedGroupPoliciesPaginator(conn, in)
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
if errs.IsA[*awstypes.NoSuchEntityException](err) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: in,
}
}
return policyARNs, err
}

for _, p := range page.AttachedPolicies {
if p.PolicyArn != nil {
policyARNs = append(policyARNs, aws.ToString(p.PolicyArn))
}
}
}

return policyARNs, nil
}

type resourceGroupPolicyAttachmentsExclusiveData struct {
GroupName types.String `tfsdk:"group_name"`
PolicyARNs types.Set `tfsdk:"policy_arns"`
}
Loading

0 comments on commit 14689f2

Please sign in to comment.