mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 11:21:49 +00:00
refactor: move AWS REST-JSON router to ingress/aws/rest
This commit is contained in:
@@ -273,23 +273,23 @@ func resolveNonQueryAction(r *http.Request, service string) string {
|
||||
method, path := r.Method, r.URL.EscapedPath()
|
||||
switch service {
|
||||
case "eks":
|
||||
if action, _, _, ok := eksRouter.lookup(method, path); ok {
|
||||
if action, _, _, ok := eksRouter.Lookup(method, path); ok {
|
||||
return action
|
||||
}
|
||||
case "bedrock":
|
||||
if action, _, _, ok := bedrockRouter.lookup(method, path); ok {
|
||||
if action, _, _, ok := bedrockRouter.Lookup(method, path); ok {
|
||||
return action
|
||||
}
|
||||
case "bedrock-runtime":
|
||||
if action, _, _, ok := bedrockRuntimeRouter.lookup(method, path); ok {
|
||||
if action, _, _, ok := bedrockRuntimeRouter.Lookup(method, path); ok {
|
||||
return action
|
||||
}
|
||||
case "bedrock-agent":
|
||||
if action, _, _, ok := bedrockAgentRouter.lookup(method, path); ok {
|
||||
if action, _, _, ok := bedrockAgentRouter.Lookup(method, path); ok {
|
||||
return action
|
||||
}
|
||||
case "bedrock-agent-runtime":
|
||||
if action, _, _, ok := bedrockAgentRuntimeRouter.lookup(method, path); ok {
|
||||
if action, _, _, ok := bedrockAgentRuntimeRouter.Lookup(method, path); ok {
|
||||
return action
|
||||
}
|
||||
}
|
||||
|
||||
+36
-35
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/rest"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// bedrockRoute maps one HTTP method + chi path pattern to an AWS action and handler.
|
||||
type bedrockRoute = restRoute[bedrockRouteHandler]
|
||||
type bedrockRoute = rest.Route[bedrockRouteHandler]
|
||||
|
||||
// bedrockRouteHandler invokes a per-action bedrock (control-plane) gateway
|
||||
// function. params holds the path params, PathUnescape'd. resolver
|
||||
@@ -28,16 +29,16 @@ type bedrockRouteHandler func(ctx context.Context, accountID string, params []st
|
||||
// bedrockRoutes is the dispatch table. Order is presentational: the router
|
||||
// matches through a chi trie, which prefers a literal segment over a {param} one.
|
||||
var bedrockRoutes = []bedrockRoute{
|
||||
{"GET", "/foundation-models", "ListFoundationModels",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, resolver gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, access gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "GET", Pattern: "/foundation-models", Action: "ListFoundationModels",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, resolver gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, access gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.ListFoundationModels(ctx, acct, resolver, access, new(bedrock.ListFoundationModelsInput))
|
||||
}},
|
||||
{"GET", "/foundation-models/{modelIdentifier}", "GetFoundationModel",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, access gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "GET", Pattern: "/foundation-models/{modelIdentifier}", Action: "GetFoundationModel",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, access gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.GetFoundationModel(ctx, acct, p[0], access)
|
||||
}},
|
||||
{"PUT", "/logging/modelinvocations", "PutModelInvocationLoggingConfiguration",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, store *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "PUT", Pattern: "/logging/modelinvocations", Action: "PutModelInvocationLoggingConfiguration",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, store *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.PutModelInvocationLoggingConfigurationInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -46,16 +47,16 @@ var bedrockRoutes = []bedrockRoute{
|
||||
}
|
||||
return gateway_bedrock.PutModelInvocationLoggingConfiguration(ctx, acct, store, input)
|
||||
}},
|
||||
{"GET", "/logging/modelinvocations", "GetModelInvocationLoggingConfiguration",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, store *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "GET", Pattern: "/logging/modelinvocations", Action: "GetModelInvocationLoggingConfiguration",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, store *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.GetModelInvocationLoggingConfiguration(ctx, acct, store, new(bedrock.GetModelInvocationLoggingConfigurationInput))
|
||||
}},
|
||||
{"DELETE", "/logging/modelinvocations", "DeleteModelInvocationLoggingConfiguration",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, store *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/logging/modelinvocations", Action: "DeleteModelInvocationLoggingConfiguration",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, store *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.DeleteModelInvocationLoggingConfiguration(ctx, acct, store, new(bedrock.DeleteModelInvocationLoggingConfigurationInput))
|
||||
}},
|
||||
{"POST", "/provisioned-model-throughput", "CreateProvisionedModelThroughput",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "POST", Pattern: "/provisioned-model-throughput", Action: "CreateProvisionedModelThroughput",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.CreateProvisionedModelThroughputInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -64,16 +65,16 @@ var bedrockRoutes = []bedrockRoute{
|
||||
}
|
||||
return gateway_bedrock.CreateProvisionedModelThroughput(ctx, acct, provisioned, input)
|
||||
}},
|
||||
{"GET", "/provisioned-model-throughput/{provisionedModelId}", "GetProvisionedModelThroughput",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "GET", Pattern: "/provisioned-model-throughput/{provisionedModelId}", Action: "GetProvisionedModelThroughput",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.GetProvisionedModelThroughput(ctx, acct, provisioned, &bedrock.GetProvisionedModelThroughputInput{ProvisionedModelId: aws.String(p[0])})
|
||||
}},
|
||||
{"GET", "/provisioned-model-throughputs", "ListProvisionedModelThroughputs",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "GET", Pattern: "/provisioned-model-throughputs", Action: "ListProvisionedModelThroughputs",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.ListProvisionedModelThroughputs(ctx, acct, provisioned, new(bedrock.ListProvisionedModelThroughputsInput))
|
||||
}},
|
||||
{"PATCH", "/provisioned-model-throughput/{provisionedModelId}", "UpdateProvisionedModelThroughput",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "PATCH", Pattern: "/provisioned-model-throughput/{provisionedModelId}", Action: "UpdateProvisionedModelThroughput",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.UpdateProvisionedModelThroughputInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -83,12 +84,12 @@ var bedrockRoutes = []bedrockRoute{
|
||||
input.ProvisionedModelId = aws.String(p[0])
|
||||
return gateway_bedrock.UpdateProvisionedModelThroughput(ctx, acct, provisioned, input)
|
||||
}},
|
||||
{"DELETE", "/provisioned-model-throughput/{provisionedModelId}", "DeleteProvisionedModelThroughput",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/provisioned-model-throughput/{provisionedModelId}", Action: "DeleteProvisionedModelThroughput",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, _ *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.DeleteProvisionedModelThroughput(ctx, acct, provisioned, &bedrock.DeleteProvisionedModelThroughputInput{ProvisionedModelId: aws.String(p[0])})
|
||||
}},
|
||||
{"POST", "/guardrails", "CreateGuardrail",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "POST", Pattern: "/guardrails", Action: "CreateGuardrail",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.CreateGuardrailInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -97,8 +98,8 @@ var bedrockRoutes = []bedrockRoute{
|
||||
}
|
||||
return gateway_bedrock.CreateGuardrail(ctx, acct, guardrails, input)
|
||||
}},
|
||||
{"GET", "/guardrails/{guardrailIdentifier}", "GetGuardrail",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "GET", Pattern: "/guardrails/{guardrailIdentifier}", Action: "GetGuardrail",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.GetGuardrailInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -108,12 +109,12 @@ var bedrockRoutes = []bedrockRoute{
|
||||
input.GuardrailIdentifier = aws.String(p[0])
|
||||
return gateway_bedrock.GetGuardrail(ctx, acct, guardrails, input)
|
||||
}},
|
||||
{"GET", "/guardrails", "ListGuardrails",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "GET", Pattern: "/guardrails", Action: "ListGuardrails",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
return gateway_bedrock.ListGuardrails(ctx, acct, guardrails, new(bedrock.ListGuardrailsInput))
|
||||
}},
|
||||
{"PUT", "/guardrails/{guardrailIdentifier}", "UpdateGuardrail",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "PUT", Pattern: "/guardrails/{guardrailIdentifier}", Action: "UpdateGuardrail",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.UpdateGuardrailInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -123,8 +124,8 @@ var bedrockRoutes = []bedrockRoute{
|
||||
input.GuardrailIdentifier = aws.String(p[0])
|
||||
return gateway_bedrock.UpdateGuardrail(ctx, acct, guardrails, input)
|
||||
}},
|
||||
{"DELETE", "/guardrails/{guardrailIdentifier}", "DeleteGuardrail",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/guardrails/{guardrailIdentifier}", Action: "DeleteGuardrail",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.DeleteGuardrailInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -134,8 +135,8 @@ var bedrockRoutes = []bedrockRoute{
|
||||
input.GuardrailIdentifier = aws.String(p[0])
|
||||
return gateway_bedrock.DeleteGuardrail(ctx, acct, guardrails, input)
|
||||
}},
|
||||
{"POST", "/guardrails/{guardrailIdentifier}", "CreateGuardrailVersion",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
{Method: "POST", Pattern: "/guardrails/{guardrailIdentifier}", Action: "CreateGuardrailVersion",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ *gateway_bedrock.LoggingConfigStore, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore) (any, error) {
|
||||
input := new(bedrock.CreateGuardrailVersionInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -148,13 +149,13 @@ var bedrockRoutes = []bedrockRoute{
|
||||
}
|
||||
|
||||
// bedrockRouter matches an escaped request path against bedrockRoutes.
|
||||
var bedrockRouter = newRESTRouter("bedrock", bedrockRoutes)
|
||||
var bedrockRouter = rest.NewRouter("bedrock", bedrockRoutes)
|
||||
|
||||
// Bedrock_Request dispatches bedrock (control-plane) REST-JSON requests:
|
||||
// resolves method+path to an action, reads the body, calls the handler, and
|
||||
// serialises the output as JSON.
|
||||
func (gw *GatewayConfig) Bedrock_Request(w http.ResponseWriter, r *http.Request) error {
|
||||
action, params, handler, ok := bedrockRouter.lookup(r.Method, r.URL.EscapedPath())
|
||||
action, params, handler, ok := bedrockRouter.Lookup(r.Method, r.URL.EscapedPath())
|
||||
if !ok {
|
||||
slog.DebugContext(r.Context(), "bedrock: no route for request", "method", r.Method, "path", r.URL.Path)
|
||||
return errors.New(awserrors.ErrorInvalidAction)
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
// fails too.
|
||||
func TestBedrockScopeTablesAreExhaustive(t *testing.T) {
|
||||
served := map[string][]string{
|
||||
"bedrock": actionsOf(bedrockRoutes, func(r bedrockRoute) string { return r.action }),
|
||||
"bedrock-runtime": actionsOf(bedrockRuntimeRoutes, func(r bedrockRuntimeRoute) string { return r.action }),
|
||||
"bedrock-agent": actionsOf(bedrockAgentRoutes, func(r bedrockAgentRoute) string { return r.action }),
|
||||
"bedrock-agent-runtime": actionsOf(bedrockAgentRuntimeRoutes, func(r bedrockAgentRuntimeRoute) string { return r.action }),
|
||||
"bedrock": actionsOf(bedrockRoutes, func(r bedrockRoute) string { return r.Action }),
|
||||
"bedrock-runtime": actionsOf(bedrockRuntimeRoutes, func(r bedrockRuntimeRoute) string { return r.Action }),
|
||||
"bedrock-agent": actionsOf(bedrockAgentRoutes, func(r bedrockAgentRoute) string { return r.Action }),
|
||||
"bedrock-agent-runtime": actionsOf(bedrockAgentRuntimeRoutes, func(r bedrockAgentRuntimeRoute) string { return r.Action }),
|
||||
}
|
||||
|
||||
for service, actions := range served {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestLookupBedrockAction_ResolvesKnownRoutes(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+"_"+tc.path, func(t *testing.T) {
|
||||
action, params, handler, ok := bedrockRouter.lookup(tc.method, tc.path)
|
||||
action, params, handler, ok := bedrockRouter.Lookup(tc.method, tc.path)
|
||||
require.True(t, ok, "expected route to match for %s %s", tc.method, tc.path)
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, tc.wantAction, action)
|
||||
@@ -31,13 +31,13 @@ func TestLookupBedrockAction_ResolvesKnownRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLookupBedrockAction_UnknownReturnsFalse(t *testing.T) {
|
||||
_, _, handler, ok := bedrockRouter.lookup("DELETE", "/foundation-models")
|
||||
_, _, handler, ok := bedrockRouter.Lookup("DELETE", "/foundation-models")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, handler)
|
||||
}
|
||||
|
||||
func TestLookupBedrockRuntimeAction_ResolvesConverse(t *testing.T) {
|
||||
action, params, handler, ok := bedrockRuntimeRouter.lookup("POST", "/model/meta.llama3-70b-instruct-v1:0/converse")
|
||||
action, params, handler, ok := bedrockRuntimeRouter.Lookup("POST", "/model/meta.llama3-70b-instruct-v1:0/converse")
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, "Converse", action)
|
||||
@@ -45,7 +45,7 @@ func TestLookupBedrockRuntimeAction_ResolvesConverse(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLookupBedrockRuntimeAction_UnknownReturnsFalse(t *testing.T) {
|
||||
_, _, handler, ok := bedrockRuntimeRouter.lookup("GET", "/model/foo/converse")
|
||||
_, _, handler, ok := bedrockRuntimeRouter.Lookup("GET", "/model/foo/converse")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, handler)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/rest"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
// bedrockAgentRoute maps one HTTP method + chi path pattern to an AWS action and handler.
|
||||
type bedrockAgentRoute = restRoute[bedrockAgentRouteHandler]
|
||||
type bedrockAgentRoute = rest.Route[bedrockAgentRouteHandler]
|
||||
|
||||
// bedrockAgentRouteHandler invokes a per-action bedrock-agent (control-plane)
|
||||
// gateway function. params holds the path params, PathUnescape'd.
|
||||
@@ -36,8 +37,8 @@ type bedrockAgentRouteHandler func(ctx context.Context, accountID, region string
|
||||
// definitions), not invented ones — including the trailing slashes, which
|
||||
// chi matches exactly.
|
||||
var bedrockAgentRoutes = []bedrockAgentRoute{
|
||||
{"PUT", "/knowledgebases/", "CreateKnowledgeBase",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, _ *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
{Method: "PUT", Pattern: "/knowledgebases/", Action: "CreateKnowledgeBase",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, _ *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
input := new(bedrockagent.CreateKnowledgeBaseInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -46,20 +47,20 @@ var bedrockAgentRoutes = []bedrockAgentRoute{
|
||||
}
|
||||
return CreateKnowledgeBase(ctx, acct, region, kb, vector, input)
|
||||
}},
|
||||
{"POST", "/knowledgebases/", "ListKnowledgeBases",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, _ *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
{Method: "POST", Pattern: "/knowledgebases/", Action: "ListKnowledgeBases",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, _ *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
return ListKnowledgeBases(ctx, acct, kb, new(bedrockagent.ListKnowledgeBasesInput))
|
||||
}},
|
||||
{"GET", "/knowledgebases/{knowledgeBaseId}", "GetKnowledgeBase",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, _ *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
{Method: "GET", Pattern: "/knowledgebases/{knowledgeBaseId}", Action: "GetKnowledgeBase",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, _ *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
return GetKnowledgeBase(ctx, acct, region, kb, &bedrockagent.GetKnowledgeBaseInput{KnowledgeBaseId: aws.String(p[0])})
|
||||
}},
|
||||
{"DELETE", "/knowledgebases/{knowledgeBaseId}", "DeleteKnowledgeBase",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/knowledgebases/{knowledgeBaseId}", Action: "DeleteKnowledgeBase",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
return DeleteKnowledgeBase(ctx, acct, kb, ds, vector, &bedrockagent.DeleteKnowledgeBaseInput{KnowledgeBaseId: aws.String(p[0])})
|
||||
}},
|
||||
{"PUT", "/knowledgebases/{knowledgeBaseId}/datasources/", "CreateDataSource",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
{Method: "PUT", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/", Action: "CreateDataSource",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
input := new(bedrockagent.CreateDataSourceInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -69,44 +70,44 @@ var bedrockAgentRoutes = []bedrockAgentRoute{
|
||||
input.KnowledgeBaseId = aws.String(p[0])
|
||||
return CreateDataSource(ctx, acct, region, kb, ds, input)
|
||||
}},
|
||||
{"POST", "/knowledgebases/{knowledgeBaseId}/datasources/", "ListDataSources",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
{Method: "POST", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/", Action: "ListDataSources",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
return ListDataSources(ctx, acct, kb, ds, &bedrockagent.ListDataSourcesInput{KnowledgeBaseId: aws.String(p[0])})
|
||||
}},
|
||||
{"GET", "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}", "GetDataSource",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, _ *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
{Method: "GET", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}", Action: "GetDataSource",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, _ *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
return GetDataSource(ctx, acct, region, ds, &bedrockagent.GetDataSourceInput{KnowledgeBaseId: aws.String(p[0]), DataSourceId: aws.String(p[1])})
|
||||
}},
|
||||
{"DELETE", "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}", "DeleteDataSource",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, _ *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}", Action: "DeleteDataSource",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, _ *ochrevector.KBStore, ds *ochrevector.DataSourceStore, _ ochrevector.VectorService) (any, error) {
|
||||
return DeleteDataSource(ctx, acct, ds, &bedrockagent.DeleteDataSourceInput{KnowledgeBaseId: aws.String(p[0]), DataSourceId: aws.String(p[1])})
|
||||
}},
|
||||
{"PUT", "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/", "StartIngestionJob",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
{Method: "PUT", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/", Action: "StartIngestionJob",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
return StartIngestionJob(ctx, acct, kb, ds, vector, &bedrockagent.StartIngestionJobInput{KnowledgeBaseId: aws.String(p[0]), DataSourceId: aws.String(p[1])})
|
||||
}},
|
||||
{"POST", "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/", "ListIngestionJobs",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
{Method: "POST", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/", Action: "ListIngestionJobs",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
return ListIngestionJobs(ctx, acct, kb, ds, vector, &bedrockagent.ListIngestionJobsInput{KnowledgeBaseId: aws.String(p[0]), DataSourceId: aws.String(p[1])})
|
||||
}},
|
||||
{"GET", "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/{ingestionJobId}", "GetIngestionJob",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
{Method: "GET", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/{ingestionJobId}", Action: "GetIngestionJob",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
return GetIngestionJob(ctx, acct, kb, ds, vector, &bedrockagent.GetIngestionJobInput{KnowledgeBaseId: aws.String(p[0]), DataSourceId: aws.String(p[1]), IngestionJobId: aws.String(p[2])})
|
||||
}},
|
||||
{"PUT", "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/{ingestionJobId}/stop", "StopIngestionJob",
|
||||
func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
{Method: "PUT", Pattern: "/knowledgebases/{knowledgeBaseId}/datasources/{dataSourceId}/ingestionjobs/{ingestionJobId}/stop", Action: "StopIngestionJob",
|
||||
Handler: func(ctx context.Context, acct, region string, p []string, b []byte, kb *ochrevector.KBStore, ds *ochrevector.DataSourceStore, vector ochrevector.VectorService) (any, error) {
|
||||
return StopIngestionJob(ctx, acct, kb, ds, vector, &StopIngestionJobInput{KnowledgeBaseId: aws.String(p[0]), DataSourceId: aws.String(p[1]), IngestionJobId: aws.String(p[2])})
|
||||
}},
|
||||
}
|
||||
|
||||
// bedrockAgentRouter matches an escaped request path against bedrockAgentRoutes.
|
||||
var bedrockAgentRouter = newRESTRouter("bedrock-agent", bedrockAgentRoutes)
|
||||
var bedrockAgentRouter = rest.NewRouter("bedrock-agent", bedrockAgentRoutes)
|
||||
|
||||
// BedrockAgent_Request dispatches bedrock-agent (control-plane) REST-JSON
|
||||
// requests: resolves method+path to an action, reads the body, calls the
|
||||
// handler, and serialises the output as JSON, mirroring Bedrock_Request.
|
||||
func (gw *GatewayConfig) BedrockAgent_Request(w http.ResponseWriter, r *http.Request) error {
|
||||
action, params, handler, ok := bedrockAgentRouter.lookup(r.Method, r.URL.EscapedPath())
|
||||
action, params, handler, ok := bedrockAgentRouter.Lookup(r.Method, r.URL.EscapedPath())
|
||||
if !ok {
|
||||
slog.DebugContext(r.Context(), "bedrock-agent: no route for request", "method", r.Method, "path", r.URL.Path)
|
||||
return errors.New(awserrors.ErrorInvalidAction)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/rest"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -27,7 +28,7 @@ type converseFn func(ctx context.Context, accountID, modelID string, input *bedr
|
||||
|
||||
// bedrockAgentRuntimeRoute maps one HTTP method + chi path pattern to an AWS
|
||||
// action and handler, mirroring bedrockAgentRoute.
|
||||
type bedrockAgentRuntimeRoute = restRoute[bedrockAgentRuntimeRouteHandler]
|
||||
type bedrockAgentRuntimeRoute = rest.Route[bedrockAgentRuntimeRouteHandler]
|
||||
|
||||
// bedrockAgentRuntimeRouteHandler invokes a per-action bedrock-agent-runtime
|
||||
// (data-plane) gateway function. params holds the path params,
|
||||
@@ -43,8 +44,8 @@ type bedrockAgentRuntimeRouteHandler func(ctx context.Context, accountID string,
|
||||
// verified against the vendored aws-sdk-go bedrockagentruntime request
|
||||
// definitions (opRetrieve/opRetrieveAndGenerate).
|
||||
var bedrockAgentRuntimeRoutes = []bedrockAgentRuntimeRoute{
|
||||
{"POST", "/knowledgebases/{knowledgeBaseId}/retrieve", "Retrieve",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, kb *ochrevector.KBStore, vector ochrevector.VectorService, _ converseFn) (any, error) {
|
||||
{Method: "POST", Pattern: "/knowledgebases/{knowledgeBaseId}/retrieve", Action: "Retrieve",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, kb *ochrevector.KBStore, vector ochrevector.VectorService, _ converseFn) (any, error) {
|
||||
input := new(bedrockagentruntime.RetrieveInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -54,8 +55,8 @@ var bedrockAgentRuntimeRoutes = []bedrockAgentRuntimeRoute{
|
||||
input.KnowledgeBaseId = aws.String(p[0])
|
||||
return Retrieve(ctx, acct, kb, vector, b, input)
|
||||
}},
|
||||
{"POST", "/retrieveAndGenerate", "RetrieveAndGenerate",
|
||||
func(ctx context.Context, acct string, _ []string, b []byte, kb *ochrevector.KBStore, vector ochrevector.VectorService, converse converseFn) (any, error) {
|
||||
{Method: "POST", Pattern: "/retrieveAndGenerate", Action: "RetrieveAndGenerate",
|
||||
Handler: func(ctx context.Context, acct string, _ []string, b []byte, kb *ochrevector.KBStore, vector ochrevector.VectorService, converse converseFn) (any, error) {
|
||||
input := new(bedrockagentruntime.RetrieveAndGenerateInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -67,7 +68,7 @@ var bedrockAgentRuntimeRoutes = []bedrockAgentRuntimeRoute{
|
||||
}
|
||||
|
||||
// bedrockAgentRuntimeRouter matches an escaped request path against bedrockAgentRuntimeRoutes.
|
||||
var bedrockAgentRuntimeRouter = newRESTRouter("bedrock-agent-runtime", bedrockAgentRuntimeRoutes)
|
||||
var bedrockAgentRuntimeRouter = rest.NewRouter("bedrock-agent-runtime", bedrockAgentRuntimeRoutes)
|
||||
|
||||
// BedrockAgentRuntime_Request dispatches bedrock-agent-runtime (data-plane)
|
||||
// REST-JSON requests: resolves method+path to an action, reads the body,
|
||||
@@ -76,7 +77,7 @@ var bedrockAgentRuntimeRouter = newRESTRouter("bedrock-agent-runtime", bedrockAg
|
||||
// is on bedrock-runtime (it ends up calling gateway_bedrock.Converse
|
||||
// in-process); Retrieve never reaches a model, so it is not metered.
|
||||
func (gw *GatewayConfig) BedrockAgentRuntime_Request(w http.ResponseWriter, r *http.Request) error {
|
||||
action, params, handler, ok := bedrockAgentRuntimeRouter.lookup(r.Method, r.URL.EscapedPath())
|
||||
action, params, handler, ok := bedrockAgentRuntimeRouter.Lookup(r.Method, r.URL.EscapedPath())
|
||||
if !ok {
|
||||
slog.DebugContext(r.Context(), "bedrock-agent-runtime: no route for request", "method", r.Method, "path", r.URL.Path)
|
||||
return errors.New(awserrors.ErrorInvalidAction)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/rest"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
@@ -21,7 +22,7 @@ const (
|
||||
)
|
||||
|
||||
// bedrockRuntimeRoute maps one HTTP method + chi path pattern to an AWS action and handler.
|
||||
type bedrockRuntimeRoute = restRoute[bedrockRuntimeRouteHandler]
|
||||
type bedrockRuntimeRoute = rest.Route[bedrockRuntimeRouteHandler]
|
||||
|
||||
// bedrockRuntimeRouteHandler invokes a per-action bedrock-runtime (data-plane)
|
||||
// gateway function. params holds the path params, PathUnescape'd.
|
||||
@@ -38,8 +39,8 @@ type bedrockRuntimeRouteHandler func(ctx context.Context, accountID string, para
|
||||
// function here: BedrockRuntime_Request special-cases its action to bypass
|
||||
// the JSON-marshaling dispatch below, since its response is raw bytes.
|
||||
var bedrockRuntimeRoutes = []bedrockRuntimeRoute{
|
||||
{"POST", "/model/{modelId}/converse", "Converse",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, resolver gateway_bedrock.CredentialResolver, endpoints gateway_bedrock.EndpointResolver, recorder gateway_bedrock.Recorder, access gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore, embedder gateway_bedrock.Embedder) (any, error) {
|
||||
{Method: "POST", Pattern: "/model/{modelId}/converse", Action: "Converse",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, resolver gateway_bedrock.CredentialResolver, endpoints gateway_bedrock.EndpointResolver, recorder gateway_bedrock.Recorder, access gateway_bedrock.AccessResolver, provisioned *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore, embedder gateway_bedrock.Embedder) (any, error) {
|
||||
input := new(bedrockruntime.ConverseInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -48,11 +49,11 @@ var bedrockRuntimeRoutes = []bedrockRuntimeRoute{
|
||||
}
|
||||
return gateway_bedrock.Converse(ctx, acct, p[0], input, resolver, endpoints, recorder, access, provisioned, guardrails, embedder)
|
||||
}},
|
||||
{"POST", "/model/{modelId}/invoke", "InvokeModel", nil},
|
||||
{"POST", "/model/{modelId}/converse-stream", "ConverseStream", nil},
|
||||
{"POST", "/model/{modelId}/invoke-with-response-stream", "InvokeModelWithResponseStream", nil},
|
||||
{"POST", "/guardrail/{guardrailIdentifier}/version/{guardrailVersion}/apply", "ApplyGuardrail",
|
||||
func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ gateway_bedrock.EndpointResolver, _ gateway_bedrock.Recorder, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore, embedder gateway_bedrock.Embedder) (any, error) {
|
||||
{Method: "POST", Pattern: "/model/{modelId}/invoke", Action: "InvokeModel", Handler: nil},
|
||||
{Method: "POST", Pattern: "/model/{modelId}/converse-stream", Action: "ConverseStream", Handler: nil},
|
||||
{Method: "POST", Pattern: "/model/{modelId}/invoke-with-response-stream", Action: "InvokeModelWithResponseStream", Handler: nil},
|
||||
{Method: "POST", Pattern: "/guardrail/{guardrailIdentifier}/version/{guardrailVersion}/apply", Action: "ApplyGuardrail",
|
||||
Handler: func(ctx context.Context, acct string, p []string, b []byte, _ gateway_bedrock.CredentialResolver, _ gateway_bedrock.EndpointResolver, _ gateway_bedrock.Recorder, _ gateway_bedrock.AccessResolver, _ *gateway_bedrock.ProvisionedStore, guardrails *gateway_bedrock.GuardrailStore, embedder gateway_bedrock.Embedder) (any, error) {
|
||||
input := new(bedrockruntime.ApplyGuardrailInput)
|
||||
if len(b) > 0 {
|
||||
if err := json.Unmarshal(b, input); err != nil {
|
||||
@@ -66,13 +67,13 @@ var bedrockRuntimeRoutes = []bedrockRuntimeRoute{
|
||||
}
|
||||
|
||||
// bedrockRuntimeRouter matches an escaped request path against bedrockRuntimeRoutes.
|
||||
var bedrockRuntimeRouter = newRESTRouter("bedrock-runtime", bedrockRuntimeRoutes)
|
||||
var bedrockRuntimeRouter = rest.NewRouter("bedrock-runtime", bedrockRuntimeRoutes)
|
||||
|
||||
// BedrockRuntime_Request dispatches bedrock-runtime (data-plane) REST-JSON
|
||||
// requests: resolves method+path to an action, reads the body, calls the
|
||||
// handler, and serialises the output as JSON.
|
||||
func (gw *GatewayConfig) BedrockRuntime_Request(w http.ResponseWriter, r *http.Request) error {
|
||||
action, params, handler, ok := bedrockRuntimeRouter.lookup(r.Method, r.URL.EscapedPath())
|
||||
action, params, handler, ok := bedrockRuntimeRouter.Lookup(r.Method, r.URL.EscapedPath())
|
||||
if !ok {
|
||||
slog.DebugContext(r.Context(), "bedrock-runtime: no route for request", "method", r.Method, "path", r.URL.Path)
|
||||
return errors.New(awserrors.ErrorInvalidAction)
|
||||
|
||||
+85
-84
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/rest"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -26,7 +27,7 @@ func jsonErrorType(code string) string {
|
||||
}
|
||||
|
||||
// eksRoute maps one HTTP method + chi path pattern to an AWS action and handler.
|
||||
type eksRoute = restRoute[eksRouteHandler]
|
||||
type eksRoute = rest.Route[eksRouteHandler]
|
||||
|
||||
// eksRouteHandler invokes a per-action EKS gateway function. callerARN is used
|
||||
// by CreateCluster for the bootstrap-creator-admin AccessEntry; ignored by others.
|
||||
@@ -36,44 +37,44 @@ type eksRouteHandler func(ctx context.Context, gw *GatewayConfig, accountID, cal
|
||||
// through a chi trie, which prefers a literal segment over a {param} one.
|
||||
var eksRoutes = []eksRoute{
|
||||
// Cluster
|
||||
{"POST", "/clusters", "CreateCluster",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters", Action: "CreateCluster",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateCluster(ctx, gw.NATSConn, acct, callerARN, b)
|
||||
}},
|
||||
{"GET", "/clusters", "ListClusters",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters", Action: "ListClusters",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListClusters(ctx, gw.NATSConn, acct)
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/update-config", "UpdateClusterConfig",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/update-config", Action: "UpdateClusterConfig",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateClusterConfig(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/updates", "UpdateClusterVersion",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/updates", Action: "UpdateClusterVersion",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateClusterVersion(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
// The update surface these two read is absent, so they refuse here rather
|
||||
// than through a service method: a round-trip to reach a constant refusal
|
||||
// is surface for its own sake. Left unregistered they would answer
|
||||
// InvalidAction, which blames the caller's spelling for a routing gap.
|
||||
{"GET", "/clusters/{clusterName}/updates", "ListUpdates",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/updates", Action: "ListUpdates",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return nil, errors.New(awserrors.ErrorNotImplemented)
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/updates/{updateId}", "DescribeUpdate",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/updates/{updateId}", Action: "DescribeUpdate",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return nil, errors.New(awserrors.ErrorNotImplemented)
|
||||
}},
|
||||
// Control-plane VM broker: relays bootstrap/state POSTs onto eks.bus.*/eks.state.* NATS subjects.
|
||||
// acct and callerARN are ignored; cluster account comes from the body.
|
||||
{"POST", "/clusters/{clusterName}/internal-publish", "PublishInternal",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/internal-publish", Action: "PublishInternal",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.PublishInternal(ctx, gw.NATSConn, p[0], b)
|
||||
}},
|
||||
// Token review broker: the eks-token-webhook POSTs bearer tokens here;
|
||||
// the gateway resolves them host-side (STS verify + AccessEntry lookup).
|
||||
{"POST", "/clusters/{clusterName}/token-review", "WebhookTokenReview",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/token-review", Action: "WebhookTokenReview",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.WebhookTokenReview(ctx, gw.NATSConn, p[0], b)
|
||||
}},
|
||||
// Control-plane VM add-on delivery: the on-VM addon-sync agent GETs the set
|
||||
@@ -82,148 +83,148 @@ var eksRoutes = []eksRoute{
|
||||
// ignored — the cluster account is the {accountId} path segment, since a GET
|
||||
// carries no body to hold it (cf. PublishInternal). AuthorizeInternal binds
|
||||
// that segment to the caller's own cluster.
|
||||
{"GET", "/clusters/{clusterName}/internal-addons/{accountId}", "ListInternalAddons",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/internal-addons/{accountId}", Action: "ListInternalAddons",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListInternalAddons(ctx, gw.NATSConn, p[0], p[1])
|
||||
}},
|
||||
|
||||
// Internal control-plane route: the on-VM k3s-recovery agent pulls its
|
||||
// per-member recovery directive (cluster-reset / wipe-rejoin) at boot. Same
|
||||
// system-cred carve-out as internal-addons; instance ID is the third segment.
|
||||
{"GET", "/clusters/{clusterName}/internal-recovery/{accountId}/{instanceId}", "GetRecoveryDirective",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/internal-recovery/{accountId}/{instanceId}", Action: "GetRecoveryDirective",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.GetRecoveryDirective(ctx, gw.NATSConn, p[0], p[1], p[2])
|
||||
}},
|
||||
|
||||
// Nodegroup
|
||||
{"POST", "/clusters/{clusterName}/node-groups", "CreateNodegroup",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/node-groups", Action: "CreateNodegroup",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateNodegroup(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/node-groups", "ListNodegroups",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/node-groups", Action: "ListNodegroups",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListNodegroups(ctx, gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/node-groups/{nodegroupName}/update-config", "UpdateNodegroupConfig",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/node-groups/{nodegroupName}/update-config", Action: "UpdateNodegroupConfig",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateNodegroupConfig(ctx, gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/node-groups/{nodegroupName}/update-version", "UpdateNodegroupVersion",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/node-groups/{nodegroupName}/update-version", Action: "UpdateNodegroupVersion",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateNodegroupVersion(ctx, gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/node-groups/{nodegroupName}", "DescribeNodegroup",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/node-groups/{nodegroupName}", Action: "DescribeNodegroup",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeNodegroup(ctx, gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"DELETE", "/clusters/{clusterName}/node-groups/{nodegroupName}", "DeleteNodegroup",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/clusters/{clusterName}/node-groups/{nodegroupName}", Action: "DeleteNodegroup",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteNodegroup(ctx, gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
|
||||
// AccessEntry / AccessPolicy
|
||||
{"POST", "/clusters/{clusterName}/access-entries", "CreateAccessEntry",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/access-entries", Action: "CreateAccessEntry",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateAccessEntry(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/access-entries", "ListAccessEntries",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/access-entries", Action: "ListAccessEntries",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAccessEntries(ctx, gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/access-entries/{principalArn}/access-policies", "AssociateAccessPolicy",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/access-entries/{principalArn}/access-policies", Action: "AssociateAccessPolicy",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.AssociateAccessPolicy(ctx, gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"DELETE", "/clusters/{clusterName}/access-entries/{principalArn}/access-policies/{policyArn}", "DisassociateAccessPolicy",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/clusters/{clusterName}/access-entries/{principalArn}/access-policies/{policyArn}", Action: "DisassociateAccessPolicy",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DisassociateAccessPolicy(ctx, gw.NATSConn, acct, p[0], p[1], p[2])
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/access-entries/{principalArn}/access-policies", "ListAssociatedAccessPolicies",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/access-entries/{principalArn}/access-policies", Action: "ListAssociatedAccessPolicies",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAssociatedAccessPolicies(ctx, gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/access-entries/{principalArn}", "DescribeAccessEntry",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/access-entries/{principalArn}", Action: "DescribeAccessEntry",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeAccessEntry(ctx, gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/access-entries/{principalArn}", "UpdateAccessEntry",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/access-entries/{principalArn}", Action: "UpdateAccessEntry",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateAccessEntry(ctx, gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"DELETE", "/clusters/{clusterName}/access-entries/{principalArn}", "DeleteAccessEntry",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/clusters/{clusterName}/access-entries/{principalArn}", Action: "DeleteAccessEntry",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteAccessEntry(ctx, gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"GET", "/access-policies", "ListAccessPolicies",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/access-policies", Action: "ListAccessPolicies",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAccessPolicies(ctx, gw.NATSConn, acct)
|
||||
}},
|
||||
|
||||
// Addons
|
||||
{"GET", "/addons/supported-versions", "DescribeAddonVersions",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/addons/supported-versions", Action: "DescribeAddonVersions",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeAddonVersions(ctx, gw.NATSConn, acct)
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/addons", "ListAddons",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/addons", Action: "ListAddons",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListAddons(ctx, gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/addons", "CreateAddon",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/addons", Action: "CreateAddon",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.CreateAddon(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/addons/{addonName}/update", "UpdateAddon",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/addons/{addonName}/update", Action: "UpdateAddon",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UpdateAddon(ctx, gw.NATSConn, acct, p[0], p[1], b)
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/addons/{addonName}", "DescribeAddon",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/addons/{addonName}", Action: "DescribeAddon",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeAddon(ctx, gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
{"DELETE", "/clusters/{clusterName}/addons/{addonName}", "DeleteAddon",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/clusters/{clusterName}/addons/{addonName}", Action: "DeleteAddon",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteAddon(ctx, gw.NATSConn, acct, p[0], p[1])
|
||||
}},
|
||||
|
||||
// OIDC identity-provider configs
|
||||
{"POST", "/clusters/{clusterName}/identity-provider-configs/associate", "AssociateIdentityProviderConfig",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/identity-provider-configs/associate", Action: "AssociateIdentityProviderConfig",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.AssociateIdentityProviderConfig(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/identity-provider-configs/describe", "DescribeIdentityProviderConfig",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/identity-provider-configs/describe", Action: "DescribeIdentityProviderConfig",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeIdentityProviderConfig(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"POST", "/clusters/{clusterName}/identity-provider-configs/disassociate", "DisassociateIdentityProviderConfig",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/clusters/{clusterName}/identity-provider-configs/disassociate", Action: "DisassociateIdentityProviderConfig",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DisassociateIdentityProviderConfig(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", "/clusters/{clusterName}/identity-provider-configs", "ListIdentityProviderConfigs",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}/identity-provider-configs", Action: "ListIdentityProviderConfigs",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListIdentityProviderConfigs(ctx, gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
|
||||
// Cluster CRUD — listed after more-specific /clusters/{name}/... routes.
|
||||
{"GET", "/clusters/{clusterName}", "DescribeCluster",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/clusters/{clusterName}", Action: "DescribeCluster",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DescribeCluster(ctx, gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
{"DELETE", "/clusters/{clusterName}", "DeleteCluster",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/clusters/{clusterName}", Action: "DeleteCluster",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.DeleteCluster(ctx, gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
|
||||
// Tags
|
||||
{"POST", "/tags/*", "TagResource",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "POST", Pattern: "/tags/*", Action: "TagResource",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.TagResource(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"DELETE", "/tags/*", "UntagResource",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "DELETE", Pattern: "/tags/*", Action: "UntagResource",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.UntagResource(ctx, gw.NATSConn, acct, p[0], b)
|
||||
}},
|
||||
{"GET", "/tags/*", "ListTagsForResource",
|
||||
func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
{Method: "GET", Pattern: "/tags/*", Action: "ListTagsForResource",
|
||||
Handler: func(ctx context.Context, gw *GatewayConfig, acct, callerARN string, p []string, b []byte) (any, error) {
|
||||
return gateway_eks.ListTagsForResource(ctx, gw.NATSConn, acct, p[0])
|
||||
}},
|
||||
}
|
||||
@@ -233,19 +234,19 @@ var eksRoutes = []eksRoute{
|
||||
func eksActionNames() []string {
|
||||
names := make([]string, 0, len(eksRoutes))
|
||||
for _, route := range eksRoutes {
|
||||
names = append(names, route.action)
|
||||
names = append(names, route.Action)
|
||||
}
|
||||
slices.Sort(names)
|
||||
return slices.Compact(names)
|
||||
}
|
||||
|
||||
// eksRouter matches an escaped request path against eksRoutes.
|
||||
var eksRouter = newRESTRouter("eks", eksRoutes)
|
||||
var eksRouter = rest.NewRouter("eks", eksRoutes)
|
||||
|
||||
// EKS_Request dispatches EKS REST-JSON requests: resolves method+path to an
|
||||
// action, reads the body, calls the handler, and serialises the output as JSON.
|
||||
func (gw *GatewayConfig) EKS_Request(w http.ResponseWriter, r *http.Request) error {
|
||||
action, params, handler, ok := eksRouter.lookup(r.Method, r.URL.EscapedPath())
|
||||
action, params, handler, ok := eksRouter.Lookup(r.Method, r.URL.EscapedPath())
|
||||
if !ok {
|
||||
slog.DebugContext(r.Context(), "EKS: no route for request", "method", r.Method, "path", r.URL.Path)
|
||||
return errors.New(awserrors.ErrorInvalidAction)
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
func TestEKSScopeTableIsExhaustive(t *testing.T) {
|
||||
served := make(map[string]bool, len(eksRoutes))
|
||||
for _, route := range eksRoutes {
|
||||
served[route.action] = true
|
||||
assert.True(t, gateway_eks.HasScope(route.action),
|
||||
"eks action %q has no resource scope entry: add one to eksScopes in gateway/eks/authz.go", route.action)
|
||||
served[route.Action] = true
|
||||
assert.True(t, gateway_eks.HasScope(route.Action),
|
||||
"eks action %q has no resource scope entry: add one to eksScopes in gateway/eks/authz.go", route.Action)
|
||||
}
|
||||
|
||||
for _, action := range gateway_eks.ScopedActions() {
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestLookupEKSAction_ResolvesKnownRoutes(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+"_"+tc.path, func(t *testing.T) {
|
||||
action, params, handler, ok := eksRouter.lookup(tc.method, tc.path)
|
||||
action, params, handler, ok := eksRouter.Lookup(tc.method, tc.path)
|
||||
require.True(t, ok, "expected route to match for %s %s", tc.method, tc.path)
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, tc.wantAction, action)
|
||||
@@ -87,7 +87,7 @@ func TestLookupEKSAction_EncodedPrincipalARN(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.method+"_"+tc.wantAction, func(t *testing.T) {
|
||||
action, params, handler, ok := eksRouter.lookup(tc.method, tc.path)
|
||||
action, params, handler, ok := eksRouter.Lookup(tc.method, tc.path)
|
||||
require.True(t, ok, "encoded ARN path should match: %s %s", tc.method, tc.path)
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, tc.wantAction, action)
|
||||
@@ -107,7 +107,7 @@ func TestLookupEKSAction_DisassociateEncodedARNs(t *testing.T) {
|
||||
policyEscaped = "arn%3Aaws%3Aeks%3A%3Aaws%3Acluster-access-policy%2FAmazonEKSViewPolicy"
|
||||
)
|
||||
path := "/clusters/alpha/access-entries/" + principalEscaped + "/access-policies/" + policyEscaped
|
||||
action, params, handler, ok := eksRouter.lookup("DELETE", path)
|
||||
action, params, handler, ok := eksRouter.Lookup("DELETE", path)
|
||||
require.True(t, ok, "encoded disassociate path should match: %s", path)
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, "DisassociateAccessPolicy", action)
|
||||
@@ -159,8 +159,8 @@ func TestLookupEKSAction_CoversAllActions(t *testing.T) {
|
||||
"ListTagsForResource": false,
|
||||
}
|
||||
for _, route := range eksRoutes {
|
||||
if _, ok := expected[route.action]; ok {
|
||||
expected[route.action] = true
|
||||
if _, ok := expected[route.Action]; ok {
|
||||
expected[route.Action] = true
|
||||
}
|
||||
}
|
||||
for action, seen := range expected {
|
||||
@@ -170,10 +170,10 @@ func TestLookupEKSAction_CoversAllActions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLookupEKSAction_UnknownReturnsFalse(t *testing.T) {
|
||||
_, _, _, ok := eksRouter.lookup("PATCH", "/clusters/alpha")
|
||||
_, _, _, ok := eksRouter.Lookup("PATCH", "/clusters/alpha")
|
||||
assert.False(t, ok)
|
||||
|
||||
_, _, _, ok = eksRouter.lookup("GET", "/clusters/alpha/wat")
|
||||
_, _, _, ok = eksRouter.Lookup("GET", "/clusters/alpha/wat")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ func errAWS(code string) error { return &awsCodeError{code: code} }
|
||||
func TestLookupEKSAction_UpdateReadsRefuseAsNotImplemented(t *testing.T) {
|
||||
for _, path := range []string{"/clusters/alpha/updates", "/clusters/alpha/updates/upd1"} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
_, params, handler, ok := eksRouter.lookup(http.MethodGet, path)
|
||||
_, params, handler, ok := eksRouter.Lookup(http.MethodGet, path)
|
||||
require.True(t, ok)
|
||||
out, err := handler(context.Background(), nil, "000000000001", "", params, nil)
|
||||
assert.Nil(t, out)
|
||||
@@ -251,6 +251,6 @@ func TestLookupEKSAction_UpdateReadsRefuseAsNotImplemented(t *testing.T) {
|
||||
// reachable at it on AWS, so keeping it registered would serve a path no caller
|
||||
// sends and hide the one they do.
|
||||
func TestLookupEKSAction_ClusterUpdateVersionIsNotAtTheNodegroupPath(t *testing.T) {
|
||||
_, _, _, ok := eksRouter.lookup(http.MethodPost, "/clusters/alpha/update-version")
|
||||
_, _, _, ok := eksRouter.Lookup(http.MethodPost, "/clusters/alpha/update-version")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//test:in-package — the route tables and restRouter are unexported, and these
|
||||
// tests exist to hold the chi trie to the matching the regex tables used to do.
|
||||
//test:in-package — the route tables are unexported, and these tests hold each
|
||||
// service table to what the REST router resolves.
|
||||
|
||||
package gateway
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/ingress/aws/rest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -26,14 +27,14 @@ func samplePath(pattern string) string {
|
||||
// assertRoutesResolve walks a table and checks each route is reachable through
|
||||
// the router, guarding against a pattern the trie canonicalises away from its
|
||||
// entry key — which lookup would otherwise report as a plain no-match.
|
||||
func assertRoutesResolve[H any](t *testing.T, rr *restRouter[H], routes []restRoute[H]) {
|
||||
func assertRoutesResolve[H any](t *testing.T, rr *rest.Router[H], routes []rest.Route[H]) {
|
||||
t.Helper()
|
||||
for _, route := range routes {
|
||||
path := samplePath(route.pattern)
|
||||
action, params, _, ok := rr.lookup(route.method, path)
|
||||
require.True(t, ok, "%s %s (pattern %s) should match", route.method, path, route.pattern)
|
||||
assert.Equal(t, route.action, action, "%s %s", route.method, path)
|
||||
assert.Len(t, params, len(restPatternParams(route.pattern)), "%s %s", route.method, path)
|
||||
path := samplePath(route.Pattern)
|
||||
action, params, _, ok := rr.Lookup(route.Method, path)
|
||||
require.True(t, ok, "%s %s (pattern %s) should match", route.Method, path, route.Pattern)
|
||||
assert.Equal(t, route.Action, action, "%s %s", route.Method, path)
|
||||
assert.Len(t, params, strings.Count(route.Pattern, "{")+strings.Count(route.Pattern, "*"), "%s %s", route.Method, path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ func TestRESTRouter_NoMatchCases(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
action, params, _, ok := eksRouter.lookup(tc.method, tc.path)
|
||||
action, params, _, ok := eksRouter.Lookup(tc.method, tc.path)
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, action)
|
||||
assert.Nil(t, params)
|
||||
@@ -78,11 +79,11 @@ func TestRESTRouter_NoMatchCases(t *testing.T) {
|
||||
// bedrock-agent's real AWS paths carry a trailing slash on the collection
|
||||
// routes; chi reports those patterns without it, so the entry key trims both.
|
||||
func TestRESTRouter_TrailingSlashIsExact(t *testing.T) {
|
||||
action, _, _, ok := bedrockAgentRouter.lookup("PUT", "/knowledgebases/")
|
||||
action, _, _, ok := bedrockAgentRouter.Lookup("PUT", "/knowledgebases/")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "CreateKnowledgeBase", action)
|
||||
|
||||
_, _, _, ok = bedrockAgentRouter.lookup("PUT", "/knowledgebases")
|
||||
_, _, _, ok = bedrockAgentRouter.Lookup("PUT", "/knowledgebases")
|
||||
assert.False(t, ok, "the slashless path is a different route to AWS")
|
||||
}
|
||||
|
||||
@@ -94,12 +95,12 @@ func TestRESTRouter_UnescapesParams(t *testing.T) {
|
||||
escaped = "arn%3Aaws%3Aiam%3A%3A000000000001%3Auser%2Fadmin"
|
||||
)
|
||||
|
||||
action, params, _, ok := eksRouter.lookup("GET", "/clusters/alpha/access-entries/"+escaped)
|
||||
action, params, _, ok := eksRouter.Lookup("GET", "/clusters/alpha/access-entries/"+escaped)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "DescribeAccessEntry", action)
|
||||
assert.Equal(t, []string{"alpha", arn}, params)
|
||||
|
||||
action, params, _, ok = eksRouter.lookup("GET", "/tags/"+escaped)
|
||||
action, params, _, ok = eksRouter.Lookup("GET", "/tags/"+escaped)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "ListTagsForResource", action)
|
||||
assert.Equal(t, []string{arn}, params)
|
||||
@@ -107,13 +108,13 @@ func TestRESTRouter_UnescapesParams(t *testing.T) {
|
||||
|
||||
// chi keeps the last registration of a duplicated method+pattern silently,
|
||||
// leaving the shadowed route's action unreachable.
|
||||
func assertNoDuplicateRoutes[H any](t *testing.T, routes []restRoute[H]) {
|
||||
func assertNoDuplicateRoutes[H any](t *testing.T, routes []rest.Route[H]) {
|
||||
t.Helper()
|
||||
seen := make(map[string]string, len(routes))
|
||||
for _, route := range routes {
|
||||
key := restRouteKey(route.method, route.pattern)
|
||||
assert.NotContains(t, seen, key, "%s shadows %s on %s", route.action, seen[key], key)
|
||||
seen[key] = route.action
|
||||
key := route.Method + " " + strings.TrimSuffix(route.Pattern, "/")
|
||||
assert.NotContains(t, seen, key, "%s shadows %s on %s", route.Action, seen[key], key)
|
||||
seen[key] = route.Action
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +125,3 @@ func TestRESTRouters_NoDuplicateRoutes(t *testing.T) {
|
||||
t.Run("bedrock-agent", func(t *testing.T) { assertNoDuplicateRoutes(t, bedrockAgentRoutes) })
|
||||
t.Run("bedrock-agent-runtime", func(t *testing.T) { assertNoDuplicateRoutes(t, bedrockAgentRuntimeRoutes) })
|
||||
}
|
||||
|
||||
func TestRestPatternParams(t *testing.T) {
|
||||
assert.Nil(t, restPatternParams("/foundation-models"))
|
||||
assert.Equal(t, []string{"clusterName"}, restPatternParams("/clusters/{clusterName}/node-groups"))
|
||||
assert.Equal(t, []string{"clusterName", "nodegroupName"},
|
||||
restPatternParams("/clusters/{clusterName}/node-groups/{nodegroupName}"))
|
||||
assert.Equal(t, []string{"*"}, restPatternParams("/tags/*"))
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
package gateway
|
||||
// Package rest dispatches AWS REST-JSON requests: it resolves an HTTP method and
|
||||
// escaped path to the AWS action, path parameters and handler a service
|
||||
// registered for it.
|
||||
package rest
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
@@ -9,65 +12,65 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// restRoute binds one HTTP method and chi path pattern to an AWS action and the
|
||||
// Route binds one HTTP method and chi path pattern to an AWS action and the
|
||||
// handler that serves it. Params are positional: a handler indexes them in the
|
||||
// order their {name} segments appear in the pattern.
|
||||
type restRoute[H any] struct {
|
||||
method string
|
||||
pattern string
|
||||
action string
|
||||
handler H
|
||||
type Route[H any] struct {
|
||||
Method string
|
||||
Pattern string
|
||||
Action string
|
||||
Handler H
|
||||
}
|
||||
|
||||
// restEntry is one registered route plus the param names read off its pattern.
|
||||
type restEntry[H any] struct {
|
||||
route restRoute[H]
|
||||
// routeEntry is one registered route plus the param names read off its pattern.
|
||||
type routeEntry[H any] struct {
|
||||
route Route[H]
|
||||
params []string
|
||||
}
|
||||
|
||||
// restRouter resolves a method and path to a route through a chi trie, which
|
||||
// Router resolves a method and path to a route through a chi trie, which
|
||||
// orders specific paths ahead of {param} ones itself rather than relying on the
|
||||
// order the table lists them in.
|
||||
type restRouter[H any] struct {
|
||||
type Router[H any] struct {
|
||||
service string
|
||||
mux *chi.Mux
|
||||
entries map[string]restEntry[H]
|
||||
entries map[string]routeEntry[H]
|
||||
}
|
||||
|
||||
// newRESTRouter builds the router for one service's dispatch table. A duplicate
|
||||
// NewRouter builds the router for one service's dispatch table. A duplicate
|
||||
// method+pattern would shadow its twin here as it does in chi; the route tables
|
||||
// are checked for one by test.
|
||||
func newRESTRouter[H any](service string, routes []restRoute[H]) *restRouter[H] {
|
||||
rr := &restRouter[H]{
|
||||
func NewRouter[H any](service string, routes []Route[H]) *Router[H] {
|
||||
rr := &Router[H]{
|
||||
service: service,
|
||||
mux: chi.NewMux(),
|
||||
entries: make(map[string]restEntry[H], len(routes)),
|
||||
entries: make(map[string]routeEntry[H], len(routes)),
|
||||
}
|
||||
for _, route := range routes {
|
||||
rr.entries[restRouteKey(route.method, route.pattern)] = restEntry[H]{
|
||||
rr.entries[routeKey(route.Method, route.Pattern)] = routeEntry[H]{
|
||||
route: route,
|
||||
params: restPatternParams(route.pattern),
|
||||
params: patternParams(route.Pattern),
|
||||
}
|
||||
// The trie is consulted through Match, which never serves the handler:
|
||||
// the table above holds the real one.
|
||||
rr.mux.MethodFunc(route.method, route.pattern, func(http.ResponseWriter, *http.Request) {})
|
||||
rr.mux.MethodFunc(route.Method, route.Pattern, func(http.ResponseWriter, *http.Request) {})
|
||||
}
|
||||
return rr
|
||||
}
|
||||
|
||||
// lookup matches method+path, returning the action, path params, and handler, or
|
||||
// Lookup matches method+path, returning the action, path params, and handler, or
|
||||
// ("", nil, zero, false) on no match. path must be r.URL.EscapedPath(): ARNs in
|
||||
// path segments are percent-encoded by the CLI (e.g. user%2Fadmin), and matching
|
||||
// the decoded path would split one param across two segments. Params are
|
||||
// PathUnescape'd before returning.
|
||||
func (rr *restRouter[H]) lookup(method, path string) (string, []string, H, bool) {
|
||||
func (rr *Router[H]) Lookup(method, path string) (string, []string, H, bool) {
|
||||
var zero H
|
||||
|
||||
rctx := chi.NewRouteContext()
|
||||
if !rr.mux.Match(rctx, method, path) {
|
||||
return "", nil, zero, false
|
||||
}
|
||||
entry, ok := rr.entries[restRouteKey(method, rctx.RoutePattern())]
|
||||
entry, ok := rr.entries[routeKey(method, rctx.RoutePattern())]
|
||||
if !ok {
|
||||
slog.Debug(rr.service+": matched route has no table entry", "method", method, "pattern", rctx.RoutePattern())
|
||||
return "", nil, zero, false
|
||||
@@ -91,22 +94,22 @@ func (rr *restRouter[H]) lookup(method, path string) (string, []string, H, bool)
|
||||
params = append(params, decoded)
|
||||
}
|
||||
}
|
||||
return entry.route.action, params, entry.route.handler, true
|
||||
return entry.route.Action, params, entry.route.Handler, true
|
||||
}
|
||||
|
||||
// restRouteKey keys the entry table. The trailing slash is trimmed because chi
|
||||
// routeKey keys the entry table. The trailing slash is trimmed because chi
|
||||
// reports a pattern registered as "/knowledgebases/" as "/knowledgebases", while
|
||||
// still matching only the slashed path.
|
||||
func restRouteKey(method, pattern string) string {
|
||||
func routeKey(method, pattern string) string {
|
||||
if len(pattern) > 1 {
|
||||
pattern = strings.TrimSuffix(pattern, "/")
|
||||
}
|
||||
return method + " " + pattern
|
||||
}
|
||||
|
||||
// restPatternParams lists the param names of a chi pattern in path order, the
|
||||
// patternParams lists the param names of a chi pattern in path order, the
|
||||
// order handlers index them in.
|
||||
func restPatternParams(pattern string) []string {
|
||||
func patternParams(pattern string) []string {
|
||||
var names []string
|
||||
for segment := range strings.SplitSeq(pattern, "/") {
|
||||
switch {
|
||||
@@ -0,0 +1,18 @@
|
||||
//test:in-package — patternParams is unexported and fixes the param order every
|
||||
// handler indexes by.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPatternParams(t *testing.T) {
|
||||
assert.Nil(t, patternParams("/foundation-models"))
|
||||
assert.Equal(t, []string{"clusterName"}, patternParams("/clusters/{clusterName}/node-groups"))
|
||||
assert.Equal(t, []string{"clusterName", "nodegroupName"},
|
||||
patternParams("/clusters/{clusterName}/node-groups/{nodegroupName}"))
|
||||
assert.Equal(t, []string{"*"}, patternParams("/tags/*"))
|
||||
}
|
||||
Reference in New Issue
Block a user