Skip to content

Commit

Permalink
Merge pull request #41589 from hashicorp/f-quicksight_role_membership
Browse files Browse the repository at this point in the history
r/aws_quicksight_role_membership: new resource
  • Loading branch information
jar-b authored Feb 27, 2025
2 parents affb06d + 336bc07 commit a4ccfc9
Show file tree
Hide file tree
Showing 9 changed files with 521 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .changelog/41589.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_quicksight_role_membership
```
1 change: 1 addition & 0 deletions docs/acc-test-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Environment variables (beyond standard AWS Go SDK ones) used by acceptance testi
| `TF_AWS_LICENSE_MANAGER_GRANT_HOME_REGION` | Region where a License Manager license is imported. |
| `TF_AWS_LICENSE_MANAGER_GRANT_LICENSE_ARN` | ARN for a License Manager license imported into the current account. |
| `TF_AWS_LICENSE_MANAGER_GRANT_PRINCIPAL` | ARN of a principal to share the License Manager license with. Either a root user, Organization, or Organizational Unit. |
| `TF_AWS_QUICKSIGHT_IDC_GROUP` | Name of the IAM Identity Center Group to be assigned role membership. |
| `TF_TEST_CLOUDFRONT_RETAIN` | Flag to disable but dangle CloudFront Distributions during testing to reduce feedback time (must be manually destroyed afterwards) |
| `TF_TEST_ELASTICACHE_RESERVED_CACHE_NODE` | Flag to enable resource tests for ElastiCache reserved nodes. Set to `1` to run tests |
| `TRUST_ANCHOR_CERTIFICATE` | Trust anchor certificate for KMS custom key store acceptance tests. |
2 changes: 2 additions & 0 deletions internal/service/quicksight/exports_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
ResourceIngestion = newIngestionResource
ResourceNamespace = newNamespaceResource
ResourceRefreshSchedule = newRefreshScheduleResource
ResourceRoleMembership = newResourceRoleMembership
ResourceTemplate = resourceTemplate
ResourceTemplateAlias = newTemplateAliasResource
ResourceTheme = resourceTheme
Expand All @@ -41,6 +42,7 @@ var (
FindIngestionByThreePartKey = findIngestionByThreePartKey
FindNamespaceByTwoPartKey = findNamespaceByTwoPartKey
FindRefreshScheduleByThreePartKey = findRefreshScheduleByThreePartKey
FindRoleMembershipByMultiPartKey = findRoleMembershipByMultiPartKey
FindTemplateAliasByThreePartKey = findTemplateAliasByThreePartKey
FindTemplateByTwoPartKey = findTemplateByTwoPartKey
FindThemeByTwoPartKey = findThemeByTwoPartKey
Expand Down
5 changes: 5 additions & 0 deletions internal/service/quicksight/quicksight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func TestAccQuickSight_serial(t *testing.T) {
acctest.CtBasic: testAccAccountSubscription_basic,
acctest.CtDisappears: testAccAccountSubscription_disappears,
},
"RoleMembership": {
acctest.CtBasic: testAccRoleMembership_basic,
acctest.CtDisappears: testAccRoleMembership_disappears,
"role": testAccRoleMembership_role,
},
}

acctest.RunSerialTests2Levels(t, testCases, 0)
Expand Down
233 changes: 233 additions & 0 deletions internal/service/quicksight/role_membership.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package quicksight

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/quicksight"
awstypes "github.com/aws/aws-sdk-go-v2/service/quicksight/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/stringdefault"
"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"
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

// @FrameworkResource("aws_quicksight_role_membership", name="Role Membership")
func newResourceRoleMembership(_ context.Context) (resource.ResourceWithConfigure, error) {
return &resourceRoleMembership{}, nil
}

const (
ResNameRoleMembership = "Role Membership"
)

type resourceRoleMembership struct {
framework.ResourceWithConfigure
framework.WithNoUpdate
}

func (r *resourceRoleMembership) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
names.AttrAWSAccountID: schema.StringAttribute{
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
"member_name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
names.AttrNamespace: schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("default"),
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
names.AttrRole: schema.StringAttribute{
CustomType: fwtypes.StringEnumType[awstypes.Role](),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
},
}
}

func (r *resourceRoleMembership) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
conn := r.Meta().QuickSightClient(ctx)

var plan resourceRoleMembershipModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

if plan.AWSAccountID.IsUnknown() || plan.AWSAccountID.IsNull() {
plan.AWSAccountID = types.StringValue(r.Meta().AccountID(ctx))
}

input := quicksight.CreateRoleMembershipInput{
AwsAccountId: plan.AWSAccountID.ValueStringPointer(),
MemberName: plan.MemberName.ValueStringPointer(),
Namespace: plan.Namespace.ValueStringPointer(),
Role: plan.Role.ValueEnum(),
}

_, err := conn.CreateRoleMembership(ctx, &input)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.QuickSight, create.ErrActionCreating, ResNameRoleMembership, plan.MemberName.String(), err),
err.Error(),
)
return
}

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

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

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

err := findRoleMembershipByMultiPartKey(ctx, conn, state.AWSAccountID.ValueString(), state.Namespace.ValueString(), state.Role.ValueEnum(), state.MemberName.ValueString())
if tfresource.NotFound(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.QuickSight, create.ErrActionSetting, ResNameRoleMembership, state.MemberName.String(), err),
err.Error(),
)
return
}

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

func (r *resourceRoleMembership) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
conn := r.Meta().QuickSightClient(ctx)

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

input := quicksight.DeleteRoleMembershipInput{
AwsAccountId: state.AWSAccountID.ValueStringPointer(),
MemberName: state.MemberName.ValueStringPointer(),
Namespace: state.Namespace.ValueStringPointer(),
Role: state.Role.ValueEnum(),
}

_, err := conn.DeleteRoleMembership(ctx, &input)
if err != nil {
if errs.IsA[*awstypes.ResourceNotFoundException](err) {
return
}

resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.QuickSight, create.ErrActionDeleting, ResNameRoleMembership, state.MemberName.String(), err),
err.Error(),
)
return
}
}

const roleMembershipIDParts = 4

func (r *resourceRoleMembership) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
parts, err := intflex.ExpandResourceId(req.ID, roleMembershipIDParts, false)
if err != nil {
resp.Diagnostics.AddError(
"Unexpected Import Identifier",
fmt.Sprintf("Expected import identifier with format: aws_account_id,namespace,role,member_name. Got: %q", req.ID),
)
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(names.AttrAWSAccountID), parts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(names.AttrNamespace), parts[1])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(names.AttrRole), parts[2])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("member_name"), parts[3])...)
}

// findRoleMembershipByMultiPartKey verifies the existence of a role membership
//
// No value is returned, but the error will be non-nil if no matching member name
// is found in the list of group members for the provided role.
func findRoleMembershipByMultiPartKey(ctx context.Context, conn *quicksight.Client, accountID string, namespace string, role awstypes.Role, member string) error {
input := quicksight.ListRoleMembershipsInput{
AwsAccountId: aws.String(accountID),
Namespace: aws.String(namespace),
Role: role,
}

out, err := findRoleMemberships(ctx, conn, &input)
if err != nil {
return err
}

for _, m := range out {
if m == member {
return nil
}
}

return &retry.NotFoundError{
LastRequest: input,
}
}

func findRoleMemberships(ctx context.Context, conn *quicksight.Client, input *quicksight.ListRoleMembershipsInput) ([]string, error) {
paginator := quicksight.NewListRoleMembershipsPaginator(conn, input)

var memberNames []string
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return nil, err
}

memberNames = append(memberNames, page.MembersList...)
}

return memberNames, nil
}

type resourceRoleMembershipModel struct {
AWSAccountID types.String `tfsdk:"aws_account_id"`
MemberName types.String `tfsdk:"member_name"`
Namespace types.String `tfsdk:"namespace"`
Role fwtypes.StringEnum[awstypes.Role] `tfsdk:"role"`
}
Loading

0 comments on commit a4ccfc9

Please sign in to comment.