diff --git a/.changelog/36905.txt b/.changelog/36905.txt new file mode 100644 index 000000000000..a1f06ed277b3 --- /dev/null +++ b/.changelog/36905.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_bedrockagent_agent_alias +``` \ No newline at end of file diff --git a/internal/service/bedrockagent/agent.go b/internal/service/bedrockagent/agent.go index 75bb4d13010a..4d0e40931e87 100644 --- a/internal/service/bedrockagent/agent.go +++ b/internal/service/bedrockagent/agent.go @@ -9,6 +9,7 @@ import ( "fmt" "time" + "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/bedrockagent" awstypes "github.com/aws/aws-sdk-go-v2/service/bedrockagent/types" @@ -69,6 +70,9 @@ func (r *agentResource) Schema(ctx context.Context, request resource.SchemaReque "agent_id": framework.IDAttribute(), "agent_name": schema.StringAttribute{ Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^([0-9a-zA-Z][_-]?){1,100}$`), "valid characters are a-z, A-Z, 0-9, _ (underscore) and - (hyphen). The name can have up to 100 characters"), + }, }, "agent_resource_role_arn": schema.StringAttribute{ CustomType: fwtypes.ARNType, @@ -438,7 +442,7 @@ func waitAgentCreated(ctx context.Context, conn *bedrockagent.Client, id string, outputRaw, err := stateConf.WaitForStateContext(ctx) if output, ok := outputRaw.(*awstypes.Agent); ok { - tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, func(s string) error { return errors.New(s) })...)) + tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, errors.New)...)) return output, err } @@ -457,7 +461,7 @@ func waitAgentUpdated(ctx context.Context, conn *bedrockagent.Client, id string, outputRaw, err := stateConf.WaitForStateContext(ctx) if output, ok := outputRaw.(*awstypes.Agent); ok { - tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, func(s string) error { return errors.New(s) })...)) + tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, errors.New)...)) return output, err } @@ -476,7 +480,26 @@ func waitAgentPrepared(ctx context.Context, conn *bedrockagent.Client, id string outputRaw, err := stateConf.WaitForStateContext(ctx) if output, ok := outputRaw.(*awstypes.Agent); ok { - tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, func(s string) error { return errors.New(s) })...)) + tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, errors.New)...)) + + return output, err + } + + return nil, err +} + +func waitAgentVersioned(ctx context.Context, conn *bedrockagent.Client, id string, timeout time.Duration) (*awstypes.Agent, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.AgentStatusVersioning), + Target: enum.Slice(awstypes.AgentStatusPrepared), + Refresh: statusAgent(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.Agent); ok { + tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, errors.New)...)) return output, err } @@ -495,7 +518,7 @@ func waitAgentDeleted(ctx context.Context, conn *bedrockagent.Client, id string, outputRaw, err := stateConf.WaitForStateContext(ctx) if output, ok := outputRaw.(*awstypes.Agent); ok { - tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, func(s string) error { return errors.New(s) })...)) + tfresource.SetLastError(err, errors.Join(tfslices.ApplyToAll(output.FailureReasons, errors.New)...)) return output, err } diff --git a/internal/service/bedrockagent/agent_alias.go b/internal/service/bedrockagent/agent_alias.go new file mode 100644 index 000000000000..2af0e6ce90da --- /dev/null +++ b/internal/service/bedrockagent/agent_alias.go @@ -0,0 +1,385 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bedrockagent + +import ( + "context" + "fmt" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/bedrockagent" + awstypes "github.com/aws/aws-sdk-go-v2/service/bedrockagent/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "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="Agent Alias") +// @Tags(identifierAttribute="agent_alias_arn") +func newAgentAliasResource(context.Context) (resource.ResourceWithConfigure, error) { + r := &agentAliasResource{} + + r.SetDefaultCreateTimeout(5 * time.Minute) + r.SetDefaultUpdateTimeout(5 * time.Minute) + r.SetDefaultDeleteTimeout(5 * time.Minute) + + return r, nil +} + +type agentAliasResource struct { + framework.ResourceWithConfigure + framework.WithImportByID + framework.WithTimeouts +} + +func (*agentAliasResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_bedrockagent_agent_alias" +} + +func (r *agentAliasResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "agent_alias_arn": framework.ARNAttributeComputedOnly(), + "agent_alias_id": framework.IDAttribute(), + "agent_alias_name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches(regexache.MustCompile(`^([0-9a-zA-Z][_-]?){1,100}$`), "valid characters are a-z, A-Z, 0-9, _ (underscore) and - (hyphen). The name can have up to 100 characters"), + }, + }, + "agent_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 200), + }, + }, + names.AttrID: framework.IDAttribute(), + "routing_configuration": schema.ListAttribute{ + CustomType: fwtypes.NewListNestedObjectTypeOf[agentAliasRoutingConfigurationListItemModel](ctx), + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + ElementType: types.ObjectType{ + AttrTypes: fwtypes.AttributeTypesMust[agentAliasRoutingConfigurationListItemModel](ctx), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *agentAliasResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data agentAliasResourceModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BedrockAgentClient(ctx) + + input := &bedrockagent.CreateAgentAliasInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + if response.Diagnostics.HasError() { + return + } + + input.ClientToken = aws.String(id.UniqueId()) + input.Tags = getTagsIn(ctx) + + output, err := conn.CreateAgentAlias(ctx, input) + + if err != nil { + response.Diagnostics.AddError("creating Bedrock Agent Alias", err.Error()) + + return + } + + // Set values for unknowns. + data.AgentAliasID = fwflex.StringToFramework(ctx, output.AgentAlias.AgentAliasId) + data.setID() + + alias, err := waitAgentAliasCreated(ctx, conn, data.AgentAliasID.ValueString(), data.AgentID.ValueString(), r.CreateTimeout(ctx, data.Timeouts)) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Bedrock Agent Alias (%s) create", data.ID.ValueString()), err.Error()) + + return + } + + if _, err := waitAgentVersioned(ctx, conn, data.AgentID.ValueString(), r.CreateTimeout(ctx, data.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Bedrock Agent (%s) version", data.ID.ValueString()), err.Error()) + + return + } + + // Set values for unknowns. + response.Diagnostics.Append(fwflex.Flatten(ctx, alias, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *agentAliasResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data agentAliasResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + if err := data.InitFromID(); err != nil { + response.Diagnostics.AddError("parsing resource ID", err.Error()) + + return + } + + conn := r.Meta().BedrockAgentClient(ctx) + + output, err := findAgentAliasByTwoPartKey(ctx, conn, data.AgentAliasID.ValueString(), data.AgentID.ValueString()) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Bedrock Agent Alias (%s)", data.ID.String()), err.Error()) + + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *agentAliasResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var old, new agentAliasResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &old)...) + if response.Diagnostics.HasError() { + return + } + response.Diagnostics.Append(request.Plan.Get(ctx, &new)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BedrockAgentClient(ctx) + + if !new.AgentAliasName.Equal(old.AgentAliasName) || + !new.Description.Equal(old.Description) || + !new.RoutingConfiguration.Equal(old.RoutingConfiguration) { + input := &bedrockagent.UpdateAgentAliasInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, new, input)...) + if response.Diagnostics.HasError() { + return + } + + _, err := conn.UpdateAgentAlias(ctx, input) + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("reading Bedrock Agent Alias (%s)", new.ID.String()), err.Error()) + + return + } + + if _, err := waitAgentAliasUpdated(ctx, conn, new.AgentAliasID.ValueString(), new.AgentID.ValueString(), r.CreateTimeout(ctx, new.Timeouts)); err != nil { + response.Diagnostics.AddError(fmt.Sprintf("waiting for Bedrock Agent Alias (%s) update", new.ID.ValueString()), err.Error()) + + return + } + } + + response.Diagnostics.Append(response.State.Set(ctx, &new)...) +} + +func (r *agentAliasResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data agentAliasResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().BedrockAgentClient(ctx) + + _, err := conn.DeleteAgentAlias(ctx, &bedrockagent.DeleteAgentAliasInput{ + AgentAliasId: fwflex.StringFromFramework(ctx, data.AgentAliasID), + AgentId: fwflex.StringFromFramework(ctx, data.AgentID), + }) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + if err != nil { + response.Diagnostics.AddError(fmt.Sprintf("deleting Bedrock Agent Alias (%s)", data.ID.ValueString()), err.Error()) + + return + } +} + +func (r *agentAliasResource) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +func findAgentAliasByTwoPartKey(ctx context.Context, conn *bedrockagent.Client, agentAliasID, agentID string) (*awstypes.AgentAlias, error) { + input := &bedrockagent.GetAgentAliasInput{ + AgentAliasId: aws.String(agentAliasID), + AgentId: aws.String(agentID), + } + + output, err := conn.GetAgentAlias(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.AgentAlias == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.AgentAlias, nil +} + +func statusAgentAlias(ctx context.Context, conn *bedrockagent.Client, agentAliasID, agentID string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findAgentAliasByTwoPartKey(ctx, conn, agentAliasID, agentID) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.AgentAliasStatus), nil + } +} + +func waitAgentAliasCreated(ctx context.Context, conn *bedrockagent.Client, agentAliasID, agentID string, timeout time.Duration) (*awstypes.AgentAlias, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.AgentAliasStatusCreating), + Target: enum.Slice(awstypes.AgentAliasStatusPrepared), + Refresh: statusAgentAlias(ctx, conn, agentAliasID, agentID), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.AgentAlias); ok { + return output, err + } + + return nil, err +} + +func waitAgentAliasUpdated(ctx context.Context, conn *bedrockagent.Client, agentAliasID, agentID string, timeout time.Duration) (*awstypes.AgentAlias, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.AgentAliasStatusUpdating), + Target: enum.Slice(awstypes.AgentAliasStatusPrepared), + Refresh: statusAgentAlias(ctx, conn, agentAliasID, agentID), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.AgentAlias); ok { + return output, err + } + + return nil, err +} + +type agentAliasResourceModel struct { + AgentAliasARN types.String `tfsdk:"agent_alias_arn"` + AgentAliasID types.String `tfsdk:"agent_alias_id"` + AgentAliasName types.String `tfsdk:"agent_alias_name"` + AgentID types.String `tfsdk:"agent_id"` + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + RoutingConfiguration fwtypes.ListNestedObjectValueOf[agentAliasRoutingConfigurationListItemModel] `tfsdk:"routing_configuration"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +const ( + agentAliasResourceIDPartCount = 2 +) + +func (m *agentAliasResourceModel) InitFromID() error { + id := m.ID.ValueString() + parts, err := flex.ExpandResourceId(id, agentAliasResourceIDPartCount, false) + + if err != nil { + return err + } + + m.AgentAliasID = types.StringValue(parts[0]) + m.AgentID = types.StringValue(parts[1]) + + return nil +} + +func (m *agentAliasResourceModel) setID() { + m.ID = types.StringValue(errs.Must(flex.FlattenResourceId([]string{m.AgentAliasID.ValueString(), m.AgentID.ValueString()}, agentAliasResourceIDPartCount, false))) +} + +type agentAliasRoutingConfigurationListItemModel struct { + AgentVersion types.String `tfsdk:"agent_version"` +} diff --git a/internal/service/bedrockagent/agent_alias_test.go b/internal/service/bedrockagent/agent_alias_test.go new file mode 100644 index 000000000000..f78ccb8d6978 --- /dev/null +++ b/internal/service/bedrockagent/agent_alias_test.go @@ -0,0 +1,358 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bedrockagent_test + +import ( + "context" + "fmt" + "testing" + + awstypes "github.com/aws/aws-sdk-go-v2/service/bedrockagent/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" + tfbedrockagent "github.com/hashicorp/terraform-provider-aws/internal/service/bedrockagent" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccBedrockAgentAlias_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrockagent_agent_alias.test" + var v awstypes.AgentAlias + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockAgentServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAgentAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAgentAliasConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "agent_alias_name", rName), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_arn"), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_id"), + resource.TestCheckResourceAttrSet(resourceName, "agent_id"), + resource.TestCheckResourceAttrSet(resourceName, "description"), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccBedrockAgentAlias_disappears(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrockagent_agent_alias.test" + var v awstypes.AgentAlias + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockAgentServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAgentAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAgentAliasConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfbedrockagent.ResourceAgentAlias, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccBedrockAgentAlias_update(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + descriptionOld := "Agent Alias Before Update" + descriptionNew := "Agent Alias After Update" + resourceName := "aws_bedrockagent_agent_alias.test" + var v awstypes.AgentAlias + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockAgentServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAgentAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAgentAliasConfig_update(rName, descriptionOld), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "agent_alias_name", rName), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_arn"), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_id"), + resource.TestCheckResourceAttrSet(resourceName, "agent_id"), + resource.TestCheckResourceAttr(resourceName, "description", descriptionOld), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.0.agent_version", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAgentAliasConfig_update(rName, descriptionNew), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "agent_alias_name", rName), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_arn"), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_id"), + resource.TestCheckResourceAttrSet(resourceName, "agent_id"), + resource.TestCheckResourceAttr(resourceName, "description", descriptionNew), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.0.agent_version", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccBedrockAgentAlias_routingUpdate(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrockagent_agent_alias.test" + updatedVersion := "2" + var v awstypes.AgentAlias + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockAgentServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAgentAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccagentAliasConfig_routingUpdateOne(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "agent_alias_name", rName), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_arn"), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_id"), + resource.TestCheckResourceAttrSet(resourceName, "agent_id"), + resource.TestCheckResourceAttr(resourceName, "description", "Test ALias"), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.0.agent_version", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccagentAliasConfig_routingUpdateTwo(rName, updatedVersion), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "agent_alias_name", rName), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_arn"), + resource.TestCheckResourceAttrSet(resourceName, "agent_alias_id"), + resource.TestCheckResourceAttrSet(resourceName, "agent_id"), + resource.TestCheckResourceAttr(resourceName, "description", "Test ALias"), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "routing_configuration.0.agent_version", updatedVersion), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + }, + }) +} + +func TestAccBedrockAgentAlias_tags(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_bedrockagent_agent_alias.test" + var v awstypes.AgentAlias + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockAgentServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAgentAliasDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAgentAliasConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAgentAliasConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAgentAliasConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAgentAliasExists(ctx, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckAgentAliasDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).BedrockAgentClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_bedrockagent_agent_alias" { + continue + } + + _, err := tfbedrockagent.FindAgentAliasByTwoPartKey(ctx, conn, rs.Primary.Attributes["agent_alias_id"], rs.Primary.Attributes["agent_id"]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Bedrock Agent Alias %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckAgentAliasExists(ctx context.Context, n string, v *awstypes.AgentAlias) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).BedrockAgentClient(ctx) + + output, err := tfbedrockagent.FindAgentAliasByTwoPartKey(ctx, conn, rs.Primary.Attributes["agent_alias_id"], rs.Primary.Attributes["agent_id"]) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccAgentAliasConfig_basic(rName string) string { + return acctest.ConfigCompose(testAccAgentConfig_basic(rName, "anthropic.claude-v2", "basic claude"), testAccAgentAliasConfig_alias(rName)) +} + +func testAccAgentAliasConfig_alias(name string) string { + return fmt.Sprintf(` +resource "aws_bedrockagent_agent_alias" "test" { + agent_alias_name = %[1]q + agent_id = aws_bedrockagent_agent.test.agent_id + description = "Test ALias" +} +`, name) +} + +func testAccAgentAliasConfig_routing(name, version string) string { + return fmt.Sprintf(` +resource "aws_bedrockagent_agent_alias" "test" { + agent_alias_name = %[1]q + agent_id = aws_bedrockagent_agent.test.agent_id + description = "Test ALias" + routing_configuration { + agent_version = %[2]q + } +} +`, name, version) +} + +func testAccagentAliasConfig_routingUpdateOne(rName string) string { + return acctest.ConfigCompose( + testAccAgentConfig_basic(rName, "anthropic.claude-v2", "basic claude"), + testAccAgentAliasConfig_alias(rName), + fmt.Sprintf(` +resource "aws_bedrockagent_agent_alias" "second" { + agent_alias_name = %[1]q + agent_id = aws_bedrockagent_agent.test.agent_id + description = "Test ALias" + depends_on = [aws_bedrockagent_agent_alias.test] +} +`, rName+"2"), + ) +} + +func testAccagentAliasConfig_routingUpdateTwo(rName, version string) string { + return acctest.ConfigCompose( + testAccAgentConfig_basic(rName, "anthropic.claude-v2", "basic claude"), + testAccAgentAliasConfig_routing(rName, version), + ) +} + +func testAccAgentAliasConfig_update(rName, desc string) string { + return acctest.ConfigCompose(testAccAgentConfig_basic(rName, "anthropic.claude-v2", "basic claude"), fmt.Sprintf(` +resource "aws_bedrockagent_agent_alias" "test" { + agent_alias_name = %[1]q + agent_id = aws_bedrockagent_agent.test.agent_id + description = %[2]q +} +`, rName, desc)) +} + +func testAccAgentAliasConfig_tags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose(testAccAgentConfig_basic(rName, "anthropic.claude-v2", "basic claude"), fmt.Sprintf(` +resource "aws_bedrockagent_agent_alias" "test" { + agent_alias_name = %[1]q + agent_id = aws_bedrockagent_agent.test.agent_id + description = "Test ALias" + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1)) +} + +func testAccAgentAliasConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose(testAccAgentConfig_basic(rName, "anthropic.claude-v2", "basic claude"), fmt.Sprintf(` +resource "aws_bedrockagent_agent_alias" "test" { + agent_alias_name = %[1]q + agent_id = aws_bedrockagent_agent.test.agent_id + description = "Test ALias" + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} diff --git a/internal/service/bedrockagent/agent_test.go b/internal/service/bedrockagent/agent_test.go index 43e8a6405862..456819fb28ee 100644 --- a/internal/service/bedrockagent/agent_test.go +++ b/internal/service/bedrockagent/agent_test.go @@ -5,21 +5,16 @@ package bedrockagent_test import ( "context" - "errors" "fmt" "testing" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/bedrockagent" awstypes "github.com/aws/aws-sdk-go-v2/service/bedrockagent/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" 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" + tfbedrockagent "github.com/hashicorp/terraform-provider-aws/internal/service/bedrockagent" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -28,7 +23,7 @@ func TestAccBedrockAgent_basic(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_bedrockagent_agent.test" - var v bedrockagent.GetAgentOutput + var v awstypes.Agent resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, @@ -58,7 +53,7 @@ func TestAccBedrockAgent_full(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_bedrockagent_agent.test" - var v bedrockagent.GetAgentOutput + var v awstypes.Agent resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, @@ -88,7 +83,7 @@ func TestAccBedrockAgent_update(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_bedrockagent_agent.test" - var v bedrockagent.GetAgentOutput + var v awstypes.Agent resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, @@ -136,7 +131,7 @@ func TestAccBedrockAgent_tags(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_bedrockagent_agent.test" - var agent bedrockagent.GetAgentOutput + var agent awstypes.Agent resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) }, @@ -187,23 +182,24 @@ func testAccCheckAgentDestroy(ctx context.Context) resource.TestCheckFunc { continue } - _, err := findAgentByID(ctx, conn, rs.Primary.ID) + _, err := tfbedrockagent.FindAgentByID(ctx, conn, rs.Primary.ID) - if errs.IsA[*awstypes.ResourceNotFoundException](err) { - return nil + if tfresource.NotFound(err) { + continue } + if err != nil { - return create.Error(names.BedrockAgent, create.ErrActionCheckingDestroyed, "Bedrock Agent", rs.Primary.ID, err) + return err } - return create.Error(names.BedrockAgent, create.ErrActionCheckingDestroyed, "Bedrock Agent", rs.Primary.ID, errors.New("not destroyed")) + return fmt.Errorf("Bedrock Agent %s still exists", rs.Primary.ID) } return nil } } -func testAccCheckAgentExists(ctx context.Context, n string, v *bedrockagent.GetAgentOutput) resource.TestCheckFunc { +func testAccCheckAgentExists(ctx context.Context, n string, v *awstypes.Agent) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -212,7 +208,7 @@ func testAccCheckAgentExists(ctx context.Context, n string, v *bedrockagent.GetA conn := acctest.Provider.Meta().(*conns.AWSClient).BedrockAgentClient(ctx) - output, err := findAgentByID(ctx, conn, rs.Primary.ID) + output, err := tfbedrockagent.FindAgentByID(ctx, conn, rs.Primary.ID) if err != nil { return err @@ -224,31 +220,6 @@ func testAccCheckAgentExists(ctx context.Context, n string, v *bedrockagent.GetA } } -func findAgentByID(ctx context.Context, conn *bedrockagent.Client, id string) (*bedrockagent.GetAgentOutput, error) { - input := &bedrockagent.GetAgentInput{ - AgentId: aws.String(id), - } - - output, err := conn.GetAgent(ctx, input) - - if errs.IsA[*awstypes.ResourceNotFoundException](err) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if output == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return output, nil -} - func testAccAgent_base(rName, model string) string { return fmt.Sprintf(` resource "aws_iam_role" "test" { diff --git a/internal/service/bedrockagent/exports_test.go b/internal/service/bedrockagent/exports_test.go index fab1595f09d3..120eb53070a3 100644 --- a/internal/service/bedrockagent/exports_test.go +++ b/internal/service/bedrockagent/exports_test.go @@ -5,5 +5,9 @@ package bedrockagent // Exports for use in tests only. var ( - ResourceAgent = newAgentResource + ResourceAgent = newAgentResource + ResourceAgentAlias = newAgentAliasResource + + FindAgentAliasByTwoPartKey = findAgentAliasByTwoPartKey + FindAgentByID = findAgentByID ) diff --git a/internal/service/bedrockagent/service_package_gen.go b/internal/service/bedrockagent/service_package_gen.go index 58bdf183ccd4..375197ba2b5b 100644 --- a/internal/service/bedrockagent/service_package_gen.go +++ b/internal/service/bedrockagent/service_package_gen.go @@ -20,6 +20,13 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { return []*types.ServicePackageFrameworkResource{ + { + Factory: newAgentAliasResource, + Name: "Agent Alias", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "agent_alias_arn", + }, + }, { Factory: newAgentResource, Name: "Agent", diff --git a/website/docs/r/bedrockagent_agent_alias.html.markdown b/website/docs/r/bedrockagent_agent_alias.html.markdown new file mode 100644 index 000000000000..77664c8e6980 --- /dev/null +++ b/website/docs/r/bedrockagent_agent_alias.html.markdown @@ -0,0 +1,124 @@ +--- +subcategory: "Agents for Amazon Bedrock" +layout: "aws" +page_title: "AWS: aws_bedrockagent_agent_alias" +description: |- + Terraform resource for managing an AWS Agents for Amazon Bedrock Agent Alias. +--- +# Resource: aws_bedrockagent_agent_alias + +Terraform resource for managing an AWS Agents for Amazon Bedrock Agent Alias. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_iam_role" "example" { + assume_role_policy = data.aws_iam_policy_document.example_agent_trust.json + name_prefix = "AmazonBedrockExecutionRoleForAgents_" +} + +data "aws_iam_policy_document" "example_agent_trust" { + statement { + actions = ["sts:AssumeRole"] + principals { + identifiers = ["bedrock.amazonaws.com"] + type = "Service" + } + condition { + test = "StringEquals" + values = [data.aws_caller_identity.current.account_id] + variable = "aws:SourceAccount" + } + + condition { + test = "ArnLike" + values = ["arn:aws:bedrock:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:agent/*"] + variable = "AWS:SourceArn" + } + } +} + +data "aws_iam_policy_document" "example_agent_permissions" { + statement { + actions = ["bedrock:InvokeModel"] + resources = [ + "arn:aws:bedrock:${data.aws_region.current.name}::foundation-model/anthropic.claude-v2", + ] + } +} + +resource "aws_iam_role_policy" "example" { + policy = data.aws_iam_policy_document.example_agent_permissions.json + role = aws_iam_role.example.id +} + +data "aws_caller_identity" "current" {} + +data "aws_region" "current" {} + +resource "aws_bedrockagent_agent" "test" { + agent_name = "my-agent-name" + agent_resource_role_arn = aws_iam_role.example.arn + idle_ttl = 500 + foundation_model = "anthropic.claude-v2" +} +resource "aws_bedrockagent_agent_alias" "example" { + agent_alias_name = "my-agent-alias" + agent_id = aws_bedrockagent_agent.test.agent_id + description = "Test ALias" +} +``` + +## Argument Reference + +The following arguments are required: + +* `agent_alias_name` - (Required) Name of the alias. +* `agent_id` - (Required) Identifier of the agent to create an alias for. +* `tags` - (Optional) Key-value tags for the place index. 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. + +The following arguments are optional: + +* `description` - (Optional) Description of the alias of the agent. +* `routing_configuration` - (Optional) Routing configuration of the alias + +### routing_configuration + +This argument is processed in [attribute-as-blocks mode](https://www.terraform.io/docs/configuration/attr-as-blocks.html). + +The following arguments are required: + +* `agent_version` - (Required) Version of the agent the alias routes to. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `agent_alias_arn` - ARN of the Agent Alias. + +## 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 Agents for Amazon Bedrock Agent Alias using the `ABCDE12345,FGHIJ67890`. For example: + +```terraform +import { + to = aws_bedrockagent_agent_alias.example + id = "ABCDE12345,FGHIJ67890" +} +``` + +Using `terraform import`, import Agents for Amazon Bedrock Agent Alias using the `AGENT_ID,ALIAS_ID`. For example: + +```console +% terraform import aws_bedrockagent_agent_alias.example AGENT_ID,ALIAS_ID +```