diff --git a/.changelog/25315.txt b/.changelog/25315.txt new file mode 100644 index 00000000000..3fc3978c350 --- /dev/null +++ b/.changelog/25315.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_kendra_experience +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f2105597015..b55e55e4606 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1588,6 +1588,7 @@ func Provider() *schema.Provider { "aws_mskconnect_custom_plugin": kafkaconnect.ResourceCustomPlugin(), "aws_mskconnect_worker_configuration": kafkaconnect.ResourceWorkerConfiguration(), + "aws_kendra_experience": kendra.ResourceExperience(), "aws_kendra_faq": kendra.ResourceFaq(), "aws_kendra_index": kendra.ResourceIndex(), "aws_kendra_query_suggestions_block_list": kendra.ResourceQuerySuggestionsBlockList(), diff --git a/internal/service/kendra/experience.go b/internal/service/kendra/experience.go new file mode 100644 index 00000000000..ee4d0938071 --- /dev/null +++ b/internal/service/kendra/experience.go @@ -0,0 +1,576 @@ +package kendra + +import ( + "context" + "errors" + "fmt" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/aws/aws-sdk-go-v2/service/kendra" + "github.com/aws/aws-sdk-go-v2/service/kendra/types" + "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/resource" + "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/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceExperience() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceExperienceCreate, + ReadWithoutTimeout: resourceExperienceRead, + UpdateWithoutTimeout: resourceExperienceUpdate, + DeleteWithoutTimeout: resourceExperienceDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "configuration": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "content_source_configuration": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + AtLeastOneOf: []string{ + "configuration.0.content_source_configuration", + "configuration.0.user_identity_configuration", + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "data_source_ids": { + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + MaxItems: 100, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9][a-zA-Z0-9_\-]*`), ""), + }, + }, + "direct_put_content": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "faq_ids": { + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + MaxItems: 100, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9][a-zA-Z0-9_\-]*`), ""), + }, + }, + }, + }, + }, + "user_identity_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + AtLeastOneOf: []string{ + "configuration.0.user_identity_configuration", + "configuration.0.content_source_configuration", + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "identity_attribute_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9][a-zA-Z0-9_\-]*`), ""), + }, + }, + }, + }, + }, + }, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 1000), + }, + "endpoints": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "endpoint": { + Type: schema.TypeString, + Computed: true, + }, + "endpoint_type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "experience_id": { + Type: schema.TypeString, + Computed: true, + }, + "index_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9][a-zA-Z0-9-]*`), ""), + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 1000), + validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9][a-zA-Z0-9_\-]*`), ""), + ), + }, + "role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + + CustomizeDiff: customdiff.Sequence( + customdiff.ForceNewIfChange("description", func(_ context.Context, old, new, meta interface{}) bool { + // Any existing value cannot be cleared. + return new.(string) == "" + }), + ), + } +} + +func resourceExperienceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).KendraConn + + in := &kendra.CreateExperienceInput{ + ClientToken: aws.String(resource.UniqueId()), + IndexId: aws.String(d.Get("index_id").(string)), + Name: aws.String(d.Get("name").(string)), + RoleArn: aws.String(d.Get("role_arn").(string)), + } + + if v, ok := d.GetOk("description"); ok { + in.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + in.Configuration = expandConfiguration(v.([]interface{})) + } + + out, err := conn.CreateExperience(ctx, in) + if err != nil { + return diag.Errorf("creating Amazon Kendra Experience (%s): %s", d.Get("name").(string), err) + } + + if out == nil { + return diag.Errorf("creating Amazon Kendra Experience (%s): empty output", d.Get("name").(string)) + } + + id := aws.ToString(out.Id) + indexId := d.Get("index_id").(string) + + d.SetId(fmt.Sprintf("%s/%s", id, indexId)) + + if err := waitExperienceCreated(ctx, conn, id, indexId, d.Timeout(schema.TimeoutCreate)); err != nil { + return diag.Errorf("waiting for Amazon Kendra Experience (%s) create: %s", d.Id(), err) + } + + return resourceExperienceRead(ctx, d, meta) +} + +func resourceExperienceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).KendraConn + + id, indexId, err := ExperienceParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + out, err := FindExperienceByID(ctx, conn, id, indexId) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Kendra Experience (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading Kendra Experience (%s): %s", d.Id(), err) + } + + arn := arn.ARN{ + Partition: meta.(*conns.AWSClient).Partition, + Region: meta.(*conns.AWSClient).Region, + Service: "kendra", + AccountID: meta.(*conns.AWSClient).AccountID, + Resource: fmt.Sprintf("index/%s/experience/%s", indexId, id), + }.String() + + d.Set("arn", arn) + d.Set("index_id", out.IndexId) + d.Set("description", out.Description) + d.Set("experience_id", out.Id) + d.Set("name", out.Name) + d.Set("role_arn", out.RoleArn) + d.Set("status", out.Status) + + if err := d.Set("endpoints", flattenEndpoints(out.Endpoints)); err != nil { + return diag.Errorf("setting endpoints argument: %s", err) + } + + if err := d.Set("configuration", flattenConfiguration(out.Configuration)); err != nil { + return diag.Errorf("setting configuration argument: %s", err) + } + + return nil +} + +func resourceExperienceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).KendraConn + + id, indexId, err := ExperienceParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + in := &kendra.UpdateExperienceInput{ + Id: aws.String(id), + IndexId: aws.String(indexId), + } + + if d.HasChange("configuration") { + in.Configuration = expandConfiguration(d.Get("configuration").([]interface{})) + } + + if d.HasChange("description") { + in.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("name") { + in.Name = aws.String(d.Get("name").(string)) + } + + if d.HasChange("role_arn") { + in.RoleArn = aws.String(d.Get("role_arn").(string)) + } + + log.Printf("[DEBUG] Updating Kendra Experience (%s): %#v", d.Id(), in) + _, err = conn.UpdateExperience(ctx, in) + if err != nil { + return diag.Errorf("updating Kendra Experience (%s): %s", d.Id(), err) + } + + if err := waitExperienceUpdated(ctx, conn, id, indexId, d.Timeout(schema.TimeoutUpdate)); err != nil { + return diag.Errorf("waiting for Kendra Experience (%s) update: %s", d.Id(), err) + } + + return resourceExperienceRead(ctx, d, meta) +} + +func resourceExperienceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).KendraConn + + log.Printf("[INFO] Deleting Kendra Experience %s", d.Id()) + + id, indexId, err := ExperienceParseResourceID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + _, err = conn.DeleteExperience(ctx, &kendra.DeleteExperienceInput{ + Id: aws.String(id), + IndexId: aws.String(indexId), + }) + + var resourceNotFoundException *types.ResourceNotFoundException + if errors.As(err, &resourceNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("deleting Kendra Experience (%s): %s", d.Id(), err) + } + + if err := waitExperienceDeleted(ctx, conn, id, indexId, d.Timeout(schema.TimeoutDelete)); err != nil { + return diag.Errorf("waiting for Kendra Experience (%s) to be deleted: %s", d.Id(), err) + } + + return nil +} + +func waitExperienceCreated(ctx context.Context, conn *kendra.Client, id, indexId string, timeout time.Duration) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{string(types.ExperienceStatusCreating)}, + Target: []string{string(types.ExperienceStatusActive)}, + Refresh: statusExperience(ctx, conn, id, indexId), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*kendra.DescribeExperienceOutput); ok { + if out.Status == types.ExperienceStatusFailed { + tfresource.SetLastError(err, errors.New(aws.ToString(out.ErrorMessage))) + } + } + + return err +} + +func waitExperienceUpdated(ctx context.Context, conn *kendra.Client, id, indexId string, timeout time.Duration) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{string(types.ExperienceStatusActive)}, + Refresh: statusExperience(ctx, conn, id, indexId), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*kendra.DescribeExperienceOutput); ok { + if out.Status == types.ExperienceStatusFailed { + tfresource.SetLastError(err, errors.New(aws.ToString(out.ErrorMessage))) + } + } + + return err +} + +func waitExperienceDeleted(ctx context.Context, conn *kendra.Client, id, indexId string, timeout time.Duration) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{string(types.ExperienceStatusDeleting)}, + Target: []string{}, + Refresh: statusExperience(ctx, conn, id, indexId), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*kendra.DescribeExperienceOutput); ok { + if out.Status == types.ExperienceStatusFailed { + tfresource.SetLastError(err, errors.New(aws.ToString(out.ErrorMessage))) + } + } + + return err +} + +func statusExperience(ctx context.Context, conn *kendra.Client, id, indexId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := FindExperienceByID(ctx, conn, id, indexId) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.Status), nil + } +} + +func FindExperienceByID(ctx context.Context, conn *kendra.Client, id, indexId string) (*kendra.DescribeExperienceOutput, error) { + in := &kendra.DescribeExperienceInput{ + Id: aws.String(id), + IndexId: aws.String(indexId), + } + + out, err := conn.DescribeExperience(ctx, in) + var resourceNotFoundException *types.ResourceNotFoundException + + if errors.As(err, &resourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + if err != nil { + return nil, err + } + + if out == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +func flattenConfiguration(apiObject *types.ExperienceConfiguration) []interface{} { + if apiObject == nil { + return nil + } + + m := map[string]interface{}{} + + if v := apiObject.ContentSourceConfiguration; v != nil { + m["content_source_configuration"] = flattenContentSourceConfiguration(v) + } + + if v := apiObject.UserIdentityConfiguration; v != nil { + m["user_identity_configuration"] = flattenUserIdentityConfiguration(v) + } + + return []interface{}{m} +} + +func flattenContentSourceConfiguration(apiObject *types.ContentSourceConfiguration) []interface{} { + if apiObject == nil { + return nil + } + + m := map[string]interface{}{ + "direct_put_content": apiObject.DirectPutContent, + } + + if v := apiObject.DataSourceIds; len(v) > 0 { + m["data_source_ids"] = v + } + + if v := apiObject.FaqIds; len(v) > 0 { + m["faq_ids"] = v + } + return []interface{}{m} +} + +func flattenEndpoints(apiObjects []types.ExperienceEndpoint) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + l := make([]interface{}, 0, len(apiObjects)) + + for _, apiObject := range apiObjects { + m := make(map[string]interface{}) + + if v := apiObject.Endpoint; v != nil { + m["endpoint"] = aws.ToString(v) + } + + if v := string(apiObject.EndpointType); v != "" { + m["endpoint_type"] = v + } + + l = append(l, m) + } + + return l + +} + +func flattenUserIdentityConfiguration(apiObject *types.UserIdentityConfiguration) []interface{} { + if apiObject == nil { + return nil + } + + m := make(map[string]interface{}) + + if v := apiObject.IdentityAttributeName; v != nil { + m["identity_attribute_name"] = aws.ToString(v) + } + + return []interface{}{m} +} + +func expandConfiguration(tfList []interface{}) *types.ExperienceConfiguration { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } + + result := &types.ExperienceConfiguration{} + + if v, ok := tfMap["content_source_configuration"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.ContentSourceConfiguration = expandContentSourceConfiguration(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["user_identity_configuration"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + result.UserIdentityConfiguration = expandUserIdentityConfiguration(v[0].(map[string]interface{})) + } + + return result +} + +func expandContentSourceConfiguration(tfMap map[string]interface{}) *types.ContentSourceConfiguration { + if tfMap == nil { + return nil + } + + result := &types.ContentSourceConfiguration{} + + if v, ok := tfMap["data_source_ids"].(*schema.Set); ok && v.Len() > 0 { + result.DataSourceIds = expandStringList(v.List()) + } + + if v, ok := tfMap["direct_put_content"].(bool); ok { + result.DirectPutContent = v + } + + if v, ok := tfMap["faq_ids"].(*schema.Set); ok && v.Len() > 0 { + result.FaqIds = expandStringList(v.List()) + } + + return result +} + +func expandUserIdentityConfiguration(tfMap map[string]interface{}) *types.UserIdentityConfiguration { + if tfMap == nil { + return nil + } + + result := &types.UserIdentityConfiguration{} + + if v, ok := tfMap["identity_attribute_name"].(string); ok && v != "" { + result.IdentityAttributeName = aws.String(v) + } + + return result +} + +func expandStringList(tfList []interface{}) []string { + var result []string + + for _, rawVal := range tfList { + if v, ok := rawVal.(string); ok && v != "" { + result = append(result, v) + } + } + + return result +} diff --git a/internal/service/kendra/experience_test.go b/internal/service/kendra/experience_test.go new file mode 100644 index 00000000000..f649dc2e9fe --- /dev/null +++ b/internal/service/kendra/experience_test.go @@ -0,0 +1,954 @@ +package kendra_test + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + 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" + tfkendra "github.com/hashicorp/terraform-provider-aws/internal/service/kendra" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccExperience_basic(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "kendra", regexp.MustCompile(`index/.+/experience/.+$`)), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "0"), + resource.TestCheckResourceAttr(resourceName, "endpoints.#", "1"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "index_id", "aws_kendra_index.test", "id"), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttrSet(resourceName, "status"), + resource.TestCheckResourceAttrSet(resourceName, "experience_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_disappears(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfkendra.ResourceExperience(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccExperience_Description(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_description(rName, "description1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", "description1"), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccExperienceConfig_description(rName, "description2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", "description2"), + // Update should return a default "configuration" block + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + ), + }, + { + // Removing the description should force a new resource as + // the update to an empty value is not currently supported + Config: testAccExperienceConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "0"), + ), + }, + }, + }) +} + +func testAccExperience_Name(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName1 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_basic(rName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName1), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "0"), + ), + }, + { + Config: testAccExperienceConfig_name(rName1, rName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", rName2), + // Update should return a default "configuration" block + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_roleARN(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "0"), + ), + }, + { + Config: testAccExperienceConfig_roleARN(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "role_arn", "aws_iam_role.test2", "arn"), + // Update should return a default "configuration" block + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_Configuration_ContentSourceConfiguration_DirectPutContent(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_configuration_contentSourceConfiguration_empty(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccExperienceConfig_configuration_contentSourceConfiguration_directPutContent(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "true"), + ), + }, + { + Config: testAccExperienceConfig_configuration_contentSourceConfiguration_directPutContent(rName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + ), + }, + }, + }) +} + +func testAccExperience_Configuration_ContentSourceConfiguration_FaqIDs(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_configuration_contentSourceConfiguration_faqIDs(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.faq_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "configuration.0.content_source_configuration.0.faq_ids.*", "aws_kendra_faq.test", "faq_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_Configuration_ContentSourceConfiguration_updateFaqIDs(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_configuration_contentSourceConfiguration_faqIDs(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.faq_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "configuration.0.content_source_configuration.0.faq_ids.*", "aws_kendra_faq.test", "faq_id"), + ), + }, + { + Config: testAccExperienceConfig_configuration_contentSourceConfiguration_empty(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "false"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.faq_ids.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_Configuration_UserIdentityConfiguration(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") + if userId == "" { + t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_configuration_userIdentityConfiguration(rName, userId), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.0.identity_attribute_name", userId), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_Configuration_ContentSourceConfigurationAndUserIdentityConfiguration(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") + if userId == "" { + t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_configuration_contentSourceConfigurationAndUserIdentityConfiguration(rName, userId), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.0.identity_attribute_name", userId), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_Configuration_ContentSourceConfigurationWithUserIdentityConfigurationRemoved(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") + if userId == "" { + t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_configuration_contentSourceConfigurationAndUserIdentityConfiguration(rName, userId), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.0.identity_attribute_name", userId), + ), + }, + { + Config: testAccExperienceConfig_configuration_contentSourceConfiguration_directPutContent(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperience_Configuration_UserIdentityConfigurationWithContentSourceConfigurationRemoved(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + userId := os.Getenv("AWS_IDENTITY_STORE_USER_ID") + if userId == "" { + t.Skip("Environment variable AWS_IDENTITY_STORE_USER_ID is not set") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_kendra_experience.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckOrganizationManagementAccount(t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.KendraEndpointID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckExperienceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccExperienceConfig_configuration_contentSourceConfigurationAndUserIdentityConfiguration(rName, userId), + Check: resource.ComposeTestCheckFunc( + testAccCheckExperienceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.content_source_configuration.0.direct_put_content", "true"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "configuration.0.user_identity_configuration.0.identity_attribute_name", userId), + ), + }, + { + // Since configuration.content_source_configuration is Optional+Computed, removal in the test config should not trigger changes + PlanOnly: true, + Config: testAccExperienceConfig_configuration_userIdentityConfiguration(rName, userId), + ExpectNonEmptyPlan: false, + }, + }, + }) +} + +func testAccCheckExperienceDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).KendraConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_kendra_experience" { + continue + } + + id, indexId, err := tfkendra.ExperienceParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + + _, err = tfkendra.FindExperienceByID(context.TODO(), conn, id, indexId) + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + } + + return nil +} + +func testAccCheckExperienceExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Kendra Experience is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).KendraConn + + id, indexId, err := tfkendra.ExperienceParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + + _, err = tfkendra.FindExperienceByID(context.TODO(), conn, id, indexId) + if err != nil { + return err + } + + return nil + } +} + +func testAccExperienceBaseConfig(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +data "aws_region" "current" {} + +data "aws_iam_policy_document" "assume_role" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + principals { + type = "Service" + identifiers = ["kendra.${data.aws_partition.current.dns_suffix}"] + } + } +} + +resource "aws_iam_role" "test" { + name = %[1]q + path = "/" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +data "aws_iam_policy_document" "test" { + statement { + effect = "Allow" + actions = [ + "cloudwatch:PutMetricData" + ] + resources = [ + "*" + ] + condition { + test = "StringEquals" + variable = "cloudwatch:namespace" + values = ["Kendra"] + } + } + statement { + effect = "Allow" + actions = [ + "logs:DescribeLogGroups" + ] + resources = [ + "*" + ] + } + statement { + effect = "Allow" + actions = [ + "logs:CreateLogGroup" + ] + resources = [ + "arn:${data.aws_partition.current.partition}:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/kendra/*" + ] + } + statement { + effect = "Allow" + actions = [ + "logs:DescribeLogStreams", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + resources = [ + "arn:${data.aws_partition.current.partition}:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/kendra/*:log-stream:*" + ] + } +} + +resource "aws_iam_policy" "test" { + name = %[1]q + description = "Allow Kendra to access cloudwatch logs" + policy = data.aws_iam_policy_document.test.json +} + +data "aws_iam_policy_document" "experience" { + statement { + sid = "AllowsKendraSearchAppToCallKendraApi" + effect = "Allow" + actions = [ + "kendra:GetQuerySuggestions", + "kendra:Query", + "kendra:DescribeIndex", + "kendra:ListFaqs", + "kendra:DescribeDataSource", + "kendra:ListDataSources", + "kendra:DescribeFaq" + ] + resources = [ + "arn:${data.aws_partition.current.partition}:kendra:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:index/${aws_kendra_index.test.id}" + ] + } +} + +resource "aws_iam_policy" "experience" { + name = "%[1]s-experience" + description = "Allow Kendra to search app access" + policy = data.aws_iam_policy_document.experience.json +} + +resource "aws_iam_role_policy_attachment" "test" { + role = aws_iam_role.test.name + policy_arn = aws_iam_policy.test.arn +} + +resource "aws_iam_role_policy_attachment" "experience" { + role = aws_iam_role.test.name + policy_arn = aws_iam_policy.experience.arn +} + +resource "aws_kendra_index" "test" { + depends_on = [aws_iam_role_policy_attachment.test] + + name = %[1]q + role_arn = aws_iam_role.test.arn +} +`, rName) +} + +func testAccExperienceConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn +} +`, rName)) +} + +func testAccExperienceConfig_description(rName, description string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + description = %[2]q + name = %[1]q + role_arn = aws_iam_role.test.arn +} +`, rName, description)) +} + +func testAccExperienceConfig_name(rName, name string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn +} +`, name)) +} + +func testAccExperienceConfig_roleARN(rName string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_iam_role" "test2" { + name = "%[1]s-2" + path = "/" + assume_role_policy = data.aws_iam_policy_document.assume_role.json +} + +resource "aws_iam_policy" "experience2" { + name = "%[1]s-experience-2" + description = "Allow Kendra to search app access" + policy = data.aws_iam_policy_document.experience.json +} + +resource "aws_iam_role_policy_attachment" "experience2" { + role = aws_iam_role.test2.name + policy_arn = aws_iam_policy.experience2.arn +} + +resource "aws_kendra_experience" "test" { + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test2.arn +} +`, rName)) +} + +func testAccExperienceConfig_configuration_contentSourceConfiguration_empty(rName string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn + + configuration { + content_source_configuration {} + } +} +`, rName)) +} + +func testAccExperienceConfig_configuration_contentSourceConfiguration_directPutContent(rName string, directPutContent bool) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn + + configuration { + content_source_configuration { + direct_put_content = %[2]t + } + } +} +`, rName, directPutContent)) +} + +func testAccExperienceConfig_configuration_contentSourceConfiguration_faqIDs(rName string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_s3_object" "test" { + bucket = aws_s3_bucket.test.bucket + source = "test-fixtures/basic.csv" + key = "test/basic.csv" +} + +data "aws_iam_policy_document" "faq" { + statement { + sid = "AllowKendraToAccessS3" + effect = "Allow" + actions = [ + "s3:GetObject" + ] + resources = [ + "${aws_s3_bucket.test.arn}/*" + ] + } +} + +resource "aws_iam_policy" "faq" { + name = "%[1]s-faq" + description = "Allow Kendra to access S3" + policy = data.aws_iam_policy_document.faq.json +} + +resource "aws_iam_role_policy_attachment" "faq" { + role = aws_iam_role.test.name + policy_arn = aws_iam_policy.faq.arn +} + +resource "aws_kendra_faq" "test" { + depends_on = [aws_iam_role_policy_attachment.faq] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn + + s3_path { + bucket = aws_s3_bucket.test.id + key = aws_s3_object.test.key + } +} + +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn + + configuration { + content_source_configuration { + faq_ids = [aws_kendra_faq.test.faq_id] + } + } +} +`, rName)) +} + +func testAccExperienceConfig_configuration_userIdentityConfiguration(rName, userId string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn + + configuration { + user_identity_configuration { + identity_attribute_name = %[2]q + } + } +} +`, rName, userId)) +} + +func testAccExperienceConfig_configuration_contentSourceConfigurationAndUserIdentityConfiguration(rName, userId string) string { + return acctest.ConfigCompose( + testAccExperienceBaseConfig(rName), + fmt.Sprintf(` +resource "aws_kendra_experience" "test" { + depends_on = [aws_iam_role_policy_attachment.experience] + + index_id = aws_kendra_index.test.id + name = %[1]q + role_arn = aws_iam_role.test.arn + + configuration { + content_source_configuration { + direct_put_content = true + } + user_identity_configuration { + identity_attribute_name = %[2]q + } + } +} +`, rName, userId)) +} diff --git a/internal/service/kendra/id.go b/internal/service/kendra/id.go index b0cf39169f8..ba2d1756de1 100644 --- a/internal/service/kendra/id.go +++ b/internal/service/kendra/id.go @@ -5,6 +5,16 @@ import ( "strings" ) +func ExperienceParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, "/") + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("please make sure ID is in format EXPERIENCE_ID/INDEX_ID") + } + + return parts[0], parts[1], nil +} + func FaqParseResourceID(id string) (string, string, error) { parts := strings.Split(id, "/") diff --git a/internal/service/kendra/id_test.go b/internal/service/kendra/id_test.go index 0d3e47f0460..7e5eb25eef2 100644 --- a/internal/service/kendra/id_test.go +++ b/internal/service/kendra/id_test.go @@ -6,6 +6,74 @@ import ( tfkendra "github.com/hashicorp/terraform-provider-aws/internal/service/kendra" ) +func TestExperienceParseResourceID(t *testing.T) { + testCases := []struct { + TestName string + Input string + ExpectedId string + ExpectedIndexId string + Error bool + }{ + { + TestName: "empty", + Input: "", + ExpectedId: "", + ExpectedIndexId: "", + Error: true, + }, + { + TestName: "Invalid ID", + Input: "abcdefg12345678/", + ExpectedId: "", + ExpectedIndexId: "", + Error: true, + }, + { + TestName: "Invalid ID separator", + Input: "abcdefg12345678:qwerty09876", + ExpectedId: "", + ExpectedIndexId: "", + Error: true, + }, + { + TestName: "Invalid ID with more than 1 separator", + Input: "abcdefg12345678/qwerty09876/zxcvbnm123456", + ExpectedId: "", + ExpectedIndexId: "", + Error: true, + }, + { + TestName: "Valid ID", + Input: "abcdefg12345678/qwerty09876", + ExpectedId: "abcdefg12345678", + ExpectedIndexId: "qwerty09876", + Error: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.TestName, func(t *testing.T) { + gotId, gotIndexId, err := tfkendra.ExperienceParseResourceID(testCase.Input) + + if err != nil && !testCase.Error { + t.Errorf("got error (%s), expected no error", err) + } + + if err == nil && testCase.Error { + t.Errorf("got (Id: %s, IndexId: %s) and no error, expected error", gotId, gotIndexId) + } + + if gotId != testCase.ExpectedId { + t.Errorf("got %s, expected %s", gotId, testCase.ExpectedIndexId) + } + + if gotIndexId != testCase.ExpectedIndexId { + t.Errorf("got %s, expected %s", gotIndexId, testCase.ExpectedIndexId) + } + }) + } +} + func TestQuerySuggestionsBlockListParseID(t *testing.T) { testCases := []struct { TestName string diff --git a/internal/service/kendra/kendra_test.go b/internal/service/kendra/kendra_test.go index 7577ac5e084..04d8988f670 100644 --- a/internal/service/kendra/kendra_test.go +++ b/internal/service/kendra/kendra_test.go @@ -5,6 +5,28 @@ import "testing" // Serialize to limit service quota exceeded errors. func TestAccKendra_serial(t *testing.T) { testCases := map[string]map[string]func(t *testing.T){ + "Experience": { + "basic": testAccExperience_basic, + "disappears": testAccExperience_disappears, + "Description": testAccExperience_Description, + "Name": testAccExperience_Name, + "RoleARN": testAccExperience_roleARN, + "Configuration_ContentSourceConfiguration_DirectPutContent": testAccExperience_Configuration_ContentSourceConfiguration_DirectPutContent, + "Configuration_ContentSourceConfiguration_FaqIDs": testAccExperience_Configuration_ContentSourceConfiguration_FaqIDs, + "Configuration_ContentSourceConfiguration_updateFaqIDs": testAccExperience_Configuration_ContentSourceConfiguration_updateFaqIDs, + "Configuration_UserIdentityConfiguration": testAccExperience_Configuration_UserIdentityConfiguration, + "Configuration_ContentSourceConfigurationAndUserIdentityConfiguration": testAccExperience_Configuration_ContentSourceConfigurationAndUserIdentityConfiguration, + "Configuration_ContentSourceConfigurationWithUserIdentityConfigurationRemoved": testAccExperience_Configuration_ContentSourceConfigurationWithUserIdentityConfigurationRemoved, + "Configuration_UserIdentityConfigurationWithContentSourceConfigurationRemoved": testAccExperience_Configuration_UserIdentityConfigurationWithContentSourceConfigurationRemoved, + }, + "Faq": { + "basic": testAccFaq_basic, + "disappears": testAccFaq_disappears, + "tags": testAccFaq_tags, + "Description": testAccFaq_description, + "FileFormat": testAccFaq_fileFormat, + "LanguageCode": testAccFaq_languageCode, + }, "Index": { "basic": testAccIndex_basic, "disappears": testAccIndex_disappears, @@ -34,14 +56,6 @@ func TestAccKendra_serial(t *testing.T) { "RoleARN": testAccThesaurus_roleARN, "SourceS3Path": testAccThesaurus_sourceS3Path, }, - "Faq": { - "basic": testAccFaq_basic, - "disappears": testAccFaq_disappears, - "tags": testAccFaq_tags, - "Description": testAccFaq_description, - "FileFormat": testAccFaq_fileFormat, - "LanguageCode": testAccFaq_languageCode, - }, } for group, m := range testCases { diff --git a/website/docs/r/kendra_experience.html.markdown b/website/docs/r/kendra_experience.html.markdown new file mode 100644 index 00000000000..4d952ed2e07 --- /dev/null +++ b/website/docs/r/kendra_experience.html.markdown @@ -0,0 +1,100 @@ +--- +subcategory: "Kendra" +layout: "aws" +page_title: "AWS: aws_kendra_experience" +description: |- + Terraform resource for managing an AWS Kendra Experience. +--- + +# Resource: aws_kendra_experience + +Terraform resource for managing an AWS Kendra Experience. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_kendra_experience" "example" { + index_id = aws_kendra_index.example.id + description = "My Kendra Experience" + name = "example" + role_arn = aws_iam_role.example.arn + + configuration { + content_source_configuration { + direct_put_content = true + faq_ids = [aws_kendra_faq.example.faq_id] + } + user_identity_configuration { + identity_attribute_name = "12345ec453-1546651e-79c4-4554-91fa-00b43ccfa245" + } + } +} +``` + +## Argument Reference + +~> **NOTE:** By default of the AWS Kendra API, updates to an existing `aws_kendra_experience` resource (e.g. updating the `name`) will also update the `configuration.content_source_configuration.direct_put_content` parameter to `false` if not already provided. + +The following arguments are required: + +* `index_id` - (Required, Forces new resource) The identifier of the index for your Amazon Kendra experience. +* `name` - (Required) A name for your Amazon Kendra experience. +* `role_arn` - (Required) The Amazon Resource Name (ARN) of a role with permission to access `Query API`, `QuerySuggestions API`, `SubmitFeedback API`, and `AWS SSO` that stores your user and group information. For more information, see [IAM roles for Amazon Kendra](https://docs.aws.amazon.com/kendra/latest/dg/iam-roles.html). + +The following arguments are optional: + +* `description` - (Optional, Forces new resource if removed) A description for your Amazon Kendra experience. +* `configuration` - (Optional) Configuration information for your Amazon Kendra experience. Terraform will only perform drift detection of its value when present in a configuration. [Detailed below](#configuration). + +### `configuration` + +~> **NOTE:** By default of the AWS Kendra API, the `content_source_configuration.direct_put_content` parameter will be set to `false` if not provided. + +The `configuration` configuration block supports the following arguments: + +* `content_source_configuration` - (Optional, Required if `user_identity_configuration` not provided) The identifiers of your data sources and FAQs. Or, you can specify that you want to use documents indexed via the `BatchPutDocument API`. Terraform will only perform drift detection of its value when present in a configuration. [Detailed below](#content_source_configuration). +* `user_identity_configuration` - (Optional, Required if `content_source_configuration` not provided) The AWS SSO field name that contains the identifiers of your users, such as their emails. [Detailed below](#user_identity_configuration). + +### `content_source_configuration` + +The `content_source_configuration` configuration block supports the following arguments: + +* `data_source_ids` - (Optional) The identifiers of the data sources you want to use for your Amazon Kendra experience. Maximum number of 100 items. +* `direct_put_content` - (Optional) Whether to use documents you indexed directly using the `BatchPutDocument API`. Defaults to `false`. +* `faq_ids` - (Optional) The identifier of the FAQs that you want to use for your Amazon Kendra experience. Maximum number of 100 items. + +### `user_identity_configuration` + +The `user_identity_configuration` configuration block supports the following argument: + +* `identity_attribute_name` - (Required) The AWS SSO field name that contains the identifiers of your users, such as their emails. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The unique identifiers of the experience and index separated by a slash (`/`). +* `arn` - ARN of the Experience. +* `endpoints` - Shows the endpoint URLs for your Amazon Kendra experiences. The URLs are unique and fully hosted by AWS. + * `endpoint` - The endpoint of your Amazon Kendra experience. + * `endpoint_type` - The type of endpoint for your Amazon Kendra experience. +* `experience_id` - The unique identifier of the experience. +* `status` - The current processing status of your Amazon Kendra experience. + +## Timeouts + +`aws_kendra_experience` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +* `create` - (Optional, Default: `30m`) +* `update` - (Optional, Default: `30m`) +* `delete` - (Optional, Default: `30m`) + +## Import + +Kendra Experience can be imported using the unique identifiers of the experience and index separated by a slash (`/`) e.g., + +``` +$ terraform import aws_kendra_experience.example 1045d08d-66ef-4882-b3ed-dfb7df183e90/b34dfdf7-1f2b-4704-9581-79e00296845f +```