diff --git a/spinifex/gateway/auth.go b/spinifex/gateway/auth.go index 408312b47..ae4e19061 100644 --- a/spinifex/gateway/auth.go +++ b/spinifex/gateway/auth.go @@ -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 } } diff --git a/spinifex/gateway/bedrock.go b/spinifex/gateway/bedrock.go index 459fbf7ca..f1d80a9a1 100644 --- a/spinifex/gateway/bedrock.go +++ b/spinifex/gateway/bedrock.go @@ -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) diff --git a/spinifex/gateway/bedrock_authz_completeness_test.go b/spinifex/gateway/bedrock_authz_completeness_test.go index 33c2aa241..7e5ef7919 100644 --- a/spinifex/gateway/bedrock_authz_completeness_test.go +++ b/spinifex/gateway/bedrock_authz_completeness_test.go @@ -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 { diff --git a/spinifex/gateway/bedrock_test.go b/spinifex/gateway/bedrock_test.go index 109407ef1..02485041b 100644 --- a/spinifex/gateway/bedrock_test.go +++ b/spinifex/gateway/bedrock_test.go @@ -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) } diff --git a/spinifex/gateway/bedrockagent.go b/spinifex/gateway/bedrockagent.go index 734ca1c91..9fa44183a 100644 --- a/spinifex/gateway/bedrockagent.go +++ b/spinifex/gateway/bedrockagent.go @@ -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) diff --git a/spinifex/gateway/bedrockagentruntime.go b/spinifex/gateway/bedrockagentruntime.go index 118040619..8b6853bc7 100644 --- a/spinifex/gateway/bedrockagentruntime.go +++ b/spinifex/gateway/bedrockagentruntime.go @@ -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) diff --git a/spinifex/gateway/bedrockruntime.go b/spinifex/gateway/bedrockruntime.go index c0666efc5..724f5db19 100644 --- a/spinifex/gateway/bedrockruntime.go +++ b/spinifex/gateway/bedrockruntime.go @@ -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) diff --git a/spinifex/gateway/eks.go b/spinifex/gateway/eks.go index 42d8a0a53..8b33dd9d9 100644 --- a/spinifex/gateway/eks.go +++ b/spinifex/gateway/eks.go @@ -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) diff --git a/spinifex/gateway/eks_authz_completeness_test.go b/spinifex/gateway/eks_authz_completeness_test.go index 4f166245d..b7230ea62 100644 --- a/spinifex/gateway/eks_authz_completeness_test.go +++ b/spinifex/gateway/eks_authz_completeness_test.go @@ -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() { diff --git a/spinifex/gateway/eks_test.go b/spinifex/gateway/eks_test.go index c32493c68..2f090e139 100644 --- a/spinifex/gateway/eks_test.go +++ b/spinifex/gateway/eks_test.go @@ -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) } diff --git a/spinifex/gateway/restrouter_test.go b/spinifex/gateway/restroutes_test.go similarity index 72% rename from spinifex/gateway/restrouter_test.go rename to spinifex/gateway/restroutes_test.go index 8d90c6115..7b726f575 100644 --- a/spinifex/gateway/restrouter_test.go +++ b/spinifex/gateway/restroutes_test.go @@ -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/*")) -} diff --git a/spinifex/gateway/restrouter.go b/spinifex/ingress/aws/rest/router.go similarity index 61% rename from spinifex/gateway/restrouter.go rename to spinifex/ingress/aws/rest/router.go index f0f05611a..253b168ee 100644 --- a/spinifex/gateway/restrouter.go +++ b/spinifex/ingress/aws/rest/router.go @@ -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 { diff --git a/spinifex/ingress/aws/rest/router_test.go b/spinifex/ingress/aws/rest/router_test.go new file mode 100644 index 000000000..9c829fc22 --- /dev/null +++ b/spinifex/ingress/aws/rest/router_test.go @@ -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/*")) +}