Skip to content

Commit

Permalink
r/aws_quicksight_role_membership: new resource
Browse files Browse the repository at this point in the history
This resource will allow practitioners to manage QuickSight role memberships via Terraform. The role membership APIs are disabled for identities managed by QuickSight. This resource can only be used when the QuickSight account subscription uses the IAM Identity Center authentication method.

```console
% TF_AWS_QUICKSIGHT_IDC_GROUP=jb-test-quicksight make testacc PKG=quicksight TESTS=TestAccQuickSight_serial/RoleMembership
make: Verifying source code with gofmt...
==> Checking that code complies with gofmt requirements...
TF_ACC=1 go1.23.5 test ./internal/service/quicksight/... -v -count 1 -parallel 20 -run='TestAccQuickSight_serial/RoleMembership'  -timeout 360m -vet=off
2025/02/27 11:19:46 Initializing Terraform AWS Provider...

--- PASS: TestAccQuickSight_serial (36.75s)
    --- PASS: TestAccQuickSight_serial/RoleMembership (36.75s)
        --- PASS: TestAccQuickSight_serial/RoleMembership/role (16.79s)
        --- PASS: TestAccQuickSight_serial/RoleMembership/basic (10.74s)
        --- PASS: TestAccQuickSight_serial/RoleMembership/disappears (9.21s)
PASS
ok      github.com/hashicorp/terraform-provider-aws/internal/service/quicksight 43.592s
testing: warning: no tests to run
PASS
ok      github.com/hashicorp/terraform-provider-aws/internal/service/quicksight/schema  0.307s [no tests to run]
```
  • Loading branch information
jar-b committed Feb 27, 2025
1 parent 6995a54 commit bd38a01
Show file tree
Hide file tree
Showing 8 changed files with 518 additions and 0 deletions.
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 bd38a01

Please sign in to comment.