diff --git a/.changelog/30400.txt b/.changelog/30400.txt new file mode 100644 index 000000000000..954e54e181d4 --- /dev/null +++ b/.changelog/30400.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_quicksight_folder +``` \ No newline at end of file diff --git a/internal/service/quicksight/data_set.go b/internal/service/quicksight/data_set.go index f64a6aa3fa26..19d0f3805235 100644 --- a/internal/service/quicksight/data_set.go +++ b/internal/service/quicksight/data_set.go @@ -280,6 +280,7 @@ func ResourceDataSet() *schema.Resource { "tags": tftags.TagsSchema(), "tags_all": tftags.TagsSchemaComputed(), }, + CustomizeDiff: verify.SetTagsDiff, } } @@ -795,7 +796,7 @@ func resourceDataSetCreate(ctx context.Context, d *schema.ResourceData, meta int } if v, ok := d.GetOk("permissions"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - input.Permissions = expandDataSetPermissions(v.([]interface{})) + input.Permissions = expandResourcePermissions(v.([]interface{})) } if v, ok := d.GetOk("row_level_permission_data_set"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { @@ -1710,22 +1711,6 @@ func expandDataSetUploadSettings(tfMap map[string]interface{}) *quicksight.Uploa return uploadSettings } -func expandDataSetPermissions(tfList []interface{}) []*quicksight.ResourcePermission { - permissions := make([]*quicksight.ResourcePermission, len(tfList)) - - for i, tfListRaw := range tfList { - tfMap := tfListRaw.(map[string]interface{}) - - permission := &quicksight.ResourcePermission{ - Actions: flex.ExpandStringSet(tfMap["actions"].(*schema.Set)), - Principal: aws.String(tfMap["principal"].(string)), - } - - permissions[i] = permission - } - return permissions -} - func expandDataSetRowLevelPermissionDataSet(tfList []interface{}) *quicksight.RowLevelPermissionDataSet { if len(tfList) == 0 || tfList[0] == nil { return nil diff --git a/internal/service/quicksight/data_set_test.go b/internal/service/quicksight/data_set_test.go index e8d80622f880..244d9c8272fa 100644 --- a/internal/service/quicksight/data_set_test.go +++ b/internal/service/quicksight/data_set_test.go @@ -354,7 +354,7 @@ func TestAccQuickSightDataSet_tags(t *testing.T) { CheckDestroy: testAccCheckDataSetDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccDataSetConfigTags(rId, rName, "key1", "value1"), + Config: testAccDataSetConfigTags1(rId, rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckDataSetExists(ctx, resourceName, &dataSet), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -366,6 +366,23 @@ func TestAccQuickSightDataSet_tags(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + Config: testAccDataSetConfigTags2(rId, rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataSetExists(ctx, resourceName, &dataSet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccDataSetConfigTags1(rId, rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataSetExists(ctx, resourceName, &dataSet), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, }, }) } @@ -770,7 +787,36 @@ resource "aws_quicksight_data_set" "test" { `, rId, rName)) } -func testAccDataSetConfigTags(rId, rName, key, value string) string { +func testAccDataSetConfigTags1(rId, rName, key1, value1 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" + } + } + } + tags = { + %[3]q = %[4]q + } +} +`, rId, rName, key1, value1)) +} + +func testAccDataSetConfigTags2(rId, rName, key1, value1, key2, value2 string) string { return acctest.ConfigCompose( testAccDataSetConfigBase(rId, rName), fmt.Sprintf(` @@ -794,7 +840,8 @@ resource "aws_quicksight_data_set" "test" { } tags = { %[3]q = %[4]q + %[5]q = %[6]q } } -`, rId, rName, key, value)) +`, rId, rName, key1, value1, key2, value2)) } diff --git a/internal/service/quicksight/data_source.go b/internal/service/quicksight/data_source.go index f224d7a10980..996817ca06d9 100644 --- a/internal/service/quicksight/data_source.go +++ b/internal/service/quicksight/data_source.go @@ -13,7 +13,6 @@ import ( "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" - "github.com/hashicorp/terraform-provider-aws/internal/flex" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) @@ -632,7 +631,7 @@ func resourceDataSourceCreate(ctx context.Context, d *schema.ResourceData, meta } if v, ok := d.GetOk("permission"); ok && v.(*schema.Set).Len() > 0 { - params.Permissions = expandDataSourcePermissions(v.(*schema.Set).List()) + params.Permissions = expandResourcePermissions(v.(*schema.Set).List()) } if v, ok := d.GetOk("ssl_properties"); ok && len(v.([]interface{})) != 0 && v.([]interface{})[0] != nil { @@ -1274,22 +1273,6 @@ func expandDataSourceParameters(tfList []interface{}) *quicksight.DataSourcePara return dataSourceParams } -func expandDataSourcePermissions(tfList []interface{}) []*quicksight.ResourcePermission { - permissions := make([]*quicksight.ResourcePermission, len(tfList)) - - for i, tfListRaw := range tfList { - tfMap := tfListRaw.(map[string]interface{}) - permission := &quicksight.ResourcePermission{ - Actions: flex.ExpandStringSet(tfMap["actions"].(*schema.Set)), - Principal: aws.String(tfMap["principal"].(string)), - } - - permissions[i] = permission - } - - return permissions -} - func expandDataSourceSSLProperties(tfList []interface{}) *quicksight.SslProperties { if len(tfList) == 0 { return nil @@ -1570,34 +1553,6 @@ func flattenParameters(parameters *quicksight.DataSourceParameters) []interface{ return params } -func flattenPermissions(perms []*quicksight.ResourcePermission) []interface{} { - if len(perms) == 0 { - return []interface{}{} - } - - values := make([]interface{}, 0) - - for _, p := range perms { - if p == nil { - continue - } - - perm := make(map[string]interface{}) - - if p.Principal != nil { - perm["principal"] = aws.StringValue(p.Principal) - } - - if p.Actions != nil { - perm["actions"] = flex.FlattenStringList(p.Actions) - } - - values = append(values, perm) - } - - return values -} - func flattenSSLProperties(props *quicksight.SslProperties) []interface{} { if props == nil { return []interface{}{} diff --git a/internal/service/quicksight/data_source_test.go b/internal/service/quicksight/data_source_test.go index 7508f390705f..642cd0ca0662 100644 --- a/internal/service/quicksight/data_source_test.go +++ b/internal/service/quicksight/data_source_test.go @@ -3,7 +3,6 @@ package quicksight_test import ( "context" "fmt" - "reflect" "regexp" "testing" @@ -12,224 +11,12 @@ import ( "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/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfquicksight "github.com/hashicorp/terraform-provider-aws/internal/service/quicksight" ) -func TestDataSourcePermissionsDiff(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - oldPermissions []interface{} - newPermissions []interface{} - expectedGrants []*quicksight.ResourcePermission - expectedRevokes []*quicksight.ResourcePermission - }{ - { - name: "no changes;empty", - oldPermissions: []interface{}{}, - newPermissions: []interface{}{}, - expectedGrants: nil, - expectedRevokes: nil, - }, - { - name: "no changes;same", - oldPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action2", - }), - }, - }, - newPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action2", - }), - }}, - - expectedGrants: nil, - expectedRevokes: nil, - }, - { - name: "grant only", - oldPermissions: []interface{}{}, - newPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action2", - }), - }, - }, - expectedGrants: []*quicksight.ResourcePermission{ - { - Actions: aws.StringSlice([]string{"action1", "action2"}), - Principal: aws.String("principal1"), - }, - }, - expectedRevokes: nil, - }, - { - name: "revoke only", - oldPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action2", - }), - }, - }, - newPermissions: []interface{}{}, - expectedGrants: nil, - expectedRevokes: []*quicksight.ResourcePermission{ - { - Actions: aws.StringSlice([]string{"action1", "action2"}), - Principal: aws.String("principal1"), - }, - }, - }, - { - name: "grant new action", - oldPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - }), - }, - }, - newPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action2", - }), - }, - }, - expectedGrants: []*quicksight.ResourcePermission{ - { - Actions: aws.StringSlice([]string{"action2"}), - Principal: aws.String("principal1"), - }, - }, - expectedRevokes: nil, - }, - { - name: "revoke old action", - oldPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "oldAction", - "onlyOldAction", - }), - }, - }, - newPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "oldAction", - }), - }, - }, - expectedGrants: nil, - expectedRevokes: []*quicksight.ResourcePermission{ - { - Actions: aws.StringSlice([]string{"onlyOldAction"}), - Principal: aws.String("principal1"), - }, - }, - }, - { - name: "multiple permissions", - oldPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action2", - }), - }, - map[string]interface{}{ - "principal": "principal2", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action3", - "action4", - }), - }, - map[string]interface{}{ - "principal": "principal3", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action5", - }), - }, - }, - newPermissions: []interface{}{ - map[string]interface{}{ - "principal": "principal1", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action1", - "action2", - }), - }, - map[string]interface{}{ - "principal": "principal2", - "actions": schema.NewSet(schema.HashString, []interface{}{ - "action3", - "action5", - }), - }, - }, - expectedGrants: []*quicksight.ResourcePermission{ - { - Actions: aws.StringSlice([]string{"action5"}), - Principal: aws.String("principal2"), - }, - }, - expectedRevokes: []*quicksight.ResourcePermission{ - { - Actions: aws.StringSlice([]string{"action1", "action4"}), - Principal: aws.String("principal2"), - }, - { - Actions: aws.StringSlice([]string{"action5"}), - Principal: aws.String("principal3"), - }, - }, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - toGrant, toRevoke := tfquicksight.DiffPermissions(testCase.oldPermissions, testCase.newPermissions) - if !reflect.DeepEqual(toGrant, testCase.expectedGrants) { - t.Fatalf("Expected: %v, got: %v", testCase.expectedGrants, toGrant) - } - - if !reflect.DeepEqual(toRevoke, testCase.expectedRevokes) { - t.Fatalf("Expected: %v, got: %v", testCase.expectedRevokes, toRevoke) - } - }) - } -} - func TestAccQuickSightDataSource_basic(t *testing.T) { ctx := acctest.Context(t) var dataSource quicksight.DataSource diff --git a/internal/service/quicksight/flex.go b/internal/service/quicksight/flex.go index 7a1039836e72..220b2c922961 100644 --- a/internal/service/quicksight/flex.go +++ b/internal/service/quicksight/flex.go @@ -3,12 +3,29 @@ package quicksight import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/quicksight" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/flex" ) +func expandResourcePermissions(tfList []interface{}) []*quicksight.ResourcePermission { + permissions := make([]*quicksight.ResourcePermission, len(tfList)) + + for i, tfListRaw := range tfList { + tfMap := tfListRaw.(map[string]interface{}) + + permission := &quicksight.ResourcePermission{ + Actions: flex.ExpandStringSet(tfMap["actions"].(*schema.Set)), + Principal: aws.String(tfMap["principal"].(string)), + } + + permissions[i] = permission + } + return permissions +} + func DiffPermissions(o, n []interface{}) ([]*quicksight.ResourcePermission, []*quicksight.ResourcePermission) { - old := expandDataSourcePermissions(o) - new := expandDataSourcePermissions(n) + old := expandResourcePermissions(o) + new := expandResourcePermissions(n) var toGrant, toRevoke []*quicksight.ResourcePermission @@ -29,7 +46,6 @@ func DiffPermissions(o, n []interface{}) ([]*quicksight.ResourcePermission, []*q } toRemove := oldActions.Difference(newActions) - toAdd := newActions.Difference(oldActions) if toRemove.Len() > 0 { toRevoke = append(toRevoke, &quicksight.ResourcePermission{ @@ -38,9 +54,9 @@ func DiffPermissions(o, n []interface{}) ([]*quicksight.ResourcePermission, []*q }) } - if toAdd.Len() > 0 { + if newActions.Len() > 0 { toGrant = append(toGrant, &quicksight.ResourcePermission{ - Actions: flex.ExpandStringSet(toAdd), + Actions: flex.ExpandStringSet(newActions), Principal: np.Principal, }) } @@ -68,3 +84,31 @@ func DiffPermissions(o, n []interface{}) ([]*quicksight.ResourcePermission, []*q return toGrant, toRevoke } + +func flattenPermissions(perms []*quicksight.ResourcePermission) []interface{} { + if len(perms) == 0 { + return []interface{}{} + } + + values := make([]interface{}, 0) + + for _, p := range perms { + if p == nil { + continue + } + + perm := make(map[string]interface{}) + + if p.Principal != nil { + perm["principal"] = aws.StringValue(p.Principal) + } + + if p.Actions != nil { + perm["actions"] = flex.FlattenStringList(p.Actions) + } + + values = append(values, perm) + } + + return values +} diff --git a/internal/service/quicksight/flex_test.go b/internal/service/quicksight/flex_test.go new file mode 100644 index 000000000000..8d90740766bc --- /dev/null +++ b/internal/service/quicksight/flex_test.go @@ -0,0 +1,227 @@ +package quicksight_test + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/quicksight" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tfquicksight "github.com/hashicorp/terraform-provider-aws/internal/service/quicksight" +) + +func TestDataSourcePermissionsDiff(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + oldPermissions []interface{} + newPermissions []interface{} + expectedGrants []*quicksight.ResourcePermission + expectedRevokes []*quicksight.ResourcePermission + }{ + { + name: "no changes;empty", + oldPermissions: []interface{}{}, + newPermissions: []interface{}{}, + expectedGrants: nil, + expectedRevokes: nil, + }, + { + name: "no changes;same", + oldPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action2", + }), + }, + }, + newPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action2", + }), + }}, + + expectedGrants: nil, + expectedRevokes: nil, + }, + { + name: "grant only", + oldPermissions: []interface{}{}, + newPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action2", + }), + }, + }, + expectedGrants: []*quicksight.ResourcePermission{ + { + Actions: aws.StringSlice([]string{"action1", "action2"}), + Principal: aws.String("principal1"), + }, + }, + expectedRevokes: nil, + }, + { + name: "revoke only", + oldPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action2", + }), + }, + }, + newPermissions: []interface{}{}, + expectedGrants: nil, + expectedRevokes: []*quicksight.ResourcePermission{ + { + Actions: aws.StringSlice([]string{"action1", "action2"}), + Principal: aws.String("principal1"), + }, + }, + }, + { + name: "grant new action", + oldPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + }), + }, + }, + newPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action2", + }), + }, + }, + expectedGrants: []*quicksight.ResourcePermission{ + { + Actions: aws.StringSlice([]string{"action1", "action2"}), + Principal: aws.String("principal1"), + }, + }, + expectedRevokes: nil, + }, + { + name: "revoke old action", + oldPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "oldAction", + "onlyOldAction", + }), + }, + }, + newPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "oldAction", + }), + }, + }, + expectedGrants: []*quicksight.ResourcePermission{ + { + Actions: aws.StringSlice([]string{"oldAction"}), + Principal: aws.String("principal1"), + }, + }, + expectedRevokes: []*quicksight.ResourcePermission{ + { + Actions: aws.StringSlice([]string{"onlyOldAction"}), + Principal: aws.String("principal1"), + }, + }, + }, + { + name: "multiple permissions", + oldPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action2", + }), + }, + map[string]interface{}{ + "principal": "principal2", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action3", + "action4", + }), + }, + map[string]interface{}{ + "principal": "principal3", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action5", + }), + }, + }, + newPermissions: []interface{}{ + map[string]interface{}{ + "principal": "principal1", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action1", + "action2", + }), + }, + map[string]interface{}{ + "principal": "principal2", + "actions": schema.NewSet(schema.HashString, []interface{}{ + "action3", + "action5", + }), + }, + }, + expectedGrants: []*quicksight.ResourcePermission{ + { + Actions: aws.StringSlice([]string{"action3", "action5"}), + Principal: aws.String("principal2"), + }, + }, + expectedRevokes: []*quicksight.ResourcePermission{ + { + Actions: aws.StringSlice([]string{"action1", "action4"}), + Principal: aws.String("principal2"), + }, + { + Actions: aws.StringSlice([]string{"action5"}), + Principal: aws.String("principal3"), + }, + }, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + toGrant, toRevoke := tfquicksight.DiffPermissions(testCase.oldPermissions, testCase.newPermissions) + if !reflect.DeepEqual(toGrant, testCase.expectedGrants) { + t.Fatalf("Expected: %v, got: %v", testCase.expectedGrants, toGrant) + } + + if !reflect.DeepEqual(toRevoke, testCase.expectedRevokes) { + t.Fatalf("Expected: %v, got: %v", testCase.expectedRevokes, toRevoke) + } + }) + } +} diff --git a/internal/service/quicksight/folder.go b/internal/service/quicksight/folder.go new file mode 100644 index 000000000000..cf8d9165561e --- /dev/null +++ b/internal/service/quicksight/folder.go @@ -0,0 +1,379 @@ +package quicksight + +import ( + "context" + "errors" + "fmt" + "log" + "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-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// Function annotations are used for resource registration to the Provider. DO NOT EDIT. +// @SDKResource("aws_quicksight_folder") +func ResourceFolder() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceFolderCreate, + ReadWithoutTimeout: resourceFolderRead, + UpdateWithoutTimeout: resourceFolderUpdate, + DeleteWithoutTimeout: resourceFolderDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "aws_account_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: verify.ValidAccountID, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "folder_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.NoZeroValues, + validation.StringLenBetween(1, 2048), + ), + }, + "folder_path": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "folder_type": { + Type: schema.TypeString, + Optional: true, + Default: quicksight.FolderTypeShared, + ValidateFunc: validation.StringInSlice(quicksight.FolderType_Values(), false), + }, + "last_updated_time": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.All( + validation.NoZeroValues, + validation.StringLenBetween(1, 200), + ), + }, + "parent_folder_arn": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + "permissions": { + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 64, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "actions": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 16, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "principal": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 256), + }, + }, + }, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + CustomizeDiff: verify.SetTagsDiff, + } +} + +const ( + ResNameFolder = "Folder" +) + +func resourceFolderCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).QuickSightConn() + + awsAccountId := meta.(*conns.AWSClient).AccountID + if v, ok := d.GetOk("aws_account_id"); ok { + awsAccountId = v.(string) + } + + folderId := d.Get("folder_id").(string) + + d.SetId(createFolderId(awsAccountId, folderId)) + + in := &quicksight.CreateFolderInput{ + AwsAccountId: aws.String(awsAccountId), + FolderId: aws.String(folderId), + Name: aws.String(d.Get("name").(string)), + } + + if v, ok := d.GetOk("folder_type"); ok { + in.FolderType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("parent_folder_arn"); ok { + in.ParentFolderArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("permissions"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + in.Permissions = expandResourcePermissions(v.([]interface{})) + } + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) + + if len(tags) > 0 { + in.Tags = Tags(tags.IgnoreAWS()) + } + + out, err := conn.CreateFolderWithContext(ctx, in) + if err != nil { + return create.DiagError(names.QuickSight, create.ErrActionCreating, ResNameFolder, d.Get("name").(string), err) + } + + if out == nil || out.Arn == nil { + return create.DiagError(names.QuickSight, create.ErrActionCreating, ResNameFolder, d.Get("name").(string), errors.New("empty output")) + } + + return resourceFolderRead(ctx, d, meta) +} + +func resourceFolderRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).QuickSightConn() + + awsAccountId, folderId, err := ParseFolderId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + out, err := FindFolderByID(ctx, conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] QuickSight Folder (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.QuickSight, create.ErrActionReading, ResNameFolder, d.Id(), err) + } + + d.Set("arn", out.Arn) + d.Set("aws_account_id", awsAccountId) + d.Set("created_time", out.CreatedTime.Format(time.RFC3339)) + d.Set("folder_id", out.FolderId) + d.Set("folder_type", out.FolderType) + d.Set("last_updated_time", out.LastUpdatedTime.Format(time.RFC3339)) + d.Set("name", out.Name) + + if len(out.FolderPath) > 0 { + d.Set("parent_folder_arn", out.FolderPath[0]) + } + + if err := d.Set("folder_path", flex.FlattenStringList(out.FolderPath)); err != nil { + return diag.Errorf("error setting folder_path: %s", err) + } + + tags, err := ListTags(ctx, conn, aws.StringValue(out.Arn)) + if err != nil { + return create.DiagError(names.QuickSight, create.ErrActionReading, ResNameFolder, d.Id(), err) + } + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return create.DiagError(names.QuickSight, create.ErrActionSetting, ResNameFolder, d.Id(), err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return create.DiagError(names.QuickSight, create.ErrActionSetting, ResNameFolder, d.Id(), err) + } + + permsResp, err := conn.DescribeFolderPermissionsWithContext(ctx, &quicksight.DescribeFolderPermissionsInput{ + AwsAccountId: aws.String(awsAccountId), + FolderId: aws.String(folderId), + }) + + if err != nil { + return diag.Errorf("error describing QuickSight Data Source (%s) Permissions: %s", d.Id(), err) + } + + if err := d.Set("permissions", flattenPermissions(permsResp.Permissions)); err != nil { + return diag.Errorf("error setting permissions: %s", err) + } + return nil +} + +func resourceFolderUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).QuickSightConn() + + awsAccountId, folderId, err := ParseFolderId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChangesExcept("permission", "tags", "tags_all") { + in := &quicksight.UpdateFolderInput{ + AwsAccountId: aws.String(awsAccountId), + FolderId: aws.String(folderId), + Name: aws.String(d.Get("name").(string)), + } + + log.Printf("[DEBUG] Updating QuickSight Folder (%s): %#v", d.Id(), in) + _, err = conn.UpdateFolderWithContext(ctx, in) + if err != nil { + return create.DiagError(names.QuickSight, create.ErrActionUpdating, ResNameFolder, d.Id(), err) + } + } + + if d.HasChange("permissions") { + oraw, nraw := d.GetChange("permissions") + o := oraw.([]interface{}) + n := nraw.([]interface{}) + + toGrant, toRevoke := DiffPermissions(o, n) + + params := &quicksight.UpdateFolderPermissionsInput{ + AwsAccountId: aws.String(awsAccountId), + FolderId: aws.String(folderId), + } + + if len(toGrant) > 0 { + params.GrantPermissions = toGrant + } + + if len(toRevoke) > 0 { + params.RevokePermissions = toRevoke + } + + _, err = conn.UpdateFolderPermissionsWithContext(ctx, params) + + if err != nil { + return diag.Errorf("error updating QuickSight Folder (%s) permissions: %s", folderId, err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTags(ctx, conn, d.Get("arn").(string), o, n); err != nil { + return diag.Errorf("error updating QuickSight Folder (%s) tags: %s", d.Id(), err) + } + } + + return resourceFolderRead(ctx, d, meta) +} + +func resourceFolderDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).QuickSightConn() + + log.Printf("[INFO] Deleting QuickSight Folder %s", d.Id()) + + awsAccountId, folderId, err := ParseFolderId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + _, err = conn.DeleteFolderWithContext(ctx, &quicksight.DeleteFolderInput{ + AwsAccountId: aws.String(awsAccountId), + FolderId: aws.String(folderId), + }) + + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return create.DiagError(names.QuickSight, create.ErrActionDeleting, ResNameFolder, d.Id(), err) + } + + return nil +} + +func FindFolderByID(ctx context.Context, conn *quicksight.QuickSight, id string) (*quicksight.Folder, error) { + awsAccountId, folderId, err := ParseFolderId(id) + if err != nil { + return nil, err + } + + descOpts := &quicksight.DescribeFolderInput{ + AwsAccountId: aws.String(awsAccountId), + FolderId: aws.String(folderId), + } + + out, err := conn.DescribeFolderWithContext(ctx, descOpts) + + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: descOpts, + } + } + + if err != nil { + return nil, err + } + + if out == nil || out.Folder == nil { + return nil, tfresource.NewEmptyResultError(descOpts) + } + + return out.Folder, nil +} + +func ParseFolderId(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,FOLDER_ID", id) + } + return parts[0], parts[1], nil +} + +func createFolderId(awsAccountID, folderId string) string { + return fmt.Sprintf("%s,%s", awsAccountID, folderId) +} diff --git a/internal/service/quicksight/folder_test.go b/internal/service/quicksight/folder_test.go new file mode 100644 index 000000000000..9c98bef1bd9a --- /dev/null +++ b/internal/service/quicksight/folder_test.go @@ -0,0 +1,398 @@ +package quicksight_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "testing" + + "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 TestAccQuickSightFolder_basic(t *testing.T) { + ctx := acctest.Context(t) + var folder quicksight.Folder + rId := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_quicksight_folder.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, quicksight.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, quicksight.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckFolderDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccFolderConfig_basic(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + resource.TestCheckResourceAttr(resourceName, "folder_id", rId), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "folder_type", quicksight.FolderTypeShared), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "quicksight", fmt.Sprintf("folder/%s", rId)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccQuickSightFolder_disappears(t *testing.T) { + ctx := acctest.Context(t) + var folder quicksight.Folder + rId := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_quicksight_folder.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, quicksight.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, quicksight.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckFolderDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccFolderConfig_basic(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfquicksight.ResourceFolder(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccQuickSightFolder_permissions(t *testing.T) { + ctx := acctest.Context(t) + var folder quicksight.Folder + resourceName := "aws_quicksight_folder.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: testAccCheckFolderDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccFolderConfig_permissions(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + resource.TestCheckResourceAttr(resourceName, "permissions.#", "1"), + resource.TestMatchTypeSetElemNestedAttrs(resourceName, "permissions.*", map[string]*regexp.Regexp{ + "principal": regexp.MustCompile(fmt.Sprintf(`user/default/%s`, rName)), + }), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:DescribeFolder"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccFolderConfig_permissionsUpdate(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + resource.TestCheckResourceAttr(resourceName, "permissions.#", "1"), + resource.TestMatchTypeSetElemNestedAttrs(resourceName, "permissions.*", map[string]*regexp.Regexp{ + "principal": regexp.MustCompile(fmt.Sprintf(`user/default/%s`, rName)), + }), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:CreateFolder"), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:DescribeFolder"), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:UpdateFolder"), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:DeleteFolder"), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:CreateFolderMembership"), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:DeleteFolderMembership"), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:DescribeFolderPermissions"), + resource.TestCheckTypeSetElemAttr(resourceName, "permissions.*.actions.*", "quicksight:UpdateFolderPermissions"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccFolderConfig_basic(rId, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + resource.TestCheckResourceAttr(resourceName, "permission.#", "0"), + ), + }, + }, + }) +} + +func TestAccQuickSightFolder_tags(t *testing.T) { + ctx := acctest.Context(t) + var folder quicksight.Folder + rId := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_quicksight_folder.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, quicksight.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, quicksight.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckFolderDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccFolderConfig_tags1(rId, rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccFolderConfig_tags2(rId, rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccFolderConfig_tags1(rId, rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccQuickSightFolder_parentFolder(t *testing.T) { + ctx := acctest.Context(t) + var folder quicksight.Folder + rId := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + parentId1 := rId + "-parent1" + parentName1 := rName + "-parent1" + parentId2 := rId + "-parent2" + parentName2 := rName + "-parent2" + resourceName := "aws_quicksight_folder.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, quicksight.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, quicksight.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckFolderDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccFolderConfig_parentFolder(rId, rName, parentId1, parentName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + acctest.CheckResourceAttrRegionalARN(resourceName, "parent_folder_arn", "quicksight", fmt.Sprintf("folder/%s", parentId1)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccFolderConfig_parentFolder(rId, rName, parentId2, parentName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckFolderExists(ctx, resourceName, &folder), + acctest.CheckResourceAttrRegionalARN(resourceName, "parent_folder_arn", "quicksight", fmt.Sprintf("folder/%s", parentId2)), + ), + }, + }, + }) +} + +func testAccCheckFolderDestroy(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_folder" { + continue + } + + output, err := tfquicksight.FindFolderByID(ctx, conn, rs.Primary.ID) + if err != nil { + if tfawserr.ErrCodeEquals(err, quicksight.ErrCodeResourceNotFoundException) { + return nil + } + return err + } + + if output != nil { + return fmt.Errorf("QuickSight Folder (%s) still exists", rs.Primary.ID) + } + } + + return nil + } +} + +func testAccCheckFolderExists(ctx context.Context, name string, folder *quicksight.Folder) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.QuickSight, create.ErrActionCheckingExistence, tfquicksight.ResNameFolder, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.QuickSight, create.ErrActionCheckingExistence, tfquicksight.ResNameFolder, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).QuickSightConn() + output, err := tfquicksight.FindFolderByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.QuickSight, create.ErrActionCheckingExistence, tfquicksight.ResNameFolder, rs.Primary.ID, err) + } + + *folder = *output + + return nil + } +} + +func testAccFolderConfig_basic(rId, rName string) string { + return fmt.Sprintf(` +resource "aws_quicksight_folder" "test" { + folder_id = %[1]q + name = %[2]q +} +`, rId, rName) +} + +func testAccFolderConfigUserBase(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_quicksight_user" "test" { + aws_account_id = data.aws_caller_identity.current.account_id + user_name = %[1]q + email = %[2]q + identity_type = "QUICKSIGHT" + user_role = "AUTHOR" + + lifecycle { + create_before_destroy = true + } +} +`, rName, acctest.DefaultEmailAddress) +} + +func testAccFolderConfig_permissions(rId, rName string) string { + return acctest.ConfigCompose( + testAccFolderConfigUserBase(rName), + fmt.Sprintf(` +resource "aws_quicksight_folder" "test" { + folder_id = %[1]q + name = %[2]q + permissions { + actions = [ + "quicksight:DescribeFolder", + ] + principal = aws_quicksight_user.test.arn + } +} +`, rId, rName)) +} + +func testAccFolderConfig_permissionsUpdate(rId, rName string) string { + return acctest.ConfigCompose( + testAccFolderConfigUserBase(rName), + fmt.Sprintf(` +resource "aws_quicksight_folder" "test" { + folder_id = %[1]q + name = %[2]q + permissions { + actions = [ + "quicksight:CreateFolder", + "quicksight:DescribeFolder", + "quicksight:UpdateFolder", + "quicksight:DeleteFolder", + "quicksight:CreateFolderMembership", + "quicksight:DeleteFolderMembership", + "quicksight:DescribeFolderPermissions", + "quicksight:UpdateFolderPermissions", + ] + principal = aws_quicksight_user.test.arn + } +} +`, rId, rName)) +} + +func testAccFolderConfig_tags1(rId, rName, key1, value1 string) string { + return fmt.Sprintf(` +resource "aws_quicksight_folder" "test" { + folder_id = %[1]q + name = %[2]q + + tags = { + %[3]q = %[4]q + } +} +`, rId, rName, key1, value1) +} + +func testAccFolderConfig_tags2(rId, rName, key1, value1, key2, value2 string) string { + return fmt.Sprintf(` +resource "aws_quicksight_folder" "test" { + folder_id = %[1]q + name = %[2]q + + tags = { + %[3]q = %[4]q + %[5]q = %[6]q + } +} +`, rId, rName, key1, value1, key2, value2) +} + +func testAccFolderConfig_parentFolder(rId, rName, parentId, parentName string) string { + return fmt.Sprintf(` +resource "aws_quicksight_folder" "parent" { + folder_id = %[3]q + name = %[4]q +} + +resource "aws_quicksight_folder" "test" { + folder_id = %[1]q + name = %[2]q + parent_folder_arn = aws_quicksight_folder.parent.arn +} +`, rId, rName, parentId, parentName) +} diff --git a/internal/service/quicksight/service_package_gen.go b/internal/service/quicksight/service_package_gen.go index 819563f1ae78..fca664501d50 100644 --- a/internal/service/quicksight/service_package_gen.go +++ b/internal/service/quicksight/service_package_gen.go @@ -33,6 +33,10 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Factory: ResourceDataSource, TypeName: "aws_quicksight_data_source", }, + { + Factory: ResourceFolder, + TypeName: "aws_quicksight_folder", + }, { Factory: ResourceGroup, TypeName: "aws_quicksight_group", diff --git a/website/docs/r/quicksight_folder.html.markdown b/website/docs/r/quicksight_folder.html.markdown new file mode 100644 index 000000000000..f97da90364af --- /dev/null +++ b/website/docs/r/quicksight_folder.html.markdown @@ -0,0 +1,109 @@ +--- +subcategory: "QuickSight" +layout: "aws" +page_title: "AWS: aws_quicksight_folder" +description: |- + Manages a QuickSight Folder. +--- + +# Resource: aws_quicksight_folder + +Resource for managing a QuickSight Folder. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_quicksight_folder" "example" { + folder_id = "example-id" + name = "example-name" +} +``` + +### With Permissions + +```terraform +resource "aws_quicksight_folder" "example" { + folder_id = "example-id" + name = "example-name" + + permissions { + actions = [ + "quicksight:CreateFolder", + "quicksight:DescribeFolder", + "quicksight:UpdateFolder", + "quicksight:DeleteFolder", + "quicksight:CreateFolderMembership", + "quicksight:DeleteFolderMembership", + "quicksight:DescribeFolderPermissions", + "quicksight:UpdateFolderPermissions", + ] + principal = aws_quicksight_user.example.arn + } +} +``` + +### With Parent Folder + +```terraform +resource "aws_quicksight_folder" "parent" { + folder_id = "parent-id" + name = "parent-name" +} + +resource "aws_quicksight_folder" "example" { + folder_id = "example-id" + name = "example-name" + + parent_folder_arn = aws_quicksight_folder.parent.arn +} +``` + +## Argument Reference + +The following arguments are required: + +* `folder_id` - (Required, Forces new resource) Identifier for the folder. +* `name` - (Required) Display name for the folder. + +The following arguments are optional: + +* `aws_account_id` - (Optional, Forces new resource) AWS account ID. +* `folder_type` - (Optional) The type of folder. By default, it is `SHARED`. Valid values are: `SHARED`. +* `parent_folder_arn` - (Optional) The Amazon Resource Name (ARN) for the parent folder. If not set, creates a root-level folder. +* `permissions` - (Optional) A set of resource permissions on the folder. Maximum of 64 items. See [permissions](#permissions). +* `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. + +### permissions + +* `actions` - (Required) List of IAM actions to grant or revoke permissions on. +* `principal` - (Required) ARN of the principal. See the [ResourcePermission documentation](https://docs.aws.amazon.com/quicksight/latest/APIReference/API_ResourcePermission.html) for the applicable ARN values. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the folder. +* `created_time` - The time that the folder was created. +* `folder_path` - An array of ancestor ARN strings for the folder. Empty for root-level folders. +* `id` - A comma-delimited string joining AWS account ID and folder ID. +* `last_updated_time` - The time that the folder was last updated. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `5m`) +* `read` - (Default `5m`) +* `update` - (Default `5m`) +* `delete` - (Default `5m`) + +## Import + +A QuickSight folder can be imported using the AWS account ID and folder ID name separated by a comma (`,`) e.g., + +``` +$ terraform import aws_quicksight_folder.example 123456789012,example-id +```