diff --git a/.changelog/31309.txt b/.changelog/31309.txt new file mode 100644 index 00000000000..b3039be6d6e --- /dev/null +++ b/.changelog/31309.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_quicksight_vpc_connection +``` \ No newline at end of file diff --git a/internal/service/quicksight/exports_test.go b/internal/service/quicksight/exports_test.go index b89d5811aeb..1e466944b40 100644 --- a/internal/service/quicksight/exports_test.go +++ b/internal/service/quicksight/exports_test.go @@ -8,4 +8,5 @@ var ( ResourceNamespace = newResourceNamespace ResourceRefreshSchedule = newResourceRefreshSchedule ResourceTemplateAlias = newResourceTemplateAlias + ResourceVPCConnection = newResourceVPCConnection ) diff --git a/internal/service/quicksight/iam_policy_assignment.go b/internal/service/quicksight/iam_policy_assignment.go index 7af8cd52af3..68e572fe99b 100644 --- a/internal/service/quicksight/iam_policy_assignment.go +++ b/internal/service/quicksight/iam_policy_assignment.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "strings" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/quicksight" @@ -39,7 +38,6 @@ const ( ResNameIAMPolicyAssignment = "IAM Policy Assignment" DefaultIAMPolicyAssignmentNamespace = "default" - iamPropagationTimeout = 2 * time.Minute identitiesUserKey = "user" identitiesGroupKey = "group" ) diff --git a/internal/service/quicksight/service_package_gen.go b/internal/service/quicksight/service_package_gen.go index fa975df79a6..d5b164541a5 100644 --- a/internal/service/quicksight/service_package_gen.go +++ b/internal/service/quicksight/service_package_gen.go @@ -44,6 +44,13 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newResourceTemplateAlias, Name: "Template Alias", }, + { + Factory: newResourceVPCConnection, + Name: "VPC Connection", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, } } diff --git a/internal/service/quicksight/vpc_connection.go b/internal/service/quicksight/vpc_connection.go new file mode 100644 index 00000000000..58cb825237d --- /dev/null +++ b/internal/service/quicksight/vpc_connection.go @@ -0,0 +1,533 @@ +package quicksight + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/quicksight" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="VPC Connection") +// @Tags(identifierAttribute="arn") +func newResourceVPCConnection(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceVPCConnection{} + r.SetDefaultCreateTimeout(5 * time.Minute) + r.SetDefaultUpdateTimeout(5 * time.Minute) + r.SetDefaultDeleteTimeout(5 * time.Minute) + + return r, nil +} + +type resourceVPCConnection struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceVPCConnection) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_quicksight_vpc_connection" +} + +const ( + ResNameVPCConnection = "VPC Connection" + vpcConnectionIdRegex = "[\\w\\-]+" + subnetIdRegex = "^subnet-[0-9a-z]*$" + securityGroupIdRegex = "^sg-[0-9a-z]*$" +) + +func (r *resourceVPCConnection) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "arn": framework.ARNAttributeComputedOnly(), + "aws_account_id": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + }, + "id": framework.IDAttribute(), + "vpc_connection_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.LengthAtMost(1000), + stringvalidator.RegexMatches(regexp.MustCompile(vpcConnectionIdRegex), "VPC Connection ID must match regex: "+vpcConnectionIdRegex), + ), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(128), + }, + }, + "role_arn": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(20, 2048), + }, + }, + "security_group_ids": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 16), + setvalidator.ValueStringsAre( + stringvalidator.All( + stringvalidator.LengthAtMost(255), + stringvalidator.RegexMatches(regexp.MustCompile(securityGroupIdRegex), "Security group ID must match regex: "+securityGroupIdRegex), + ), + ), + }, + }, + "subnet_ids": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeBetween(2, 15), + setvalidator.ValueStringsAre( + stringvalidator.All( + stringvalidator.LengthAtMost(255), + stringvalidator.RegexMatches(regexp.MustCompile(subnetIdRegex), "Subnet ID must match regex: "+subnetIdRegex), + ), + ), + }, + }, + "dns_resolvers": schema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 15), + setvalidator.ValueStringsAre( + stringvalidator.All( + stringvalidator.LengthBetween(7, 15), + ), + ), + }, + }, + "availability_status": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *resourceVPCConnection) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().QuickSightConn() + + var plan resourceVPCConnectionData + 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) + } + plan.ID = types.StringValue(createVPCConnectionID(plan.AWSAccountID.ValueString(), plan.VPCConnectionID.ValueString())) + + in := &quicksight.CreateVPCConnectionInput{ + AwsAccountId: aws.String(plan.AWSAccountID.ValueString()), + VPCConnectionId: aws.String(plan.VPCConnectionID.ValueString()), + Name: aws.String(plan.Name.ValueString()), + RoleArn: aws.String(plan.RoleArn.ValueString()), + SecurityGroupIds: flex.ExpandFrameworkStringSet(ctx, plan.SecurityGroupIds), + SubnetIds: flex.ExpandFrameworkStringSet(ctx, plan.SubnetIds), + Tags: GetTagsIn(ctx), + } + + if !plan.DnsResolvers.IsNull() { + in.DnsResolvers = flex.ExpandFrameworkStringSet(ctx, plan.DnsResolvers) + } + + // account for IAM propagation when attempting to assume role + out, err := retryVPCConnectionCreate(ctx, conn, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionCreating, ResNameVPCConnection, plan.Name.String(), err), + err.Error(), + ) + return + } + + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionCreating, ResNameVPCConnection, plan.Name.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + waitOut, err := waitVPCConnectionCreated(ctx, conn, plan.ID.ValueString(), createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionWaitingForCreation, ResNameVPCConnection, plan.Name.String(), err), + err.Error(), + ) + return + } + + plan.ARN = flex.StringToFramework(ctx, waitOut.Arn) + plan.AvailabilityStatus = flex.StringToFramework(ctx, waitOut.AvailabilityStatus) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceVPCConnection) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().QuickSightConn() + + var state resourceVPCConnectionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindVPCConnectionByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionReading, ResNameVPCConnection, state.ID.String(), err), + err.Error(), + ) + return + } + if aws.StringValue(out.Status) == quicksight.VPCConnectionResourceStatusDeleted { + resp.State.RemoveResource(ctx) + return + } + + // To support import, parse the ID for the component keys and set + // individual values in state + awsAccountID, vpcConnectionID, err := ParseVPCConnectionID(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionReading, ResNameVPCConnection, state.ID.String(), nil), + err.Error(), + ) + return + } + state.AWSAccountID = flex.StringValueToFramework(ctx, awsAccountID) + state.VPCConnectionID = flex.StringValueToFramework(ctx, vpcConnectionID) + state.ARN = flex.StringToFramework(ctx, out.Arn) + state.Name = flex.StringToFramework(ctx, out.Name) + state.RoleArn = flex.StringToFramework(ctx, out.RoleArn) + state.SecurityGroupIds = flex.FlattenFrameworkStringSet(ctx, out.SecurityGroupIds) + state.DnsResolvers = flex.FlattenFrameworkStringSet(ctx, out.DnsResolvers) + state.AvailabilityStatus = flex.StringToFramework(ctx, out.AvailabilityStatus) + var subnetIds []*string + for _, iface := range out.NetworkInterfaces { + subnetIds = append(subnetIds, iface.SubnetId) + } + state.SubnetIds = flex.FlattenFrameworkStringSet(ctx, subnetIds) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceVPCConnection) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().QuickSightConn() + + var plan, state resourceVPCConnectionData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Name.Equal(state.Name) || + !plan.DnsResolvers.Equal(state.DnsResolvers) || + !plan.RoleArn.Equal(state.RoleArn) || + !plan.SecurityGroupIds.Equal(state.SecurityGroupIds) || + !plan.SubnetIds.Equal(state.SubnetIds) { + in := quicksight.UpdateVPCConnectionInput{ + AwsAccountId: aws.String(plan.AWSAccountID.ValueString()), + VPCConnectionId: aws.String(plan.VPCConnectionID.ValueString()), + Name: aws.String(plan.Name.ValueString()), + RoleArn: aws.String(plan.RoleArn.ValueString()), + SecurityGroupIds: flex.ExpandFrameworkStringSet(ctx, plan.SecurityGroupIds), + SubnetIds: flex.ExpandFrameworkStringSet(ctx, plan.SubnetIds), + } + + if !plan.DnsResolvers.IsNull() { + in.DnsResolvers = flex.ExpandFrameworkStringSet(ctx, plan.DnsResolvers) + } + + out, err := conn.UpdateVPCConnectionWithContext(ctx, &in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionUpdating, ResNameVPCConnection, plan.ID.String(), nil), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionUpdating, ResNameVPCConnection, plan.ID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + _, err = waitVPCConnectionUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionWaitingForUpdate, ResNameVPCConnection, plan.ID.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + } + + // ensure tag only updates are copied into state + if !plan.Tags.Equal(state.Tags) { + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + } +} + +func (r *resourceVPCConnection) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().QuickSightConn() + + var state resourceVPCConnectionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + in := &quicksight.DeleteVPCConnectionInput{ + AwsAccountId: aws.String(state.AWSAccountID.ValueString()), + VPCConnectionId: aws.String(state.VPCConnectionID.ValueString()), + } + + _, err := conn.DeleteVPCConnectionWithContext(ctx, in) + if err != nil { + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return + } + if tfawserr.ErrMessageContains(err, quicksight.ErrCodeConflictException, "Cannot perform operation on deleted VPCConnection") { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionDeleting, ResNameVPCConnection, state.ID.String(), err), + err.Error(), + ) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitVPCConnectionDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionWaitingForDeletion, ResNameVPCConnection, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceVPCConnection) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *resourceVPCConnection) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, req, resp) +} + +func FindVPCConnectionByID(ctx context.Context, conn *quicksight.QuickSight, id string) (*quicksight.VPCConnection, error) { + awsAccountID, vpcConnectionId, err := ParseVPCConnectionID(id) + if err != nil { + return nil, err + } + + in := &quicksight.DescribeVPCConnectionInput{ + AwsAccountId: aws.String(awsAccountID), + VPCConnectionId: aws.String(vpcConnectionId), + } + + out, err := conn.DescribeVPCConnectionWithContext(ctx, in) + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + if err != nil { + return nil, err + } + if out == nil || out.VPCConnection == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.VPCConnection, nil +} + +func retryVPCConnectionCreate(ctx context.Context, conn *quicksight.QuickSight, in *quicksight.CreateVPCConnectionInput) (*quicksight.CreateVPCConnectionOutput, error) { + outputRaw, err := tfresource.RetryWhen(ctx, + iamPropagationTimeout, + func() (interface{}, error) { + return conn.CreateVPCConnectionWithContext(ctx, in) + }, + func(err error) (bool, error) { + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeAccessDeniedException) { + return true, err + } + + return false, err + }, + ) + + output, _ := outputRaw.(*quicksight.CreateVPCConnectionOutput) + return output, err +} + +func waitVPCConnectionCreated(ctx context.Context, conn *quicksight.QuickSight, id string, timeout time.Duration) (*quicksight.VPCConnection, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{ + quicksight.VPCConnectionResourceStatusCreationInProgress, + }, + Target: []string{ + quicksight.VPCConnectionResourceStatusCreationSuccessful, + }, + Refresh: statusVPCConnection(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if output, ok := outputRaw.(*quicksight.VPCConnection); ok { + return output, err + } + + return nil, err +} + +func waitVPCConnectionUpdated(ctx context.Context, conn *quicksight.QuickSight, id string, timeout time.Duration) (*quicksight.VPCConnection, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{ + quicksight.VPCConnectionResourceStatusUpdateInProgress, + }, + Target: []string{ + quicksight.VPCConnectionResourceStatusUpdateSuccessful, + }, + Refresh: statusVPCConnection(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if output, ok := outputRaw.(*quicksight.VPCConnection); ok { + return output, err + } + + return nil, err +} + +func waitVPCConnectionDeleted(ctx context.Context, conn *quicksight.QuickSight, id string, timeout time.Duration) (*quicksight.VPCConnection, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{ + quicksight.VPCConnectionResourceStatusDeletionInProgress, + }, + Target: []string{ + quicksight.VPCConnectionResourceStatusDeleted, + }, + Refresh: statusVPCConnection(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if output, ok := outputRaw.(*quicksight.VPCConnection); ok { + return output, err + } + + return nil, err +} + +func statusVPCConnection(ctx context.Context, conn *quicksight.QuickSight, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindVPCConnectionByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func ParseVPCConnectionID(id string) (string, string, error) { + parts := strings.SplitN(id, ",", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected AWS_ACCOUNT_ID,VPC_CONNECTION_ID", id) + } + return parts[0], parts[1], nil +} + +func createVPCConnectionID(awsAccountID, vpcConnectionID string) string { + return strings.Join([]string{awsAccountID, vpcConnectionID}, ",") +} + +type resourceVPCConnectionData struct { + ID types.String `tfsdk:"id"` + ARN types.String `tfsdk:"arn"` + AWSAccountID types.String `tfsdk:"aws_account_id"` + VPCConnectionID types.String `tfsdk:"vpc_connection_id"` + Name types.String `tfsdk:"name"` + RoleArn types.String `tfsdk:"role_arn"` + AvailabilityStatus types.String `tfsdk:"availability_status"` + SecurityGroupIds types.Set `tfsdk:"security_group_ids"` + SubnetIds types.Set `tfsdk:"subnet_ids"` + DnsResolvers types.Set `tfsdk:"dns_resolvers"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} diff --git a/internal/service/quicksight/vpc_connection_test.go b/internal/service/quicksight/vpc_connection_test.go new file mode 100644 index 00000000000..85131def5c4 --- /dev/null +++ b/internal/service/quicksight/vpc_connection_test.go @@ -0,0 +1,277 @@ +package quicksight_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/quicksight" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfquicksight "github.com/hashicorp/terraform-provider-aws/internal/service/quicksight" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccQuickSightVPCConnection_basic(t *testing.T) { + ctx := acctest.Context(t) + var vpcConnection quicksight.VPCConnection + resourceName := "aws_quicksight_vpc_connection.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rId := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, quicksight.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCConnectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCConnectionConfig_basic(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCConnectionExists(ctx, resourceName, &vpcConnection), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "quicksight", fmt.Sprintf("vpcConnection/%[1]s", rId)), + resource.TestCheckResourceAttr(resourceName, "vpc_connection_id", rId), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "2"), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccQuickSightVPCConnection_disappears(t *testing.T) { + ctx := acctest.Context(t) + var vpcConnection quicksight.VPCConnection + resourceName := "aws_quicksight_vpc_connection.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rId := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, quicksight.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCConnectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCConnectionConfig_basic(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCConnectionExists(ctx, resourceName, &vpcConnection), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfquicksight.ResourceVPCConnection, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccQuickSightVPCConnection_tags(t *testing.T) { + ctx := acctest.Context(t) + var vpcConnection quicksight.VPCConnection + resourceName := "aws_quicksight_vpc_connection.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rId := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, quicksight.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCConnectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCConnectionConfig_tags1(rId, rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCConnectionExists(ctx, resourceName, &vpcConnection), + resource.TestCheckResourceAttr(resourceName, "vpc_connection_id", rId), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccVPCConnectionConfig_tags2(rId, rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCConnectionExists(ctx, resourceName, &vpcConnection), + resource.TestCheckResourceAttr(resourceName, "vpc_connection_id", rId), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccVPCConnectionConfig_tags1(rId, rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCConnectionExists(ctx, resourceName, &vpcConnection), + resource.TestCheckResourceAttr(resourceName, "vpc_connection_id", rId), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckVPCConnectionExists(ctx context.Context, resourceName string, vpcConnection *quicksight.VPCConnection) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).QuickSightConn() + output, err := tfquicksight.FindVPCConnectionByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.QuickSight, create.ErrActionCheckingExistence, tfquicksight.ResNameVPCConnection, rs.Primary.ID, err) + } + + *vpcConnection = *output + + return nil + } +} + +func testAccCheckVPCConnectionDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).QuickSightConn() + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_quicksight_vpc_connection" { + continue + } + + output, err := tfquicksight.FindVPCConnectionByID(ctx, conn, rs.Primary.ID) + if err != nil { + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return nil + } + return err + } + + if output != nil && aws.StringValue(output.Status) == quicksight.VPCConnectionResourceStatusDeleted { + return nil + } + + return create.Error(names.QuickSight, create.ErrActionCheckingDestroyed, tfquicksight.ResNameVPCConnection, rs.Primary.ID, err) + } + + return nil + } +} + +func testAccBaseVPCConnectionConfig(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 2), + ` +resource "aws_security_group" "test" { + vpc_id = aws_vpc.test.id +} + +resource "aws_iam_role" "test" { + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + Service = "quicksight.amazonaws.com" + } + } + ] + }) + inline_policy { + name = "QuicksightVPCConnectionRolePolicy" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ec2:CreateNetworkInterface", + "ec2:ModifyNetworkInterfaceAttribute", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups" + ] + Resource = ["*"] + } + ] + }) + } +} +`) +} + +func testAccVPCConnectionConfig_basic(rId string, rName string) string { + return acctest.ConfigCompose( + testAccBaseVPCConnectionConfig(rName), + fmt.Sprintf(` +resource "aws_quicksight_vpc_connection" "test" { + vpc_connection_id = %[1]q + name = %[2]q + role_arn = aws_iam_role.test.arn + security_group_ids = [ + aws_security_group.test.id, + ] + subnet_ids = aws_subnet.test[*].id +} +`, rId, rName)) +} + +func testAccVPCConnectionConfig_tags1(rId, rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose( + testAccBaseVPCConnectionConfig(rName), + fmt.Sprintf(` +resource "aws_quicksight_vpc_connection" "test" { + vpc_connection_id = %[1]q + name = %[2]q + role_arn = aws_iam_role.test.arn + security_group_ids = [ + aws_security_group.test.id, + ] + subnet_ids = aws_subnet.test[*].id + + tags = { + %[3]q = %[4]q + } +} +`, rId, rName, tagKey1, tagValue1)) +} + +func testAccVPCConnectionConfig_tags2(rId, rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose( + testAccBaseVPCConnectionConfig(rName), + fmt.Sprintf(` +resource "aws_quicksight_vpc_connection" "test" { + vpc_connection_id = %[1]q + name = %[2]q + role_arn = aws_iam_role.test.arn + security_group_ids = [ + aws_security_group.test.id, + ] + subnet_ids = aws_subnet.test[*].id + + tags = { + %[3]q = %[4]q + %[5]q = %[6]q + } +} +`, rId, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} diff --git a/internal/service/quicksight/wait.go b/internal/service/quicksight/wait.go index dd60fa4d005..609ee1f1133 100644 --- a/internal/service/quicksight/wait.go +++ b/internal/service/quicksight/wait.go @@ -14,6 +14,7 @@ import ( ) const ( + iamPropagationTimeout = 2 * time.Minute dataSourceCreateTimeout = 5 * time.Minute dataSourceUpdateTimeout = 5 * time.Minute ) diff --git a/website/docs/r/quicksight_vpc_connection.html.markdown b/website/docs/r/quicksight_vpc_connection.html.markdown new file mode 100644 index 00000000000..1cdef75a3b9 --- /dev/null +++ b/website/docs/r/quicksight_vpc_connection.html.markdown @@ -0,0 +1,104 @@ +--- +subcategory: "QuickSight" +layout: "aws" +page_title: "AWS: aws_quicksight_vpc_connection" +description: |- + Terraform resource for managing an AWS QuickSight VPC Connection. +--- + +# Resource: aws_quicksight_vpc_connection + +Terraform resource for managing an AWS QuickSight VPC Connection. + +## Example Usage + +### Basic Usage + +```terraform + +resource "aws_iam_role" "vpc_connection_role" { + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = "sts:AssumeRole" + Principal = { + Service = "quicksight.amazonaws.com" + } + } + ] + }) + inline_policy { + name = "QuickSightVPCConnectionRolePolicy" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ec2:CreateNetworkInterface", + "ec2:ModifyNetworkInterfaceAttribute", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups" + ] + Resource = ["*"] + } + ] + }) + } +} + +resource "aws_quicksight_vpc_connection" "example" { + vpc_connection_id = "example-connection-id" + name = "Example Connection" + role_arn = aws_iam_role.vpc_connection_role.arn + security_group_ids = ["sg-00000000000000000"] + subnet_ids = [ + "subnet-00000000000000000", + "subnet-00000000000000001", + ] +} +``` + +## Argument Reference + +The following arguments are required: + +* `vpc_connection_id` - (Required) The ID of the VPC connection. +* `name` - (Required) The display name for the VPC connection. +* `role_arn` - (Required) The IAM role to associate with the VPC connection. +* `security_group_ids` - (Required) A list of security group IDs for the VPC connection. +* `subnet_ids` - (Required) A list of subnet IDs for the VPC connection. + +The following arguments are optional: + +* `aws_account_id` - (Optional) AWS account ID. +* `dns_resolvers` - (Optional) A list of IP addresses of DNS resolver endpoints for the VPC connection. +* `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the VPC connection. +* `availability_status` - The availability status of the VPC connection. Valid values are `AVAILABLE`, `UNAVAILABLE` or `PARTIALLY_AVAILABLE`. +* `id` - A comma-delimited string joining AWS account ID and VPC connection ID. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `5m`) +* `update` - (Default `5m`) +* `delete` - (Default `5m`) + +## Import + +QuickSight VPC connection can be imported using the AWS account ID and VPC connection ID separated by commas (`,`) e.g., + +``` +$ terraform import aws_quicksight_vpc_connection.example 123456789012,example +```