From 72c420473e1ee136bf765deb1c33ab87d537125b Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 5 Apr 2023 15:18:35 -0400 Subject: [PATCH 1/4] r/aws_quicksight_ingestion: resource implementation --- internal/service/quicksight/exports_test.go | 6 + internal/service/quicksight/ingestion.go | 256 ++++++++++++++++++ .../service/quicksight/service_package_gen.go | 6 +- 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 internal/service/quicksight/exports_test.go create mode 100644 internal/service/quicksight/ingestion.go diff --git a/internal/service/quicksight/exports_test.go b/internal/service/quicksight/exports_test.go new file mode 100644 index 000000000000..8454411badf4 --- /dev/null +++ b/internal/service/quicksight/exports_test.go @@ -0,0 +1,6 @@ +package quicksight + +// Exports for use in tests only. +var ( + ResourceIngestion = newResourceIngestion +) diff --git a/internal/service/quicksight/ingestion.go b/internal/service/quicksight/ingestion.go new file mode 100644 index 000000000000..e0d9ef57c6b4 --- /dev/null +++ b/internal/service/quicksight/ingestion.go @@ -0,0 +1,256 @@ +package quicksight + +import ( + "context" + "errors" + "fmt" + "strings" + + "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-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" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource +func newResourceIngestion(_ context.Context) (resource.ResourceWithConfigure, error) { + return &resourceIngestion{}, nil +} + +const ( + ResNameIngestion = "Ingestion" +) + +type resourceIngestion struct { + framework.ResourceWithConfigure +} + +func (r *resourceIngestion) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_quicksight_ingestion" +} + +func (r *resourceIngestion) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "arn": schema.StringAttribute{ + Computed: true, + }, + "aws_account_id": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + }, + "data_set_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "id": framework.IDAttribute(), + "ingestion_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "ingestion_status": schema.StringAttribute{ + Computed: true, + }, + "ingestion_type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(quicksight.IngestionType_Values()...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *resourceIngestion) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().QuickSightConn() + + var plan resourceIngestionData + 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(createIngestionID(plan.AWSAccountID.ValueString(), plan.DataSetID.ValueString(), plan.IngestionID.ValueString())) + + in := quicksight.CreateIngestionInput{ + AwsAccountId: aws.String(plan.AWSAccountID.ValueString()), + DataSetId: aws.String(plan.DataSetID.ValueString()), + IngestionId: aws.String(plan.IngestionID.ValueString()), + IngestionType: aws.String(plan.IngestionType.ValueString()), + } + + out, err := conn.CreateIngestionWithContext(ctx, &in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionCreating, ResNameIngestion, plan.IngestionID.String(), nil), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionCreating, ResNameIngestion, plan.IngestionID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + plan.ARN = flex.StringToFramework(ctx, out.Arn) + plan.IngestionStatus = flex.StringToFramework(ctx, out.IngestionStatus) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceIngestion) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().QuickSightConn() + + var state resourceIngestionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindIngestionByID(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.ErrActionSetting, ResNameIngestion, state.ID.String(), nil), + err.Error(), + ) + return + } + + state.ARN = flex.StringToFramework(ctx, out.Arn) + state.IngestionID = flex.StringToFramework(ctx, out.IngestionId) + state.IngestionStatus = flex.StringToFramework(ctx, out.IngestionStatus) + + // To support import, parse the ID for the component keys and set + // individual values in state + awsAccountID, dataSetID, _, err := ParseIngestionID(state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionSetting, ResNameIngestion, state.ID.String(), nil), + err.Error(), + ) + return + } + state.AWSAccountID = flex.StringValueToFramework(ctx, awsAccountID) + state.DataSetID = flex.StringValueToFramework(ctx, dataSetID) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// There is no update API, so this method is a no-op +func (r *resourceIngestion) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { +} + +func (r *resourceIngestion) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().QuickSightConn() + + var state resourceIngestionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.CancelIngestionWithContext(ctx, &quicksight.CancelIngestionInput{ + AwsAccountId: aws.String(state.AWSAccountID.ValueString()), + DataSetId: aws.String(state.DataSetID.ValueString()), + IngestionId: aws.String(state.IngestionID.ValueString()), + }) + if err != nil { + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.QuickSight, create.ErrActionDeleting, ResNameIngestion, state.ID.String(), nil), + err.Error(), + ) + } +} + +func (r *resourceIngestion) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func FindIngestionByID(ctx context.Context, conn *quicksight.QuickSight, id string) (*quicksight.Ingestion, error) { + awsAccountID, dataSetID, ingestionID, err := ParseIngestionID(id) + if err != nil { + return nil, err + } + + in := &quicksight.DescribeIngestionInput{ + AwsAccountId: aws.String(awsAccountID), + DataSetId: aws.String(dataSetID), + IngestionId: aws.String(ingestionID), + } + + out, err := conn.DescribeIngestionWithContext(ctx, in) + if err != nil { + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.Ingestion == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.Ingestion, nil +} + +func ParseIngestionID(id string) (string, string, string, error) { + parts := strings.SplitN(id, ",", 3) + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return "", "", "", fmt.Errorf("unexpected format of ID (%s), expected AWS_ACCOUNT_ID,DATA_SET_ID,INGESTION_ID", id) + } + return parts[0], parts[1], parts[2], nil +} + +func createIngestionID(awsAccountID, dataSetID, ingestionID string) string { + return fmt.Sprintf("%s,%s,%s", awsAccountID, dataSetID, ingestionID) +} + +type resourceIngestionData struct { + ARN types.String `tfsdk:"arn"` + AWSAccountID types.String `tfsdk:"aws_account_id"` + DataSetID types.String `tfsdk:"data_set_id"` + ID types.String `tfsdk:"id"` + IngestionID types.String `tfsdk:"ingestion_id"` + IngestionStatus types.String `tfsdk:"ingestion_status"` + IngestionType types.String `tfsdk:"ingestion_type"` +} diff --git a/internal/service/quicksight/service_package_gen.go b/internal/service/quicksight/service_package_gen.go index c62dfd70b270..c18add397ffb 100644 --- a/internal/service/quicksight/service_package_gen.go +++ b/internal/service/quicksight/service_package_gen.go @@ -16,7 +16,11 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceIngestion, + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { From 3bcf3c9fa9fd8759d64c8517a6c0e4d6f0bcfe03 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 5 Apr 2023 15:20:20 -0400 Subject: [PATCH 2/4] r/aws_quicksight_ingestion: tests --- internal/service/quicksight/ingestion_test.go | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 internal/service/quicksight/ingestion_test.go diff --git a/internal/service/quicksight/ingestion_test.go b/internal/service/quicksight/ingestion_test.go new file mode 100644 index 000000000000..c922722bd47e --- /dev/null +++ b/internal/service/quicksight/ingestion_test.go @@ -0,0 +1,180 @@ +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 TestAccQuickSightIngestion_basic(t *testing.T) { + ctx := acctest.Context(t) + var ingestion quicksight.Ingestion + dataSetName := "aws_quicksight_data_set.test" + resourceName := "aws_quicksight_ingestion.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: testAccCheckIngestionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIngestionConfig_basic(rId, rName, quicksight.IngestionTypeFullRefresh), + Check: resource.ComposeTestCheckFunc( + testAccCheckIngestionExists(ctx, resourceName, &ingestion), + resource.TestCheckResourceAttr(resourceName, "ingestion_id", rId), + resource.TestCheckResourceAttr(resourceName, "ingestion_type", quicksight.IngestionTypeFullRefresh), + resource.TestCheckResourceAttrPair(resourceName, "data_set_id", dataSetName, "data_set_id"), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "quicksight", fmt.Sprintf("dataset/%[1]s/ingestion/%[1]s", rId)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "ingestion_type", + }, + }, + }, + }) +} + +// NOTE: There is no base _disappears test for this resource. Ingestions +// persist for the life of the parent data set, even if cancelled, so +// disappearance of this upstream resource is tested instead. +func TestAccQuickSightIngestion_disappears_dataSet(t *testing.T) { + ctx := acctest.Context(t) + var ingestion quicksight.Ingestion + dataSetName := "aws_quicksight_data_set.test" + resourceName := "aws_quicksight_ingestion.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: testAccCheckIngestionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIngestionConfig_basic(rId, rName, quicksight.IngestionTypeFullRefresh), + Check: resource.ComposeTestCheckFunc( + testAccCheckIngestionExists(ctx, resourceName, &ingestion), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfquicksight.ResourceDataSet(), dataSetName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckIngestionExists(ctx context.Context, resourceName string, ingestion *quicksight.Ingestion) 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.FindIngestionByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.QuickSight, create.ErrActionCheckingExistence, tfquicksight.ResNameIngestion, rs.Primary.ID, err) + } + + *ingestion = *output + + return nil + } +} + +func testAccCheckIngestionDestroy(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_ingestion" { + continue + } + + output, err := tfquicksight.FindIngestionByID(ctx, conn, rs.Primary.ID) + if err != nil { + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return nil + } + return err + } + + if output != nil && !isDestroyedStatus(aws.StringValue(output.IngestionStatus)) { + return create.Error(names.QuickSight, create.ErrActionCheckingDestroyed, tfquicksight.ResNameIngestion, rs.Primary.ID, err) + } + } + + return nil + } +} + +func isDestroyedStatus(status string) bool { + targetStatuses := []string{ + quicksight.IngestionStatusCancelled, + quicksight.IngestionStatusCompleted, + quicksight.IngestionStatusFailed, + } + for _, target := range targetStatuses { + if status == target { + return true + } + } + return false +} + +func testAccIngestionConfigBase(rId, rName string) string { + return acctest.ConfigCompose( + testAccDataSetConfigBase(rId, rName), + fmt.Sprintf(` +resource "aws_quicksight_data_set" "test" { + data_set_id = %[1]q + name = %[2]q + import_mode = "SPICE" + + physical_table_map { + physical_table_map_id = %[1]q + s3_source { + data_source_arn = aws_quicksight_data_source.test.arn + input_columns { + name = "Column1" + type = "STRING" + } + upload_settings { + format = "JSON" + } + } + } +} +`, rId, rName)) +} + +func testAccIngestionConfig_basic(rId, rName, ingestionType string) string { + return acctest.ConfigCompose( + testAccIngestionConfigBase(rId, rName), + fmt.Sprintf(` +resource "aws_quicksight_ingestion" "test" { + data_set_id = aws_quicksight_data_set.test.data_set_id + ingestion_id = %[1]q + ingestion_type = %[3]q +} +`, rId, rName, ingestionType)) +} From 6af6c2575c330c161272aa25983671ef89e30b4b Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 5 Apr 2023 15:37:17 -0400 Subject: [PATCH 3/4] r/aws_quicksight_ingestion: docs --- .../docs/r/quicksight_ingestion.html.markdown | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 website/docs/r/quicksight_ingestion.html.markdown diff --git a/website/docs/r/quicksight_ingestion.html.markdown b/website/docs/r/quicksight_ingestion.html.markdown new file mode 100644 index 000000000000..072f3ee261df --- /dev/null +++ b/website/docs/r/quicksight_ingestion.html.markdown @@ -0,0 +1,51 @@ +--- +subcategory: "QuickSight" +layout: "aws" +page_title: "AWS: aws_quicksight_ingestion" +description: |- + Terraform resource for managing an AWS QuickSight Ingestion. +--- + +# Resource: aws_quicksight_ingestion + +Terraform resource for managing an AWS QuickSight Ingestion. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_quicksight_ingestion" "example" { + data_set_id = aws_quicksight_data_set.example.data_set_id + ingestion_id = "example-id" + ingestion_type = "FULL_REFRESH" +} +``` + +## Argument Reference + +The following arguments are required: + +* `data_set_id` - (Required) ID of the dataset used in the ingestion. +* `ingestion_id` - (Required) ID for the ingestion. +* `ingestion_type` - (Required) Type of ingestion to be created. Valid values are `INCREMENTAL_REFRESH` and `FULL_REFRESH`. + +The following arguments are optional: + +* `aws_account_id` - (Optional) AWS account ID. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the Ingestion. +* `id` - A comma-delimited string joining AWS account ID, data set ID, and ingestion ID. +* `ingestion_status` - Ingestion status. + +## Import + +QuickSight Ingestion can be imported using the AWS account ID, data set ID, and ingestion ID separated by commas (`,`) e.g., + +``` +$ terraform import aws_quicksight_ingestion.example 123456789012,example-dataset-id,example-ingestion-id +``` From 620e5165a497b257b8fffbf8e10f3e7b19683d6e Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Wed, 5 Apr 2023 15:48:23 -0400 Subject: [PATCH 4/4] chore: changelog --- .changelog/30487.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/30487.txt diff --git a/.changelog/30487.txt b/.changelog/30487.txt new file mode 100644 index 000000000000..0381de7d8edb --- /dev/null +++ b/.changelog/30487.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_quicksight_ingestion +```