diff --git a/.changelog/45470.txt b/.changelog/45470.txt new file mode 100644 index 000000000000..b173a0a39533 --- /dev/null +++ b/.changelog/45470.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_s3vectors_index: Add `metadata_configuration` block +``` + +```release-note:enhancement +resource/aws_s3vectors_index: Add `encryption_configuration` block +``` diff --git a/internal/service/s3vectors/index.go b/internal/service/s3vectors/index.go index 17be09bfa679..a1e40895597a 100644 --- a/internal/service/s3vectors/index.go +++ b/internal/service/s3vectors/index.go @@ -11,11 +11,16 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3vectors" awstypes "github.com/aws/aws-sdk-go-v2/service/s3vectors/types" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/errs" @@ -74,6 +79,10 @@ func (r *indexResource) Schema(ctx context.Context, request resource.SchemaReque stringplanmodifier.RequiresReplace(), }, }, + names.AttrEncryptionConfiguration: framework.ResourceOptionalComputedListOfObjectsAttribute[indexEncryptionConfigurationModel](ctx, 1, nil, + listplanmodifier.UseStateForUnknown(), + listplanmodifier.RequiresReplace(), + ), "index_arn": schema.StringAttribute{ Computed: true, PlanModifiers: []planmodifier.String{ @@ -95,6 +104,31 @@ func (r *indexResource) Schema(ctx context.Context, request resource.SchemaReque }, }, }, + Blocks: map[string]schema.Block{ + "metadata_configuration": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[indexMetadataConfigurationModel](ctx), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "non_filterable_metadata_keys": schema.SetAttribute{ + Required: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 10), + }, + }, + }, + }, + }, + }, } } @@ -248,13 +282,24 @@ func findIndex(ctx context.Context, conn *s3vectors.Client, input *s3vectors.Get type indexResourceModel struct { framework.WithRegionModel - CreationTime timetypes.RFC3339 `tfsdk:"creation_time"` - DataType fwtypes.StringEnum[awstypes.DataType] `tfsdk:"data_type"` - Dimension types.Int32 `tfsdk:"dimension"` - DistanceMetric fwtypes.StringEnum[awstypes.DistanceMetric] `tfsdk:"distance_metric"` - IndexARN types.String `tfsdk:"index_arn"` - IndexName types.String `tfsdk:"index_name"` - Tags tftags.Map `tfsdk:"tags"` - TagsAll tftags.Map `tfsdk:"tags_all"` - VectorBucketName types.String `tfsdk:"vector_bucket_name"` + CreationTime timetypes.RFC3339 `tfsdk:"creation_time"` + DataType fwtypes.StringEnum[awstypes.DataType] `tfsdk:"data_type"` + Dimension types.Int32 `tfsdk:"dimension"` + DistanceMetric fwtypes.StringEnum[awstypes.DistanceMetric] `tfsdk:"distance_metric"` + EncryptionConfiguration fwtypes.ListNestedObjectValueOf[indexEncryptionConfigurationModel] `tfsdk:"encryption_configuration"` + IndexARN types.String `tfsdk:"index_arn"` + IndexName types.String `tfsdk:"index_name"` + MetadataConfiguration fwtypes.ListNestedObjectValueOf[indexMetadataConfigurationModel] `tfsdk:"metadata_configuration"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + VectorBucketName types.String `tfsdk:"vector_bucket_name"` +} + +type indexEncryptionConfigurationModel struct { + KMSKeyARN fwtypes.ARN `tfsdk:"kms_key_arn"` + SseType fwtypes.StringEnum[awstypes.SseType] `tfsdk:"sse_type"` +} + +type indexMetadataConfigurationModel struct { + NonFilterableMetadataKeys fwtypes.SetOfString `tfsdk:"non_filterable_metadata_keys"` } diff --git a/internal/service/s3vectors/index_test.go b/internal/service/s3vectors/index_test.go index 62fd5311eb0a..a8830c97c2ed 100644 --- a/internal/service/s3vectors/index_test.go +++ b/internal/service/s3vectors/index_test.go @@ -6,6 +6,7 @@ package s3vectors_test import ( "context" "fmt" + "strings" "testing" "github.com/YakDriver/regexache" @@ -79,6 +80,132 @@ func TestAccS3VectorsIndex_basic(t *testing.T) { }) } +func TestAccS3VectorsIndex_encryptionConfigurationAES256(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.Index + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3vectors_index.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.S3VectorsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIndexDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIndexConfig_encryptionConfigurationAES256(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIndexExists(ctx, resourceName, &v), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrEncryptionConfiguration).AtSliceIndex(0).AtMapKey("sse_type"), tfknownvalue.StringExact(awstypes.SseTypeAes256)), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, "index_arn"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "index_arn", + }, + }, + }) +} + +func TestAccS3VectorsIndex_encryptionConfigurationCMK(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.Index + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3vectors_index.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.S3VectorsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIndexDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIndexConfig_encryptionConfigurationCMK(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIndexExists(ctx, resourceName, &v), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrEncryptionConfiguration).AtSliceIndex(0).AtMapKey("sse_type"), tfknownvalue.StringExact(awstypes.SseTypeAwsKms)), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrEncryptionConfiguration).AtSliceIndex(0).AtMapKey(names.AttrKMSKeyARN), knownvalue.NotNull()), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, "index_arn"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "index_arn", + }, + }, + }) +} + +func TestAccS3VectorsIndex_metadataConfiguration(t *testing.T) { + ctx := acctest.Context(t) + var v awstypes.Index + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_s3vectors_index.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.S3VectorsServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckIndexDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccIndexConfig_metadataConfiguration(rName, []string{acctest.CtKey1, acctest.CtKey2}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckIndexExists(ctx, resourceName, &v), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("metadata_configuration").AtSliceIndex(0).AtMapKey("non_filterable_metadata_keys"), knownvalue.SetExact( + []knownvalue.Check{ + knownvalue.StringExact(acctest.CtKey1), + knownvalue.StringExact(acctest.CtKey2), + }, + )), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrImportStateIdFunc(resourceName, "index_arn"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "index_arn", + }, + }, + }) +} + func TestAccS3VectorsIndex_disappears(t *testing.T) { ctx := acctest.Context(t) var v awstypes.Index @@ -229,3 +356,99 @@ resource "aws_s3vectors_index" "test" { } `, rName) } + +func testAccIndexConfig_encryptionConfigurationAES256(rName string) string { + return fmt.Sprintf(` +resource "aws_s3vectors_vector_bucket" "test" { + vector_bucket_name = "%[1]s-bucket" + force_destroy = true +} + +resource "aws_s3vectors_index" "test" { + index_name = %[1]q + vector_bucket_name = aws_s3vectors_vector_bucket.test.vector_bucket_name + + data_type = "float32" + dimension = 2 + distance_metric = "euclidean" + + encryption_configuration { + sse_type = "AES256" + } +} +`, rName) +} + +func testAccIndexConfig_encryptionConfigurationCMK(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +data "aws_iam_policy_document" "kms_key_policy" { + statement { + effect = "Allow" + principals { + type = "Service" + identifiers = ["indexing.s3vectors.amazonaws.com"] + } + actions = ["kms:Decrypt"] + resources = ["*"] + } + statement { + effect = "Allow" + principals { + type = "AWS" + identifiers = ["arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root"] + } + actions = ["kms:*"] + resources = ["*"] + } +} + +resource "aws_kms_key" "test" { + deletion_window_in_days = 7 + policy = data.aws_iam_policy_document.kms_key_policy.json +} + +resource "aws_s3vectors_vector_bucket" "test" { + vector_bucket_name = "%[1]s-bucket" + force_destroy = true +} + +resource "aws_s3vectors_index" "test" { + index_name = %[1]q + vector_bucket_name = aws_s3vectors_vector_bucket.test.vector_bucket_name + + data_type = "float32" + dimension = 2 + distance_metric = "euclidean" + + encryption_configuration { + kms_key_arn = aws_kms_key.test.arn + sse_type = "aws:kms" + } +} +`, rName) +} + +func testAccIndexConfig_metadataConfiguration(rName string, keys []string) string { + return fmt.Sprintf(` +resource "aws_s3vectors_vector_bucket" "test" { + vector_bucket_name = "%[1]s-bucket" + force_destroy = true +} + +resource "aws_s3vectors_index" "test" { + index_name = %[1]q + vector_bucket_name = aws_s3vectors_vector_bucket.test.vector_bucket_name + + data_type = "float32" + dimension = 2 + distance_metric = "euclidean" + + metadata_configuration { + non_filterable_metadata_keys = ["%[2]s"] + } +} +`, rName, strings.Join(keys, `", "`)) +} diff --git a/website/docs/r/s3vectors_index.html.markdown b/website/docs/r/s3vectors_index.html.markdown index 94e2cd2fdd6d..60b29f12b793 100644 --- a/website/docs/r/s3vectors_index.html.markdown +++ b/website/docs/r/s3vectors_index.html.markdown @@ -38,8 +38,23 @@ The following arguments are required: The following arguments are optional: * `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). +* `encryption_configuration` - (Optional, Forces new resource) Block for encryption configuration for the vector index. See [`encyption_configuration` block](#encyption_configuration-block) below. +* `metadata_configuration` - (Optional, Forces new resource) Block for metadata configuration for the vector index. See [`metadata_configuration` block](#metadata_configuration-block) below. * `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +### `encyption_configuration` block + +The `encryption_configuration` block supports the following attributes: + +* `kms_key_id` - (Optional, Forces new resource) AWS Key Management Service (KMS) customer managed key ID to use for the encryption configuration. This parameter is allowed if and only if `sse_type` is set to `aws:kms`. +* `sse_type` - (Optional, Forces new resource) Type of encryption to use. Valid values: `AES256`, `aws:kms`. Defaults to `AES256`. + +### `metadata_configuration` block + +The `metadata_configuration` block supports the following attributes: + +* `non_filterable_metadata_keys` - (Required, Forces new resource) List of non-filterable metadata keys. + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: