diff --git a/.changelog/46916.txt b/.changelog/46916.txt new file mode 100644 index 000000000000..1634d10aa78a --- /dev/null +++ b/.changelog/46916.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_bedrockagentcore_gateway_target: Add `target_configuration.mcp.api_gateway` configuration block +``` diff --git a/internal/service/bedrockagentcore/gateway_target.go b/internal/service/bedrockagentcore/gateway_target.go index 0c7a2a1f19d8..82a523ff5325 100644 --- a/internal/service/bedrockagentcore/gateway_target.go +++ b/internal/service/bedrockagentcore/gateway_target.go @@ -469,6 +469,68 @@ func (r *gatewayTargetResource) Schema(ctx context.Context, request resource.Sch }, NestedObject: schema.NestedBlockObject{ Blocks: map[string]schema.Block{ + "api_gateway": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[mcpApiGatewayConfigurationModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "rest_api_id": schema.StringAttribute{ + Required: true, + }, + names.AttrStage: schema.StringAttribute{ + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "api_gateway_tool_configuration": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[mcpApiGatewayToolConfigurationModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "tool_filter": schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[mcpApiGatewayToolFilterModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "filter_path": schema.StringAttribute{ + Required: true, + }, + "methods": schema.SetAttribute{ + ElementType: fwtypes.StringEnumType[awstypes.RestApiMethod](), + Required: true, + }, + }, + }, + }, + "tool_override": schema.SetNestedBlock{ + CustomType: fwtypes.NewSetNestedObjectTypeOf[mcpApiGatewayToolOverrideModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrDescription: schema.StringAttribute{ + Optional: true, + }, + "method": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.RestApiMethod](), + Required: true, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + }, + names.AttrPath: schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, "lambda": schema.ListNestedBlock{ CustomType: fwtypes.NewListNestedObjectTypeOf[mcpLambdaConfigurationModel](ctx), Validators: []validator.List{ @@ -1168,10 +1230,11 @@ func (m targetConfigurationModel) Expand(ctx context.Context) (any, diag.Diagnos } type mcpConfigurationModel struct { - Lambda fwtypes.ListNestedObjectValueOf[mcpLambdaConfigurationModel] `tfsdk:"lambda"` - MCPServer fwtypes.ListNestedObjectValueOf[mcpServerConfigurationModel] `tfsdk:"mcp_server"` - SmithyModel fwtypes.ListNestedObjectValueOf[apiSchemaConfigurationModel] `tfsdk:"smithy_model"` - OpenApiSchema fwtypes.ListNestedObjectValueOf[apiSchemaConfigurationModel] `tfsdk:"open_api_schema"` + ApiGateway fwtypes.ListNestedObjectValueOf[mcpApiGatewayConfigurationModel] `tfsdk:"api_gateway"` + Lambda fwtypes.ListNestedObjectValueOf[mcpLambdaConfigurationModel] `tfsdk:"lambda"` + MCPServer fwtypes.ListNestedObjectValueOf[mcpServerConfigurationModel] `tfsdk:"mcp_server"` + SmithyModel fwtypes.ListNestedObjectValueOf[apiSchemaConfigurationModel] `tfsdk:"smithy_model"` + OpenApiSchema fwtypes.ListNestedObjectValueOf[apiSchemaConfigurationModel] `tfsdk:"open_api_schema"` } var ( @@ -1182,6 +1245,14 @@ var ( func (m *mcpConfigurationModel) Flatten(ctx context.Context, v any) diag.Diagnostics { var diags diag.Diagnostics switch t := v.(type) { + case awstypes.McpTargetConfigurationMemberApiGateway: + var model mcpApiGatewayConfigurationModel + smerr.AddEnrich(ctx, &diags, fwflex.Flatten(ctx, t.Value, &model)) + if diags.HasError() { + return diags + } + m.ApiGateway = fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &model) + case awstypes.McpTargetConfigurationMemberLambda: var model mcpLambdaConfigurationModel smerr.AddEnrich(ctx, &diags, fwflex.Flatten(ctx, t.Value, &model)) @@ -1226,6 +1297,20 @@ func (m *mcpConfigurationModel) Flatten(ctx context.Context, v any) diag.Diagnos func (m mcpConfigurationModel) Expand(ctx context.Context) (any, diag.Diagnostics) { var diags diag.Diagnostics switch { + case !m.ApiGateway.IsNull(): + apiGatewayMCPConfigurationData, d := m.ApiGateway.ToPtr(ctx) + smerr.AddEnrich(ctx, &diags, d) + if diags.HasError() { + return nil, diags + } + + var r awstypes.McpTargetConfigurationMemberApiGateway + smerr.AddEnrich(ctx, &diags, fwflex.Expand(ctx, apiGatewayMCPConfigurationData, &r.Value)) + if diags.HasError() { + return nil, diags + } + return &r, diags + case !m.Lambda.IsNull(): lambdaMCPConfigurationData, d := m.Lambda.ToPtr(ctx) smerr.AddEnrich(ctx, &diags, d) @@ -1303,6 +1388,29 @@ type gatewayIAMRoleProviderModel struct { // Empty struct - Gateway IAM Role provider requires no configuration } +type mcpApiGatewayConfigurationModel struct { + ApiGatewayToolConfiguration fwtypes.ListNestedObjectValueOf[mcpApiGatewayToolConfigurationModel] `tfsdk:"api_gateway_tool_configuration"` + RestApiID types.String `tfsdk:"rest_api_id"` + Stage types.String `tfsdk:"stage"` +} + +type mcpApiGatewayToolConfigurationModel struct { + ToolFilter fwtypes.SetNestedObjectValueOf[mcpApiGatewayToolFilterModel] `tfsdk:"tool_filter"` + ToolOverride fwtypes.SetNestedObjectValueOf[mcpApiGatewayToolOverrideModel] `tfsdk:"tool_override"` +} + +type mcpApiGatewayToolFilterModel struct { + FilterPath types.String `tfsdk:"filter_path"` + Methods fwtypes.SetOfStringEnum[awstypes.RestApiMethod] `tfsdk:"methods"` +} + +type mcpApiGatewayToolOverrideModel struct { + Description types.String `tfsdk:"description"` + Method fwtypes.StringEnum[awstypes.RestApiMethod] `tfsdk:"method"` + Name types.String `tfsdk:"name"` + Path types.String `tfsdk:"path"` +} + type mcpLambdaConfigurationModel struct { LambdaArn types.String `tfsdk:"lambda_arn"` ToolSchema fwtypes.ListNestedObjectValueOf[toolSchemaModel] `tfsdk:"tool_schema"` diff --git a/internal/service/bedrockagentcore/gateway_target_test.go b/internal/service/bedrockagentcore/gateway_target_test.go index 7ea43ec1b6f1..d9dc8b86dde8 100644 --- a/internal/service/bedrockagentcore/gateway_target_test.go +++ b/internal/service/bedrockagentcore/gateway_target_test.go @@ -286,6 +286,102 @@ func TestAccBedrockAgentCoreGatewayTarget_targetConfigurationMCPServer(t *testin }) } +func TestAccBedrockAgentCoreGatewayTarget_targetConfigurationAPIGateway(t *testing.T) { + ctx := acctest.Context(t) + var gatewayTarget bedrockagentcorecontrol.GetGatewayTargetOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_bedrockagentcore_gateway_target.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.BedrockEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.BedrockAgentCoreServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckGatewayTargetDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccGatewayTargetConfig_targetConfigurationAPIGateway(rName, ""), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckGatewayTargetExists(ctx, t, resourceName, &gatewayTarget), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "target_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "target_configuration.0.mcp.0.api_gateway.0.rest_api_id", "aws_api_gateway_rest_api.test", names.AttrID), + resource.TestCheckResourceAttrPair(resourceName, "target_configuration.0.mcp.0.api_gateway.0.stage", "aws_api_gateway_stage.test", "stage_name"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.filter_path", "/pets"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.methods.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.methods.*", "GET"), + resource.TestCheckTypeSetElemAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.methods.*", "POST"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_override.*", map[string]string{ + names.AttrName: "ListPets", + names.AttrPath: "/pets", + "method": "GET", + names.AttrDescription: "Retrieves all available pets", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_override.*", map[string]string{ + names.AttrName: "RegisterPets", + names.AttrPath: "/pets", + "method": "POST", + names.AttrDescription: "Register pets", + }), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, ",", "gateway_identifier", "target_id"), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "target_id", + }, + { + Config: testAccGatewayTargetConfig_targetConfigurationAPIGateway(rName, "2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckGatewayTargetExists(ctx, t, resourceName, &gatewayTarget), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "target_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "target_configuration.0.mcp.0.api_gateway.0.rest_api_id", "aws_api_gateway_rest_api.test", names.AttrID), + resource.TestCheckResourceAttrPair(resourceName, "target_configuration.0.mcp.0.api_gateway.0.stage", "aws_api_gateway_stage.test", "stage_name"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.#", "1"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.filter_path", "/pets"), + resource.TestCheckResourceAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.methods.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.methods.*", "GET"), + resource.TestCheckTypeSetElemAttr(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_filter.0.methods.*", "POST"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_override.*", map[string]string{ + names.AttrName: "ListPets2", + names.AttrPath: "/pets", + "method": "GET", + names.AttrDescription: "Retrieves all available pets2", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "target_configuration.0.mcp.0.api_gateway.0.api_gateway_tool_configuration.0.tool_override.*", map[string]string{ + names.AttrName: "RegisterPets2", + names.AttrPath: "/pets", + "method": "POST", + names.AttrDescription: "Register pets2", + }), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + }, + }) +} + func TestAccBedrockAgentCoreGatewayTarget_credentialProvider(t *testing.T) { ctx := acctest.Context(t) var gatewayTarget, gatewayTargetPrev bedrockagentcorecontrol.GetGatewayTargetOutput @@ -1175,3 +1271,108 @@ resource "aws_bedrockagentcore_gateway_target" "test" { } `, rName, headerName)) } + +func testAccGatewayTargetConfig_targetConfigurationAPIGateway(rName, toolOverrideSuffix string) string { + return acctest.ConfigCompose(testAccGatewayTargetConfig_infra(rName), fmt.Sprintf(` +resource "aws_api_gateway_rest_api" "test" { + name = %[1]q +} + +resource "aws_api_gateway_resource" "pets" { + rest_api_id = aws_api_gateway_rest_api.test.id + parent_id = aws_api_gateway_rest_api.test.root_resource_id + path_part = "pets" +} + +resource "aws_api_gateway_method" "get_pets" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.pets.id + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_method" "post_pets" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.pets.id + http_method = "POST" + authorization = "NONE" +} + +resource "aws_api_gateway_method_response" "get_pets_200" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.pets.id + http_method = aws_api_gateway_method.get_pets.http_method + status_code = "200" +} + +resource "aws_api_gateway_method_response" "post_pets_200" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.pets.id + http_method = aws_api_gateway_method.post_pets.http_method + status_code = "200" +} + +resource "aws_api_gateway_integration" "get_pets" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.pets.id + http_method = aws_api_gateway_method.get_pets.http_method + type = "MOCK" +} + +resource "aws_api_gateway_integration" "post_pets" { + rest_api_id = aws_api_gateway_rest_api.test.id + resource_id = aws_api_gateway_resource.pets.id + http_method = aws_api_gateway_method.post_pets.http_method + type = "MOCK" +} + +resource "aws_api_gateway_deployment" "test" { + rest_api_id = aws_api_gateway_rest_api.test.id + depends_on = [ + aws_api_gateway_integration.get_pets, + aws_api_gateway_integration.post_pets, + ] +} + +resource "aws_api_gateway_stage" "test" { + stage_name = "prod" + rest_api_id = aws_api_gateway_rest_api.test.id + deployment_id = aws_api_gateway_deployment.test.id +} + +resource "aws_bedrockagentcore_gateway_target" "test" { + name = %[1]q + gateway_identifier = aws_bedrockagentcore_gateway.test.gateway_id + + target_configuration { + mcp { + api_gateway { + rest_api_id = aws_api_gateway_rest_api.test.id + stage = aws_api_gateway_stage.test.stage_name + + api_gateway_tool_configuration { + tool_filter { + filter_path = "/pets" + methods = ["GET", "POST"] + } + + tool_override { + name = "ListPets%[2]s" + path = "/pets" + method = "GET" + description = "Retrieves all available pets%[2]s" + } + + tool_override { + name = "RegisterPets%[2]s" + path = "/pets" + method = "POST" + description = "Register pets%[2]s" + } + } + } + } + } +} +`, rName, toolOverrideSuffix)) +} diff --git a/website/docs/r/bedrockagentcore_gateway_target.html.markdown b/website/docs/r/bedrockagentcore_gateway_target.html.markdown index 78e3d9b22992..05ab99552e9f 100644 --- a/website/docs/r/bedrockagentcore_gateway_target.html.markdown +++ b/website/docs/r/bedrockagentcore_gateway_target.html.markdown @@ -376,11 +376,43 @@ The `target_configuration` block supports the following: The `mcp` block supports exactly one of the following: +* `api_gateway` - (Optional) API Gateway target configuration. See [`api_gateway`](#api_gateway) below. * `lambda` - (Optional) Lambda function target configuration. See [`lambda`](#lambda) below. * `mcp_server` - (Optional) MCP server target configuration. See [`mcp_server`](#mcp_server) below. * `open_api_schema` - (Optional) OpenAPI schema-based target configuration. See [`api_schema_configuration`](#api_schema_configuration) below. * `smithy_model` - (Optional) Smithy model-based target configuration. See [`api_schema_configuration`](#api_schema_configuration) below. +### `api_gateway` + +The `api_gateway` block supports the following: + +* `api_gateway_tool_configuration` - (Required) Configuration for API Gateway tools. See [`api_gateway_tool_configuration`](#api_gateway_tool_configuration) below. +* `rest_api_id` - (Required) ID of the API Gateway REST API to invoke. +* `stage` - (Required) Stage name of the REST API to add as a target. + +### `api_gateway_tool_configuration` + +The `api_gateway_tool_configuration` block supports the following: + +* `tool_filter` - (Required) Repeatable block of path and method patterns to expose as tools. See [`tool_filter`](#tool_filter) below. +* `tool_override` - (Required) Repeatable block of explicit tool definitions with optional custom names and descriptions. See [`tool_override`](#tool_override) below. + +### `tool_filter` + +The `tool_filter` block supports the following: + +* `filter_path` - (Required) Resource path to match in the REST API. Supports exact paths (for example, `/pets`) or wildcard paths (for example, `/pets/*` to match all paths under `/pets`). Must match existing paths in the REST API. +* `methods` - (Required) List of HTTP methods to filter for. Valid values: `GET`, `DELETE`, `HEAD`, `OPTIONS`, `PATCH`, `PUT` and `POST`. + +### `tool_override` + +The `tool_override` block supports the following: + +* `description` - (Optional) Description of the tool. Provides information about the purpose and usage of the tool. If not provided, uses the description from the API's OpenAPI specification. +* `method` - (Required) HTTP method to expose for the specified path. Valid values: `GET`, `DELETE`, `HEAD`, `OPTIONS`, `PATCH`, `PUT` and `POST`. +* `name` - (Optional) Name of tool. Identifies the tool in the Model Context Protocol. +* `path` - (Required) Resource path in the REST API (e.g., `/pets`). Must explicitly match an existing path in the REST API. + ### `lambda` The `lambda` block supports the following: