diff --git a/.changelog/38757.txt b/.changelog/38757.txt new file mode 100644 index 000000000000..7bedcb5da965 --- /dev/null +++ b/.changelog/38757.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_bedrock_guardrail +``` \ No newline at end of file diff --git a/internal/service/bedrock/exports_test.go b/internal/service/bedrock/exports_test.go index c01b1fe21ebf..e7ed3ffc915e 100644 --- a/internal/service/bedrock/exports_test.go +++ b/internal/service/bedrock/exports_test.go @@ -6,11 +6,14 @@ package bedrock // Exports for use in tests only. var ( ResourceCustomModel = newCustomModelResource + ResourceGuardrail = newResourceGuardrail ResourceModelInvocationLoggingConfiguration = newModelInvocationLoggingConfigurationResource FindCustomModelByID = findCustomModelByID + FindGuardrailByID = findGuardrailByID FindModelCustomizationJobByID = findModelCustomizationJobByID FindModelInvocationLoggingConfiguration = findModelInvocationLoggingConfiguration FindProvisionedModelThroughputByID = findProvisionedModelThroughputByID - WaitModelCustomizationJobCompleted = waitModelCustomizationJobCompleted + + WaitModelCustomizationJobCompleted = waitModelCustomizationJobCompleted ) diff --git a/internal/service/bedrock/guardrail.go b/internal/service/bedrock/guardrail.go new file mode 100644 index 000000000000..b9cc4faf9e93 --- /dev/null +++ b/internal/service/bedrock/guardrail.go @@ -0,0 +1,726 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bedrock + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/bedrock" + awstypes "github.com/aws/aws-sdk-go-v2/service/bedrock/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Guardrail") +// @Tags(identifierAttribute="guardrail_arn") +func newResourceGuardrail(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceGuardrail{} + + r.SetDefaultCreateTimeout(5 * time.Minute) + r.SetDefaultUpdateTimeout(5 * time.Minute) + r.SetDefaultDeleteTimeout(5 * time.Minute) + + return r, nil +} + +const ( + ResNameGuardrail = "Guardrail" +) + +type resourceGuardrail struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceGuardrail) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_bedrock_guardrail" +} + +func (r *resourceGuardrail) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "blocked_input_messaging": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 500), + }, + }, + "blocked_outputs_messaging": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 500), + }, + }, + names.AttrCreatedAt: schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrDescription: schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(0, 200), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "guardrail_arn": framework.ARNAttributeComputedOnly(), + "guardrail_id": framework.IDAttribute(), + names.AttrKMSKeyARN: schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 2048), + }, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 50), + stringvalidator.RegexMatches(guardrailNameRegex, ""), + }, + }, + names.AttrStatus: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.GuardrailStatus](), + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + names.AttrVersion: schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "content_policy_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[contentPolicyConfig](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "filters_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[filtersConfig](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "input_strength": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailFilterStrength](), + }, + "output_strength": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailFilterStrength](), + }, + names.AttrType: schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailContentFilterType](), + }, + }, + }, + }, + }, + }, + }, + "contextual_grounding_policy_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[contextualGroundingPolicyConfig](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "filters_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[contextualGroundingFiltersConfig](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "threshold": schema.Float64Attribute{ + Required: true, + Validators: []validator.Float64{ + float64validator.AtLeast(filtersConfigThresholdMin), + }, + }, + names.AttrType: schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailContextualGroundingFilterType](), + }, + }, + }, + }, + }, + }, + }, + "sensitive_information_policy_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[sensitiveInformationPolicyConfig](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "pii_entities_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[piiEntitiesConfig](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrAction: schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailSensitiveInformationAction](), + }, + names.AttrType: schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailPiiEntityType](), + }, + }, + }, + }, + "regexes_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[regexesConfig](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrAction: schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailSensitiveInformationAction](), + }, + names.AttrDescription: schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 1000), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 100), + }, + }, + "pattern": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + }, + }, + }, + }, + }, + "topic_policy_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[topicPolicyConfig](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "topics_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[topicsConfig](ctx), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "definition": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 200), + }, + }, + "examples": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(0), + listvalidator.ValueStringsAre( + stringvalidator.LengthBetween(1, 100), + ), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 100), + stringvalidator.RegexMatches(topicsConfigNameRegex, ""), + }, + }, + names.AttrType: schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailTopicType](), + }, + }, + }, + }, + }, + }, + }, + "word_policy_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[wordPolicyConfig](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "managed_word_lists_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[managedWordListsConfig](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrType: schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.GuardrailManagedWordsType](), + }, + }, + }, + }, + "words_config": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[wordsConfig](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "text": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + }, + }, + }, + }, + }, + }, + }, + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +const ( + filtersConfigThresholdMin = 0.000000 + + guardrailIDParts = 2 +) + +var ( + flexOpt = fwflex.WithFieldNameSuffix("Config") + + guardrailNameRegex = regexache.MustCompile("^[0-9a-zA-Z-_]+$") + topicsConfigNameRegex = regexache.MustCompile("^[0-9a-zA-Z-_ !?.]+$") +) + +func (r *resourceGuardrail) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().BedrockClient(ctx) + + var plan resourceGuardrailData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + in := &bedrock.CreateGuardrailInput{} + resp.Diagnostics.Append(fwflex.Expand(ctx, plan, in, flexOpt)...) + if resp.Diagnostics.HasError() { + return + } + + in.Tags = getTagsIn(ctx) + out, err := conn.CreateGuardrail(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionCreating, ResNameGuardrail, plan.Name.String(), err), + err.Error(), + ) + return + } + + plan.GuardrailArn = fwflex.StringToFramework(ctx, out.GuardrailArn) + plan.GuardrailID = fwflex.StringToFramework(ctx, out.GuardrailId) + plan.Version = fwflex.StringToFramework(ctx, out.Version) + plan.CreatedAt = fwflex.TimeToFramework(ctx, out.CreatedAt) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + _, err = waitGuardrailCreated(ctx, conn, plan.GuardrailID.ValueString(), plan.Version.ValueString(), createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionWaitingForCreation, ResNameGuardrail, plan.Name.String(), err), + err.Error(), + ) + return + } + + output, err := findGuardrailByID(ctx, conn, plan.GuardrailID.ValueString(), plan.Version.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionSetting, ResNameGuardrail, plan.GuardrailID.String(), err), + err.Error(), + ) + return + } + plan.Status = fwtypes.StringEnumValue(output.Status) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceGuardrail) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().BedrockClient(ctx) + + var state resourceGuardrailData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + out, err := findGuardrailByID(ctx, conn, state.GuardrailID.ValueString(), state.Version.ValueString()) + + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionSetting, ResNameGuardrail, state.GuardrailID.String(), err), + err.Error(), + ) + return + } + resp.Diagnostics.Append(fwflex.Flatten(ctx, out, &state, flexOpt)...) + state.KmsKeyId = fwflex.StringToFramework(ctx, out.KmsKeyArn) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceGuardrail) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().BedrockClient(ctx) + + var plan, state resourceGuardrailData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.BlockedInputMessaging.Equal(state.BlockedInputMessaging) || + !plan.BlockedOutputsMessaging.Equal(state.BlockedOutputsMessaging) || + !plan.KmsKeyId.Equal(state.KmsKeyId) || + !plan.ContentPolicy.Equal(state.ContentPolicy) || + !plan.ContextualGroundingPolicy.Equal(state.ContextualGroundingPolicy) || + !plan.SensitiveInformationPolicy.Equal(state.SensitiveInformationPolicy) || + !plan.TopicPolicy.Equal(state.TopicPolicy) || + !plan.WordPolicy.Equal(state.WordPolicy) || + !plan.Name.Equal(state.Name) || + !plan.Description.Equal(state.Description) { + in := &bedrock.UpdateGuardrailInput{ + GuardrailIdentifier: aws.String(plan.GuardrailID.ValueString()), + } + resp.Diagnostics.Append(fwflex.Expand(ctx, plan, in, flexOpt)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := conn.UpdateGuardrail(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionUpdating, ResNameGuardrail, plan.GuardrailID.String(), err), + err.Error(), + ) + return + } + if out == nil || out.GuardrailArn == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionUpdating, ResNameGuardrail, plan.GuardrailID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + plan.GuardrailArn = fwflex.StringToFramework(ctx, out.GuardrailArn) + plan.GuardrailID = fwflex.StringToFramework(ctx, out.GuardrailId) + + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + if _, err := waitGuardrailUpdated(ctx, conn, plan.GuardrailID.ValueString(), state.Version.ValueString(), updateTimeout); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionWaitingForUpdate, ResNameGuardrail, plan.GuardrailID.String(), err), + err.Error(), + ) + return + } + + output, err := findGuardrailByID(ctx, conn, plan.GuardrailID.ValueString(), plan.Version.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionSetting, ResNameGuardrail, plan.GuardrailID.String(), err), + err.Error(), + ) + return + } + plan.Status = fwtypes.StringEnumValue(output.Status) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceGuardrail) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().BedrockClient(ctx) + + var state resourceGuardrailData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + in := &bedrock.DeleteGuardrailInput{ + GuardrailIdentifier: aws.String(state.GuardrailID.ValueString()), + } + if _, err := conn.DeleteGuardrail(ctx, in); err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionDeleting, ResNameGuardrail, state.GuardrailID.String(), err), + err.Error(), + ) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + if _, err := waitGuardrailDeleted(ctx, conn, state.GuardrailID.ValueString(), state.Version.ValueString(), deleteTimeout); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Bedrock, create.ErrActionWaitingForDeletion, ResNameGuardrail, state.GuardrailID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceGuardrail) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts, err := intflex.ExpandResourceId(req.ID, guardrailIDParts, false) + if err != nil { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: guardrail_id,version. Got: %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("guardrail_id"), parts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(names.AttrVersion), parts[1])...) +} + +func (r *resourceGuardrail) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, req, resp) +} + +func waitGuardrailCreated(ctx context.Context, conn *bedrock.Client, id string, version string, timeout time.Duration) (*bedrock.GetGuardrailOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.GuardrailStatusCreating), + Target: enum.Slice(awstypes.GuardrailStatusReady), + Refresh: statusGuardrail(ctx, conn, id, version), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*bedrock.GetGuardrailOutput); ok { + return out, err + } + + return nil, err +} + +func waitGuardrailUpdated(ctx context.Context, conn *bedrock.Client, id string, version string, timeout time.Duration) (*bedrock.GetGuardrailOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.GuardrailStatusUpdating), + Target: enum.Slice(awstypes.GuardrailStatusReady), + Refresh: statusGuardrail(ctx, conn, id, version), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*bedrock.GetGuardrailOutput); ok { + return out, err + } + + return nil, err +} + +func waitGuardrailDeleted(ctx context.Context, conn *bedrock.Client, id string, version string, timeout time.Duration) (*bedrock.GetGuardrailOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.GuardrailStatusDeleting, awstypes.GuardrailStatusReady), + Target: []string{}, + Refresh: statusGuardrail(ctx, conn, id, version), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*bedrock.GetGuardrailOutput); ok { + return out, err + } + + return nil, err +} + +func statusGuardrail(ctx context.Context, conn *bedrock.Client, id string, version string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := findGuardrailByID(ctx, conn, id, version) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.Status), nil + } +} + +func findGuardrailByID(ctx context.Context, conn *bedrock.Client, id string, version string) (*bedrock.GetGuardrailOutput, error) { + in := &bedrock.GetGuardrailInput{ + GuardrailIdentifier: aws.String(id), + GuardrailVersion: aws.String(version), + } + + out, err := conn.GetGuardrail(ctx, in) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +type resourceGuardrailData struct { + BlockedInputMessaging types.String `tfsdk:"blocked_input_messaging"` + BlockedOutputsMessaging types.String `tfsdk:"blocked_outputs_messaging"` + ContentPolicy fwtypes.ListNestedObjectValueOf[contentPolicyConfig] `tfsdk:"content_policy_config"` + ContextualGroundingPolicy fwtypes.ListNestedObjectValueOf[contextualGroundingPolicyConfig] `tfsdk:"contextual_grounding_policy_config"` + CreatedAt timetypes.RFC3339 `tfsdk:"created_at"` + Description types.String `tfsdk:"description"` + GuardrailArn types.String `tfsdk:"guardrail_arn"` + GuardrailID types.String `tfsdk:"guardrail_id"` + KmsKeyId types.String `tfsdk:"kms_key_arn"` + Name types.String `tfsdk:"name"` + SensitiveInformationPolicy fwtypes.ListNestedObjectValueOf[sensitiveInformationPolicyConfig] `tfsdk:"sensitive_information_policy_config"` + Status fwtypes.StringEnum[awstypes.GuardrailStatus] `tfsdk:"status"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + TopicPolicy fwtypes.ListNestedObjectValueOf[topicPolicyConfig] `tfsdk:"topic_policy_config"` + Version types.String `tfsdk:"version"` + WordPolicy fwtypes.ListNestedObjectValueOf[wordPolicyConfig] `tfsdk:"word_policy_config"` +} + +type contentPolicyConfig struct { + Filters fwtypes.ListNestedObjectValueOf[filtersConfig] `tfsdk:"filters_config"` +} + +type filtersConfig struct { + InputStrength fwtypes.StringEnum[awstypes.GuardrailFilterStrength] `tfsdk:"input_strength"` + OutputStrength fwtypes.StringEnum[awstypes.GuardrailFilterStrength] `tfsdk:"output_strength"` + Type fwtypes.StringEnum[awstypes.GuardrailContentFilterType] `tfsdk:"type"` +} + +type contextualGroundingPolicyConfig struct { + Filters fwtypes.ListNestedObjectValueOf[contextualGroundingFiltersConfig] `tfsdk:"filters_config"` +} + +type contextualGroundingFiltersConfig struct { + Threshold types.Float64 `tfsdk:"threshold"` + Type fwtypes.StringEnum[awstypes.GuardrailContextualGroundingFilterType] `tfsdk:"type"` +} + +type sensitiveInformationPolicyConfig struct { + PIIEntities fwtypes.ListNestedObjectValueOf[piiEntitiesConfig] `tfsdk:"pii_entities_config"` + Regexes fwtypes.ListNestedObjectValueOf[regexesConfig] `tfsdk:"regexes_config"` +} + +type piiEntitiesConfig struct { + Action fwtypes.StringEnum[awstypes.GuardrailSensitiveInformationAction] `tfsdk:"action"` + Type fwtypes.StringEnum[awstypes.GuardrailPiiEntityType] `tfsdk:"type"` +} + +type regexesConfig struct { + Action fwtypes.StringEnum[awstypes.GuardrailSensitiveInformationAction] `tfsdk:"action"` + Description types.String `tfsdk:"description"` + Name types.String `tfsdk:"name"` + Pattern types.String `tfsdk:"pattern"` +} + +type topicPolicyConfig struct { + Topics fwtypes.ListNestedObjectValueOf[topicsConfig] `tfsdk:"topics_config"` +} + +type topicsConfig struct { + Definition types.String `tfsdk:"definition"` + Examples fwtypes.ListValueOf[types.String] `tfsdk:"examples"` + Name types.String `tfsdk:"name"` + Type fwtypes.StringEnum[awstypes.GuardrailTopicType] `tfsdk:"type"` +} + +type wordPolicyConfig struct { + ManagedWordLists fwtypes.ListNestedObjectValueOf[managedWordListsConfig] `tfsdk:"managed_word_lists_config"` + Words fwtypes.ListNestedObjectValueOf[wordsConfig] `tfsdk:"words_config"` +} + +type managedWordListsConfig struct { + Type fwtypes.StringEnum[awstypes.GuardrailManagedWordsType] `tfsdk:"type"` +} + +type wordsConfig struct { + Text types.String `tfsdk:"text"` +} diff --git a/internal/service/bedrock/guardrail_test.go b/internal/service/bedrock/guardrail_test.go new file mode 100644 index 000000000000..e23333be17d2 --- /dev/null +++ b/internal/service/bedrock/guardrail_test.go @@ -0,0 +1,533 @@ +// // Copyright (c) HashiCorp, Inc. +// // SPDX-License-Identifier: MPL-2.0 + +package bedrock_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/bedrock" + "github.com/aws/aws-sdk-go-v2/service/bedrock/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/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" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + tfbedrock "github.com/hashicorp/terraform-provider-aws/internal/service/bedrock" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccBedrockGuardrail_basic(t *testing.T) { + ctx := acctest.Context(t) + + var guardrail bedrock.GetGuardrailOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrock_guardrail.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGuardrailDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGuardrailConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + resource.TestCheckResourceAttrSet(resourceName, "guardrail_arn"), + resource.TestCheckResourceAttr(resourceName, "blocked_input_messaging", "test"), + resource.TestCheckResourceAttr(resourceName, "blocked_outputs_messaging", "test"), + resource.TestCheckResourceAttr(resourceName, "content_policy_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "content_policy_config.0.filters_config.#", acctest.Ct2), + resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "test"), + resource.TestCheckNoResourceAttr(resourceName, names.AttrKMSKeyARN), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.0.pii_entities_config.#", acctest.Ct3), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.0.regexes_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, names.AttrStatus, "READY"), + resource.TestCheckResourceAttr(resourceName, "topic_policy_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "topic_policy_config.0.topics_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, names.AttrVersion, "DRAFT"), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.0.managed_word_lists_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.0.words_config.#", acctest.Ct1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccGuardrailImportStateIDFunc(resourceName), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "guardrail_id", + }, + }, + }) +} + +func TestAccBedrockGuardrail_disappears(t *testing.T) { + ctx := acctest.Context(t) + + var guardrail bedrock.GetGuardrailOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrock_guardrail.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGuardrailDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGuardrailConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfbedrock.ResourceGuardrail, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccBedrockGuardrail_kmsKey(t *testing.T) { + ctx := acctest.Context(t) + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrock_guardrail.test" + var guardrail bedrock.GetGuardrailOutput + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGuardrailDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGuardrailConfig_kmsKey(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + resource.TestCheckResourceAttrSet(resourceName, names.AttrKMSKeyARN), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccGuardrailImportStateIDFunc(resourceName), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "guardrail_id", + }, + }, + }) +} + +func TestAccBedrockGuardrail_tags(t *testing.T) { + ctx := acctest.Context(t) + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrock_guardrail.test" + var guardrail bedrock.GetGuardrailOutput + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGuardrailDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGuardrailConfig_tags(rName, acctest.CtKey1, acctest.CtValue1, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + { + Config: testAccGuardrailConfig_tags(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue1Updated), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1Updated), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue1Updated), + ), + }, + }, + }) +} + +func TestAccBedrockGuardrail_update(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrock_guardrail.test" + var guardrail bedrock.GetGuardrailOutput + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGuardrailDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccGuardrailConfig_wordConfig_only(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + resource.TestCheckResourceAttrSet(resourceName, "guardrail_arn"), + resource.TestCheckResourceAttr(resourceName, "blocked_input_messaging", "test"), + resource.TestCheckResourceAttr(resourceName, "blocked_outputs_messaging", "test"), + resource.TestCheckResourceAttr(resourceName, "content_policy_config.#", acctest.Ct0), + resource.TestCheckResourceAttrSet(resourceName, names.AttrCreatedAt), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "test"), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, names.AttrStatus, "READY"), + resource.TestCheckResourceAttr(resourceName, "topic_policy_config.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, names.AttrVersion, "DRAFT"), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.0.managed_word_lists_config.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.0.words_config.#", acctest.Ct1), + ), + }, + { + Config: testAccGuardrailConfig_update(rName, "test", "test", "MEDIUM", "^\\d{3}-\\d{2}-\\d{4}$", "NAME", "investment_topic", "HATE"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + resource.TestCheckResourceAttr(resourceName, "blocked_input_messaging", "test"), + resource.TestCheckResourceAttr(resourceName, "blocked_outputs_messaging", "test"), + resource.TestCheckResourceAttr(resourceName, "content_policy_config.0.filters_config.0.input_strength", "MEDIUM"), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.0.regexes_config.0.pattern", "^\\d{3}-\\d{2}-\\d{4}$"), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.0.pii_entities_config.0.type", "NAME"), + resource.TestCheckResourceAttr(resourceName, "topic_policy_config.0.topics_config.0.name", "investment_topic"), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.0.words_config.0.text", "HATE"), + ), + }, + { + Config: testAccGuardrailConfig_update(rName, "update", "update", "HIGH", "^\\d{4}-\\d{2}-\\d{4}$", "USERNAME", "earnings_topic", "HATRED"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGuardrailExists(ctx, resourceName, &guardrail), + resource.TestCheckResourceAttr(resourceName, "blocked_input_messaging", "update"), + resource.TestCheckResourceAttr(resourceName, "blocked_outputs_messaging", "update"), + resource.TestCheckResourceAttr(resourceName, "content_policy_config.0.filters_config.0.input_strength", "HIGH"), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.0.regexes_config.0.pattern", "^\\d{4}-\\d{2}-\\d{4}$"), + resource.TestCheckResourceAttr(resourceName, "sensitive_information_policy_config.0.pii_entities_config.0.type", "USERNAME"), + resource.TestCheckResourceAttr(resourceName, "topic_policy_config.0.topics_config.0.name", "earnings_topic"), + resource.TestCheckResourceAttr(resourceName, "word_policy_config.0.words_config.0.text", "HATRED"), + ), + }, + }, + }) +} + +func testAccGuardrailImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s,%s", rs.Primary.Attributes["guardrail_id"], rs.Primary.Attributes[names.AttrVersion]), nil + } +} + +func testAccCheckGuardrailDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).BedrockClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_bedrock_guardrail" { + continue + } + + id := rs.Primary.Attributes["guardrail_id"] + version := rs.Primary.Attributes[names.AttrVersion] + + _, err := tfbedrock.FindGuardrailByID(ctx, conn, id, version) + if errs.IsA[*types.ResourceNotFoundException](err) { + return nil + } + if err != nil { + return create.Error(names.Bedrock, create.ErrActionCheckingDestroyed, tfbedrock.ResNameGuardrail, rs.Primary.ID, err) + } + + return create.Error(names.Bedrock, create.ErrActionCheckingDestroyed, tfbedrock.ResNameGuardrail, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckGuardrailExists(ctx context.Context, name string, guardrail *bedrock.GetGuardrailOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.Bedrock, create.ErrActionCheckingExistence, tfbedrock.ResNameGuardrail, name, errors.New("not found")) + } + + id := rs.Primary.Attributes["guardrail_id"] + version := rs.Primary.Attributes[names.AttrVersion] + if id == "" { + return create.Error(names.Bedrock, create.ErrActionCheckingExistence, tfbedrock.ResNameGuardrail, name, errors.New("guardrail_id not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).BedrockClient(ctx) + + out, err := tfbedrock.FindGuardrailByID(ctx, conn, id, version) + if err != nil { + return create.Error(names.Bedrock, create.ErrActionCheckingExistence, tfbedrock.ResNameGuardrail, rs.Primary.ID, err) + } + + *guardrail = *out + + return nil + } +} + +func testAccGuardrailConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_bedrock_guardrail" "test" { + name = %[1]q + blocked_input_messaging = "test" + blocked_outputs_messaging = "test" + description = "test" + + content_policy_config { + filters_config { + input_strength = "MEDIUM" + output_strength = "MEDIUM" + type = "HATE" + } + filters_config { + input_strength = "HIGH" + output_strength = "HIGH" + type = "VIOLENCE" + } + } + + contextual_grounding_policy_config { + filters_config { + threshold = 0.4 + type = "GROUNDING" + } + } + + sensitive_information_policy_config { + pii_entities_config { + action = "BLOCK" + type = "NAME" + } + pii_entities_config { + action = "BLOCK" + type = "DRIVER_ID" + } + pii_entities_config { + action = "ANONYMIZE" + type = "USERNAME" + } + regexes_config { + action = "BLOCK" + description = "example regex" + name = "regex_example" + pattern = "^\\d{3}-\\d{2}-\\d{4}$" + } + } + + topic_policy_config { + topics_config { + name = "investment_topic" + examples = ["Where should I invest my money ?"] + type = "DENY" + definition = "Investment advice refers to inquiries, guidance, or recommendations regarding the management or allocation of funds or assets with the goal of generating returns ." + } + } + + word_policy_config { + managed_word_lists_config { + type = "PROFANITY" + } + words_config { + text = "HATE" + } + } +} +`, rName) +} + +func testAccGuardrailConfig_kmsKey(rName string) string { + return acctest.ConfigCompose( + testAccCustomModelConfig_base(rName), + fmt.Sprintf(` +resource "aws_kms_key" "test" { + description = %[1]q + deletion_window_in_days = 7 +} + +resource "aws_bedrock_guardrail" "test" { + name = %[1]q + blocked_input_messaging = "test" + blocked_outputs_messaging = "test" + description = "test" + kms_key_arn = aws_kms_key.test.arn + + content_policy_config { + filters_config { + input_strength = "MEDIUM" + output_strength = "MEDIUM" + type = "HATE" + } + filters_config { + input_strength = "HIGH" + output_strength = "HIGH" + type = "VIOLENCE" + } + } + + word_policy_config { + managed_word_lists_config { + type = "PROFANITY" + } + words_config { + text = "HATE" + } + } +} +`, rName)) +} + +func testAccGuardrailConfig_tags(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose( + testAccCustomModelConfig_base(rName), + fmt.Sprintf(` +resource "aws_kms_key" "test" { + description = %[1]q + deletion_window_in_days = 7 +} + +resource "aws_bedrock_guardrail" "test" { + name = %[1]q + blocked_input_messaging = "test" + blocked_outputs_messaging = "test" + description = "test" + kms_key_arn = aws_kms_key.test.arn + + content_policy_config { + filters_config { + input_strength = "MEDIUM" + output_strength = "MEDIUM" + type = "HATE" + } + filters_config { + input_strength = "HIGH" + output_strength = "HIGH" + type = "VIOLENCE" + } + } + + word_policy_config { + managed_word_lists_config { + type = "PROFANITY" + } + words_config { + text = "HATE" + } + } + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} + +func testAccGuardrailConfig_update(rName, blockedInputMessaging, blockedOutputMessaging, inputStrength, regexPattern, piiType, topicName, wordConfig string) string { + return fmt.Sprintf(` +resource "aws_bedrock_guardrail" "test" { + name = %[1]q + blocked_input_messaging = %[2]q + blocked_outputs_messaging = %[3]q + description = "test" + + content_policy_config { + filters_config { + input_strength = %[4]q + output_strength = "MEDIUM" + type = "HATE" + } + } + + sensitive_information_policy_config { + pii_entities_config { + action = "BLOCK" + type = %[6]q + } + regexes_config { + action = "BLOCK" + description = "example regex" + name = "regex_example" + pattern = %[5]q + } + } + + topic_policy_config { + topics_config { + name = %[7]q + examples = ["Where should I invest my money ?"] + type = "DENY" + definition = "Investment advice refers to inquiries, guidance, or recommendations regarding the management or allocation of funds or assets with the goal of generating returns ." + } + } + + word_policy_config { + managed_word_lists_config { + type = "PROFANITY" + } + words_config { + text = %[8]q + } + } +} +`, rName, blockedInputMessaging, blockedOutputMessaging, inputStrength, regexPattern, piiType, topicName, wordConfig) +} + +func testAccGuardrailConfig_wordConfig_only(rName string) string { + return acctest.ConfigCompose( + testAccCustomModelConfig_base(rName), + fmt.Sprintf(` +resource "aws_bedrock_guardrail" "test" { + name = %[1]q + blocked_input_messaging = "test" + blocked_outputs_messaging = "test" + description = "test" + + word_policy_config { + managed_word_lists_config { + type = "PROFANITY" + } + words_config { + text = "HATE" + } + } +} +`, rName)) +} diff --git a/internal/service/bedrock/service_package_gen.go b/internal/service/bedrock/service_package_gen.go index 1ee0b19ee2b0..2f9cc991caaf 100644 --- a/internal/service/bedrock/service_package_gen.go +++ b/internal/service/bedrock/service_package_gen.go @@ -55,6 +55,13 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic IdentifierAttribute: "provisioned_model_arn", }, }, + { + Factory: newResourceGuardrail, + Name: "Guardrail", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "guardrail_arn", + }, + }, } } diff --git a/website/docs/r/bedrock_guardrail.html.markdown b/website/docs/r/bedrock_guardrail.html.markdown new file mode 100644 index 000000000000..8aeefbfccf76 --- /dev/null +++ b/website/docs/r/bedrock_guardrail.html.markdown @@ -0,0 +1,184 @@ +--- +subcategory: "Bedrock" +layout: "aws" +page_title: "AWS: aws_bedrock_guardrail" +description: |- + Terraform resource for managing an Amazon Bedrock Guardrail. +--- + +# Resource: aws_bedrock_guardrail + +Terraform resource for managing an Amazon Bedrock Guardrail. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_bedrock_guardrail" "example" { + name = "example" + blocked_input_messaging = "example" + blocked_outputs_messaging = "example" + description = "example" + + content_policy_config { + filters_config { + input_strength = "MEDIUM" + output_strength = "MEDIUM" + type = "HATE" + } + } + + sensitive_information_policy_config { + pii_entities_config { + action = "BLOCK" + type = "NAME" + } + + regexes_config { + action = "BLOCK" + description = "example regex" + name = "regex_example" + pattern = "^\\d{3}-\\d{2}-\\d{4}$" + } + } + + topic_policy_config { + topics_config { + name = "investment_topic" + examples = ["Where should I invest my money ?"] + type = "DENY" + definition = "Investment advice refers to inquiries, guidance, or recommendations regarding the management or allocation of funds or assets with the goal of generating returns ." + } + } + + word_policy_config { + managed_word_lists_config { + type = "PROFANITY" + } + words_config { + text = "HATE" + } + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `blocked_input_messaging` - (Required) Message to return when the guardrail blocks a prompt. +* `blocked_outputs_messaging` - (Required) Message to return when the guardrail blocks a model response. +* `name` - (Required) Name of the guardrail. + +The following arguments are optional: + +* `content_policy_config` - (Optional) Content policy config for a guardrail. See [Content Policy Config](#content-policy-config) for more information. +* `contextual_grounding_policy_config` - (Optional) Contextual grounding policy config for a guardrail. See [Contextual Grounding Policy Config](#contextual-grounding-policy-config) for more information. +* `description` (Optional) Description of the guardrail or its version. +* `kms_key_arn` (Optional) The KMS key with which the guardrail was encrypted at rest. +* `sensitive_information_policy_config` (Optional) Sensitive information policy config for a guardrail. See [Sensitive Information Policy Config](#sensitive-information-policy-config) for more information. +* `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. +* `topic_policy_config` (Optional) Topic policy config for a guardrail. See [Topic Policy Config](#topic-policy-config) for more information. +* `word_policy_config` (Optional) Word policy config for a guardrail. See [Word Policy Config](#word-policy-config) for more information. + +### Content Policy Config + +The `content_policy_config` configuration block supports the following arguments: + +* `filters_config` - (Optional) List of content filter configs in content policy. See [Filters Config](#content-filters-config) for more information. + +#### Content Filters Config + +The `filters_config` configuration block supports the following arguments: + +* `input_strength` - (Optional) Strength for filters. +* `output_strength` - (Optional) Strength for filters. +* `type` - (Optional) Type of filter in content policy. + +### Contextual Grounding Policy Config + +* `filters_config` (Required) List of contextual grounding filter configs. See [Contextual Grounding Filters Config](#contextual-grounding-filters-config) for more information. + +#### Contextual Grounding Filters Config + +The `filters_config` configuration block supports the following arguments: + +* `threshold` - (Required) The threshold for this filter. +* `type` - (Required) Type of contextual grounding filter. + +### Topic Policy Config + +* `topics_config` (Required) List of topic configs in topic policy. See [Topics Config](#topics-config) for more information. + +#### Topics Config + +* `definition` (Required) Definition of topic in topic policy. +* `name` (Required) Name of topic in topic policy. +* `type` (Required) Type of topic in a policy. +* `examples` (Optional) List of text examples. + +### Sensitive Information Policy Config + +* `pii_entities_config` (Optional) List of entities. See [PII Entities Config](#pii-entities-config) for more information. +* `regexes_config` (Optional) List of regex. See [Regexes Config](#regexes-config) for more information. + +#### PII Entities Config + +* `action` (Required) Options for sensitive information action. +* `type` (Required) The currently supported PII entities. + +#### Regexes Config + +* `action` (Required) Options for sensitive information action. +* `name` (Required) The regex name. +* `pattern` (Required) The regex pattern. +* `description` (Optional) The regex description. + +### Word Policy Config + +* `managed_word_lists_config` (Optional) A config for the list of managed words. See [Managed Word Lists Config](#managed-word-lists-config) for more information. +* `words_config` (Optional) List of custom word configs. See [Words Config](#words-config) for more information. + +#### Managed Word Lists Config + +* `type` (Required) Options for managed words. + +#### Words Config + +* `text` (Required) The custom word text. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `created_at` - Unix epoch timestamp in seconds for when the Guardrail was created. +* `guardrail_arn` - ARN of the Guardrail. +* `guardrail_id` - ID of the Guardrail. +* `status` - Status of the Bedrock Guardrail. One of `READY`, `FAILED`. +* `version` - Version of the Guardrail. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `5m`) +* `update` - (Default `5m`) +* `delete` - (Default `5m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Amazon Bedrock Guardrail using a comma-delimited string of `guardrail_id` and `version`. For example: + +```terraform +import { + to = aws_bedrock_guardrail.example + id = "guardrail-id-12345678,DRAFT" +} +``` + +Using `terraform import`, import Amazon Bedrock Guardrail using using a comma-delimited string of `guardrail_id` and `version`. For example: + +```console +% terraform import aws_bedrock_guardrail.example guardrail-id-12345678,DRAFT +```