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/aws_iam_role_policies_exclusive: new resource #39203

Merged
merged 3 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .changelog/39203.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```release-note:new-resource
aws_iam_role_policies_exclusive
```
```release-note:note
resource/aws_iam_role: The `inline_policy` argument is deprecated. Use the `aws_iam_role_policy` resource instead. If Terraform should exclusively manage all inline policy associations (the current behavior of this argument), use the `aws_iam_role_policies_exclusive` resource as well.
```
2 changes: 2 additions & 0 deletions internal/service/iam/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
ResourceOpenIDConnectProvider = resourceOpenIDConnectProvider
ResourcePolicy = resourcePolicy
ResourcePolicyAttachment = resourcePolicyAttachment
ResourceRolePoliciesLock = newResourceRolePoliciesExclusive
jar-b marked this conversation as resolved.
Show resolved Hide resolved
ResourceRolePolicy = resourceRolePolicy
ResourceRolePolicyAttachment = resourceRolePolicyAttachment
ResourceSAMLProvider = resourceSAMLProvider
Expand Down Expand Up @@ -44,6 +45,7 @@ var (
FindInstanceProfileByName = findInstanceProfileByName
FindOpenIDConnectProviderByARN = findOpenIDConnectProviderByARN
FindPolicyByARN = findPolicyByARN
FindRolePoliciesByName = findRolePoliciesByName
FindSAMLProviderByARN = findSAMLProviderByARN
FindServerCertificateByName = findServerCertificateByName
FindSSHPublicKeyByThreePartKey = findSSHPublicKeyByThreePartKey
Expand Down
7 changes: 4 additions & 3 deletions internal/service/iam/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ func resourceRole() *schema.Resource {
Default: false,
},
"inline_policy": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Type: schema.TypeSet,
Optional: true,
Computed: true,
Deprecated: "Use the aws_iam_role_policy resource instead. If Terraform should exclusively manage all inline policy associations (the current behavior of this argument), use the aws_iam_role_policies_exclusive resource as well.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
names.AttrName: {
Expand Down
217 changes: 217 additions & 0 deletions internal/service/iam/role_policies_exclusive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// 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_role_policies_exclusive", name="Role Policies Exclusive")
func newResourceRolePoliciesExclusive(_ context.Context) (resource.ResourceWithConfigure, error) {
return &resourceRolePoliciesExclusive{}, nil
}

const (
ResNameRolePoliciesExclusive = "Role Policies Exclusive"
)

type resourceRolePoliciesExclusive struct {
framework.ResourceWithConfigure
framework.WithNoOpDelete
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

have, err := findRolePoliciesByName(ctx, conn, roleName)
if err != nil {
return err
}

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

for _, name := range create {
in := &iam.PutRolePolicyInput{
RoleName: aws.String(roleName),
PolicyName: aws.String(name),
}

_, err := conn.PutRolePolicy(ctx, in)
if err != nil {
return err
}
}

for _, name := range remove {
in := &iam.DeleteRolePolicyInput{
RoleName: aws.String(roleName),
PolicyName: aws.String(name),
}

_, err := conn.DeleteRolePolicy(ctx, in)
if err != nil {
return err
}
}

return nil
}

func (r *resourceRolePoliciesExclusive) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("role_name"), req, resp)
}

func findRolePoliciesByName(ctx context.Context, conn *iam.Client, roleName string) ([]string, error) {
in := &iam.ListRolePoliciesInput{
RoleName: aws.String(roleName),
}

var policyNames []string
paginator := iam.NewListRolePoliciesPaginator(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 policyNames, err
}

policyNames = append(policyNames, page.PolicyNames...)
}

return policyNames, nil
}

type resourceRolePoliciesExclusiveData struct {
RoleName types.String `tfsdk:"role_name"`
PolicyNames types.Set `tfsdk:"policy_names"`
}
Loading
Loading