refactor: move AWS REST-JSON router to ingress/aws/rest

This commit is contained in:
joshsiv-mulga
2026-10-07 15:54:32 +10:00
parent 7303059fc8
commit cd92661768
13 changed files with 260 additions and 241 deletions
+5 -5
View File
@@ -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
View File
@@ -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 {
+4 -4
View File
@@ -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)
}
+28 -27
View File
@@ -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)
+8 -7
View File
@@ -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)
+11 -10
View File
@@ -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
View File
@@ -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() {
+9 -9
View File
@@ -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 {
+18
View File
@@ -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/*"))
}