diff --git a/.changelog/30744.txt b/.changelog/30744.txt new file mode 100644 index 000000000000..571d12399913 --- /dev/null +++ b/.changelog/30744.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +aws_quicksight_data_set: Add support for configuring refresh properties +``` \ No newline at end of file diff --git a/internal/service/quicksight/data_set.go b/internal/service/quicksight/data_set.go index 52cef3524e97..0f913d52b965 100644 --- a/internal/service/quicksight/data_set.go +++ b/internal/service/quicksight/data_set.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/quicksight" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" @@ -279,10 +280,68 @@ func ResourceDataSet() *schema.Resource { }, }, }, + "refresh_properties": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "refresh_configuration": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "incremental_refresh": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "lookback_window": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "column_name": { + Type: schema.TypeString, + Required: true, + }, + "size": { + Type: schema.TypeInt, + Required: true, + }, + "size_unit": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(quicksight.LookbackWindowSizeUnit_Values(), false), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), }, - CustomizeDiff: verify.SetTagsDiff, + CustomizeDiff: customdiff.All( + func(_ context.Context, diff *schema.ResourceDiff, _ interface{}) error { + mode := diff.Get("import_mode").(string) + if v, ok := diff.Get("refresh_properties").([]interface{}); ok && v != nil && len(v) > 0 && mode == "DIRECT_QUERY" { + return fmt.Errorf("refresh_properties cannot be set when import_mode is 'DIRECT_QUERY'") + } + return nil + }, + verify.SetTagsDiff, + ), } } @@ -809,6 +868,19 @@ func resourceDataSetCreate(ctx context.Context, d *schema.ResourceData, meta int return diag.Errorf("error creating QuickSight Data Set: %s", err) } + if v, ok := d.GetOk("refresh_properties"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input := &quicksight.PutDataSetRefreshPropertiesInput{ + AwsAccountId: aws.String(awsAccountId), + DataSetId: aws.String(dataSetID), + DataSetRefreshProperties: expandDataSetRefreshProperties(v.([]interface{})), + } + + _, err := conn.PutDataSetRefreshPropertiesWithContext(ctx, input) + if err != nil { + return diag.Errorf("error putting QuickSight Data Set Refresh Properties: %s", err) + } + } + return resourceDataSetRead(ctx, d, meta) } @@ -893,13 +965,29 @@ func resourceDataSetRead(ctx context.Context, d *schema.ResourceData, meta inter if err := d.Set("permissions", flattenPermissions(permsResp.Permissions)); err != nil { return diag.Errorf("error setting permissions: %s", err) } + + propsResp, err := conn.DescribeDataSetRefreshPropertiesWithContext(ctx, &quicksight.DescribeDataSetRefreshPropertiesInput{ + AwsAccountId: aws.String(awsAccountId), + DataSetId: aws.String(dataSetId), + }) + + if err != nil && !tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return diag.Errorf("error describing refresh Properties (%s): %s", d.Id(), err) + } + + if err == nil { + if err := d.Set("refresh_properties", flattenRefreshProperties(propsResp.DataSetRefreshProperties)); err != nil { + return diag.Errorf("error setting refresh properties: %s", err) + } + } + return nil } func resourceDataSetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).QuickSightConn() - if d.HasChangesExcept("permissions", "tags", "tags_all") { + if d.HasChangesExcept("permissions", "tags", "tags_all", "refresh_properties") { awsAccountId, dataSetId, err := ParseDataSetID(d.Id()) if err != nil { return diag.FromErr(err) @@ -979,6 +1067,35 @@ func resourceDataSetUpdate(ctx context.Context, d *schema.ResourceData, meta int } } + if d.HasChange("refresh_properties") { + awsAccountId, dataSetId, err := ParseDataSetID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + oldraw, newraw := d.GetChange("refresh_properties") + old := oldraw.([]interface{}) + new := newraw.([]interface{}) + if len(old) == 1 && len(new) == 0 { + _, err := conn.DeleteDataSetRefreshPropertiesWithContext(ctx, &quicksight.DeleteDataSetRefreshPropertiesInput{ + AwsAccountId: aws.String(awsAccountId), + DataSetId: aws.String(dataSetId), + }) + if err != nil { + return diag.Errorf("error deleting QuickSight Data Set Refresh Properties (%s): %s", d.Id(), err) + } + } else { + _, err = conn.PutDataSetRefreshPropertiesWithContext(ctx, &quicksight.PutDataSetRefreshPropertiesInput{ + AwsAccountId: aws.String(awsAccountId), + DataSetId: aws.String(dataSetId), + DataSetRefreshProperties: expandDataSetRefreshProperties(d.Get("refresh_properties").([]interface{})), + }) + if err != nil { + return diag.Errorf("error updating QuickSight Data Set Refresh Properties (%s): %s", d.Id(), err) + } + } + } + return resourceDataSetRead(ctx, d, meta) } @@ -1730,6 +1847,76 @@ func expandDataSetRowLevelPermissionTagConfigurations(tfList []interface{}) *qui return rowLevelPermissionTagConfiguration } +func expandDataSetRefreshProperties(tfList []interface{}) *quicksight.DataSetRefreshProperties { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } + props := &quicksight.DataSetRefreshProperties{} + if v, ok := tfMap["refresh_configuration"].([]interface{}); ok { + props.RefreshConfiguration = expandDataSetRefreshConfiguration(v) + } + return props +} + +func expandDataSetRefreshConfiguration(tfList []interface{}) *quicksight.RefreshConfiguration { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } + config := &quicksight.RefreshConfiguration{} + if v, ok := tfMap["incremental_refresh"].([]interface{}); ok { + config.IncrementalRefresh = expandIncrementalRefresh(v) + } + return config +} + +func expandIncrementalRefresh(tfList []interface{}) *quicksight.IncrementalRefresh { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } + refresh := &quicksight.IncrementalRefresh{} + if v, ok := tfMap["lookback_window"].([]interface{}); ok { + refresh.LookbackWindow = expandLookbackWindow(v) + } + return refresh +} + +func expandLookbackWindow(tfList []interface{}) *quicksight.LookbackWindow { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } + window := &quicksight.LookbackWindow{} + if v, ok := tfMap["column_name"].(string); ok { + window.ColumnName = aws.String(v) + } + if v, ok := tfMap["size"].(int); ok { + window.Size = aws.Int64(int64(v)) + } + if v, ok := tfMap["size_unit"].(string); ok { + window.SizeUnit = aws.String(v) + } + return window +} + func expandDataSetTagRules(tfList []interface{}) []*quicksight.RowLevelPermissionTagRule { if len(tfList) == 0 { return nil @@ -2384,6 +2571,64 @@ func flattenRowLevelPermissionTagConfiguration(apiObject *quicksight.RowLevelPer return []interface{}{tfMap} } +func flattenRefreshProperties(apiObject *quicksight.DataSetRefreshProperties) interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + if apiObject.RefreshConfiguration != nil { + tfMap["refresh_configuration"] = flattenRefreshConfiguration(apiObject.RefreshConfiguration) + } + + return []interface{}{tfMap} +} + +func flattenRefreshConfiguration(apiObject *quicksight.RefreshConfiguration) interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + if apiObject.IncrementalRefresh != nil { + tfMap["incremental_refresh"] = flattenIncrementalRefresh(apiObject.IncrementalRefresh) + } + + return []interface{}{tfMap} +} + +func flattenIncrementalRefresh(apiObject *quicksight.IncrementalRefresh) interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + if apiObject.LookbackWindow != nil { + tfMap["lookback_window"] = flattenLookbackWindow(apiObject.LookbackWindow) + } + + return []interface{}{tfMap} +} + +func flattenLookbackWindow(apiObject *quicksight.LookbackWindow) interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + if apiObject.ColumnName != nil { + tfMap["column_name"] = aws.StringValue(apiObject.ColumnName) + } + if apiObject.Size != nil { + tfMap["size"] = aws.Int64Value(apiObject.Size) + } + if apiObject.SizeUnit != nil { + tfMap["size_unit"] = aws.StringValue(apiObject.SizeUnit) + } + + return []interface{}{tfMap} +} + func flattenTagRules(apiObject []*quicksight.RowLevelPermissionTagRule) []interface{} { if len(apiObject) == 0 { return nil diff --git a/internal/service/quicksight/data_set_test.go b/internal/service/quicksight/data_set_test.go index 244d9c8272fa..3a36c2e0c4e5 100644 --- a/internal/service/quicksight/data_set_test.go +++ b/internal/service/quicksight/data_set_test.go @@ -340,6 +340,41 @@ func TestAccQuickSightDataSet_rowLevelPermissionTagConfiguration(t *testing.T) { }) } +func TestAccQuickSightDataSet_refreshProperties(t *testing.T) { + ctx := acctest.Context(t) + var dataSet quicksight.DataSet + resourceName := "aws_quicksight_data_set.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: testAccCheckDataSetDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccDataSetConfigRefreshProperties(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataSetExists(ctx, resourceName, &dataSet), + resource.TestCheckResourceAttr(resourceName, "refresh_properties.#", "1"), + resource.TestCheckResourceAttr(resourceName, "refresh_properties.0.refresh_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "refresh_properties.0.refresh_configuration.0.incremental_refresh.#", "1"), + resource.TestCheckResourceAttr(resourceName, "refresh_properties.0.refresh_configuration.0.incremental_refresh.0.lookback_window.#", "1"), + resource.TestCheckResourceAttr(resourceName, "refresh_properties.0.refresh_configuration.0.incremental_refresh.0.lookback_window.0.column_name", "column1"), + resource.TestCheckResourceAttr(resourceName, "refresh_properties.0.refresh_configuration.0.incremental_refresh.0.lookback_window.0.size", "1"), + resource.TestCheckResourceAttr(resourceName, "refresh_properties.0.refresh_configuration.0.incremental_refresh.0.lookback_window.0.size_unit", "DAY"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccQuickSightDataSet_tags(t *testing.T) { ctx := acctest.Context(t) var dataSet quicksight.DataSet @@ -787,6 +822,92 @@ resource "aws_quicksight_data_set" "test" { `, rId, rName)) } +func testAccDataSetConfigRefreshProperties(rId, rName string) string { + // NOTE: Must use Athena data source here as incremental refresh is not supported by S3 + return acctest.ConfigCompose( + testAccBaseDataSourceConfig(rName), + fmt.Sprintf(` + +resource "aws_glue_catalog_database" "test" { + name = %[2]q +} + +resource "aws_glue_catalog_table" "test" { + name = %[2]q + database_name = aws_glue_catalog_database.test.name + table_type = "EXTERNAL_TABLE" + + parameters = { + EXTERNAL = "TRUE" + classification = "json" + } + + storage_descriptor { + location = "s3://${aws_s3_bucket.test.id}/data/" + input_format = "org.apache.hadoop.mapred.TextInputFormat" + output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat" + + ser_de_info { + name = "jsonserde" + serialization_library = "org.openx.data.jsonserde.JsonSerDe" + parameters = { + "serialization.format" = "1" + } + } + columns { + name = "column1" + type = "date" + } + } +} + +resource "aws_quicksight_data_source" "test" { + data_source_id = %[1]q + name = %[2]q + type = "ATHENA" + parameters { + athena { + work_group = "primary" + } + } + ssl_properties { + disable_ssl = false + } +} + +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 + relational_table { + data_source_arn = aws_quicksight_data_source.test.arn + catalog = "AwsDataCatalog" + schema = aws_glue_catalog_database.test.name + name = aws_glue_catalog_table.test.name + input_columns { + name = "column1" + type = "DATETIME" + } + } + } + refresh_properties { + refresh_configuration { + incremental_refresh { + lookback_window { + column_name = "column1" + size = 1 + size_unit = "DAY" + } + } + } + } +} +`, rId, rName)) +} + func testAccDataSetConfigTags1(rId, rName, key1, value1 string) string { return acctest.ConfigCompose( testAccDataSetConfigBase(rId, rName), diff --git a/website/docs/r/quicksight_data_set.html.markdown b/website/docs/r/quicksight_data_set.html.markdown index 22c381e09064..b24adc6f6d8a 100644 --- a/website/docs/r/quicksight_data_set.html.markdown +++ b/website/docs/r/quicksight_data_set.html.markdown @@ -180,6 +180,7 @@ The following arguments are optional: * `permissions` - (Optional) A set of resource permissions on the data source. Maximum of 64 items. See [permissions](#permissions). * `row_level_permission_data_set` - (Optional) The row-level security configuration for the data that you want to create. See [row_level_permission_data_set](#row_level_permission_data_set). * `row_level_permission_tag_configuration` - (Optional) The configuration of tags on a dataset to set row-level security. Row-level security tags are currently supported for anonymous embedding only. See [row_level_permission_tag_configuration](#row_level_permission_tag_configuration). +* `refresh_properties` - (Optional) The refresh properties for the data set. **NOTE**: Only valid when `import_mode` is set to `SPICE`. See [refresh_properties](#refresh_properties). * `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ### physical_table_map @@ -362,6 +363,24 @@ For a `physical_table_map` item to be valid, only one of `custom_sql`, `relation * `tag_rules` - (Required) A set of rules associated with row-level security, such as the tag names and columns that they are assigned to. See [tag_rules](#tag_rules). * `status` - (Optional) The status of row-level security tags. If enabled, the status is `ENABLED`. If disabled, the status is `DISABLED`. +### refresh_properties + +* `refresh_configuration` - (Required) The refresh configuration for the data set. See [refresh_configuration](#refresh_configuration). + +### refresh_configuration + +* `incremental_refresh` - (Required) The incremental refresh for the data set. See [incremental_refresh](#incremental_refresh). + +### incremental_refresh + +* `lookback_window` - (Required) The lookback window setup for an incremental refresh configuration. See [lookback_window](#lookback_window). + +### lookback_window + +* `column_name` - (Required) The name of the lookback window column. +* `size` - (Required) The lookback window column size. +* `size_unit` - (Required) The size unit that is used for the lookback window column. Valid values for this structure are `HOUR`, `DAY`, and `WEEK`. + ### tag_rules * `column_name` - (Required) Column name that a tag key is assigned to.