diff --git a/pkg/agentgateway/plugins/backend_policies.go b/pkg/agentgateway/plugins/backend_policies.go index 1da56902404..f7cbdd0aed0 100644 --- a/pkg/agentgateway/plugins/backend_policies.go +++ b/pkg/agentgateway/plugins/backend_policies.go @@ -93,7 +93,11 @@ func translateBackendPolicyToAgw( } if backend.MCP.Authentication != nil { - pol := translateBackendMCPAuthentication(ctx, policy, policyTarget) + pol, err := translateBackendMCPAuthentication(ctx, policy, policyTarget) + if err != nil { + logger.Error("error processing backend mcp auth", "err", err) + errs = append(errs, err) + } agwPolicies = append(agwPolicies, pol...) } } @@ -283,14 +287,14 @@ func translateBackendMCPAuthorization(policy *agentgateway.AgentgatewayPolicy, t return []AgwPolicy{{Policy: mcpPolicy}} } -func translateBackendMCPAuthentication(ctx PolicyCtx, policy *agentgateway.AgentgatewayPolicy, target *api.PolicyTarget) []AgwPolicy { +func translateBackendMCPAuthentication(ctx PolicyCtx, policy *agentgateway.AgentgatewayPolicy, target *api.PolicyTarget) ([]AgwPolicy, error) { backend := policy.Spec.Backend if backend == nil || backend.MCP == nil || backend.MCP.Authentication == nil { - return nil + return nil, nil } authnPolicy := backend.MCP.Authentication if authnPolicy == nil { - return nil + return nil, nil } idp := api.BackendPolicySpec_McpAuthentication_AUTH0 @@ -301,9 +305,10 @@ func translateBackendMCPAuthentication(ctx PolicyCtx, policy *agentgateway.Agent translatedInlineJwks, err := resolveRemoteJWKSInline(ctx, authnPolicy.JWKS.JwksUri) if err != nil { logger.Error("failed resolving jwks", "jwks_uri", authnPolicy.JWKS.JwksUri, "error", err) - return nil + return nil, err } + var errs []error var extraResourceMetadata map[string]*structpb.Value for k, v := range authnPolicy.ResourceMetadata { if extraResourceMetadata == nil { @@ -313,6 +318,7 @@ func translateBackendMCPAuthentication(ctx PolicyCtx, policy *agentgateway.Agent pbVal, err := structpb.NewValue(v) if err != nil { logger.Error("error converting resource metadata", "key", k, "error", err) + errs = append(errs, err) continue } @@ -345,7 +351,7 @@ func translateBackendMCPAuthentication(ctx PolicyCtx, policy *agentgateway.Agent "policy", policy.Name, "agentgateway_policy", mcpAuthnPolicy.Name) - return []AgwPolicy{{Policy: mcpAuthnPolicy}} + return []AgwPolicy{{Policy: mcpAuthnPolicy}}, errors.Join(errs...) } // translateBackendAI processes AI configuration and creates corresponding Agw policies diff --git a/pkg/agentgateway/translator/backend_translator.go b/pkg/agentgateway/translator/backend_translator.go index e1a96c77266..5037198a3e4 100644 --- a/pkg/agentgateway/translator/backend_translator.go +++ b/pkg/agentgateway/translator/backend_translator.go @@ -1,12 +1,8 @@ package translator import ( - "github.com/agentgateway/agentgateway/go/api" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" - "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/plugins" - agwbackend "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/agentgatewaysyncer/backend" sdk "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir" ) @@ -28,11 +24,3 @@ func NewAgwBackendTranslator(extensions sdk.Plugin) *AgwBackendTranslator { } return translator } - -// TranslateBackend converts a BackendObjectIR to agent gateway Backend and Policy resources -func (t *AgwBackendTranslator) TranslateBackend( - ctx plugins.PolicyCtx, - backend *agentgateway.AgentgatewayBackend, -) ([]*api.Backend, error) { - return agwbackend.BuildAgwBackend(ctx, backend) -} diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-missing-authorization.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-missing-authorization.yaml new file mode 100644 index 00000000000..c4e2443a5b7 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-missing-authorization.yaml @@ -0,0 +1,33 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: openai-backend +spec: + ai: + provider: + openai: + model: gpt-4 + policies: + auth: + secretRef: + name: secret-without-auth +--- +apiVersion: v1 +kind: Secret +metadata: + name: secret-without-auth + namespace: default +data: + someOtherKey: dGVzdA== +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: 'failed to translate backend: secret default/secret-without-auth missing + Authorization value' + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-ref-not-found.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-ref-not-found.yaml new file mode 100644 index 00000000000..36d3eb945b4 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-ref-not-found.yaml @@ -0,0 +1,24 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: openai-backend +spec: + ai: + provider: + openai: + model: gpt-4 + policies: + auth: + secretRef: + name: missing-secret +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: 'failed to translate backend: secret default/missing-secret not found' + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-ref.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-ref.yaml new file mode 100644 index 00000000000..3c1216f9274 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-auth-secret-ref.yaml @@ -0,0 +1,47 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: anthropic-backend +spec: + ai: + provider: + anthropic: + model: claude-4-5-sonnet + policies: + auth: + secretRef: + name: valid-secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: valid-secret + namespace: default +data: + Authorization: QmVhcmVyIHRlc3Q= +--- +# Output +output: +- backend: + ai: + providerGroups: + - providers: + - anthropic: + model: claude-4-5-sonnet + name: backend + inlinePolicies: + - auth: + key: + secret: test + key: default/anthropic-backend + name: + name: anthropic-backend + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups-secret-ref-invalid.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups-secret-ref-invalid.yaml new file mode 100644 index 00000000000..512dfb0f53a --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups-secret-ref-invalid.yaml @@ -0,0 +1,37 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: ai-priority-with-secret +spec: + ai: + groups: + - providers: + - name: openai-secure + openai: + model: gpt-4o + policies: + auth: + secretRef: + name: openai-secret +--- +# Output +output: +- backend: + ai: + providerGroups: + - providers: + - name: openai-secure + openai: + model: gpt-4o + key: default/ai-priority-with-secret + name: + name: ai-priority-with-secret + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups-secret-ref.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups-secret-ref.yaml new file mode 100644 index 00000000000..c8db5cde783 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups-secret-ref.yaml @@ -0,0 +1,71 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: ai-priority-with-secret +spec: + ai: + groups: + - providers: + - name: openai-secure + openai: + model: gpt-4o + policies: + auth: + secretRef: + name: openai-secret + - name: anthropic-secure + anthropic: + model: claude-3-5-sonnet + policies: + auth: + secretRef: + name: anthropic-secret +--- +apiVersion: v1 +kind: Secret +metadata: + name: openai-secret + namespace: default +data: + Authorization: QmVhcmVyIHRlc3Q= +--- +apiVersion: v1 +kind: Secret +metadata: + name: anthropic-secret + namespace: default +data: + Authorization: QmVhcmVyIHRlc3Q= +--- +# Output +output: +- backend: + ai: + providerGroups: + - providers: + - inlinePolicies: + - auth: + key: + secret: test + name: openai-secure + openai: + model: gpt-4o + - anthropic: + model: claude-3-5-sonnet + inlinePolicies: + - auth: + key: + secret: test + name: anthropic-secure + key: default/ai-priority-with-secret + name: + name: ai-priority-with-secret + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups.yaml new file mode 100644 index 00000000000..e0ef9d87a9d --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-priority-groups.yaml @@ -0,0 +1,63 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: multi-providers +spec: + ai: + groups: + - providers: + - name: openai + openai: + model: gpt-4o + - name: anthropic + anthropic: + model: claude-3-5-sonnet + - name: gemini + gemini: + model: gemini-1.5-pro + - name: vertex + vertexai: + model: gemini-pro + region: us-west1 + projectId: my-gcp-project + - name: bedrock + bedrock: + model: anthropic.claude-3-sonnet-20240229-v1:0 + region: us-east-1 +--- +# Output +output: +- backend: + ai: + providerGroups: + - providers: + - name: openai + openai: + model: gpt-4o + - anthropic: + model: claude-3-5-sonnet + name: anthropic + - gemini: + model: gemini-1.5-pro + name: gemini + - name: vertex + vertex: + model: gemini-pro + projectId: my-gcp-project + region: us-west1 + - bedrock: + model: anthropic.claude-3-sonnet-20240229-v1:0 + region: us-east-1 + name: bedrock + key: default/multi-providers + name: + name: multi-providers + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-webhook-backend-ref-not-found.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-webhook-backend-ref-not-found.yaml new file mode 100644 index 00000000000..9eac511d34d --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/ai-webhook-backend-ref-not-found.yaml @@ -0,0 +1,35 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: anthropic-backend +spec: + ai: + provider: + anthropic: + model: claude-4-5-sonnet + policies: + ai: + promptGuard: + request: + - webhook: + backendRef: + name: invalid-request-ref + port: 123 + response: + - webhook: + backendRef: + name: invalid-response-ref + port: 456 +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: |- + failed to translate backend: failed to build webhook: unable to find the Service default/invalid-request-ref + failed to build webhook: unable to find the Service default/invalid-response-ref + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/dynamic-forward-proxy-with-auth.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/dynamic-forward-proxy-with-auth.yaml new file mode 100644 index 00000000000..9b7b58943ae --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/dynamic-forward-proxy-with-auth.yaml @@ -0,0 +1,30 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: dynamic-backend-with-auth +spec: + dynamicForwardProxy: {} + policies: + auth: + key: sk-test-token +--- +# Output +output: +- backend: + dynamic: {} + inlinePolicies: + - auth: + key: + secret: sk-test-token + key: default/dynamic-backend-with-auth + name: + name: dynamic-backend-with-auth + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/dynamic-forward-proxy.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/dynamic-forward-proxy.yaml new file mode 100644 index 00000000000..b759cea4e98 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/dynamic-forward-proxy.yaml @@ -0,0 +1,23 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: dynamic-backend +spec: + dynamicForwardProxy: {} +--- +# Output +output: +- backend: + dynamic: {} + key: default/dynamic-backend + name: + name: dynamic-backend + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-authentication-jwks-store-init.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-authentication-jwks-store-init.yaml new file mode 100644 index 00000000000..ad38f196939 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-authentication-jwks-store-init.yaml @@ -0,0 +1,28 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-backend +spec: + mcp: + targets: + - name: mcp-example + selector: + namespaces: + matchLabels: + kubernetes.io/metadata.name: mcp-servers + policies: + mcp: + authentication: + jwks: + uri: http://store-uninitialized/ +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: 'failed to translate backend: jwks store hasn''t been initialized' + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-service-selector-sse.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-service-selector-sse.yaml new file mode 100644 index 00000000000..18899465835 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-service-selector-sse.yaml @@ -0,0 +1,52 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-service-selector +spec: + mcp: + targets: + - name: mcp-services + selector: + services: + matchLabels: + app: mcp-server +--- +apiVersion: v1 +kind: Service +metadata: + name: mcp-service-sse + namespace: default + labels: + app: mcp-server + annotations: + kgateway.dev/mcp-http-path: /custom-path +spec: + ports: + - name: mcp + port: 8080 + appProtocol: kgateway.dev/mcp-sse +--- +# Output +output: +- backend: + key: default/mcp-service-selector + mcp: + targets: + - backend: + port: 8080 + service: + hostname: mcp-service-sse.default.svc.cluster.local + namespace: default + name: mcp-service-sse-mcp + protocol: SSE + name: + name: mcp-service-selector + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-service-selector-streamable.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-service-selector-streamable.yaml new file mode 100644 index 00000000000..bc9d21dae5d --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-service-selector-streamable.yaml @@ -0,0 +1,50 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-service-streamable +spec: + mcp: + targets: + - name: mcp-services + selector: + services: + matchLabels: + app: mcp-server +--- +apiVersion: v1 +kind: Service +metadata: + name: mcp-service-http + namespace: default + labels: + app: mcp-server +spec: + ports: + - name: mcp-http + port: 9090 + appProtocol: kgateway.dev/mcp +--- +# Output +output: +- backend: + key: default/mcp-service-streamable + mcp: + targets: + - backend: + port: 9090 + service: + hostname: mcp-service-http.default.svc.cluster.local + namespace: default + name: mcp-service-http-mcp-http + protocol: STREAMABLE_HTTP + name: + name: mcp-service-streamable + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-multiple-targets.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-multiple-targets.yaml new file mode 100644 index 00000000000..6d03e0c0383 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-multiple-targets.yaml @@ -0,0 +1,63 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-multi-static +spec: + mcp: + targets: + - name: sse-target + static: + host: sse.example.com + port: 8080 + path: /sse + protocol: SSE + - name: http-target + static: + host: http.example.com + port: 9090 + path: /mcp + protocol: StreamableHTTP +--- +# Output +output: +- backend: + key: default/mcp-multi-static/sse-target + name: + name: mcp-multi-static + namespace: default + static: + host: sse.example.com + port: 8080 +- backend: + key: default/mcp-multi-static/http-target + name: + name: mcp-multi-static + namespace: default + static: + host: http.example.com + port: 9090 +- backend: + key: default/mcp-multi-static + mcp: + targets: + - backend: + backend: default/mcp-multi-static/sse-target + name: sse-target + path: /sse + protocol: SSE + - backend: + backend: default/mcp-multi-static/http-target + name: http-target + path: /mcp + protocol: STREAMABLE_HTTP + name: + name: mcp-multi-static + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-no-protocol.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-no-protocol.yaml new file mode 100644 index 00000000000..5f5c6a32b43 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-no-protocol.yaml @@ -0,0 +1,40 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-static-no-protocol +spec: + mcp: + targets: + - name: default-target + static: + host: mcp-server.example.com + port: 8080 +--- +# Output +output: +- backend: + key: default/mcp-static-no-protocol/default-target + name: + name: mcp-static-no-protocol + namespace: default + static: + host: mcp-server.example.com + port: 8080 +- backend: + key: default/mcp-static-no-protocol + mcp: + targets: + - backend: + backend: default/mcp-static-no-protocol/default-target + name: default-target + name: + name: mcp-static-no-protocol + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-policy-error.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-policy-error.yaml new file mode 100644 index 00000000000..49fd825be2a --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-policy-error.yaml @@ -0,0 +1,27 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-static-policy-error +spec: + mcp: + targets: + - name: target-with-bad-policy + static: + host: mcp-server.example.com + port: 8080 + protocol: SSE + policies: + auth: + secretRef: + name: missing-secret +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: 'failed to translate backend: secret default/missing-secret not found' + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-sse-protocol.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-sse-protocol.yaml new file mode 100644 index 00000000000..28bf1dfba6c --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-sse-protocol.yaml @@ -0,0 +1,44 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-static-sse +spec: + mcp: + targets: + - name: sse-target + static: + host: mcp-server.example.com + port: 8080 + path: /custom-sse + protocol: SSE +--- +# Output +output: +- backend: + key: default/mcp-static-sse/sse-target + name: + name: mcp-static-sse + namespace: default + static: + host: mcp-server.example.com + port: 8080 +- backend: + key: default/mcp-static-sse + mcp: + targets: + - backend: + backend: default/mcp-static-sse/sse-target + name: sse-target + path: /custom-sse + protocol: SSE + name: + name: mcp-static-sse + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-streamable.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-streamable.yaml new file mode 100644 index 00000000000..b519e8c4193 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-streamable.yaml @@ -0,0 +1,44 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-static-streamable +spec: + mcp: + targets: + - name: streamable-target + static: + host: mcp.example.com + port: 9090 + path: /mcp-endpoint + protocol: StreamableHTTP +--- +# Output +output: +- backend: + key: default/mcp-static-streamable/streamable-target + name: + name: mcp-static-streamable + namespace: default + static: + host: mcp.example.com + port: 9090 +- backend: + key: default/mcp-static-streamable + mcp: + targets: + - backend: + backend: default/mcp-static-streamable/streamable-target + name: streamable-target + path: /mcp-endpoint + protocol: STREAMABLE_HTTP + name: + name: mcp-static-streamable + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-with-policies.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-with-policies.yaml new file mode 100644 index 00000000000..0e7b7e3fc03 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/mcp-static-with-policies.yaml @@ -0,0 +1,49 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: mcp-static-with-auth +spec: + mcp: + targets: + - name: auth-target + static: + host: mcp-server.example.com + port: 8080 + protocol: SSE + policies: + auth: + key: mcp-api-token +--- +# Output +output: +- backend: + inlinePolicies: + - auth: + key: + secret: mcp-api-token + key: default/mcp-static-with-auth/auth-target + name: + name: mcp-static-with-auth + namespace: default + static: + host: mcp-server.example.com + port: 8080 +- backend: + key: default/mcp-static-with-auth + mcp: + targets: + - backend: + backend: default/mcp-static-with-auth/auth-target + name: auth-target + protocol: SSE + name: + name: mcp-static-with-auth + namespace: default +status: + conditions: + - lastTransitionTime: fake + message: Backend successfully accepted + reason: Accepted + status: "True" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-auth-passthrough.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-auth-passthrough.yaml new file mode 100644 index 00000000000..827033917b9 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-auth-passthrough.yaml @@ -0,0 +1,24 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: static-backend +spec: + static: + host: example.com + port: 8888 + policies: + auth: + # TODO: kubebuilder:validate allows this but `translateBackendAuth` exepects "inline key or secretRef" + passthrough: {} +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: 'failed to translate backend: backend auth requires either inline key + or secretRef' + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-secrets-not-found.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-secrets-not-found.yaml new file mode 100644 index 00000000000..a15838c29e5 --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-secrets-not-found.yaml @@ -0,0 +1,23 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: static-backend +spec: + static: + host: example.com + port: 8888 + policies: + auth: + secretRef: + name: missing-secret +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: 'failed to translate backend: secret default/missing-secret not found' + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-tls-ca-bundle-not-found.yaml b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-tls-ca-bundle-not-found.yaml new file mode 100644 index 00000000000..fc45ec1f96f --- /dev/null +++ b/pkg/kgateway/agentgatewaysyncer/backend/testdata/backend/static-tls-ca-bundle-not-found.yaml @@ -0,0 +1,27 @@ +apiVersion: agentgateway.dev/v1alpha1 +kind: AgentgatewayBackend +metadata: + namespace: default + name: tls-backend +spec: + static: + host: example.com + port: 8888 + policies: + tls: + mtlsCertificateRef: + - name: unknown-mtls + caCertificateRefs: + - name: unknown-ca-bundle +--- +# Output +output: [] +status: + conditions: + - lastTransitionTime: fake + message: |- + failed to translate backend: secret default/unknown-mtls not found + ConfigMap default/unknown-ca-bundle not found + reason: TranslationError + status: "False" + type: Accepted diff --git a/pkg/kgateway/agentgatewaysyncer/backend/translate.go b/pkg/kgateway/agentgatewaysyncer/backend/translate.go index b22ea4768e0..85e3c4f1feb 100644 --- a/pkg/kgateway/agentgatewaysyncer/backend/translate.go +++ b/pkg/kgateway/agentgatewaysyncer/backend/translate.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/agentgateway/agentgateway/go/api" + "istio.io/istio/pilot/pkg/model/kstatus" "istio.io/istio/pkg/kube/krt" "istio.io/istio/pkg/ptr" corev1 "k8s.io/api/core/v1" @@ -13,7 +14,9 @@ import ( apiannotations "github.com/kgateway-dev/kgateway/v2/api/annotations" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" + agwir "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/ir" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/plugins" + "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/translator" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/utils" "github.com/kgateway-dev/kgateway/v2/pkg/logging" "github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils" @@ -26,10 +29,10 @@ func BuildAgwBackend( ctx plugins.PolicyCtx, backend *agentgateway.AgentgatewayBackend, ) ([]*api.Backend, error) { + errs := []error{} pols, err := translateBackendPolicies(ctx, backend.Namespace, backend.Spec.Policies) if err != nil { - // TODO: bubble this up to a status message without blocking the entire Backend - logger.Warn("failed to translate backend policies", "err", err) + errs = append(errs, err) } if b := backend.Spec.Static; b != nil { @@ -43,7 +46,7 @@ func BuildAgwBackend( }, }, InlinePolicies: pols, - }}, nil + }}, errors.Join(errs...) } if b := backend.Spec.DynamicForwardProxy; b != nil { return []*api.Backend{{ @@ -53,19 +56,63 @@ func BuildAgwBackend( Dynamic: &api.DynamicForwardProxy{}, }, InlinePolicies: pols, - }}, nil + }}, errors.Join(errs...) } if b := backend.Spec.MCP; b != nil { - return translateMCPBackends(ctx, backend, pols) + be, err := translateMCPBackends(ctx, backend, pols) + return be, errors.Join(append(errs, err)...) } if b := backend.Spec.AI; b != nil { be, err := translateAIBackends(ctx, backend, pols) if err != nil { - return nil, err + return nil, errors.Join(append(errs, err)...) } - return []*api.Backend{be}, nil + return []*api.Backend{be}, errors.Join(errs...) } - return nil, errors.New("unknown backend") + return nil, errors.Join(append(errs, errors.New("unknown backend"))...) +} + +func TranslateAgwBackend( + ctx plugins.PolicyCtx, + backend *agentgateway.AgentgatewayBackend, +) (*agentgateway.AgentgatewayBackendStatus, []agwir.AgwResource) { + var results []agwir.AgwResource + backends, err := BuildAgwBackend(ctx, backend) + if err != nil { + logger.Error("failed to translate backend", "backend", backend.Name, "namespace", backend.Namespace, "err", err) + return &agentgateway.AgentgatewayBackendStatus{ + Conditions: kstatus.UpdateConditionIfChanged(backend.Status.Conditions, metav1.Condition{ + Type: "Accepted", + Status: metav1.ConditionFalse, + Reason: "TranslationError", + Message: fmt.Sprintf("failed to translate backend: %v", err), + ObservedGeneration: backend.Generation, + LastTransitionTime: metav1.Now(), + }), + }, results + } + + // handle all backends created as an MCPBackend backend may create multiple backends + for _, backend := range backends { + logger.Debug("creating backend", "backend", backend.Name) + resourceWrapper := translator.ToResourceGlobal(&api.Resource{ + Kind: &api.Resource_Backend{ + Backend: backend, + }, + }) + results = append(results, resourceWrapper) + } + + return &agentgateway.AgentgatewayBackendStatus{ + Conditions: kstatus.UpdateConditionIfChanged(backend.Status.Conditions, metav1.Condition{ + Type: "Accepted", + Status: metav1.ConditionTrue, + Reason: "Accepted", + Message: "Backend successfully accepted", + ObservedGeneration: backend.Generation, + LastTransitionTime: metav1.Now(), + }), + }, results } func translateMCPBackends(ctx plugins.PolicyCtx, be *agentgateway.AgentgatewayBackend, inlinePolicies []*api.BackendPolicySpec) ([]*api.Backend, error) { @@ -78,8 +125,8 @@ func translateMCPBackends(ctx plugins.PolicyCtx, be *agentgateway.AgentgatewayBa staticBackendRef := utils.InternalMCPStaticBackendName(be.Namespace, be.Name, string(target.Name)) pol, err := translateMCPBackendPolicies(ctx, be.Namespace, s.Policies) if err != nil { - // TODO: bubble this up to a status message without blocking the entire Backend - logger.Warn("failed to translate AI backend policies", "err", err) + logger.Error("failed to translate static MCP backend policies", "err", err) + errs = append(errs, err) } staticBackend := &api.Backend{ Key: staticBackendRef, diff --git a/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go b/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go index 1de7cc25ac8..839ecf8ff7c 100644 --- a/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go +++ b/pkg/kgateway/agentgatewaysyncer/backend/translate_test.go @@ -15,12 +15,23 @@ import ( "sigs.k8s.io/yaml" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1/agentgateway" + agwir "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/ir" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/plugins" "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/testutils" agentgatewaybackend "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/agentgatewaysyncer/backend" "github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils" ) +func TestTranslateAgwBackend(t *testing.T) { + testutils.RunForDirectory(t, "testdata/backend", func(t *testing.T, ctx plugins.PolicyCtx) (*agentgateway.AgentgatewayBackendStatus, []*api.Resource) { + backend := testutils.GetTestResource(t, ctx.Collections.Backends) + status, results := agentgatewaybackend.TranslateAgwBackend(ctx, backend) + return status, slices.Map(results, func(r agwir.AgwResource) *api.Resource { + return r.Resource + }) + }) +} + func TestBuildMCP(t *testing.T) { tests := []struct { name string @@ -715,7 +726,7 @@ func createMockMCPService(namespace, serviceName, labels string) *corev1.Service return mockService } -// createMockServiceCollectionMultiNamespace creates a mock service collection with services in multiple namespaces +// createMockMultipleNamespaceServices creates a mock service collection with services in multiple namespaces func createMockMultipleNamespaceServices() []any { services := []any{ &corev1.Service{ diff --git a/pkg/kgateway/agentgatewaysyncer/syncer.go b/pkg/kgateway/agentgatewaysyncer/syncer.go index 6f72a3b63ee..9184e9fe43f 100644 --- a/pkg/kgateway/agentgatewaysyncer/syncer.go +++ b/pkg/kgateway/agentgatewaysyncer/syncer.go @@ -7,13 +7,11 @@ import ( "sync/atomic" "github.com/agentgateway/agentgateway/go/api" - "istio.io/istio/pilot/pkg/model/kstatus" "istio.io/istio/pkg/config" "istio.io/istio/pkg/kube/krt" "istio.io/istio/pkg/ptr" "istio.io/istio/pkg/slices" "istio.io/istio/pkg/util/sets" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/cache" @@ -28,6 +26,7 @@ import ( "github.com/kgateway-dev/kgateway/v2/pkg/agentgateway/utils" "github.com/kgateway-dev/kgateway/v2/pkg/apiclient" "github.com/kgateway-dev/kgateway/v2/pkg/deployer" + agentgatewaybackend "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/agentgatewaysyncer/backend" "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/agentgatewaysyncer/krtxds" "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/agentgatewaysyncer/nack" "github.com/kgateway-dev/kgateway/v2/pkg/kgateway/agentgatewaysyncer/status" @@ -323,63 +322,20 @@ func (s *Syncer) buildListenerFromGateway(obj *translator.GatewayListener) *agwi }, translator.AgwListener{l})) } -// buildBackendFromBackendIR creates a backend resource from Backend -func (s *Syncer) buildBackendFromBackend(ctx krt.HandlerContext, backend *agentgateway.AgentgatewayBackend) ([]agwir.AgwResource, *agentgateway.AgentgatewayBackendStatus) { - var results []agwir.AgwResource - var backendStatus *agentgateway.AgentgatewayBackendStatus - pc := plugins.PolicyCtx{ - Krt: ctx, - Collections: s.agwCollections, - } - backends, err := s.translator.BackendTranslator().TranslateBackend(pc, backend) - if err != nil { - logger.Error("failed to translate backend", "backend", backend.Name, "namespace", backend.Namespace, "error", err) - backendStatus = &agentgateway.AgentgatewayBackendStatus{ - Conditions: kstatus.UpdateConditionIfChanged(backend.Status.Conditions, metav1.Condition{ - Type: "Accepted", - Status: metav1.ConditionFalse, - Reason: "TranslationError", - Message: fmt.Sprintf("failed to translate backend %v", err), - ObservedGeneration: backend.Generation, - LastTransitionTime: metav1.Now(), - }), - } - return results, backendStatus - } - // handle all backends created as an MCPBackend backend may create multiple backends - for _, backend := range backends { - logger.Debug("creating backend", "backend", backend.Name) - resourceWrapper := translator.ToResourceGlobal(&api.Resource{ - Kind: &api.Resource_Backend{ - Backend: backend, - }, - }) - results = append(results, resourceWrapper) - } - backendStatus = &agentgateway.AgentgatewayBackendStatus{ - Conditions: kstatus.UpdateConditionIfChanged(backend.Status.Conditions, metav1.Condition{ - Type: "Accepted", - Status: metav1.ConditionTrue, - Reason: "Accepted", - Message: "Backend successfully accepted", - ObservedGeneration: backend.Generation, - LastTransitionTime: metav1.Now(), - }), - } - return results, backendStatus -} - -// newADPBackendCollection creates the ADP backend collection for agent gateway resources +// newAgwBackendCollection creates the ADP backend collection for agent gateway resources func (s *Syncer) newAgwBackendCollection(finalBackends krt.Collection[*agentgateway.AgentgatewayBackend], krtopts krtutil.KrtOptions) ( krt.StatusCollection[*agentgateway.AgentgatewayBackend, agentgateway.AgentgatewayBackendStatus], krt.Collection[agwir.AgwResource], ) { - return krt.NewStatusManyCollection(finalBackends, func(krtctx krt.HandlerContext, backend *agentgateway.AgentgatewayBackend) ( + return krt.NewStatusManyCollection(finalBackends, func(ctx krt.HandlerContext, backend *agentgateway.AgentgatewayBackend) ( *agentgateway.AgentgatewayBackendStatus, []agwir.AgwResource, ) { - resources, status := s.buildBackendFromBackend(krtctx, backend) - return status, resources + pc := plugins.PolicyCtx{ + Krt: ctx, + Collections: s.agwCollections, + } + return agentgatewaybackend.TranslateAgwBackend(pc, backend) }, krtopts.ToOptions("Backends")...) } diff --git a/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/backend-tls.yaml b/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/backend-tls.yaml index 2cea75f2a6e..3e66f56904f 100644 --- a/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/backend-tls.yaml +++ b/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/backend-tls.yaml @@ -194,6 +194,14 @@ spec: auth: key: "sk-anthropic-primary" --- +apiVersion: v1 +kind: Secret +metadata: + name: openai-primary-secret +type: Opaque +data: + Authorization: QmVhcmVyIHNrLW9wZW5haS1wcmltYXJ5LWtleQ== # Bearer sk-openai-primary-key +--- apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayBackend metadata: diff --git a/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/bedrock-backend.yaml b/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/bedrock-backend.yaml index 296bd9fbb06..94c0da1ec37 100644 --- a/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/bedrock-backend.yaml +++ b/pkg/kgateway/agentgatewaysyncer/testdata/inputs/backend/bedrock-backend.yaml @@ -5,9 +5,9 @@ metadata: spec: gatewayClassName: agentgateway-v2 listeners: - - name: http - protocol: HTTP - port: 80 + - name: http + protocol: HTTP + port: 80 --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute @@ -15,14 +15,14 @@ metadata: name: example-route spec: parentRefs: - - name: example-gateway + - name: example-gateway hostnames: - - "example.com" + - "example.com" rules: - - backendRefs: - - group: agentgateway.dev - kind: AgentgatewayBackend - name: bedrock + - backendRefs: + - group: agentgateway.dev + kind: AgentgatewayBackend + name: bedrock --- apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayBackend @@ -49,6 +49,4 @@ kind: Secret metadata: name: bedrock-secret data: - accessKey: "ZmFrZUFjY2Vzc0tleUlk" - secretKey: "ZmFrZVNlY3JldEtleVN0cmluZw==" - sessionToken: "" \ No newline at end of file + Authorization: QmVhcmVyIHRlc3Q= diff --git a/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/backend-tls.yaml b/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/backend-tls.yaml index 6eadcfdcf98..2a1dd2cdbaf 100644 --- a/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/backend-tls.yaml +++ b/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/backend-tls.yaml @@ -9,7 +9,11 @@ Backends: - ai: providerGroups: - providers: - - name: openai + - inlinePolicies: + - auth: + key: + secret: sk-openai-primary-key + name: openai openai: model: gpt-4o - anthropic: @@ -68,6 +72,10 @@ Backends: - name: backend openai: model: gpt-4o + inlinePolicies: + - auth: + key: + secret: sk-openai-primary-key key: default/openai-single name: name: openai-single diff --git a/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/bedrock-backend.yaml b/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/bedrock-backend.yaml index c29eee02f6b..41dfd0a3769 100644 --- a/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/bedrock-backend.yaml +++ b/pkg/kgateway/agentgatewaysyncer/testdata/outputs/backend/bedrock-backend.yaml @@ -8,6 +8,10 @@ Backends: model: anthropic.claude-3-5-haiku-20241022-v1:0 region: us-west-2 name: backend + inlinePolicies: + - auth: + key: + secret: test key: default/bedrock name: name: bedrock